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; title?: string; emptyText?: ReactNode; height?: number | string; mode?: 'inline' | 'card'; collapsible?: boolean; defaultVisible?: boolean; spinWhenEmpty?: boolean; style?: CSSProperties; bodyStyle?: CSSProperties; } export default function RealtimeLogPanel({ logs, connected = false, title = '实时日志', emptyText = '暂无日志', height = '20vh', mode = 'inline', collapsible = false, defaultVisible = true, spinWhenEmpty = false, style, bodyStyle, }: 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' }); } }, [filteredLogs, visible]); const logColors: Record = { error: token.colorError, success: token.colorSuccess, warning: token.colorWarning, info: token.colorText, }; const logBody = (
{filteredLogs.length === 0 ? ( spinWhenEmpty && connected ? ( ) : ( {emptyText} ) ) : ( filteredLogs.map((log, index) => (
{log.message}
)) )}
); if (mode === 'card') { return ( ({ 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 } }} > {logBody} ); } return (
collapsible && setVisible((value) => !value)} > {title}