feat(license): 退出授权文案精简与两态布局
未授权隐藏退出;Pro+管理员可见「退出授权」;成功只 reload 授权态不整页刷。
This commit is contained in:
1 parent
cec7c6eeaf
commit
914eec1e4b
4 files changed
+45
-29
No files matched your search
@@ -76,7 +76,7 @@ curl -sS 'https://licence.rlwj.cn/api/v1/products/code/lokistack'
|
||||
|
||||
## 退出授权(本机)
|
||||
|
||||
- UI:设置 → 授权,Pro 态管理员可见「退出授权 / 释放本实例」,二次确认。
|
||||
- UI:设置 → 授权,Pro 态(含宽限仍 pro)管理员可见「退出授权」,二次确认;免费/未激活不渲染。成功后只重拉授权态,不整页刷新。
|
||||
- 后端:`POST /api/v1/system/license/deactivate`(Admin)。
|
||||
- 在线 `LIC-`:调用 `POST {LICENSE_API_BASE}/v1/license/deactivate`,body 含 `license_key`、`device_id`(=instance_id)、`activation_token`(若有);**成功才清本地**,失败保留现状并报错。
|
||||
- 离线 `LICOFF-`:只清本地,响应 `offlineLocalOnly=true`,提示后台名额可能仍占。
|
||||
|
||||
@@ -21,10 +21,11 @@ export default {
|
||||
activating: 'Activating…',
|
||||
activated: 'Pro activated — all features unlocked',
|
||||
activateFailed: 'Activation failed',
|
||||
deactivate: 'Deactivate / release this instance',
|
||||
deactivate: 'Deactivate license',
|
||||
deactivateConfirmTitle: 'Deactivate license?',
|
||||
deactivateConfirmDesc: 'This releases the seat for this instance and returns the machine to Free. Online keys notify the license server first; on failure local state is kept (never silently cleared).',
|
||||
deactivateConfirmDesc: 'This machine returns to Free. Online keys notify the server first; on failure local state is kept.',
|
||||
deactivateConfirm: 'Confirm deactivate',
|
||||
graceBadge: 'Grace {{days}}d',
|
||||
deactivating: 'Deactivating…',
|
||||
deactivated: 'License deactivated — this machine is Free again',
|
||||
deactivateFailed: 'Deactivation failed',
|
||||
|
||||
@@ -21,10 +21,11 @@ export default {
|
||||
activating: '激活中…',
|
||||
activated: 'Pro 已激活,全部功能已解锁',
|
||||
activateFailed: '激活失败',
|
||||
deactivate: '退出授权 / 释放本实例',
|
||||
deactivate: '退出授权',
|
||||
deactivateConfirmTitle: '退出授权?',
|
||||
deactivateConfirmDesc: '将释放本实例占用的授权名额,本机回到免费版。在线授权会先通知授权服务器;失败则保留现状,不会静默清码。',
|
||||
deactivateConfirmDesc: '确认后本机回到免费版。在线授权会先通知服务器;失败则保留现状,不会清码。',
|
||||
deactivateConfirm: '确认退出',
|
||||
graceBadge: '宽限 {{days}} 天',
|
||||
deactivating: '退出中…',
|
||||
deactivated: '已退出授权,本机已回到免费版',
|
||||
deactivateFailed: '退出授权失败',
|
||||
|
||||
@@ -13,11 +13,12 @@ import { licenseApi } from '@/api/license'
|
||||
import { useLicense } from '@/hooks/useLicense'
|
||||
import { getUser } from '@/lib/auth-storage'
|
||||
|
||||
/** 授权区块:当前版本状态 + 许可码激活 / 退出授权(仅管理员可操作) */
|
||||
/** 授权区块:免费态激活 / Pro 态退出(仅管理员;未授权不渲染退出) */
|
||||
export const LicenseSection = memo(function LicenseSection() {
|
||||
const { t } = useTranslation('license')
|
||||
const { pro, info, reload } = useLicense()
|
||||
const isAdmin = getUser()?.role === 'admin'
|
||||
const canDeactivate = Boolean(pro) && isAdmin
|
||||
const [key, setKey] = useState('')
|
||||
const [activating, setActivating] = useState(false)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
@@ -36,7 +37,7 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
await licenseApi.activate(key.trim())
|
||||
toast.success(t('activated'))
|
||||
setKey('')
|
||||
reload()
|
||||
await Promise.resolve(reload())
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : t('activateFailed'))
|
||||
} finally {
|
||||
@@ -54,8 +55,10 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
toast.success(res?.message || t('deactivated'))
|
||||
}
|
||||
setConfirmOpen(false)
|
||||
reload()
|
||||
// 只重拉授权态,禁止整页 window.location.reload
|
||||
await Promise.resolve(reload())
|
||||
} catch (err) {
|
||||
// 失败:弹层保留、不清码、toast
|
||||
toast.error(err instanceof Error ? err.message : t('deactivateFailed'))
|
||||
} finally {
|
||||
setDeactivating(false)
|
||||
@@ -69,6 +72,7 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
: null
|
||||
|
||||
const instanceLimit = info?.maxDevices || info?.instances
|
||||
const inGrace = Boolean(info?.inGrace)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -79,10 +83,15 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
<Badge variant={pro ? 'default' : 'secondary'} className={pro ? 'bg-success text-success-foreground' : ''}>
|
||||
{pro ? 'Pro' : t('freeBadge')}
|
||||
</Badge>
|
||||
{pro && inGrace && (
|
||||
<Badge variant="outline" className="border-warning/50 text-warning-foreground">
|
||||
{t('graceBadge', { days: info?.graceDaysLeft ?? 0 })}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<LicenseStatusBanners info={info} pro={pro} onAction={isAdmin ? focusKey : undefined} />
|
||||
<LicenseStatusBanners info={info} pro={pro} onAction={isAdmin && !pro ? focusKey : undefined} />
|
||||
<InstanceIdRow instanceId={info?.instanceId} />
|
||||
<div className="text-sm">
|
||||
{pro ? (
|
||||
@@ -98,7 +107,8 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
<p className="text-muted-foreground">{t('freeDesc')}</p>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
{/* 未激活:管理员可激活;已激活:仅管理员退出(对齐 Yaak — 无 Deactivate 于未授权态) */}
|
||||
{isAdmin && !pro && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">{t('activateHint')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -112,29 +122,33 @@ export const LicenseSection = memo(function LicenseSection() {
|
||||
<Button onClick={() => void activate()} disabled={activating || !key.trim()}>
|
||||
{activating ? t('activating') : t('activate')}
|
||||
</Button>
|
||||
{pro && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-destructive/40 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
disabled={deactivating}
|
||||
>
|
||||
{t('deactivate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canDeactivate && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-destructive/40 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
disabled={deactivating}
|
||||
>
|
||||
{t('deactivate')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onOpenChange={setConfirmOpen}
|
||||
title={t('deactivateConfirmTitle')}
|
||||
description={t('deactivateConfirmDesc')}
|
||||
confirmText={t('deactivateConfirm')}
|
||||
loading={deactivating}
|
||||
onConfirm={() => void deactivate()}
|
||||
/>
|
||||
{canDeactivate && (
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onOpenChange={setConfirmOpen}
|
||||
title={t('deactivateConfirmTitle')}
|
||||
description={t('deactivateConfirmDesc')}
|
||||
confirmText={t('deactivateConfirm')}
|
||||
loading={deactivating}
|
||||
onConfirm={() => void deactivate()}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user