- 添加反激活许可证的确认弹窗功能,包含二次确认提示和操作反馈。 - 添加停用许可证的接口合约定义。 - 添加许可证停用功能,确保激活记录存在后将其许可证信息和激活时间清空。
464 lines
18 KiB
TypeScript
464 lines
18 KiB
TypeScript
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>
|
||
)
|
||
}
|