/** * [ARCHIVED] 日志查看器页面。 * * 已废弃:功能已合并到 ETLTasks 页面的"任务管理"Tab。 * 归档日期:2026-03-25 * 归档原因:admin-web-restructure spec,需求 8(LogViewer 废弃) * * - 输入执行 ID,通过 WebSocket 实时接收日志 * - 支持加载历史日志 * - 关键词过滤(大小写不敏感) */ import React, { useState, useRef, useCallback, useEffect } from "react"; import { Input, Button, Space, message, Card, Typography, Tag, Badge, Segmented } from "antd"; import { LinkOutlined, DisconnectOutlined, HistoryOutlined, FileTextOutlined, SearchOutlined, ClearOutlined, AppstoreOutlined, UnorderedListOutlined, } from "@ant-design/icons"; import { apiClient } from "../../api/client"; import LogStream from "../../components/LogStream"; import TaskLogViewer from "../../components/TaskLogViewer"; const { Title, Text } = Typography; /* ------------------------------------------------------------------ */ /* 纯函数:日志过滤 */ /* ------------------------------------------------------------------ */ export function filterLogLines(lines: string[], keyword: string): string[] { if (!keyword.trim()) return lines; const lower = keyword.toLowerCase(); return lines.filter((line) => line.toLowerCase().includes(lower)); } /* ------------------------------------------------------------------ */ /* 页面组件 */ /* ------------------------------------------------------------------ */ const LogViewer: React.FC = () => { const [executionId, setExecutionId] = useState(""); const [lines, setLines] = useState([]); const [filterKeyword, setFilterKeyword] = useState(""); const [connected, setConnected] = useState(false); /** 展示模式:raw = 原始流,grouped = 按任务分组 */ const [viewMode, setViewMode] = useState<"raw" | "grouped">("grouped"); const wsRef = useRef(null); useEffect(() => { return () => { wsRef.current?.close(); }; }, []); const handleConnect = useCallback(() => { const id = executionId.trim(); if (!id) { message.warning("请输入执行 ID"); return; } wsRef.current?.close(); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; const ws = new WebSocket(`${protocol}//${host}/ws/logs/${id}`); wsRef.current = ws; ws.onopen = () => { setConnected(true); message.success("WebSocket 已连接"); }; ws.onmessage = (event) => { setLines((prev) => [...prev, event.data]); }; ws.onclose = () => { setConnected(false); }; ws.onerror = () => { message.error("WebSocket 连接失败"); setConnected(false); }; }, [executionId]); const handleDisconnect = useCallback(() => { wsRef.current?.close(); wsRef.current = null; setConnected(false); }, []); const handleLoadHistory = useCallback(async () => { const id = executionId.trim(); if (!id) { message.warning("请输入执行 ID"); return; } try { const { data } = await apiClient.get<{ execution_id: string; output_log: string | null; error_log: string | null }>( `/execution/${id}/logs` ); const parts: string[] = []; if (data.output_log) parts.push(data.output_log); if (data.error_log) parts.push(data.error_log); const historyLines = parts.join("\n").split("\n"); setLines(historyLines); message.success("历史日志加载完成"); } catch { message.error("加载历史日志失败"); } }, [executionId]); const handleClear = useCallback(() => { setLines([]); }, []); const filteredLines = filterLogLines(lines, filterKeyword); return (
<FileTextOutlined style={{ marginRight: 8 }} /> 日志查看器 {connected && 已连接} />} {lines.length} 行 {filterKeyword && {filteredLines.length} 条匹配}
{/* 操作栏 */} setExecutionId(e.target.value)} style={{ width: 280, fontFamily: "monospace" }} onPressEnter={handleConnect} allowClear /> {connected ? ( ) : ( )} setViewMode(v as "raw" | "grouped")} options={[ { value: "grouped", icon: , label: "按任务" }, { value: "raw", icon: , label: "原始" }, ]} size="small" /> {viewMode === "raw" && ( } placeholder="过滤关键词..." value={filterKeyword} onChange={(e) => setFilterKeyword(e.target.value)} allowClear style={{ width: 220 }} /> )} {/* 日志展示区域 */}
{viewMode === "grouped" ? ( ) : ( )}
); }; export default LogViewer;