diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue index d3f54a1bf..0e15a8575 100644 --- a/dashboard/src/components/shared/ConsoleDisplayer.vue +++ b/dashboard/src/components/shared/ConsoleDisplayer.vue @@ -6,36 +6,29 @@ import { resolveApiUrl } from "@/utils/request"; @@ -55,32 +48,22 @@ export default { data() { return { autoScroll: true, - logColorAnsiMap: { - "\u001b[1;34m": "color: #39C5BB; font-weight: bold;", - "\u001b[1;36m": "color: #00FFFF; font-weight: bold;", - "\u001b[1;33m": "color: #FFFF00; font-weight: bold;", - "\u001b[31m": "color: #FF0000;", - "\u001b[1;31m": "color: #FF0000; font-weight: bold;", - "\u001b[0m": "color: inherit; font-weight: normal;", - "\u001b[32m": "color: #00FF00;", - default: "color: #FFFFFF;", - }, logLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], selectedLevels: [0, 1, 2, 3, 4], levelColors: { - DEBUG: "grey", - INFO: "blue-lighten-3", - WARNING: "amber", - ERROR: "red", - CRITICAL: "purple", + DEBUG: "#4A4A5A", + INFO: "#00F2FF", + WARNING: "#CC7000", + ERROR: "#FF4D4D", + CRITICAL: "#FF00AA", }, - localLogCache: [], - eventSource: null, - retryTimer: null, + localLogCache: [] as Array<{ time: number; data: string; level: string }>, + eventSource: null as EventSourcePolyfill | null, + retryTimer: null as ReturnType | null, retryAttempts: 0, maxRetryAttempts: 10, baseRetryDelay: 1000, - lastEventId: null, + lastEventId: null as string | null, isUnmounted: false, }; }, @@ -121,21 +104,15 @@ export default { this.eventSource = null; } - console.log(`正在连接日志流... (尝试次数: ${this.retryAttempts})`); - this.eventSource = new EventSourcePolyfill( resolveApiUrl("/api/live-log"), - { - heartbeatTimeout: 300000, - }, + { heartbeatTimeout: 300000 }, ); this.eventSource.onopen = () => { - console.log("日志流连接成功!"); this.retryAttempts = 0; - if (!this.lastEventId) { - this.fetchLogHistory(); + void this.fetchLogHistory(); } }; @@ -144,28 +121,20 @@ export default { if (event.lastEventId) { this.lastEventId = event.lastEventId; } - const payload = JSON.parse(event.data); this.processNewLogs([payload]); - } catch (e) { - console.error("解析日志失败:", e); + } catch { + // silently ignore parse errors in production } }; - this.eventSource.onerror = (err) => { - if (err.status === 401) { - console.error("鉴权失败 (401),可能是 Token 过期了。"); - } else { - console.warn("日志流连接错误:", err); - } - + this.eventSource.onerror = () => { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } if (this.retryAttempts >= this.maxRetryAttempts) { - console.error("❌ 已达到最大重试次数,停止重连。请刷新页面重试。"); return; } @@ -174,33 +143,20 @@ export default { 30000, ); - console.log( - `⏳ ${delay}ms 后尝试第 ${this.retryAttempts + 1} 次重连...`, - ); - - if (this.retryTimer) { - clearTimeout(this.retryTimer); - this.retryTimer = null; - } - this.retryTimer = setTimeout(async () => { if (this.isUnmounted) return; this.retryAttempts++; - if (!this.lastEventId) { await this.fetchLogHistory(); } - this.connectSSE(); }, delay); }; }, - processNewLogs(newLogs) { + processNewLogs(newLogs: Array<{ time: number; data: string; level: string }>) { if (!newLogs || newLogs.length === 0) return; - let hasUpdate = false; - newLogs.forEach((log) => { const exists = this.localLogCache.some( (existing) => @@ -208,24 +164,18 @@ export default { existing.data === log.data && existing.level === log.level, ); - if (!exists) { this.localLogCache.push(log); - hasUpdate = true; - if (this.isLevelSelected(log.level)) { - this.printLog(log.data); + this.printLog(log.data, log.level); } } }); - if (hasUpdate) { - this.localLogCache.sort((a, b) => a.time - b.time); - - const maxSize = this.commonStore.log_cache_max_len || 200; - if (this.localLogCache.length > maxSize) { - this.localLogCache.splice(0, this.localLogCache.length - maxSize); - } + this.localLogCache.sort((a, b) => a.time - b.time); + const maxSize = this.commonStore.log_cache_max_len || 200; + if (this.localLogCache.length > maxSize) { + this.localLogCache.splice(0, this.localLogCache.length - maxSize); } }, @@ -235,64 +185,68 @@ export default { if (res.data.data.logs && res.data.data.logs.length > 0) { this.processNewLogs(res.data.data.logs); } - } catch (err) { - console.error("Failed to fetch log history:", err); + } catch { + // silently ignore } }, - getLevelColor(level) { - return this.levelColors[level] || "grey"; - }, - - isLevelSelected(level) { - for (let i = 0; i < this.selectedLevels.length; ++i) { - const level_ = this.logLevels[this.selectedLevels[i]]; - if (level_ === level) { - return true; - } - } - return false; + isLevelSelected(level: string) { + return this.selectedLevels.some( + (i) => this.logLevels[i] === level, + ); }, refreshDisplay() { const termElement = document.getElementById("term"); if (termElement) { termElement.innerHTML = ""; - - if (this.localLogCache && this.localLogCache.length > 0) { - this.localLogCache.forEach((logItem) => { - if (this.isLevelSelected(logItem.level)) { - this.printLog(logItem.data); - } - }); - } + this.localLogCache.forEach((logItem) => { + if (this.isLevelSelected(logItem.level)) { + this.printLog(logItem.data, logItem.level); + } + }); } }, - toggleAutoScroll() { - this.autoScroll = !this.autoScroll; - }, - - printLog(log) { - const ele = document.getElementById("term"); - if (!ele) { - return; - } - - const span = document.createElement("pre"); - let style = this.logColorAnsiMap["default"]; - for (const key in this.logColorAnsiMap) { - if (log.startsWith(key)) { - style = this.logColorAnsiMap[key]; - log = log.replace(key, "").replace("\u001b[0m", ""); + parseAnsiLog(log: string): { text: string; color: string } { + // ANSI color code map + const ansiMap: Record = { + "\u001b[1;34m": "#39C5BB", + "\u001b[1;36m": "#00FFFF", + "\u001b[1;33m": "#FFD54F", + "\u001b[31m": "#FF4D4D", + "\u001b[1;31m": "#FF4D4D", + "\u001b[0m": "inherit", + "\u001b[32m": "#69F0AE", + }; + let color = "rgba(255,255,255,0.75)"; + for (const [code, c] of Object.entries(ansiMap)) { + if (log.startsWith(code)) { + color = c; + log = log.replace(code, "").replace("\u001b[0m", ""); break; } } + return { text: log, color }; + }, - span.style = style; - span.classList.add("console-log-line", "fade-in"); - span.innerText = `${log}`; - ele.appendChild(span); + printLog(log: string, level?: string) { + const ele = document.getElementById("term"); + if (!ele) return; + + const { text, color } = this.parseAnsiLog(log); + + const line = document.createElement("div"); + line.classList.add("log-line"); + + if (level) { + line.classList.add(`log-line-${level.toLowerCase()}`); + } + + line.innerText = text; + line.style.color = color; + + ele.appendChild(line); if (this.autoScroll) { ele.scrollTop = ele.scrollHeight; } @@ -302,35 +256,192 @@ export default { diff --git a/dashboard/src/views/ConsolePage.vue b/dashboard/src/views/ConsolePage.vue index 348143bc7..d4dfa1edf 100644 --- a/dashboard/src/views/ConsolePage.vue +++ b/dashboard/src/views/ConsolePage.vue @@ -8,19 +8,7 @@ const { tm } = useModuleI18n("features/console");