feat: 添加许可证停用功能及确认弹窗

- 添加反激活许可证的确认弹窗功能,包含二次确认提示和操作反馈。
- 添加停用许可证的接口合约定义。
- 添加许可证停用功能,确保激活记录存在后将其许可证信息和激活时间清空。
This commit is contained in:
2026-01-26 15:14:23 +08:00
parent 8952bf4205
commit 492fba3105
3 changed files with 98 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ export const Route = createFileRoute('/license')({
function License() { function License() {
const [licenseInput, setLicenseInput] = useState('') const [licenseInput, setLicenseInput] = useState('')
const [copySuccess, setCopySuccess] = useState(false) const [copySuccess, setCopySuccess] = useState(false)
const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
// 获取激活状态 // 获取激活状态
@@ -32,11 +33,26 @@ function License() {
}, },
}) })
// 反激活 mutation
const deactivateMutation = useMutation({
...orpc.license.deactivate.mutationOptions(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: orpc.license.getActivation.key(),
})
setShowDeactivateConfirm(false)
},
})
const handleActivate = () => { const handleActivate = () => {
if (!licenseInput.trim()) return if (!licenseInput.trim()) return
activateMutation.mutate({ license: licenseInput.trim() }) activateMutation.mutate({ license: licenseInput.trim() })
} }
const handleDeactivate = () => {
deactivateMutation.mutate()
}
const handleCopyFingerprint = async () => { const handleCopyFingerprint = async () => {
try { try {
await navigator.clipboard.writeText(data.fingerprint) await navigator.clipboard.writeText(data.fingerprint)
@@ -352,6 +368,62 @@ function License() {
</p> </p>
)} )}
</div> </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>
) : ( ) : (
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">

View File

@@ -12,3 +12,7 @@ export const getActivation = oc.input(z.void()).output(
export const activate = oc export const activate = oc
.input(z.object({ license: z.string().min(1) })) .input(z.object({ license: z.string().min(1) }))
.output(z.object({ success: z.boolean() })) .output(z.object({ success: z.boolean() }))
export const deactivate = oc
.input(z.void())
.output(z.object({ success: z.boolean() }))

View File

@@ -43,3 +43,25 @@ export const activate = os.license.activate
return { success: true } return { success: true }
}) })
export const deactivate = os.license.deactivate
.use(dbProvider)
.handler(async ({ context }) => {
await ensureLicenseActivationInitialized()
const record = await context.db.query.licenseActivationTable.findFirst()
if (!record) {
throw new Error('License activation record not found')
}
await context.db
.update(licenseActivationTable)
.set({
license: null,
licenseActivatedAt: null,
})
.where(eq(licenseActivationTable.id, record.id))
return { success: true }
})