Files
fullstack-starter/apps/server/src/routes/license.tsx
imbytecat 492fba3105 feat: 添加许可证停用功能及确认弹窗
- 添加反激活许可证的确认弹窗功能,包含二次确认提示和操作反馈。
- 添加停用许可证的接口合约定义。
- 添加许可证停用功能,确保激活记录存在后将其许可证信息和激活时间清空。
2026-01-26 15:14:23 +08:00

464 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
useMutation,
useQueryClient,
useSuspenseQuery,
} from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { orpc } from '@/client/query-client'
export const Route = createFileRoute('/license')({
component: License,
})
function License() {
const [licenseInput, setLicenseInput] = useState('')
const [copySuccess, setCopySuccess] = useState(false)
const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false)
const queryClient = useQueryClient()
// 获取激活状态
const { data } = useSuspenseQuery(orpc.license.getActivation.queryOptions())
// 激活 mutation
const activateMutation = useMutation({
...orpc.license.activate.mutationOptions(),
onSuccess: () => {
// 刷新数据
queryClient.invalidateQueries({
queryKey: orpc.license.getActivation.key(),
})
// 清空输入
setLicenseInput('')
},
})
// 反激活 mutation
const deactivateMutation = useMutation({
...orpc.license.deactivate.mutationOptions(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: orpc.license.getActivation.key(),
})
setShowDeactivateConfirm(false)
},
})
const handleActivate = () => {
if (!licenseInput.trim()) return
activateMutation.mutate({ license: licenseInput.trim() })
}
const handleDeactivate = () => {
deactivateMutation.mutate()
}
const handleCopyFingerprint = async () => {
try {
await navigator.clipboard.writeText(data.fingerprint)
setCopySuccess(true)
setTimeout(() => setCopySuccess(false), 2000)
} catch (err) {
console.error('Failed to copy fingerprint:', err)
}
}
const isActivated = !!data.license
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4 font-sans">
<div className="w-full max-w-2xl space-y-6">
{/* 页面标题 */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
License
</h1>
<p className="text-gray-500 mt-2"></p>
</div>
{/* 设备信息卡片 */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-blue-500"
>
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>
</h2>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<p className="text-sm text-gray-500 mb-1">
(Device Fingerprint)
</p>
<div className="flex items-center gap-3">
<code className="flex-1 font-mono text-sm text-gray-700 break-all select-all">
{data.fingerprint}
</code>
<button
type="button"
onClick={handleCopyFingerprint}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center gap-1.5 ${
copySuccess
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-white border border-gray-200 text-gray-700 hover:bg-gray-50 hover:text-gray-900'
}`}
>
{copySuccess ? (
<>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</>
) : (
<>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* License 激活卡片 */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-purple-500"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
License
</h2>
<div className="space-y-4">
<div>
<label
htmlFor="license-key"
className="block text-sm font-medium text-gray-700 mb-1"
>
License Key
</label>
<input
id="license-key"
type="text"
value={licenseInput}
onChange={(e) => setLicenseInput(e.target.value)}
disabled={isActivated || activateMutation.isPending}
placeholder={
isActivated ? '已激活,无需输入' : '请输入您的 License Key'
}
className="w-full px-4 py-2.5 bg-white border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 outline-none transition-all disabled:bg-gray-100 disabled:text-gray-500 placeholder:text-gray-400"
/>
</div>
{activateMutation.isError && (
<div className="p-3 bg-red-50 border border-red-100 rounded-lg text-red-600 text-sm flex items-start gap-2">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mt-0.5 shrink-0"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span>激活失败: 请检查 License </span>
</div>
)}
<button
type="button"
onClick={handleActivate}
disabled={
isActivated ||
activateMutation.isPending ||
!licenseInput.trim()
}
className={`w-full px-6 py-2.5 rounded-lg font-medium text-white shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 ${
isActivated
? 'bg-gray-300 cursor-not-allowed'
: activateMutation.isPending
? 'bg-purple-500 opacity-80 cursor-wait'
: 'bg-purple-600 hover:bg-purple-700 hover:shadow-md active:scale-[0.99] focus:ring-purple-500'
}`}
>
{activateMutation.isPending ? (
<span className="flex items-center justify-center gap-2">
<svg
aria-hidden="true"
className="animate-spin h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
...
</span>
) : isActivated ? (
'已完成激活'
) : (
'立即激活'
)}
</button>
</div>
</div>
</div>
{/* 激活状态卡片 */}
<div
className={`rounded-xl shadow-sm border overflow-hidden transition-colors ${
isActivated
? 'bg-green-50/50 border-green-100'
: 'bg-white border-gray-100'
}`}
>
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={isActivated ? 'text-green-500' : 'text-gray-400'}
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</h2>
{isActivated ? (
<div className="space-y-3">
<div className="flex items-center gap-2 text-green-700 font-medium text-lg">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-green-500"
>
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="M9 12l2 2 4-4"></path>
</svg>
<span>License </span>
</div>
<div className="pl-8 space-y-1">
<p className="text-gray-600 text-sm">
<span className="font-medium text-gray-700">
License:{' '}
</span>
<span className="font-mono">{data.license}</span>
</p>
{data.licenseActivatedAt && (
<p className="text-gray-500 text-sm">
<span className="font-medium text-gray-700">
:{' '}
</span>
{new Date(data.licenseActivatedAt).toLocaleString(
'zh-CN',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
},
)}
</p>
)}
</div>
{!showDeactivateConfirm ? (
<button
type="button"
onClick={() => setShowDeactivateConfirm(true)}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg text-sm font-medium hover:bg-red-600 transition-colors shadow-sm"
>
</button>
) : (
<div className="mt-4 p-4 bg-red-50 rounded-lg border border-red-200">
<p className="text-red-700 text-sm mb-3 flex items-center gap-2">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
License
</p>
<div className="flex gap-2">
<button
type="button"
onClick={handleDeactivate}
disabled={deactivateMutation.isPending}
className="px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700 disabled:bg-gray-300 transition-colors"
>
{deactivateMutation.isPending
? '反激活中...'
: '确认反激活'}
</button>
<button
type="button"
onClick={() => setShowDeactivateConfirm(false)}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md text-sm font-medium hover:bg-gray-300 transition-colors"
>
</button>
</div>
{deactivateMutation.isError && (
<p className="text-red-500 mt-2 text-xs">
</p>
)}
</div>
)}
</div>
) : (
<div className="flex items-start gap-3">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-orange-500 shrink-0 mt-0.5"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<div>
<p className="text-orange-700 font-medium text-lg mb-1">
</p>
<p className="text-gray-500 text-sm">
License Key
</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}