245 lines
9.3 KiB
TypeScript
245 lines
9.3 KiB
TypeScript
'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>
|
||
);
|
||
}
|