- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
213 lines
7.3 KiB
TypeScript
213 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
|
||
interface LicenseData {
|
||
type: 'FREE' | 'STANDARD' | 'PRO';
|
||
status: 'active' | 'expired' | 'grace';
|
||
expiresAt: 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: '宽限期',
|
||
};
|
||
|
||
export default function LicenseInfo() {
|
||
const [license, setLicense] = useState<LicenseData | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
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>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span
|
||
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||
license.type === 'PRO'
|
||
? 'bg-purple-100 text-purple-700'
|
||
: license.type === 'STANDARD'
|
||
? 'bg-blue-100 text-blue-700'
|
||
: 'bg-gray-100 text-gray-700'
|
||
}`}
|
||
>
|
||
{LICENSE_TYPE_NAMES[license.type]}
|
||
</span>
|
||
<span
|
||
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||
license.status === 'active'
|
||
? 'bg-green-100 text-green-700'
|
||
: license.status === 'grace'
|
||
? 'bg-yellow-100 text-yellow-700'
|
||
: 'bg-red-100 text-red-700'
|
||
}`}
|
||
>
|
||
{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-full overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full ${
|
||
license.usage.users.current / license.usage.users.max > 0.9
|
||
? 'bg-red-500'
|
||
: 'bg-blue-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-full overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full ${
|
||
license.usage.materials.current / license.usage.materials.max > 0.9
|
||
? 'bg-red-500'
|
||
: 'bg-blue-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">
|
||
<button className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||
升级许可证
|
||
</button>
|
||
</div>
|
||
)}
|
||
</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="text-green-600 dark:text-green-400">✓</span>
|
||
) : (
|
||
<span className="text-gray-300 dark:text-slate-600">✗</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|