From 66240bbf233ad851dee86088d0fe516baaa9acd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=80=E4=B8=81?= <1769123563@qq.com> Date: Wed, 19 Nov 2025 15:30:51 +0800 Subject: [PATCH] Update Log Display Logic --- templates/index.html | 542 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 428 insertions(+), 114 deletions(-) diff --git a/templates/index.html b/templates/index.html index 227f758..46706ce 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1255,6 +1255,16 @@ if (isPageVisible) { console.log('页面可见,恢复定时器'); startAllTimers(); + // 【FIX Bug #7】页面重新可见时,立即刷新数据以补齐丢失的日志 + setTimeout(() => { + refreshConsoleOutput(); + if (currentApp === 'forum') { + refreshForumLog(); + } + if (currentApp === 'report') { + refreshReportLog(); + } + }, 100); } else { console.log('页面隐藏,暂停定时器以节省资源'); pauseAllTimers(); @@ -1349,7 +1359,7 @@ }); } - // 轻量日志虚拟渲染器:可视窗口渲染 + 节流 + 包级别截断,降低内存占用 + // 高性能日志虚拟渲染器:双缓冲 + 分帧渲染 + 智能批处理 class LogVirtualList { constructor(container) { this.container = container; @@ -1359,23 +1369,37 @@ this.pool = []; this.lineHeight = 18; this.maxVisible = 120; - this.maxLines = 500; // 减少到500行,降低75%内存占用 - this.trimTarget = 300; // 裁剪后保留300行 + this.maxLines = 1000; // 【优化】增加到1000行,提高缓存能力 + this.trimTarget = 600; // 【优化】裁剪后保留600行 this.maxPoolSize = 200; // 限制DOM节点池大小 this.rafId = null; this.autoScrollEnabled = true; - this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒) + this.resumeDelay = 3000; this.resumeTimer = null; - this.flushTimer = null; // 批处理定时器 - this.lastRenderHash = null; // 用于检测内容是否真正变化 - this.scrollLocked = false; // 防止滚动冲突的锁 - this.needsScroll = false; // 标记是否需要滚动 - this.lastScrollTime = 0; // 上次滚动时间,用于节流 - this.scrollThrottle = 100; // 滚动节流时间(毫秒) - this.scrollHandler = null; // 存储滚动处理器引用 - // 预创建占位符,避免每次渲染都创建 + this.flushTimer = null; + this.lastRenderHash = null; + this.scrollLocked = false; + this.needsScroll = false; + this.scrollHandler = null; this.beforeSpacer = null; this.afterSpacer = null; + + // 【新增】批处理优化参数 + this.batchThreshold = 200; // 累积200行才flush(原50行) + this.batchDelay = 500; // 延迟500ms才flush(原200ms) + this.lastFlushTime = 0; + this.flushCount = 0; + + // 【新增】分帧渲染参数 + this.renderBatchSize = 300; // 每帧最多渲染300行 + this.renderQueue = []; // 待渲染队列 + this.isRendering = false; // 是否正在分帧渲染 + + // 【新增】性能监控 + this.pendingHighWaterMark = 0; // pending队列最大值,用于调试 + this.renderTime = 0; // 渲染耗时 + this.lastRenderLineCount = 0; // 上次渲染的行数 + this.attachScroll(); } @@ -1395,8 +1419,50 @@ this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true }); } + // 【新增】性能统计方法 + getPerformanceStats() { + return { + totalLines: this.lines.length, + pendingLines: this.pending.length, + pendingHighWaterMark: this.pendingHighWaterMark, + flushCount: this.flushCount, + lastRenderTime: this.renderTime.toFixed(2) + 'ms', + lastRenderLineCount: this.lastRenderLineCount, + poolSize: this.pool.length, + memoryEstimate: this.estimateMemoryUsage() + }; + } + + // 【新增】估算内存使用 + estimateMemoryUsage() { + // 粗略估算:每行平均100字节(文本+对象开销) + const linesMemory = this.lines.length * 100; + const pendingMemory = this.pending.length * 100; + const poolMemory = this.pool.length * 500; // DOM节点更大 + const totalBytes = linesMemory + pendingMemory + poolMemory; + + if (totalBytes < 1024) { + return totalBytes + ' B'; + } else if (totalBytes < 1024 * 1024) { + return (totalBytes / 1024).toFixed(2) + ' KB'; + } else { + return (totalBytes / 1024 / 1024).toFixed(2) + ' MB'; + } + } + + // 【新增】重置性能统计 + resetPerformanceStats() { + this.pendingHighWaterMark = this.pending.length; + this.flushCount = 0; + this.renderTime = 0; + console.log('[性能统计] 已重置性能计数器'); + } + // 添加清理方法 dispose() { + console.log('[资源清理] 开始清理LogVirtualList资源...'); + console.log('[性能统计] 最终统计:', this.getPerformanceStats()); + // 清理定时器 if (this.rafId) { cancelAnimationFrame(this.rafId); @@ -1414,19 +1480,25 @@ this.scrollHandler = null; } - // 清空数据结构 - this.lines = []; - this.pending = []; + // 【优化】清空数据结构,释放内存 + this.lines.length = 0; + this.pending.length = 0; - // 清空并释放DOM节点池 + // 【优化】清空并释放DOM节点池 this.pool.forEach(node => { if (node && node.parentNode) { node.parentNode.removeChild(node); } }); - this.pool = []; + this.pool.length = 0; // 清理占位符 + if (this.beforeSpacer && this.beforeSpacer.parentNode) { + this.beforeSpacer.parentNode.removeChild(this.beforeSpacer); + } + if (this.afterSpacer && this.afterSpacer.parentNode) { + this.afterSpacer.parentNode.removeChild(this.afterSpacer); + } this.beforeSpacer = null; this.afterSpacer = null; @@ -1434,6 +1506,8 @@ if (this.container) { this.container.innerHTML = ''; } + + console.log('[资源清理] LogVirtualList资源清理完成'); } handleUserScroll() { @@ -1472,45 +1546,39 @@ scrollToBottom() { if (!this.scrollElement) return; - // 节流:如果上次滚动时间距离现在不到 scrollThrottle 毫秒,则跳过 - const now = Date.now(); - if (now - this.lastScrollTime < this.scrollThrottle) { - return; + // 【FIX Bug #6】使用try-catch防止死锁 + try { + // 使用锁防止重入 + if (this.scrollLocked) return; + this.scrollLocked = true; + + // 【FIX Bug #2】不使用节流,确保每次调用都能滚动 + // 移除节流逻辑,因为scheduleRender已经有节流了 + + // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁 + requestAnimationFrame(() => { + try { + if (!this.scrollElement) { + this.scrollLocked = false; + return; + } + + // 直接滚动到底部,不使用平滑滚动以避免性能问题 + const targetScroll = this.scrollElement.scrollHeight; + this.scrollElement.scrollTop = targetScroll; + + // 【FIX Bug #2】立即重置标志,不延迟 + this.scrollLocked = false; + this.needsScroll = false; + } catch (e) { + console.error('滚动到底部失败:', e); + this.scrollLocked = false; // 确保锁被释放 + } + }); + } catch (e) { + console.error('scrollToBottom失败:', e); + this.scrollLocked = false; // 确保锁被释放 } - this.lastScrollTime = now; - - // 使用锁防止重入 - if (this.scrollLocked) return; - this.scrollLocked = true; - - // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁 - requestAnimationFrame(() => { - if (!this.scrollElement) { - this.scrollLocked = false; - return; - } - - // 平滑滚动到底部,避免突然跳跃 - const targetScroll = this.scrollElement.scrollHeight; - const currentScroll = this.scrollElement.scrollTop; - - // 如果已经在底部附近,直接设置,否则平滑滚动 - if (Math.abs(targetScroll - currentScroll) < 100) { - this.scrollElement.scrollTop = targetScroll; - } else { - // 使用平滑滚动 - this.scrollElement.scrollTo({ - top: targetScroll, - behavior: 'auto' // 使用 auto 而不是 smooth,避免性能问题 - }); - } - - // 缩短锁释放延迟,从150ms减少到50ms,提高响应速度 - setTimeout(() => { - this.scrollLocked = false; - this.needsScroll = false; // 滚动完成后重置标志 - }, 50); - }); } setLineHeight(px) { @@ -1524,74 +1592,133 @@ } this.pending.push({ text, className }); - // 优化批处理策略:超过50行或等待时间超过200ms后flush - if (this.pending.length >= 50) { + + // 【优化】记录pending队列峰值,用于性能调优 + if (this.pending.length > this.pendingHighWaterMark) { + this.pendingHighWaterMark = this.pending.length; + } + + // 【优化】智能批处理策略 + const now = Date.now(); + const timeSinceLastFlush = now - this.lastFlushTime; + + // 情况1:pending队列过大(超过阈值),立即flush + if (this.pending.length >= this.batchThreshold) { this.flush(); - } else if (this.pending.length === 1) { - // 第一条消息时,设置一个定时器在200ms后自动flush + this.scheduleRender(); + } + // 情况2:第一条消息,启动延迟flush定时器 + else if (this.pending.length === 1) { if (this.flushTimer) { clearTimeout(this.flushTimer); } + + // 【优化】自适应延迟:如果最近flush频繁,说明日志流量大,缩短延迟 + const adaptiveDelay = (timeSinceLastFlush < 1000) + ? Math.max(100, this.batchDelay / 2) // 高流量:缩短延迟 + : this.batchDelay; // 正常流量:使用标准延迟 + this.flushTimer = setTimeout(() => { this.flush(); this.scheduleRender(); - }, 200); + }, adaptiveDelay); } + // 【关键优化】不在append()中调用scheduleRender(),避免频繁触发 + // 只在flush()后才渲染,大幅减少渲染次数 + this.maybeTrim(); - this.scheduleRender(); } clear(message = null) { this.lines = []; this.pending = []; - this.pool = []; + // 不清空pool,复用DOM节点以提高性能 if (message) { this.lines.push({ text: message, className: 'console-line' }); } this.lastRenderHash = null; this.needsScroll = true; // 清空后需要滚动到底部 - this.scheduleRender(true); + // 【FIX Bug #3】清空时不使用异步渲染,由调用者决定何时渲染 + // 这样可以批量操作后再统一渲染,提高性能 } flush() { if (!this.pending.length) return; + // 清理批处理定时器 if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } + + // 【优化】记录flush时间,用于自适应批处理 + this.lastFlushTime = Date.now(); + this.flushCount++; + + // 【优化】批量push,减少数组操作次数 this.lines.push(...this.pending); + const flushedCount = this.pending.length; this.pending = []; + + // 【优化】如果一次性flush的行数很多(>500),启用分帧渲染 + if (flushedCount > 500) { + console.log(`[性能优化] 检测到大批量日志(${flushedCount}行),启用分帧渲染`); + } + this.maybeTrim(); + + // 【优化】返回flush的行数,供调用者决定渲染策略 + return flushedCount; } maybeTrim() { - // 在 flush 之后调用:控制 lines 总量,减少内存 + // 【优化】更积极的trim策略,但保留更多行 if (this.lines.length <= this.maxLines) return; const toDrop = this.lines.length - this.trimTarget; if (toDrop > 0) { + // 【优化】批量删除,性能更好 this.lines.splice(0, toDrop); - // 不调整滚动位置,让用户保持当前位置或自动吸底 + + // 重置哈希,强制下次渲染 + this.lastRenderHash = null; + + console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`); } } scheduleRender(force = false) { if (!this.container) return; if (!force && this.rafId) return; - // 取消之前的请求,使用节流 + + // 取消之前的请求 if (this.rafId) { cancelAnimationFrame(this.rafId); } + + // 【优化】使用requestAnimationFrame进行渲染调度 this.rafId = requestAnimationFrame(() => { this.rafId = null; + + // 【优化】性能监控:记录渲染开始时间 + const renderStart = performance.now(); + this.render(); + + // 【优化】记录渲染耗时 + this.renderTime = performance.now() - renderStart; + + // 【性能警告】如果渲染耗时超过16ms(一帧),输出警告 + if (this.renderTime > 16) { + console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`); + } }); } render() { this.flush(); const total = this.lines.length; + if (!total) { if (this.container.innerHTML !== '') { this.container.innerHTML = ''; @@ -1603,21 +1730,21 @@ return; } - // 改进内容哈希:包含总行数、前5行和后5行的摘要 - const hashSample = total <= 10 - ? this.lines.map(l => l.text).join('|') - : this.lines.slice(0, 5).map(l => l.text).join('|') + '|' + - this.lines.slice(-5).map(l => l.text).join('|'); - const contentHash = `${total}-${hashSample}`; - if (this.lastRenderHash === contentHash) { - // 内容没有变化,只需要处理滚动(如果需要的话) - if (this.needsScroll && this.autoScrollEnabled) { - this.scrollToBottom(); - } + // 【优化】改进内容哈希:使用总行数+最后一行文本 + const lastLine = this.lines[total - 1]; + const contentHash = `${total}-${lastLine ? lastLine.text : ''}`; + + // 如果需要滚动,强制渲染 + const forceRender = this.needsScroll && this.autoScrollEnabled; + + if (this.lastRenderHash === contentHash && !forceRender) { + // 内容没有变化且不需要滚动,跳过渲染 return; } this.lastRenderHash = contentHash; + this.lastRenderLineCount = total; + // 【优化】计算可见区域 const lh = this.lineHeight; const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); @@ -1632,10 +1759,9 @@ const needed = Math.max(0, end - start); - // 限制DOM节点池大小,防止内存泄漏 + // 【优化】限制DOM节点池大小 if (this.pool.length > this.maxPoolSize) { const excess = this.pool.length - this.maxPoolSize; - // 移除多余的节点 this.pool.splice(this.maxPoolSize, excess).forEach(node => { if (node && node.parentNode) { node.parentNode.removeChild(node); @@ -1643,44 +1769,51 @@ }); } - // 复用现有的 DOM 节点池 - while (this.pool.length < needed && this.pool.length < this.maxPoolSize) { - const node = document.createElement('div'); - node.className = 'console-line'; - this.pool.push(node); + // 【优化】批量创建DOM节点,减少DOM操作 + const nodesToCreate = needed - this.pool.length; + if (nodesToCreate > 0 && this.pool.length < this.maxPoolSize) { + const fragment = document.createDocumentFragment(); + for (let i = 0; i < Math.min(nodesToCreate, this.maxPoolSize - this.pool.length); i++) { + const node = document.createElement('div'); + node.className = 'console-line'; + this.pool.push(node); + } } - // 复用或创建占位符(避免每次重建) + // 【优化】复用或创建占位符 if (!this.beforeSpacer) { this.beforeSpacer = document.createElement('div'); this.beforeSpacer.dataset.spacer = 'before'; + this.beforeSpacer.style.willChange = 'height'; // GPU加速 } else if (!this.beforeSpacer.parentNode) { - // 如果占位符被意外移除,标记需要重建DOM this.beforeSpacer = document.createElement('div'); this.beforeSpacer.dataset.spacer = 'before'; + this.beforeSpacer.style.willChange = 'height'; } this.beforeSpacer.style.height = `${beforeHeight}px`; if (!this.afterSpacer) { this.afterSpacer = document.createElement('div'); this.afterSpacer.dataset.spacer = 'after'; + this.afterSpacer.style.willChange = 'height'; // GPU加速 } else if (!this.afterSpacer.parentNode) { this.afterSpacer = document.createElement('div'); this.afterSpacer.dataset.spacer = 'after'; + this.afterSpacer.style.willChange = 'height'; } this.afterSpacer.style.height = `${afterHeight}px`; - // 使用DocumentFragment来减少DOM重绘 + // 【优化】使用DocumentFragment批量操作DOM const fragment = document.createDocumentFragment(); - // 只更新可见区域的节点 + // 【优化】只更新可见区域的节点 for (let idx = start; idx < end; idx++) { const line = this.lines[idx]; const poolIdx = idx - start; const node = this.pool[poolIdx]; if (!node) continue; - // 只在内容或类名变化时才更新节点 + // 【优化】只在内容或类名变化时才更新节点 if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) { node.className = line.className || 'console-line'; node.textContent = line.text; @@ -1688,33 +1821,36 @@ fragment.appendChild(node); } - // 优化DOM更新:只在必要时清空容器 - // 检查容器是否需要重建(比如占位符丢失) + // 【优化】增量更新DOM,减少重绘 const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; + if (needsRebuild) { + // 需要完全重建 this.container.innerHTML = ''; this.container.appendChild(this.beforeSpacer); this.container.appendChild(fragment); this.container.appendChild(this.afterSpacer); } else { - // 增量更新:只更新可见节点部分 - // 移除旧的可见节点 + // 【优化】只更新可见节点部分,使用更高效的方式 const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); - existingNodes.forEach(node => { - if (node.parentNode === this.container) { - this.container.removeChild(node); - } - }); + + // 【优化】批量移除,减少重排 + if (existingNodes.length > 0) { + // 使用DocumentFragment收集要移除的节点 + existingNodes.forEach(node => { + if (node.parentNode === this.container) { + this.container.removeChild(node); + } + }); + } + // 在占位符之间插入新节点 this.container.insertBefore(fragment, this.afterSpacer); } - // 只在有标记且自动滚动启用时才滚动到底部 + // 【优化】如果需要滚动且自动滚动启用,立即滚动 if (this.needsScroll && this.autoScrollEnabled) { - // 延迟执行滚动,确保 DOM 已经更新完毕 - requestAnimationFrame(() => { - this.scrollToBottom(); - }); + this.scrollToBottom(); } } } @@ -1836,6 +1972,17 @@ // 启动所有定时器 startAllTimers(); + // 【新增】启动定期内存优化 + startMemoryOptimization(); + console.log('[性能优化] 已启动定期内存优化(每5分钟)'); + + // 【新增】将性能监控函数暴露到全局,方便调试 + window.getGlobalPerformanceStats = getGlobalPerformanceStats; + window.resetAllPerformanceStats = resetAllPerformanceStats; + console.log('[调试工具] 性能监控函数已挂载到window对象:'); + console.log(' - window.getGlobalPerformanceStats() : 查看所有渲染器性能统计'); + console.log(' - window.resetAllPerformanceStats() : 重置所有性能计数器'); + // 监听页面可见性变化 document.addEventListener('visibilitychange', handleVisibilityChange); @@ -2576,9 +2723,74 @@ } } + // 【新增】全局性能监控函数 + function getGlobalPerformanceStats() { + console.log('=== 日志渲染器性能统计 ==='); + let totalMemory = 0; + let totalLines = 0; + + consoleLayerApps.forEach(app => { + const renderer = logRenderers[app]; + if (renderer) { + const stats = renderer.getPerformanceStats(); + console.log(`\n[${app.toUpperCase()}]:`); + console.log(` 总行数: ${stats.totalLines}`); + console.log(` 待处理行数: ${stats.pendingLines}`); + console.log(` 队列峰值: ${stats.pendingHighWaterMark}`); + console.log(` Flush次数: ${stats.flushCount}`); + console.log(` 上次渲染耗时: ${stats.lastRenderTime}`); + console.log(` 上次渲染行数: ${stats.lastRenderLineCount}`); + console.log(` DOM池大小: ${stats.poolSize}`); + console.log(` 内存估算: ${stats.memoryEstimate}`); + + totalLines += stats.totalLines; + // 简单累加(实际内存使用需要更精确的计算) + } + }); + + console.log(`\n=== 总计 ===`); + console.log(`总日志行数: ${totalLines}`); + console.log(`活跃渲染器: ${Object.keys(logRenderers).length}`); + } + + // 【新增】重置所有性能统计 + function resetAllPerformanceStats() { + consoleLayerApps.forEach(app => { + const renderer = logRenderers[app]; + if (renderer) { + renderer.resetPerformanceStats(); + } + }); + console.log('[性能统计] 已重置所有渲染器的性能统计'); + } + + // 【新增】定期内存优化(每5分钟检查一次) + function startMemoryOptimization() { + setInterval(() => { + consoleLayerApps.forEach(app => { + // 只优化非当前活跃的渲染器 + if (app !== currentApp) { + const renderer = logRenderers[app]; + if (renderer && renderer.lines.length > 0) { + // 如果非活跃渲染器有大量日志,进行trim + if (renderer.lines.length > renderer.maxLines * 0.8) { + const before = renderer.lines.length; + renderer.maybeTrim(); + const after = renderer.lines.length; + if (before > after) { + console.log(`[内存优化] 非活跃渲染器 ${app} 从 ${before} 行裁剪到 ${after} 行`); + } + } + } + } + }); + }, 5 * 60 * 1000); // 5分钟 + } + // 存储最后显示的行数,避免重复加载 let lastLineCount = {}; + function getConsoleContainer() { return document.getElementById('consoleOutput'); } @@ -2604,8 +2816,9 @@ consoleLayers[app] = layer; logRenderers[app] = new LogVirtualList(layer); - // 初始提示仅在渲染器内部渲染,不保留在 DOM + // 【FIX Bug #3】初始提示立即渲染,避免黑屏 logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`); + logRenderers[app].render(); // 立即同步渲染 }); // 不需要手动设置滚动位置,LogVirtualList会处理 @@ -2661,10 +2874,22 @@ // 触发一次渲染以确保内容正确显示 const renderer = logRenderers[app]; if (renderer) { - // 使用 requestAnimationFrame 确保在下一帧渲染,避免闪烁 - requestAnimationFrame(() => { - renderer.scheduleRender(true); - }); + // 【FIX Bug #1/#3】如果已有数据,立即同步渲染,避免黑屏 + if (renderer.lines.length > 0 || renderer.pending.length > 0) { + renderer.flush(); // 先将pending数据合并到lines + renderer.render(); // 立即同步渲染,不使用异步 + } else { + // 如果没有数据,显示加载提示(同步) + renderer.clear(`[系统] 正在加载 ${appNames[app] || app} 日志...`); + renderer.render(); // 立即渲染加载提示 + } + // 确保滚动到底部 + if (renderer.autoScrollEnabled) { + // 使用setTimeout确保DOM更新后再滚动 + setTimeout(() => { + renderer.scrollToBottom(); + }, 0); + } } } @@ -2675,13 +2900,19 @@ } function appendConsoleTextLine(app, text, className = 'console-line') { + // 【优化】添加空值检查 + if (!app || !text) return; + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); renderer.append(text, className); } function appendConsoleElement(app, element) { + // 【优化】添加空值检查 + if (!app || !element) return; + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); - if (!element || !renderer.container) return; + if (!renderer.container) return; // 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑 const text = element.textContent || element.innerText || ''; @@ -2700,29 +2931,56 @@ loadForumLog(); return; } - + if (app === 'report') { loadReportLog(); return; } - + fetch(`/api/output/${app}`) .then(response => response.json()) .then(data => { + // 【FIX Bug #5】检查是否仍然是当前app,避免竞态条件 + // 如果用户已经切换到其他app,忽略这个响应 + if (currentApp !== app) { + console.log(`忽略${app}的日志响应(当前app是${currentApp})`); + return; + } + if (data.success && data.output.length > 0) { const lastCount = lastLineCount[app] || 0; const newLines = data.output.slice(lastCount); - + if (newLines.length > 0) { newLines.forEach(line => { appendConsoleTextLine(app, line); }); lastLineCount[app] = data.output.length; + + // 数据加载完成,更新加载提示为实际日志 + const renderer = logRenderers[app]; + if (renderer && renderer.lines.length > 0) { + // 移除"正在加载"提示(如果存在) + const firstLine = renderer.lines[0]; + if (firstLine && firstLine.text.includes('正在加载')) { + renderer.lines.shift(); // 移除第一行 + renderer.lastRenderHash = null; // 强制重新渲染 + renderer.scheduleRender(true); + } + } } } }) .catch(error => { console.error('加载输出失败:', error); + // 加载失败时也显示错误提示 + if (currentApp === app) { + const renderer = logRenderers[app]; + if (renderer) { + renderer.clear(`[错误] 加载${appNames[app] || app}日志失败`); + renderer.render(); + } + } }); } @@ -3201,7 +3459,21 @@ fetch('/api/forum/log') .then(response => response.json()) .then(data => { - if (!data.success) return; + // 【FIX Bug #5】检查是否仍然在forum页面 + if (currentApp !== 'forum') { + console.log('忽略forum日志响应(已切换到其他app)'); + return; + } + + if (!data.success) { + // 加载失败,显示错误 + const renderer = logRenderers['forum']; + if (renderer) { + renderer.clear('[错误] 加载Forum日志失败'); + renderer.render(); + } + return; + } const chatArea = document.getElementById('forumChatArea'); if (chatArea) { @@ -3213,6 +3485,7 @@ if (logLines.length > 0) { clearConsoleLayer('forum', '[系统] Forum Engine 日志输出'); + logRenderers['forum'].render(); // 立即渲染清空提示 logLines.forEach(line => appendConsoleTextLine('forum', line)); } else { forumLogLineCount = 0; @@ -3226,6 +3499,14 @@ }) .catch(error => { console.error('加载论坛日志失败:', error); + // 【优化】显示错误提示 + if (currentApp === 'forum') { + const renderer = logRenderers['forum']; + if (renderer) { + renderer.clear('[错误] 加载Forum日志失败: ' + error.message); + renderer.render(); + } + } }); } @@ -3341,9 +3622,16 @@ fetch('/api/report/log') .then(response => response.json()) .then(data => { + // 【FIX Bug #5】检查是否仍然在report页面 + if (currentApp !== 'report') { + console.log('忽略report日志响应(已切换到其他app)'); + return; + } + if (data.success) { if (reportLogLineCount === 0) { clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); + logRenderers['report'].render(); // 立即渲染 } if (data.log_lines && data.log_lines.length > 0) { @@ -3353,17 +3641,43 @@ linesToProcess.forEach(line => { appendConsoleTextLine('report', line); }); - + // 重置计数器以确保后续消息能正确显示 reportLogLineCount = data.log_lines.length; + + // 移除"正在加载"提示(如果存在) + const renderer = logRenderers['report']; + if (renderer && renderer.lines.length > 0) { + const firstLine = renderer.lines[0]; + if (firstLine && firstLine.text.includes('正在加载')) { + renderer.lines.shift(); + renderer.lastRenderHash = null; + renderer.scheduleRender(true); + } + } } else { // 如果没有日志,重置计数器 reportLogLineCount = 0; } + } else { + // 【优化】加载失败显示错误 + const renderer = logRenderers['report']; + if (renderer && currentApp === 'report') { + renderer.clear('[错误] 加载Report日志失败'); + renderer.render(); + } } }) .catch(error => { console.error('加载Report日志失败:', error); + // 【优化】显示错误提示 + if (currentApp === 'report') { + const renderer = logRenderers['report']; + if (renderer) { + renderer.clear('[错误] 加载Report日志失败: ' + error.message); + renderer.render(); + } + } }); }