From 914eec1e4bd7ed5d14cf23de5bdf33eb6d87dbf7 Mon Sep 17 00:00:00 2001 From: opsx Date: Sun, 1 Nov 2026 03:34:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(license):=20=E9=80=80=E5=87=BA=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=96=87=E6=A1=88=E7=B2=BE=E7=AE=80=E4=B8=8E=E4=B8=A4?= =?UTF-8?q?=E6=80=81=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未授权隐藏退出;Pro+管理员可见「退出授权」;成功只 reload 授权态不整页刷。 --- docs/licensing-opsx-licence.md | 2 +- frontend/src/i18n/locales/en/license.ts | 5 +- frontend/src/i18n/locales/zh/license.ts | 5 +- .../settings/components/LicenseSection.tsx | 62 ++++++++++++------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/docs/licensing-opsx-licence.md b/docs/licensing-opsx-licence.md index ade803d..5cee99a 100644 --- a/docs/licensing-opsx-licence.md +++ b/docs/licensing-opsx-licence.md @@ -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`,提示后台名额可能仍占。 diff --git a/frontend/src/i18n/locales/en/license.ts b/frontend/src/i18n/locales/en/license.ts index 930d3f2..2026c8b 100644 --- a/frontend/src/i18n/locales/en/license.ts +++ b/frontend/src/i18n/locales/en/license.ts @@ -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', diff --git a/frontend/src/i18n/locales/zh/license.ts b/frontend/src/i18n/locales/zh/license.ts index 3f55272..393b9f1 100644 --- a/frontend/src/i18n/locales/zh/license.ts +++ b/frontend/src/i18n/locales/zh/license.ts @@ -21,10 +21,11 @@ export default { activating: '激活中…', activated: 'Pro 已激活,全部功能已解锁', activateFailed: '激活失败', - deactivate: '退出授权 / 释放本实例', + deactivate: '退出授权', deactivateConfirmTitle: '退出授权?', - deactivateConfirmDesc: '将释放本实例占用的授权名额,本机回到免费版。在线授权会先通知授权服务器;失败则保留现状,不会静默清码。', + deactivateConfirmDesc: '确认后本机回到免费版。在线授权会先通知服务器;失败则保留现状,不会清码。', deactivateConfirm: '确认退出', + graceBadge: '宽限 {{days}} 天', deactivating: '退出中…', deactivated: '已退出授权,本机已回到免费版', deactivateFailed: '退出授权失败', diff --git a/frontend/src/pages/settings/components/LicenseSection.tsx b/frontend/src/pages/settings/components/LicenseSection.tsx index 4861226..6d4da2b 100644 --- a/frontend/src/pages/settings/components/LicenseSection.tsx +++ b/frontend/src/pages/settings/components/LicenseSection.tsx @@ -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 ( @@ -79,10 +83,15 @@ export const LicenseSection = memo(function LicenseSection() { {pro ? 'Pro' : t('freeBadge')} + {pro && inGrace && ( + + {t('graceBadge', { days: info?.graceDaysLeft ?? 0 })} + + )} - +
{pro ? ( @@ -98,7 +107,8 @@ export const LicenseSection = memo(function LicenseSection() {

{t('freeDesc')}

)}
- {isAdmin && ( + {/* 未激活:管理员可激活;已激活:仅管理员退出(对齐 Yaak — 无 Deactivate 于未授权态) */} + {isAdmin && !pro && (

{t('activateHint')}

@@ -112,29 +122,33 @@ export const LicenseSection = memo(function LicenseSection() { - {pro && ( - - )}
)} + {canDeactivate && ( +
+ +
+ )}
- void deactivate()} - /> + {canDeactivate && ( + void deactivate()} + /> + )}
) })