- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
204 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|