diff --git a/frontend/src/pages/Alerts.tsx b/frontend/src/pages/Alerts.tsx new file mode 100644 index 0000000..afd7468 --- /dev/null +++ b/frontend/src/pages/Alerts.tsx @@ -0,0 +1,107 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + Card, Table, Button, Tag, Space, message, Badge, Empty, Typography, +} from 'antd'; +import { + BellOutlined, CheckOutlined, ReloadOutlined, +} from '@ant-design/icons'; +import { alertApi } from '@/api'; +import type { Alert } from '@/types'; +import dayjs from 'dayjs'; + +const { Text } = Typography; + +const severityConfig: Record = { + critical: { label: '严重', color: 'red', badge: 'error' }, + warning: { label: '警告', color: 'orange', badge: 'warning' }, + info: { label: '通知', color: 'blue', badge: 'processing' }, +}; + +const alertTypeConfig: Record = { + budget_warning: '预算警告', + budget_exceeded: '预算超限', + task_failed: '任务失败', + worker_offline: 'Worker离线', +}; + +export default function Alerts() { + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(false); + const [filter, setFilter] = useState('all'); // all, unread, read + + const loadData = useCallback(() => { + setLoading(true); + const isRead = filter === 'unread' ? false : filter === 'read' ? true : undefined; + alertApi.list(isRead).then(setAlerts).catch(() => {}).finally(() => setLoading(false)); + }, [filter]); + + useEffect(() => { loadData(); }, [loadData]); + + const handleMarkRead = async (id: number) => { + try { + await alertApi.markRead(id); + message.success('已标记为已读'); + loadData(); + } catch { message.error('操作失败'); } + }; + + const columns = [ + { + title: '级别', dataIndex: 'severity', key: 'severity', width: 80, + render: (s: string) => { + const cfg = severityConfig[s] || { label: s, color: 'default', badge: 'default' }; + return ; + }, + }, + { + title: '类型', dataIndex: 'alert_type', key: 'alert_type', width: 120, + render: (t: string, r: Alert) => {alertTypeConfig[t] || t}, + }, + { + title: '消息', dataIndex: 'message', key: 'message', + render: (m: string) => {m}, + }, + { + title: '时间', dataIndex: 'created_at', key: 'created_at', width: 160, + render: (t: string) => {dayjs(t).format('MM-DD HH:mm:ss')}, + }, + { + title: '状态', dataIndex: 'is_read', key: 'is_read', width: 80, + render: (r: boolean) => r ? 已读 : 未读, + }, + { + title: '操作', key: 'action', width: 80, + render: (_: any, r: Alert) => r.is_read ? null : ( + + ), + }, + ]; + + const filterButtons = [ + { key: 'all', label: '全部' }, + { key: 'unread', label: '未读' }, + { key: 'read', label: '已读' }, + ]; + + return ( +
+ 告警中心} + extra={ + + {filterButtons.map(f => ( + + ))} +