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

204 lines
7.0 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
interface Alert {
id: string;
alertType: string;
status: string;
details: Record<string, unknown> | null;
createdAt: string;
resolvedAt: string | null;
license: {
licenseKey: string;
customer: { companyName: string } | null;
};
}
export default function AlertsPage() {
const router = useRouter();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'pending' | 'resolved'>('pending');
useEffect(() => {
fetchAlerts();
}, [filter]);
const fetchAlerts = async () => {
try {
const res = await fetch(`/api/admin/alerts?status=${filter}`);
if (res.status === 401) {
router.push('/admin/login');
return;
}
const data = await res.json();
setAlerts(data.alerts || []);
} catch (error) {
console.error('获取告警列表失败:', error);
} finally {
setLoading(false);
}
};
const handleResolve = async (id: string) => {
try {
const res = await fetch(`/api/admin/alerts/${id}/resolve`, { method: 'POST' });
if (res.ok) {
fetchAlerts();
}
} catch (error) {
console.error('处理告警失败:', error);
}
};
const handleIgnore = async (id: string) => {
try {
const res = await fetch(`/api/admin/alerts/${id}/ignore`, { method: 'POST' });
if (res.ok) {
fetchAlerts();
}
} catch (error) {
console.error('忽略告警失败:', error);
}
};
const getAlertTypeLabel = (type: string) => {
const labels: Record<string, string> = {
multi_device: '多设备激活',
fingerprint_mismatch: '指纹不匹配',
suspicious_activity: '可疑活动',
};
return labels[type] || type;
};
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">
<Link href="/admin" className="text-gray-600 hover:text-gray-900">
← 返回
</Link>
<h1 className="text-xl font-bold">告警中心</h1>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow">
<div className="p-6 border-b">
<div className="flex gap-2">
<button
onClick={() => setFilter('pending')}
className={`px-4 py-2 rounded-lg ${
filter === 'pending'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
待处理
</button>
<button
onClick={() => setFilter('resolved')}
className={`px-4 py-2 rounded-lg ${
filter === 'resolved'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
已处理
</button>
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded-lg ${
filter === 'all'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
全部
</button>
</div>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">加载中...</div>
) : (
<div className="divide-y">
{alerts.map((alert) => (
<div key={alert.id} className="p-6">
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2 mb-2">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
alert.status === 'pending'
? 'bg-red-100 text-red-700'
: alert.status === 'resolved'
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{alert.status === 'pending'
? '待处理'
: alert.status === 'resolved'
? '已处理'
: '已忽略'}
</span>
<span className="font-medium">{getAlertTypeLabel(alert.alertType)}</span>
</div>
<p className="text-sm text-gray-600 mb-1">
许可证: <span className="font-mono">{alert.license.licenseKey}</span>
</p>
{alert.license.customer && (
<p className="text-sm text-gray-600 mb-1">
客户: {alert.license.customer.companyName}
</p>
)}
<p className="text-sm text-gray-500">
时间: {new Date(alert.createdAt).toLocaleString()}
</p>
{alert.details && (
<details className="mt-2">
<summary className="text-sm text-blue-600 cursor-pointer">
查看详情
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto">
{JSON.stringify(alert.details, null, 2)}
</pre>
</details>
)}
</div>
{alert.status === 'pending' && (
<div className="flex gap-2">
<button
onClick={() => handleResolve(alert.id)}
className="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700"
>
处理
</button>
<button
onClick={() => handleIgnore(alert.id)}
className="px-3 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300"
>
忽略
</button>
</div>
)}
</div>
</div>
))}
{!alerts.length && (
<div className="p-8 text-center text-gray-500">暂无告警</div>
)}
</div>
)}
</div>
</main>
</div>
);
}