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

218 lines
7.3 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
interface DashboardStats {
customers: { total: number };
licenses: { total: number; byType: Record<string, number>; byStatus: Record<string, number> };
activations: { total: number; active: number };
alerts: { pending: number };
recentActivations: Array<{
id: string;
licenseKey: string;
licenseType: string;
activatedAt: string;
companyName: string | null;
}>;
}
export default function AdminDashboard() {
const router = useRouter();
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, []);
const fetchStats = async () => {
try {
const res = await fetch('/api/admin/dashboard');
if (res.status === 401) {
router.push('/admin/login');
return;
}
const data = await res.json();
setStats(data);
} catch (error) {
console.error('获取统计数据失败:', error);
} finally {
setLoading(false);
}
};
const handleLogout = async () => {
await fetch('/api/admin/auth/logout', { method: 'POST' });
router.push('/admin/login');
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-gray-500">加载中...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100">
{/* 顶部导航 */}
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-xl font-bold">许可证管理后台</h1>
<div className="flex items-center gap-4">
<Link href="/admin/customers" className="text-gray-600 hover:text-gray-900">
客户管理
</Link>
<Link href="/admin/licenses" className="text-gray-600 hover:text-gray-900">
许可证管理
</Link>
<Link href="/admin/alerts" className="text-gray-600 hover:text-gray-900">
告警中心
{stats?.alerts.pending ? (
<span className="ml-1 px-2 py-0.5 bg-red-500 text-white text-xs rounded-full">
{stats.alerts.pending}
</span>
) : null}
</Link>
<button
onClick={handleLogout}
className="text-gray-600 hover:text-gray-900"
>
退出
</button>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-8">
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-6 mb-8">
<StatCard title="客户总数" value={stats?.customers.total || 0} />
<StatCard title="许可证总数" value={stats?.licenses.total || 0} />
<StatCard title="活跃激活" value={stats?.activations.active || 0} />
<StatCard
title="待处理告警"
value={stats?.alerts.pending || 0}
highlight={!!stats?.alerts.pending}
/>
</div>
{/* 许可证分布 */}
<div className="grid grid-cols-2 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4">许可证类型分布</h3>
<div className="space-y-3">
{Object.entries(stats?.licenses.byType || {}).map(([type, count]) => (
<div key={type} className="flex justify-between items-center">
<span className="text-gray-600">{type}</span>
<span className="font-medium">{count}</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4">许可证状态分布</h3>
<div className="space-y-3">
{Object.entries(stats?.licenses.byStatus || {}).map(([status, count]) => (
<div key={status} className="flex justify-between items-center">
<span className="text-gray-600">
{status === 'active' ? '正常' : status === 'disabled' ? '已禁用' : '已过期'}
</span>
<span className="font-medium">{count}</span>
</div>
))}
</div>
</div>
</div>
{/* 最近激活 */}
<div className="bg-white rounded-lg shadow">
<div className="p-6 border-b">
<h3 className="text-lg font-medium">最近激活</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
许可证密钥
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
类型
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
客户
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
激活时间
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{stats?.recentActivations.map((activation) => (
<tr key={activation.id}>
<td className="px-6 py-4 font-mono text-sm">{activation.licenseKey}</td>
<td className="px-6 py-4">
<TypeBadge type={activation.licenseType} />
</td>
<td className="px-6 py-4 text-gray-600">
{activation.companyName || '-'}
</td>
<td className="px-6 py-4 text-gray-600">
{new Date(activation.activatedAt).toLocaleString()}
</td>
</tr>
))}
{!stats?.recentActivations.length && (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-gray-500">
暂无激活记录
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</main>
</div>
);
}
function StatCard({
title,
value,
highlight = false,
}: {
title: string;
value: number;
highlight?: boolean;
}) {
return (
<div className={`bg-white rounded-lg shadow p-6 ${highlight ? 'ring-2 ring-red-500' : ''}`}>
<div className="text-sm text-gray-500 mb-1">{title}</div>
<div className={`text-3xl font-bold ${highlight ? 'text-red-600' : 'text-gray-900'}`}>
{value}
</div>
</div>
);
}
function TypeBadge({ type }: { type: string }) {
const colors: Record<string, string> = {
FREE: 'bg-gray-100 text-gray-700',
STANDARD: 'bg-blue-100 text-blue-700',
PRO: 'bg-purple-100 text-purple-700',
};
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${colors[type] || colors.FREE}`}>
{type}
</span>
);
}