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>
This commit is contained in:
163
apps/admin-web/src/pages/_archived/LogViewer.tsx
Normal file
163
apps/admin-web/src/pages/_archived/LogViewer.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* [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<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;
|
||||
Reference in New Issue
Block a user