From 9cb4eeab968d42cfb69a382ac38c353a0d6b4970 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 10:32:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/RunLogView.tsx | 134 +++++++++ src/renderer/src/pages/RunsPage.tsx | 12 +- src/renderer/src/panels/RunDrawer.tsx | 56 ++-- src/renderer/src/styles/app.css | 321 ++++++++++++++++++++- 4 files changed, 488 insertions(+), 35 deletions(-) create mode 100644 src/renderer/src/components/RunLogView.tsx diff --git a/src/renderer/src/components/RunLogView.tsx b/src/renderer/src/components/RunLogView.tsx new file mode 100644 index 0000000..d48f66c --- /dev/null +++ b/src/renderer/src/components/RunLogView.tsx @@ -0,0 +1,134 @@ +import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { ArrowDown, Check, Copy, Download } from "lucide-react"; +import type { RunLog } from "../store/runStore"; + +type LogFilter = "all" | "info" | "warn" | "error"; + +const LEVEL_LABEL: Record = { + all: "全部", + info: "信息", + warn: "警告", + error: "错误" +}; + +interface RunLogViewProps { + logs: RunLog[]; + nameOf: (nodeId?: string) => string; + fileName?: string; + emptyText?: string; + className?: string; +} + +export function RunLogView({ logs, nameOf, fileName = "rpa-run-logs.log", emptyText = "运行日志会显示在这里…", className }: RunLogViewProps) { + const [filter, setFilter] = useState("all"); + const [copied, setCopied] = useState(false); + const [pinned, setPinned] = useState(true); + const consoleRef = useRef(null); + const pinnedRef = useRef(true); + + const counts = useMemo(() => ({ + all: logs.length, + info: logs.filter((log) => log.level === "info").length, + warn: logs.filter((log) => log.level === "warn").length, + error: logs.filter((log) => log.level === "error").length + }), [logs]); + + const visibleLogs = useMemo(() => ( + filter === "all" ? logs : logs.filter((log) => log.level === filter) + ), [filter, logs]); + + const logText = useMemo(() => ( + logs.map((log) => `[${log.time}] [${log.level.toUpperCase()}] ${nameOf(log.nodeId)} - ${log.message}`).join("\n") + ), [logs, nameOf]); + + useLayoutEffect(() => { + const element = consoleRef.current; + if (element && pinnedRef.current) element.scrollTop = element.scrollHeight; + }, [visibleLogs]); + + const trackPin = () => { + const element = consoleRef.current; + if (!element) return; + const nextPinned = element.scrollHeight - element.scrollTop - element.clientHeight < 48; + if (nextPinned !== pinnedRef.current) { + pinnedRef.current = nextPinned; + setPinned(nextPinned); + } + }; + + const scrollToLatest = () => { + pinnedRef.current = true; + const element = consoleRef.current; + if (element) element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }); + }; + + const copyLogs = async () => { + try { + await navigator.clipboard.writeText(logText); + setCopied(true); + window.setTimeout(() => setCopied(false), 1600); + } catch { + // Clipboard access can be blocked by system policy; download remains available. + } + }; + + const downloadLogs = () => { + const blob = new Blob([logText], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + link.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+
+ {(Object.keys(LEVEL_LABEL) as LogFilter[]).map((level) => ( + + ))} +
+ + + +
+ +
+
+ {!visibleLogs.length &&
{logs.length ? "当前级别暂无日志。" : emptyText}
} + {visibleLogs.map((log) => ( +
+ {log.time} + {LEVEL_LABEL[log.level]} + {log.nodeId ? nameOf(log.nodeId) : "流程"} + {log.message || "—"} +
+ ))} +
+ {!pinned && visibleLogs.length > 0 && ( + + )} +
+
+ ); +} diff --git a/src/renderer/src/pages/RunsPage.tsx b/src/renderer/src/pages/RunsPage.tsx index fbb9054..8cfdf6d 100644 --- a/src/renderer/src/pages/RunsPage.tsx +++ b/src/renderer/src/pages/RunsPage.tsx @@ -1,7 +1,8 @@ import { useMemo } from "react"; -import { CircleAlert, CircleCheck, History, Loader2, Play, RotateCcw, Square, Terminal, Timer } from "lucide-react"; +import { CircleAlert, CircleCheck, History, Loader2, Play, RotateCcw, Square, Terminal } from "lucide-react"; import { useEditorStore } from "../store/editorStore"; import { useRunStore } from "../store/runStore"; +import { RunLogView } from "../components/RunLogView"; const STATUS_LABEL: Record = { running: "运行中", succeeded: "成功", failed: "失败", skipped: "已跳过", cancelled: "已取消", idle: "待运行" @@ -26,7 +27,7 @@ export function RunsPage({ onBack }: { onBack: () => void }) { const component = catalog?.components.find((item) => item.id === node.componentId); map.set(node.id, node.label || component?.name || node.componentId); } - return (id: string) => map.get(id) || id; + return (id?: string) => (id ? map.get(id) || id : "流程"); }, [catalog, flow]); const entries = Object.entries(nodes); @@ -52,6 +53,7 @@ export function RunsPage({ onBack }: { onBack: () => void }) { +
节点进度{STATUS_LABEL[effectiveStatus]}
@@ -76,11 +78,9 @@ export function RunsPage({ onBack }: { onBack: () => void }) {
运行日志{logs.length}
-
- {!logs.length &&
暂无日志节点执行、重试和异常信息会实时显示。
} - {logs.map((log) =>
{log.time}{log.nodeId ? nameOf(log.nodeId) : "流程"}

{log.message}

)} -
+
+
); } diff --git a/src/renderer/src/panels/RunDrawer.tsx b/src/renderer/src/panels/RunDrawer.tsx index 893cb42..55de415 100644 --- a/src/renderer/src/panels/RunDrawer.tsx +++ b/src/renderer/src/panels/RunDrawer.tsx @@ -1,7 +1,8 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { CircleAlert, CircleCheck, Loader2, Square, Timer, X } from "lucide-react"; import { useEditorStore } from "../store/editorStore"; import { useRunStore } from "../store/runStore"; +import { RunLogView } from "../components/RunLogView"; const STATUS_LABEL: Record = { running: "运行中", @@ -13,6 +14,7 @@ const STATUS_LABEL: Record = { }; export function RunDrawer() { + const [view, setView] = useState<"nodes" | "logs">("nodes"); const running = useRunStore((state) => state.running); const stopping = useRunStore((state) => state.stopping); const nodes = useRunStore((state) => state.nodes); @@ -55,6 +57,24 @@ export function RunDrawer() { 已完成 {finished.length}/{Object.keys(nodes).length} +
+ + +
{running ? (