From 05580bd025da9256252542f57f20cf565fb3455f Mon Sep 17 00:00:00 2001 From: kyx236 Date: Sat, 31 Jan 2026 03:34:55 +0800 Subject: [PATCH] feat: Add real-time log streaming with a new backend SSE endpoint and a corresponding frontend component. --- backend/cmd/main.go | 2 +- frontend/src/components/upload/LogStream.tsx | 42 ++++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index e0e5baf..71bd805 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -273,7 +273,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { } func handleGetLogs(w http.ResponseWriter, r *http.Request) { - logs := logger.GetLogs(100) + logs := logger.GetLogs(200) api.Success(w, logs) } diff --git a/frontend/src/components/upload/LogStream.tsx b/frontend/src/components/upload/LogStream.tsx index 795aee1..d108446 100644 --- a/frontend/src/components/upload/LogStream.tsx +++ b/frontend/src/components/upload/LogStream.tsx @@ -1,13 +1,15 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import { Terminal, Trash2, Play, Pause } from 'lucide-react' import { Card, Button } from '../common' interface LogEntry { + type?: string timestamp: string level: string message: string email?: string step?: string + module?: string } const levelColors: Record = { @@ -44,9 +46,33 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr const [paused, setPaused] = useState(false) const logContainerRef = useRef(null) const eventSourceRef = useRef(null) + const isPausedRef = useRef(paused) + isPausedRef.current = paused + // 加载历史日志(最新200条) + const loadHistoryLogs = useCallback(async () => { + try { + const res = await fetch('/api/logs') + const data = await res.json() + if (data.code === 0 && Array.isArray(data.data)) { + const historyLogs: LogEntry[] = data.data.map((log: LogEntry) => ({ + type: 'log', + level: log.level || 'info', + message: log.message || '', + timestamp: log.timestamp || '', + email: log.email, + module: log.module || 'system', + })) + setLogs(historyLogs) + } + } catch (e) { + console.error('Failed to load history logs:', e) + } + }, []) + + // 组件挂载时先加载历史日志,再连接 SSE useEffect(() => { - if (paused) return + loadHistoryLogs() const eventSource = new EventSource('/api/logs/stream') eventSourceRef.current = eventSource @@ -58,7 +84,11 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr eventSource.onmessage = (event) => { try { const log = JSON.parse(event.data) as LogEntry - setLogs((prev) => [...prev.slice(-499), log]) // 增加缓存行数 + if (log.type === 'connected') return + + if (!isPausedRef.current) { + setLogs((prev) => [...prev.slice(-499), log]) + } } catch (e) { console.error('Failed to parse log:', e) } @@ -66,13 +96,12 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr eventSource.onerror = () => { setConnected(false) - eventSource.close() } return () => { eventSource.close() } - }, [paused]) + }, [loadHistoryLogs]) useEffect(() => { if (logContainerRef.current) { @@ -90,9 +119,6 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr } const togglePause = () => { - if (!paused && eventSourceRef.current) { - eventSourceRef.current.close() - } setPaused(!paused) }