diff --git a/web/frontend/src/components/RealtimeLogPanel.tsx b/web/frontend/src/components/RealtimeLogPanel.tsx index 9e17c37..89a5363 100644 --- a/web/frontend/src/components/RealtimeLogPanel.tsx +++ b/web/frontend/src/components/RealtimeLogPanel.tsx @@ -1,8 +1,25 @@ -import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'; -import { Card, Spin, Tag, theme } from 'antd'; +import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react'; +import { Card, Select, Spin, Tag, theme } from 'antd'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; import type { RealtimeLog } from '../hooks/useWebSocketLogs'; +/** 日志等级定义:value 越小越严格(只显示该等级及以上) */ +const LOG_LEVELS = [ + { value: 0, label: '错误', key: 'error' }, + { value: 1, label: '警告+', key: 'warning' }, + { value: 2, label: '成功+', key: 'success' }, + { value: 3, label: '全部', key: 'info' }, +] as const; + +/** 等级权重映射 */ +const LEVEL_WEIGHT: Record = { + error: 0, + warning: 1, + success: 2, + info: 3, + debug: 4, +}; + interface RealtimeLogPanelProps { logs: RealtimeLog[]; connected?: boolean; @@ -32,13 +49,20 @@ export default function RealtimeLogPanel({ }: RealtimeLogPanelProps) { const { token } = theme.useToken(); const [visible, setVisible] = useState(defaultVisible); + const [levelFilter, setLevelFilter] = useState(3); // 默认:全部 const endRef = useRef(null); + // 按等级过滤日志 + const filteredLogs = useMemo(() => { + if (levelFilter >= 3) return logs; + return logs.filter((log) => (LEVEL_WEIGHT[log.level] ?? 3) <= levelFilter); + }, [logs, levelFilter]); + useEffect(() => { if (visible) { endRef.current?.scrollIntoView({ behavior: 'smooth' }); } - }, [logs, visible]); + }, [filteredLogs, visible]); const logColors: Record = { error: token.colorError, @@ -60,14 +84,14 @@ export default function RealtimeLogPanel({ ...bodyStyle, }} > - {logs.length === 0 ? ( + {filteredLogs.length === 0 ? ( spinWhenEmpty && connected ? ( ) : ( {emptyText} ) ) : ( - logs.map((log, index) => ( + filteredLogs.map((log, index) => (
({ value: l.value, label: l.label }))} + style={{ width: 80 }} + popupMatchSelectWidth={false} + /> + } style={{ flex: 1, overflow: 'hidden', ...style }} styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }} > @@ -100,6 +134,15 @@ export default function RealtimeLogPanel({ onClick={() => collapsible && setVisible((value) => !value)} > {title} +