feat(web): CopilotAlert 告警组件 + 告警 API 扩展

- CopilotAlert: 分级告警列表,30秒轮询刷新,危急 banner
- copilot.ts 新增 listAlerts 函数
This commit is contained in:
iven
2026-05-12 22:36:36 +08:00
parent a48ad6ed33
commit 6d97328ff6
3 changed files with 111 additions and 0 deletions

View File

@@ -54,6 +54,10 @@ export function dismissInsight(id: string) {
return client.post(`/copilot/insights/${id}/dismiss`);
}
export function listAlerts(params?: { severity?: string; page?: number; page_size?: number }) {
return listInsights({ insight_type: 'anomaly', ...params });
}
export function listRules(params?: { page?: number; page_size?: number }) {
return client.get<PaginatedResponse<Record<string, unknown>>>('/copilot/rules', { params });
}

View File

@@ -0,0 +1,106 @@
import { useState, useEffect, useCallback } from 'react';
import { Alert, Badge, List, Button, Space, Typography, Spin } from 'antd';
import { CheckOutlined } from '@ant-design/icons';
import { listAlerts, dismissInsight } from '../../api/copilot';
import type { CopilotInsight } from '../../api/copilot';
const severityConfig: Record<string, { type: 'success' | 'info' | 'warning' | 'error'; label: string }> = {
critical: { type: 'error', label: '危急' },
warning: { type: 'warning', label: '警告' },
info: { type: 'info', label: '提示' },
};
export function CopilotAlert() {
const [alerts, setAlerts] = useState<CopilotInsight[]>([]);
const [loading, setLoading] = useState(false);
const [dismissing, setDismissing] = useState<string | null>(null);
const fetchAlerts = useCallback(async () => {
setLoading(true);
try {
const res = await listAlerts({ page_size: 50 });
const result = res.data as unknown as { items: CopilotInsight[]; total: number };
setAlerts(result.items ?? []);
} catch {
// 静默
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAlerts();
const timer = setInterval(fetchAlerts, 30_000);
return () => clearInterval(timer);
}, [fetchAlerts]);
const handleDismiss = async (id: string) => {
setDismissing(id);
try {
await dismissInsight(id);
setAlerts((prev) => prev.filter((a) => a.id !== id));
} finally {
setDismissing(null);
}
};
if (!alerts.length && !loading) return null;
const criticalCount = alerts.filter((a) => a.severity === 'critical').length;
return (
<div>
{criticalCount > 0 && (
<Alert
type="error"
showIcon
message={`${criticalCount} 条危急告警`}
banner
style={{ marginBottom: 16 }}
/>
)}
{loading && alerts.length === 0 ? (
<Spin />
) : (
<List
size="small"
dataSource={alerts}
renderItem={(item) => {
const config = severityConfig[item.severity] ?? severityConfig.info;
return (
<List.Item
actions={[
<Button
key="dismiss"
size="small"
icon={<CheckOutlined />}
loading={dismissing === item.id}
onClick={() => handleDismiss(item.id)}
>
</Button>,
]}
>
<List.Item.Meta
title={
<Space>
<Badge status={config.type} />
<Typography.Text>{item.title}</Typography.Text>
</Space>
}
description={
item.content?.suggestion ? (
<Typography.Text type="secondary">
{item.content.suggestion as string}
</Typography.Text>
) : undefined
}
/>
</List.Item>
);
}}
/>
)}
</div>
);
}

View File

@@ -1,4 +1,5 @@
export { CopilotBadge } from './CopilotBadge';
export { CopilotCard } from './CopilotCard';
export { CopilotAlert } from './CopilotAlert';
export { useCopilotRisk } from './useCopilotRisk';
export { useCopilotInsights } from './useCopilotInsights';