2026-05-25 14:31:58 +08:00

298 lines
10 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link';
interface DashboardStats {
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;
customerName: string | null;
activatedAt: string;
}>;
}
interface ProjectInfo {
code: string;
name: string;
}
interface AdminInfo {
id: string;
username: string;
isSuperAdmin: boolean;
}
export default function ProjectDashboard() {
const router = useRouter();
const params = useParams();
const projectCode = params.projectCode as string;
const [stats, setStats] = useState<DashboardStats | null>(null);
const [project, setProject] = useState<ProjectInfo | null>(null);
const [admin, setAdmin] = useState<AdminInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchData();
}, [projectCode]);
const fetchData = async () => {
try {
// 获取项目信息
const projectRes = await fetch(`/api/admin/projects/${projectCode}`);
if (projectRes.status === 401) {
router.push('/admin/login');
return;
}
if (projectRes.status === 404) {
setError('项目不存在或无权访问');
setLoading(false);
return;
}
const projectData = await projectRes.json();
setProject(projectData);
const adminRes = await fetch('/api/admin/me');
if (adminRes.ok) {
const adminData = await adminRes.json();
setAdmin(adminData);
}
// 获取统计数据
const statsRes = await fetch(`/api/admin/${projectCode}/dashboard`);
if (statsRes.ok) {
const statsData = await statsRes.json();
setStats(statsData);
}
} catch (err) {
console.error('获取数据失败:', err);
setError('加载失败');
} 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>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<p className="text-red-500 mb-4">{error}</p>
<button
onClick={() => router.push('/admin/select-project')}
className="text-blue-600 hover:underline"
>
返回项目选择
</button>
</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">
<div className="flex items-center gap-4">
<button
onClick={() => router.push('/admin/select-project')}
className="text-gray-400 hover:text-gray-600"
title="切换项目"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<h1 className="text-xl font-bold">
{project?.name || projectCode}
<span className="text-sm font-normal text-gray-500 ml-2">许可证管理</span>
</h1>
</div>
<div className="flex items-center gap-4">
<Link href={`/admin/${projectCode}/licenses`} className="text-gray-600 hover:text-gray-900">
许可证管理
</Link>
<Link href={`/admin/${projectCode}/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>
<Link href={`/admin/${projectCode}/versions`} className="text-gray-600 hover:text-gray-900">
版本管理
</Link>
<Link href={`/admin/${projectCode}/customers`} className="text-gray-600 hover:text-gray-900">
客户管理
</Link>
{admin?.isSuperAdmin ? (
<Link href={`/admin/${projectCode}/api-keys`} className="text-gray-600 hover:text-gray-900">
API Key
</Link>
) : null}
<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-3 gap-6 mb-8">
<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 === 'FREE' ? '免费版' : type === 'STANDARD' ? '标准版' : '专业版'}
</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.customerName || '-'}
</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',
};
const labels: Record<string, string> = {
FREE: '免费版',
STANDARD: '标准版',
PRO: '专业版',
};
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${colors[type] || colors.FREE}`}>
{labels[type] || type}
</span>
);
}