flnxi-Wu/components/LicenseInfo.tsx
2026-05-28 20:43:12 +08:00

245 lines
9.3 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.

'use client';
import { useEffect, useState } from 'react';
import ActivationModal from './ui/ActivationModal';
import { cn } from '@/lib/ui-styles';
interface LicenseData {
type: 'FREE' | 'STANDARD' | 'PRO';
status: 'active' | 'expired' | 'grace';
expiresAt: string | null;
leaseExpiresAt: string | null;
licenseKey: string | null;
usage: {
users: { current: number; max: number };
materials: { current: number; max: number };
};
features: {
customTemplates: boolean;
departmentIsolation: boolean;
intranetPenetration: boolean;
};
lastVerified: string;
}
const LICENSE_TYPE_NAMES = {
FREE: '基础版',
STANDARD: '标准版',
PRO: '专业版',
};
const STATUS_NAMES = {
active: '正常',
expired: '已过期',
grace: '宽限期',
};
const LICENSE_TYPE_BADGE_CLASSES: Record<LicenseData['type'], string> = {
FREE: 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300',
STANDARD: 'border-slate-300 bg-white text-slate-800 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200',
PRO: 'border-slate-400 bg-slate-100 text-slate-900 dark:border-slate-500 dark:bg-slate-700 dark:text-slate-100',
};
const STATUS_BADGE_CLASSES: Record<LicenseData['status'], string> = {
active: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-300',
expired: 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300',
grace: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-300',
};
export default function LicenseInfo() {
const [license, setLicense] = useState<LicenseData | null>(null);
const [loading, setLoading] = useState(true);
const [showActivationModal, setShowActivationModal] = useState(false);
useEffect(() => {
fetchLicenseInfo();
}, []);
const fetchLicenseInfo = async () => {
try {
const res = await fetch('/api/license/info');
if (res.ok) {
const data = await res.json();
setLicense(data);
}
} catch (error) {
console.error('获取许可证信息失败:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700 p-6">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-slate-700 rounded w-1/4 mb-4"></div>
<div className="h-8 bg-gray-200 dark:bg-slate-700 rounded w-1/2"></div>
</div>
</div>
);
}
if (!license) {
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700 p-6">
<p className="text-gray-500 dark:text-slate-400">无法获取许可证信息</p>
</div>
);
}
const isExpiringSoon =
license.expiresAt &&
new Date(license.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000;
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700">
{/* 头部 */}
<div className="p-6 border-b dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-slate-100">许可证信息</h3>
<p className="text-sm text-gray-500 dark:text-slate-400 mt-1">
最后验证: {new Date(license.lastVerified).toLocaleString()}
</p>
{license.licenseKey && (
<div className="mt-2">
<p className="text-xs text-gray-500 dark:text-slate-400 mb-1">激活码</p>
<p className="text-sm font-mono font-medium text-gray-900 dark:text-slate-100 bg-gray-50 dark:bg-slate-700 px-3 py-1.5 rounded border dark:border-slate-600 inline-block">
{license.licenseKey}
</p>
</div>
)}
</div>
<div className="flex items-center gap-2">
<span
className={cn(
'rounded-md border px-2.5 py-1 text-sm font-medium',
LICENSE_TYPE_BADGE_CLASSES[license.type]
)}
>
{LICENSE_TYPE_NAMES[license.type]}
</span>
<span
className={cn(
'rounded-md border px-2.5 py-1 text-sm font-medium',
STATUS_BADGE_CLASSES[license.status]
)}
>
{STATUS_NAMES[license.status]}
</span>
</div>
</div>
</div>
{/* 到期提醒 */}
{isExpiringSoon && license.expiresAt && (
<div className="px-6 py-3 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-100 dark:border-yellow-800">
<p className="text-sm text-yellow-700 dark:text-yellow-400">
许可证将于 {new Date(license.expiresAt).toLocaleDateString()} 到期,请联系管理员处理续期
</p>
</div>
)}
{/* 使用统计 */}
<div className="p-6 border-b dark:border-slate-700">
<h4 className="text-sm font-medium text-gray-700 dark:text-slate-300 mb-4">使用情况</h4>
<div className="grid grid-cols-2 gap-4">
{/* 用户数 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-slate-400">用户数</span>
<span className="text-gray-900 dark:text-slate-100">
{license.usage.users.current} / {license.usage.users.max === -1 ? '无限' : license.usage.users.max}
</span>
</div>
{license.usage.users.max !== -1 && (
<div className="h-2 bg-gray-100 dark:bg-slate-700 rounded-sm overflow-hidden">
<div
className={`h-full ${
license.usage.users.current / license.usage.users.max > 0.9
? 'bg-red-500'
: 'bg-slate-500'
}`}
style={{
width: `${Math.min(100, (license.usage.users.current / license.usage.users.max) * 100)}%`,
}}
/>
</div>
)}
</div>
{/* 素材数 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-slate-400">素材数</span>
<span className="text-gray-900 dark:text-slate-100">
{license.usage.materials.current} / {license.usage.materials.max === -1 ? '无限' : license.usage.materials.max}
</span>
</div>
{license.usage.materials.max !== -1 && (
<div className="h-2 bg-gray-100 dark:bg-slate-700 rounded-sm overflow-hidden">
<div
className={`h-full ${
license.usage.materials.current / license.usage.materials.max > 0.9
? 'bg-red-500'
: 'bg-slate-500'
}`}
style={{
width: `${Math.min(100, (license.usage.materials.current / license.usage.materials.max) * 100)}%`,
}}
/>
</div>
)}
</div>
</div>
</div>
{/* 功能列表 */}
<div className="p-6">
<h4 className="text-sm font-medium text-gray-700 dark:text-slate-300 mb-4">功能权限</h4>
<div className="space-y-2">
<FeatureItem label="自定义模板" enabled={license.features.customTemplates} />
<FeatureItem label="部门隔离" enabled={license.features.departmentIsolation} />
<FeatureItem label="内网穿透" enabled={license.features.intranetPenetration} />
</div>
</div>
{/* 升级/激活按钮 */}
{license.type !== 'PRO' && (
<div className="px-6 pb-6 space-y-3">
<button
onClick={() => setShowActivationModal(true)}
className="w-full rounded-lg bg-slate-900 py-2.5 font-medium text-white transition-colors hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white"
>
输入激活码
</button>
<p className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
如需调整授权范围,请联系系统管理员或交付人员。
</p>
</div>
)}
{/* 激活码输入弹窗 */}
<ActivationModal
isOpen={showActivationModal}
onClose={() => setShowActivationModal(false)}
onSuccess={fetchLicenseInfo}
/>
</div>
);
}
function FeatureItem({ label, enabled }: { label: string; enabled: boolean }) {
return (
<div className="flex items-center justify-between py-1">
<span className="text-sm text-gray-600 dark:text-slate-400">{label}</span>
{enabled ? (
<span className="rounded-md border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-300">已启用</span>
) : (
<span className="rounded-md border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs font-medium text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">未启用</span>
)}
</div>
);
}