feat: 实时日志面板增加日志等级选择器

- 4级过滤:错误/警告+/成功+/全部
- inline模式和card模式均支持
- 点击Select时不触发折叠事件(stopPropagation)
- 条数统计仍显示全部日志数(不过滤)
This commit is contained in:
yml2213
2026-06-23 09:45:59 +08:00
parent 36d55ed685
commit 65683350ea
@@ -1,8 +1,25 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'; import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { Card, Spin, Tag, theme } from 'antd'; import { Card, Select, Spin, Tag, theme } from 'antd';
import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { DownOutlined, UpOutlined } from '@ant-design/icons';
import type { RealtimeLog } from '../hooks/useWebSocketLogs'; 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<string, number> = {
error: 0,
warning: 1,
success: 2,
info: 3,
debug: 4,
};
interface RealtimeLogPanelProps { interface RealtimeLogPanelProps {
logs: RealtimeLog[]; logs: RealtimeLog[];
connected?: boolean; connected?: boolean;
@@ -32,13 +49,20 @@ export default function RealtimeLogPanel({
}: RealtimeLogPanelProps) { }: RealtimeLogPanelProps) {
const { token } = theme.useToken(); const { token } = theme.useToken();
const [visible, setVisible] = useState(defaultVisible); const [visible, setVisible] = useState(defaultVisible);
const [levelFilter, setLevelFilter] = useState<number>(3); // 默认:全部
const endRef = useRef<HTMLDivElement | null>(null); const endRef = useRef<HTMLDivElement | null>(null);
// 按等级过滤日志
const filteredLogs = useMemo(() => {
if (levelFilter >= 3) return logs;
return logs.filter((log) => (LEVEL_WEIGHT[log.level] ?? 3) <= levelFilter);
}, [logs, levelFilter]);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
endRef.current?.scrollIntoView({ behavior: 'smooth' }); endRef.current?.scrollIntoView({ behavior: 'smooth' });
} }
}, [logs, visible]); }, [filteredLogs, visible]);
const logColors: Record<string, string> = { const logColors: Record<string, string> = {
error: token.colorError, error: token.colorError,
@@ -60,14 +84,14 @@ export default function RealtimeLogPanel({
...bodyStyle, ...bodyStyle,
}} }}
> >
{logs.length === 0 ? ( {filteredLogs.length === 0 ? (
spinWhenEmpty && connected ? ( spinWhenEmpty && connected ? (
<Spin spinning size="small" /> <Spin spinning size="small" />
) : ( ) : (
<span style={{ color: token.colorTextTertiary }}>{emptyText}</span> <span style={{ color: token.colorTextTertiary }}>{emptyText}</span>
) )
) : ( ) : (
logs.map((log, index) => ( filteredLogs.map((log, index) => (
<div <div
key={`${index}-${log.message}`} key={`${index}-${log.message}`}
style={{ color: logColors[log.level] || token.colorText, lineHeight: '20px' }} style={{ color: logColors[log.level] || token.colorText, lineHeight: '20px' }}
@@ -85,6 +109,16 @@ export default function RealtimeLogPanel({
<Card <Card
title={title} title={title}
size="small" size="small"
extra={
<Select
size="small"
value={levelFilter}
onChange={setLevelFilter}
options={LOG_LEVELS.map((l) => ({ value: l.value, label: l.label }))}
style={{ width: 80 }}
popupMatchSelectWidth={false}
/>
}
style={{ flex: 1, overflow: 'hidden', ...style }} style={{ flex: 1, overflow: 'hidden', ...style }}
styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }} styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }}
> >
@@ -100,6 +134,15 @@ export default function RealtimeLogPanel({
onClick={() => collapsible && setVisible((value) => !value)} onClick={() => collapsible && setVisible((value) => !value)}
> >
<span style={{ fontWeight: 500, fontSize: 13 }}>{title}</span> <span style={{ fontWeight: 500, fontSize: 13 }}>{title}</span>
<Select
size="small"
value={levelFilter}
onChange={setLevelFilter}
options={LOG_LEVELS.map((l) => ({ value: l.value, label: l.label }))}
style={{ marginLeft: 8, width: 80 }}
popupMatchSelectWidth={false}
onClick={(e) => e.stopPropagation()}
/>
{collapsible && ( {collapsible && (
visible ? <UpOutlined style={{ marginLeft: 6, fontSize: 10 }} /> : <DownOutlined style={{ marginLeft: 6, fontSize: 10 }} /> visible ? <UpOutlined style={{ marginLeft: 6, fontSize: 10 }} /> : <DownOutlined style={{ marginLeft: 6, fontSize: 10 }} />
)} )}