Files
Neo-ZQYY/apps/admin-web/src/pages/_archived/LogViewer.tsx
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

164 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* [ARCHIVED] 日志查看器页面。
*
* 已废弃:功能已合并到 ETLTasks 页面的"任务管理"Tab。
* 归档日期2026-03-25
* 归档原因admin-web-restructure spec需求 8LogViewer 废弃)
*
* - 输入执行 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<string[]>([]);
const [filterKeyword, setFilterKeyword] = useState("");
const [connected, setConnected] = useState(false);
/** 展示模式raw = 原始流grouped = 按任务分组 */
const [viewMode, setViewMode] = useState<"raw" | "grouped">("grouped");
const wsRef = useRef<WebSocket | null>(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 (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={{ marginBottom: 12, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Title level={4} style={{ margin: 0 }}>
<FileTextOutlined style={{ marginRight: 8 }} />
</Title>
<Space>
{connected && <Badge status="processing" text={<Text type="success"></Text>} />}
<Tag>{lines.length} </Tag>
{filterKeyword && <Tag color="blue">{filteredLines.length} </Tag>}
</Space>
</div>
{/* 操作栏 */}
<Card size="small" style={{ marginBottom: 12 }}>
<Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
<Space>
<Input
placeholder="执行 ID"
value={executionId}
onChange={(e) => setExecutionId(e.target.value)}
style={{ width: 280, fontFamily: "monospace" }}
onPressEnter={handleConnect}
allowClear
/>
{connected ? (
<Button icon={<DisconnectOutlined />} danger onClick={handleDisconnect}></Button>
) : (
<Button type="primary" icon={<LinkOutlined />} onClick={handleConnect}></Button>
)}
<Button icon={<HistoryOutlined />} onClick={handleLoadHistory}></Button>
<Button icon={<ClearOutlined />} onClick={handleClear} disabled={lines.length === 0}></Button>
</Space>
<Space>
<Segmented
value={viewMode}
onChange={(v) => setViewMode(v as "raw" | "grouped")}
options={[
{ value: "grouped", icon: <AppstoreOutlined />, label: "按任务" },
{ value: "raw", icon: <UnorderedListOutlined />, label: "原始" },
]}
size="small"
/>
{viewMode === "raw" && (
<Input
prefix={<SearchOutlined />}
placeholder="过滤关键词..."
value={filterKeyword}
onChange={(e) => setFilterKeyword(e.target.value)}
allowClear
style={{ width: 220 }}
/>
)}
</Space>
</Space>
</Card>
{/* 日志展示区域 */}
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
{viewMode === "grouped" ? (
<TaskLogViewer lines={lines} />
) : (
<LogStream executionId={executionId} lines={filteredLines} />
)}
</div>
</div>
);
};
export default LogViewer;