flnxi-Wu/components/LicenseInfo.tsx
WuYongJie ba98bf22cc feat: 添加许可证系统、Agent部署模块和Harbor镜像管理
- 新增许可证服务器 (license-server/)
- 新增 Agent 部署模块 (flnxi-agent/)
- 添加许可证验证和激活页面
- 添加 Harbor 镜像同步脚本
- 更新素材系统支持许可证验证
2025-12-15 21:19:46 +08:00

213 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
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>
);
}