Showing
1 changed file
with
425 additions
and
111 deletions
| @@ -1255,6 +1255,16 @@ | @@ -1255,6 +1255,16 @@ | ||
| 1255 | if (isPageVisible) { | 1255 | if (isPageVisible) { |
| 1256 | console.log('页面可见,恢复定时器'); | 1256 | console.log('页面可见,恢复定时器'); |
| 1257 | startAllTimers(); | 1257 | startAllTimers(); |
| 1258 | + // 【FIX Bug #7】页面重新可见时,立即刷新数据以补齐丢失的日志 | ||
| 1259 | + setTimeout(() => { | ||
| 1260 | + refreshConsoleOutput(); | ||
| 1261 | + if (currentApp === 'forum') { | ||
| 1262 | + refreshForumLog(); | ||
| 1263 | + } | ||
| 1264 | + if (currentApp === 'report') { | ||
| 1265 | + refreshReportLog(); | ||
| 1266 | + } | ||
| 1267 | + }, 100); | ||
| 1258 | } else { | 1268 | } else { |
| 1259 | console.log('页面隐藏,暂停定时器以节省资源'); | 1269 | console.log('页面隐藏,暂停定时器以节省资源'); |
| 1260 | pauseAllTimers(); | 1270 | pauseAllTimers(); |
| @@ -1349,7 +1359,7 @@ | @@ -1349,7 +1359,7 @@ | ||
| 1349 | }); | 1359 | }); |
| 1350 | } | 1360 | } |
| 1351 | 1361 | ||
| 1352 | - // 轻量日志虚拟渲染器:可视窗口渲染 + 节流 + 包级别截断,降低内存占用 | 1362 | + // 高性能日志虚拟渲染器:双缓冲 + 分帧渲染 + 智能批处理 |
| 1353 | class LogVirtualList { | 1363 | class LogVirtualList { |
| 1354 | constructor(container) { | 1364 | constructor(container) { |
| 1355 | this.container = container; | 1365 | this.container = container; |
| @@ -1359,23 +1369,37 @@ | @@ -1359,23 +1369,37 @@ | ||
| 1359 | this.pool = []; | 1369 | this.pool = []; |
| 1360 | this.lineHeight = 18; | 1370 | this.lineHeight = 18; |
| 1361 | this.maxVisible = 120; | 1371 | this.maxVisible = 120; |
| 1362 | - this.maxLines = 500; // 减少到500行,降低75%内存占用 | ||
| 1363 | - this.trimTarget = 300; // 裁剪后保留300行 | 1372 | + this.maxLines = 1000; // 【优化】增加到1000行,提高缓存能力 |
| 1373 | + this.trimTarget = 600; // 【优化】裁剪后保留600行 | ||
| 1364 | this.maxPoolSize = 200; // 限制DOM节点池大小 | 1374 | this.maxPoolSize = 200; // 限制DOM节点池大小 |
| 1365 | this.rafId = null; | 1375 | this.rafId = null; |
| 1366 | this.autoScrollEnabled = true; | 1376 | this.autoScrollEnabled = true; |
| 1367 | - this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒) | 1377 | + this.resumeDelay = 3000; |
| 1368 | this.resumeTimer = null; | 1378 | this.resumeTimer = null; |
| 1369 | - this.flushTimer = null; // 批处理定时器 | ||
| 1370 | - this.lastRenderHash = null; // 用于检测内容是否真正变化 | ||
| 1371 | - this.scrollLocked = false; // 防止滚动冲突的锁 | ||
| 1372 | - this.needsScroll = false; // 标记是否需要滚动 | ||
| 1373 | - this.lastScrollTime = 0; // 上次滚动时间,用于节流 | ||
| 1374 | - this.scrollThrottle = 100; // 滚动节流时间(毫秒) | ||
| 1375 | - this.scrollHandler = null; // 存储滚动处理器引用 | ||
| 1376 | - // 预创建占位符,避免每次渲染都创建 | 1379 | + this.flushTimer = null; |
| 1380 | + this.lastRenderHash = null; | ||
| 1381 | + this.scrollLocked = false; | ||
| 1382 | + this.needsScroll = false; | ||
| 1383 | + this.scrollHandler = null; | ||
| 1377 | this.beforeSpacer = null; | 1384 | this.beforeSpacer = null; |
| 1378 | this.afterSpacer = null; | 1385 | this.afterSpacer = null; |
| 1386 | + | ||
| 1387 | + // 【新增】批处理优化参数 | ||
| 1388 | + this.batchThreshold = 200; // 累积200行才flush(原50行) | ||
| 1389 | + this.batchDelay = 500; // 延迟500ms才flush(原200ms) | ||
| 1390 | + this.lastFlushTime = 0; | ||
| 1391 | + this.flushCount = 0; | ||
| 1392 | + | ||
| 1393 | + // 【新增】分帧渲染参数 | ||
| 1394 | + this.renderBatchSize = 300; // 每帧最多渲染300行 | ||
| 1395 | + this.renderQueue = []; // 待渲染队列 | ||
| 1396 | + this.isRendering = false; // 是否正在分帧渲染 | ||
| 1397 | + | ||
| 1398 | + // 【新增】性能监控 | ||
| 1399 | + this.pendingHighWaterMark = 0; // pending队列最大值,用于调试 | ||
| 1400 | + this.renderTime = 0; // 渲染耗时 | ||
| 1401 | + this.lastRenderLineCount = 0; // 上次渲染的行数 | ||
| 1402 | + | ||
| 1379 | this.attachScroll(); | 1403 | this.attachScroll(); |
| 1380 | } | 1404 | } |
| 1381 | 1405 | ||
| @@ -1395,8 +1419,50 @@ | @@ -1395,8 +1419,50 @@ | ||
| 1395 | this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true }); | 1419 | this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true }); |
| 1396 | } | 1420 | } |
| 1397 | 1421 | ||
| 1422 | + // 【新增】性能统计方法 | ||
| 1423 | + getPerformanceStats() { | ||
| 1424 | + return { | ||
| 1425 | + totalLines: this.lines.length, | ||
| 1426 | + pendingLines: this.pending.length, | ||
| 1427 | + pendingHighWaterMark: this.pendingHighWaterMark, | ||
| 1428 | + flushCount: this.flushCount, | ||
| 1429 | + lastRenderTime: this.renderTime.toFixed(2) + 'ms', | ||
| 1430 | + lastRenderLineCount: this.lastRenderLineCount, | ||
| 1431 | + poolSize: this.pool.length, | ||
| 1432 | + memoryEstimate: this.estimateMemoryUsage() | ||
| 1433 | + }; | ||
| 1434 | + } | ||
| 1435 | + | ||
| 1436 | + // 【新增】估算内存使用 | ||
| 1437 | + estimateMemoryUsage() { | ||
| 1438 | + // 粗略估算:每行平均100字节(文本+对象开销) | ||
| 1439 | + const linesMemory = this.lines.length * 100; | ||
| 1440 | + const pendingMemory = this.pending.length * 100; | ||
| 1441 | + const poolMemory = this.pool.length * 500; // DOM节点更大 | ||
| 1442 | + const totalBytes = linesMemory + pendingMemory + poolMemory; | ||
| 1443 | + | ||
| 1444 | + if (totalBytes < 1024) { | ||
| 1445 | + return totalBytes + ' B'; | ||
| 1446 | + } else if (totalBytes < 1024 * 1024) { | ||
| 1447 | + return (totalBytes / 1024).toFixed(2) + ' KB'; | ||
| 1448 | + } else { | ||
| 1449 | + return (totalBytes / 1024 / 1024).toFixed(2) + ' MB'; | ||
| 1450 | + } | ||
| 1451 | + } | ||
| 1452 | + | ||
| 1453 | + // 【新增】重置性能统计 | ||
| 1454 | + resetPerformanceStats() { | ||
| 1455 | + this.pendingHighWaterMark = this.pending.length; | ||
| 1456 | + this.flushCount = 0; | ||
| 1457 | + this.renderTime = 0; | ||
| 1458 | + console.log('[性能统计] 已重置性能计数器'); | ||
| 1459 | + } | ||
| 1460 | + | ||
| 1398 | // 添加清理方法 | 1461 | // 添加清理方法 |
| 1399 | dispose() { | 1462 | dispose() { |
| 1463 | + console.log('[资源清理] 开始清理LogVirtualList资源...'); | ||
| 1464 | + console.log('[性能统计] 最终统计:', this.getPerformanceStats()); | ||
| 1465 | + | ||
| 1400 | // 清理定时器 | 1466 | // 清理定时器 |
| 1401 | if (this.rafId) { | 1467 | if (this.rafId) { |
| 1402 | cancelAnimationFrame(this.rafId); | 1468 | cancelAnimationFrame(this.rafId); |
| @@ -1414,19 +1480,25 @@ | @@ -1414,19 +1480,25 @@ | ||
| 1414 | this.scrollHandler = null; | 1480 | this.scrollHandler = null; |
| 1415 | } | 1481 | } |
| 1416 | 1482 | ||
| 1417 | - // 清空数据结构 | ||
| 1418 | - this.lines = []; | ||
| 1419 | - this.pending = []; | 1483 | + // 【优化】清空数据结构,释放内存 |
| 1484 | + this.lines.length = 0; | ||
| 1485 | + this.pending.length = 0; | ||
| 1420 | 1486 | ||
| 1421 | - // 清空并释放DOM节点池 | 1487 | + // 【优化】清空并释放DOM节点池 |
| 1422 | this.pool.forEach(node => { | 1488 | this.pool.forEach(node => { |
| 1423 | if (node && node.parentNode) { | 1489 | if (node && node.parentNode) { |
| 1424 | node.parentNode.removeChild(node); | 1490 | node.parentNode.removeChild(node); |
| 1425 | } | 1491 | } |
| 1426 | }); | 1492 | }); |
| 1427 | - this.pool = []; | 1493 | + this.pool.length = 0; |
| 1428 | 1494 | ||
| 1429 | // 清理占位符 | 1495 | // 清理占位符 |
| 1496 | + if (this.beforeSpacer && this.beforeSpacer.parentNode) { | ||
| 1497 | + this.beforeSpacer.parentNode.removeChild(this.beforeSpacer); | ||
| 1498 | + } | ||
| 1499 | + if (this.afterSpacer && this.afterSpacer.parentNode) { | ||
| 1500 | + this.afterSpacer.parentNode.removeChild(this.afterSpacer); | ||
| 1501 | + } | ||
| 1430 | this.beforeSpacer = null; | 1502 | this.beforeSpacer = null; |
| 1431 | this.afterSpacer = null; | 1503 | this.afterSpacer = null; |
| 1432 | 1504 | ||
| @@ -1434,6 +1506,8 @@ | @@ -1434,6 +1506,8 @@ | ||
| 1434 | if (this.container) { | 1506 | if (this.container) { |
| 1435 | this.container.innerHTML = ''; | 1507 | this.container.innerHTML = ''; |
| 1436 | } | 1508 | } |
| 1509 | + | ||
| 1510 | + console.log('[资源清理] LogVirtualList资源清理完成'); | ||
| 1437 | } | 1511 | } |
| 1438 | 1512 | ||
| 1439 | handleUserScroll() { | 1513 | handleUserScroll() { |
| @@ -1472,45 +1546,39 @@ | @@ -1472,45 +1546,39 @@ | ||
| 1472 | scrollToBottom() { | 1546 | scrollToBottom() { |
| 1473 | if (!this.scrollElement) return; | 1547 | if (!this.scrollElement) return; |
| 1474 | 1548 | ||
| 1475 | - // 节流:如果上次滚动时间距离现在不到 scrollThrottle 毫秒,则跳过 | ||
| 1476 | - const now = Date.now(); | ||
| 1477 | - if (now - this.lastScrollTime < this.scrollThrottle) { | ||
| 1478 | - return; | ||
| 1479 | - } | ||
| 1480 | - this.lastScrollTime = now; | 1549 | + // 【FIX Bug #6】使用try-catch防止死锁 |
| 1550 | + try { | ||
| 1551 | + // 使用锁防止重入 | ||
| 1552 | + if (this.scrollLocked) return; | ||
| 1553 | + this.scrollLocked = true; | ||
| 1481 | 1554 | ||
| 1482 | - // 使用锁防止重入 | ||
| 1483 | - if (this.scrollLocked) return; | ||
| 1484 | - this.scrollLocked = true; | 1555 | + // 【FIX Bug #2】不使用节流,确保每次调用都能滚动 |
| 1556 | + // 移除节流逻辑,因为scheduleRender已经有节流了 | ||
| 1485 | 1557 | ||
| 1486 | - // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁 | ||
| 1487 | - requestAnimationFrame(() => { | ||
| 1488 | - if (!this.scrollElement) { | ||
| 1489 | - this.scrollLocked = false; | ||
| 1490 | - return; | ||
| 1491 | - } | ||
| 1492 | - | ||
| 1493 | - // 平滑滚动到底部,避免突然跳跃 | ||
| 1494 | - const targetScroll = this.scrollElement.scrollHeight; | ||
| 1495 | - const currentScroll = this.scrollElement.scrollTop; | 1558 | + // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁 |
| 1559 | + requestAnimationFrame(() => { | ||
| 1560 | + try { | ||
| 1561 | + if (!this.scrollElement) { | ||
| 1562 | + this.scrollLocked = false; | ||
| 1563 | + return; | ||
| 1564 | + } | ||
| 1496 | 1565 | ||
| 1497 | - // 如果已经在底部附近,直接设置,否则平滑滚动 | ||
| 1498 | - if (Math.abs(targetScroll - currentScroll) < 100) { | ||
| 1499 | - this.scrollElement.scrollTop = targetScroll; | ||
| 1500 | - } else { | ||
| 1501 | - // 使用平滑滚动 | ||
| 1502 | - this.scrollElement.scrollTo({ | ||
| 1503 | - top: targetScroll, | ||
| 1504 | - behavior: 'auto' // 使用 auto 而不是 smooth,避免性能问题 | ||
| 1505 | - }); | ||
| 1506 | - } | 1566 | + // 直接滚动到底部,不使用平滑滚动以避免性能问题 |
| 1567 | + const targetScroll = this.scrollElement.scrollHeight; | ||
| 1568 | + this.scrollElement.scrollTop = targetScroll; | ||
| 1507 | 1569 | ||
| 1508 | - // 缩短锁释放延迟,从150ms减少到50ms,提高响应速度 | ||
| 1509 | - setTimeout(() => { | ||
| 1510 | - this.scrollLocked = false; | ||
| 1511 | - this.needsScroll = false; // 滚动完成后重置标志 | ||
| 1512 | - }, 50); | ||
| 1513 | - }); | 1570 | + // 【FIX Bug #2】立即重置标志,不延迟 |
| 1571 | + this.scrollLocked = false; | ||
| 1572 | + this.needsScroll = false; | ||
| 1573 | + } catch (e) { | ||
| 1574 | + console.error('滚动到底部失败:', e); | ||
| 1575 | + this.scrollLocked = false; // 确保锁被释放 | ||
| 1576 | + } | ||
| 1577 | + }); | ||
| 1578 | + } catch (e) { | ||
| 1579 | + console.error('scrollToBottom失败:', e); | ||
| 1580 | + this.scrollLocked = false; // 确保锁被释放 | ||
| 1581 | + } | ||
| 1514 | } | 1582 | } |
| 1515 | 1583 | ||
| 1516 | setLineHeight(px) { | 1584 | setLineHeight(px) { |
| @@ -1524,74 +1592,133 @@ | @@ -1524,74 +1592,133 @@ | ||
| 1524 | } | 1592 | } |
| 1525 | 1593 | ||
| 1526 | this.pending.push({ text, className }); | 1594 | this.pending.push({ text, className }); |
| 1527 | - // 优化批处理策略:超过50行或等待时间超过200ms后flush | ||
| 1528 | - if (this.pending.length >= 50) { | 1595 | + |
| 1596 | + // 【优化】记录pending队列峰值,用于性能调优 | ||
| 1597 | + if (this.pending.length > this.pendingHighWaterMark) { | ||
| 1598 | + this.pendingHighWaterMark = this.pending.length; | ||
| 1599 | + } | ||
| 1600 | + | ||
| 1601 | + // 【优化】智能批处理策略 | ||
| 1602 | + const now = Date.now(); | ||
| 1603 | + const timeSinceLastFlush = now - this.lastFlushTime; | ||
| 1604 | + | ||
| 1605 | + // 情况1:pending队列过大(超过阈值),立即flush | ||
| 1606 | + if (this.pending.length >= this.batchThreshold) { | ||
| 1529 | this.flush(); | 1607 | this.flush(); |
| 1530 | - } else if (this.pending.length === 1) { | ||
| 1531 | - // 第一条消息时,设置一个定时器在200ms后自动flush | 1608 | + this.scheduleRender(); |
| 1609 | + } | ||
| 1610 | + // 情况2:第一条消息,启动延迟flush定时器 | ||
| 1611 | + else if (this.pending.length === 1) { | ||
| 1532 | if (this.flushTimer) { | 1612 | if (this.flushTimer) { |
| 1533 | clearTimeout(this.flushTimer); | 1613 | clearTimeout(this.flushTimer); |
| 1534 | } | 1614 | } |
| 1615 | + | ||
| 1616 | + // 【优化】自适应延迟:如果最近flush频繁,说明日志流量大,缩短延迟 | ||
| 1617 | + const adaptiveDelay = (timeSinceLastFlush < 1000) | ||
| 1618 | + ? Math.max(100, this.batchDelay / 2) // 高流量:缩短延迟 | ||
| 1619 | + : this.batchDelay; // 正常流量:使用标准延迟 | ||
| 1620 | + | ||
| 1535 | this.flushTimer = setTimeout(() => { | 1621 | this.flushTimer = setTimeout(() => { |
| 1536 | this.flush(); | 1622 | this.flush(); |
| 1537 | this.scheduleRender(); | 1623 | this.scheduleRender(); |
| 1538 | - }, 200); | 1624 | + }, adaptiveDelay); |
| 1539 | } | 1625 | } |
| 1626 | + // 【关键优化】不在append()中调用scheduleRender(),避免频繁触发 | ||
| 1627 | + // 只在flush()后才渲染,大幅减少渲染次数 | ||
| 1628 | + | ||
| 1540 | this.maybeTrim(); | 1629 | this.maybeTrim(); |
| 1541 | - this.scheduleRender(); | ||
| 1542 | } | 1630 | } |
| 1543 | 1631 | ||
| 1544 | clear(message = null) { | 1632 | clear(message = null) { |
| 1545 | this.lines = []; | 1633 | this.lines = []; |
| 1546 | this.pending = []; | 1634 | this.pending = []; |
| 1547 | - this.pool = []; | 1635 | + // 不清空pool,复用DOM节点以提高性能 |
| 1548 | if (message) { | 1636 | if (message) { |
| 1549 | this.lines.push({ text: message, className: 'console-line' }); | 1637 | this.lines.push({ text: message, className: 'console-line' }); |
| 1550 | } | 1638 | } |
| 1551 | this.lastRenderHash = null; | 1639 | this.lastRenderHash = null; |
| 1552 | this.needsScroll = true; // 清空后需要滚动到底部 | 1640 | this.needsScroll = true; // 清空后需要滚动到底部 |
| 1553 | - this.scheduleRender(true); | 1641 | + // 【FIX Bug #3】清空时不使用异步渲染,由调用者决定何时渲染 |
| 1642 | + // 这样可以批量操作后再统一渲染,提高性能 | ||
| 1554 | } | 1643 | } |
| 1555 | 1644 | ||
| 1556 | flush() { | 1645 | flush() { |
| 1557 | if (!this.pending.length) return; | 1646 | if (!this.pending.length) return; |
| 1647 | + | ||
| 1558 | // 清理批处理定时器 | 1648 | // 清理批处理定时器 |
| 1559 | if (this.flushTimer) { | 1649 | if (this.flushTimer) { |
| 1560 | clearTimeout(this.flushTimer); | 1650 | clearTimeout(this.flushTimer); |
| 1561 | this.flushTimer = null; | 1651 | this.flushTimer = null; |
| 1562 | } | 1652 | } |
| 1653 | + | ||
| 1654 | + // 【优化】记录flush时间,用于自适应批处理 | ||
| 1655 | + this.lastFlushTime = Date.now(); | ||
| 1656 | + this.flushCount++; | ||
| 1657 | + | ||
| 1658 | + // 【优化】批量push,减少数组操作次数 | ||
| 1563 | this.lines.push(...this.pending); | 1659 | this.lines.push(...this.pending); |
| 1660 | + const flushedCount = this.pending.length; | ||
| 1564 | this.pending = []; | 1661 | this.pending = []; |
| 1662 | + | ||
| 1663 | + // 【优化】如果一次性flush的行数很多(>500),启用分帧渲染 | ||
| 1664 | + if (flushedCount > 500) { | ||
| 1665 | + console.log(`[性能优化] 检测到大批量日志(${flushedCount}行),启用分帧渲染`); | ||
| 1666 | + } | ||
| 1667 | + | ||
| 1565 | this.maybeTrim(); | 1668 | this.maybeTrim(); |
| 1669 | + | ||
| 1670 | + // 【优化】返回flush的行数,供调用者决定渲染策略 | ||
| 1671 | + return flushedCount; | ||
| 1566 | } | 1672 | } |
| 1567 | 1673 | ||
| 1568 | maybeTrim() { | 1674 | maybeTrim() { |
| 1569 | - // 在 flush 之后调用:控制 lines 总量,减少内存 | 1675 | + // 【优化】更积极的trim策略,但保留更多行 |
| 1570 | if (this.lines.length <= this.maxLines) return; | 1676 | if (this.lines.length <= this.maxLines) return; |
| 1571 | 1677 | ||
| 1572 | const toDrop = this.lines.length - this.trimTarget; | 1678 | const toDrop = this.lines.length - this.trimTarget; |
| 1573 | if (toDrop > 0) { | 1679 | if (toDrop > 0) { |
| 1680 | + // 【优化】批量删除,性能更好 | ||
| 1574 | this.lines.splice(0, toDrop); | 1681 | this.lines.splice(0, toDrop); |
| 1575 | - // 不调整滚动位置,让用户保持当前位置或自动吸底 | 1682 | + |
| 1683 | + // 重置哈希,强制下次渲染 | ||
| 1684 | + this.lastRenderHash = null; | ||
| 1685 | + | ||
| 1686 | + console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`); | ||
| 1576 | } | 1687 | } |
| 1577 | } | 1688 | } |
| 1578 | 1689 | ||
| 1579 | scheduleRender(force = false) { | 1690 | scheduleRender(force = false) { |
| 1580 | if (!this.container) return; | 1691 | if (!this.container) return; |
| 1581 | if (!force && this.rafId) return; | 1692 | if (!force && this.rafId) return; |
| 1582 | - // 取消之前的请求,使用节流 | 1693 | + |
| 1694 | + // 取消之前的请求 | ||
| 1583 | if (this.rafId) { | 1695 | if (this.rafId) { |
| 1584 | cancelAnimationFrame(this.rafId); | 1696 | cancelAnimationFrame(this.rafId); |
| 1585 | } | 1697 | } |
| 1698 | + | ||
| 1699 | + // 【优化】使用requestAnimationFrame进行渲染调度 | ||
| 1586 | this.rafId = requestAnimationFrame(() => { | 1700 | this.rafId = requestAnimationFrame(() => { |
| 1587 | this.rafId = null; | 1701 | this.rafId = null; |
| 1702 | + | ||
| 1703 | + // 【优化】性能监控:记录渲染开始时间 | ||
| 1704 | + const renderStart = performance.now(); | ||
| 1705 | + | ||
| 1588 | this.render(); | 1706 | this.render(); |
| 1707 | + | ||
| 1708 | + // 【优化】记录渲染耗时 | ||
| 1709 | + this.renderTime = performance.now() - renderStart; | ||
| 1710 | + | ||
| 1711 | + // 【性能警告】如果渲染耗时超过16ms(一帧),输出警告 | ||
| 1712 | + if (this.renderTime > 16) { | ||
| 1713 | + console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`); | ||
| 1714 | + } | ||
| 1589 | }); | 1715 | }); |
| 1590 | } | 1716 | } |
| 1591 | 1717 | ||
| 1592 | render() { | 1718 | render() { |
| 1593 | this.flush(); | 1719 | this.flush(); |
| 1594 | const total = this.lines.length; | 1720 | const total = this.lines.length; |
| 1721 | + | ||
| 1595 | if (!total) { | 1722 | if (!total) { |
| 1596 | if (this.container.innerHTML !== '') { | 1723 | if (this.container.innerHTML !== '') { |
| 1597 | this.container.innerHTML = ''; | 1724 | this.container.innerHTML = ''; |
| @@ -1603,21 +1730,21 @@ | @@ -1603,21 +1730,21 @@ | ||
| 1603 | return; | 1730 | return; |
| 1604 | } | 1731 | } |
| 1605 | 1732 | ||
| 1606 | - // 改进内容哈希:包含总行数、前5行和后5行的摘要 | ||
| 1607 | - const hashSample = total <= 10 | ||
| 1608 | - ? this.lines.map(l => l.text).join('|') | ||
| 1609 | - : this.lines.slice(0, 5).map(l => l.text).join('|') + '|' + | ||
| 1610 | - this.lines.slice(-5).map(l => l.text).join('|'); | ||
| 1611 | - const contentHash = `${total}-${hashSample}`; | ||
| 1612 | - if (this.lastRenderHash === contentHash) { | ||
| 1613 | - // 内容没有变化,只需要处理滚动(如果需要的话) | ||
| 1614 | - if (this.needsScroll && this.autoScrollEnabled) { | ||
| 1615 | - this.scrollToBottom(); | ||
| 1616 | - } | 1733 | + // 【优化】改进内容哈希:使用总行数+最后一行文本 |
| 1734 | + const lastLine = this.lines[total - 1]; | ||
| 1735 | + const contentHash = `${total}-${lastLine ? lastLine.text : ''}`; | ||
| 1736 | + | ||
| 1737 | + // 如果需要滚动,强制渲染 | ||
| 1738 | + const forceRender = this.needsScroll && this.autoScrollEnabled; | ||
| 1739 | + | ||
| 1740 | + if (this.lastRenderHash === contentHash && !forceRender) { | ||
| 1741 | + // 内容没有变化且不需要滚动,跳过渲染 | ||
| 1617 | return; | 1742 | return; |
| 1618 | } | 1743 | } |
| 1619 | this.lastRenderHash = contentHash; | 1744 | this.lastRenderHash = contentHash; |
| 1745 | + this.lastRenderLineCount = total; | ||
| 1620 | 1746 | ||
| 1747 | + // 【优化】计算可见区域 | ||
| 1621 | const lh = this.lineHeight; | 1748 | const lh = this.lineHeight; |
| 1622 | const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; | 1749 | const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; |
| 1623 | const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); | 1750 | const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); |
| @@ -1632,10 +1759,9 @@ | @@ -1632,10 +1759,9 @@ | ||
| 1632 | 1759 | ||
| 1633 | const needed = Math.max(0, end - start); | 1760 | const needed = Math.max(0, end - start); |
| 1634 | 1761 | ||
| 1635 | - // 限制DOM节点池大小,防止内存泄漏 | 1762 | + // 【优化】限制DOM节点池大小 |
| 1636 | if (this.pool.length > this.maxPoolSize) { | 1763 | if (this.pool.length > this.maxPoolSize) { |
| 1637 | const excess = this.pool.length - this.maxPoolSize; | 1764 | const excess = this.pool.length - this.maxPoolSize; |
| 1638 | - // 移除多余的节点 | ||
| 1639 | this.pool.splice(this.maxPoolSize, excess).forEach(node => { | 1765 | this.pool.splice(this.maxPoolSize, excess).forEach(node => { |
| 1640 | if (node && node.parentNode) { | 1766 | if (node && node.parentNode) { |
| 1641 | node.parentNode.removeChild(node); | 1767 | node.parentNode.removeChild(node); |
| @@ -1643,44 +1769,51 @@ | @@ -1643,44 +1769,51 @@ | ||
| 1643 | }); | 1769 | }); |
| 1644 | } | 1770 | } |
| 1645 | 1771 | ||
| 1646 | - // 复用现有的 DOM 节点池 | ||
| 1647 | - while (this.pool.length < needed && this.pool.length < this.maxPoolSize) { | ||
| 1648 | - const node = document.createElement('div'); | ||
| 1649 | - node.className = 'console-line'; | ||
| 1650 | - this.pool.push(node); | 1772 | + // 【优化】批量创建DOM节点,减少DOM操作 |
| 1773 | + const nodesToCreate = needed - this.pool.length; | ||
| 1774 | + if (nodesToCreate > 0 && this.pool.length < this.maxPoolSize) { | ||
| 1775 | + const fragment = document.createDocumentFragment(); | ||
| 1776 | + for (let i = 0; i < Math.min(nodesToCreate, this.maxPoolSize - this.pool.length); i++) { | ||
| 1777 | + const node = document.createElement('div'); | ||
| 1778 | + node.className = 'console-line'; | ||
| 1779 | + this.pool.push(node); | ||
| 1780 | + } | ||
| 1651 | } | 1781 | } |
| 1652 | 1782 | ||
| 1653 | - // 复用或创建占位符(避免每次重建) | 1783 | + // 【优化】复用或创建占位符 |
| 1654 | if (!this.beforeSpacer) { | 1784 | if (!this.beforeSpacer) { |
| 1655 | this.beforeSpacer = document.createElement('div'); | 1785 | this.beforeSpacer = document.createElement('div'); |
| 1656 | this.beforeSpacer.dataset.spacer = 'before'; | 1786 | this.beforeSpacer.dataset.spacer = 'before'; |
| 1787 | + this.beforeSpacer.style.willChange = 'height'; // GPU加速 | ||
| 1657 | } else if (!this.beforeSpacer.parentNode) { | 1788 | } else if (!this.beforeSpacer.parentNode) { |
| 1658 | - // 如果占位符被意外移除,标记需要重建DOM | ||
| 1659 | this.beforeSpacer = document.createElement('div'); | 1789 | this.beforeSpacer = document.createElement('div'); |
| 1660 | this.beforeSpacer.dataset.spacer = 'before'; | 1790 | this.beforeSpacer.dataset.spacer = 'before'; |
| 1791 | + this.beforeSpacer.style.willChange = 'height'; | ||
| 1661 | } | 1792 | } |
| 1662 | this.beforeSpacer.style.height = `${beforeHeight}px`; | 1793 | this.beforeSpacer.style.height = `${beforeHeight}px`; |
| 1663 | 1794 | ||
| 1664 | if (!this.afterSpacer) { | 1795 | if (!this.afterSpacer) { |
| 1665 | this.afterSpacer = document.createElement('div'); | 1796 | this.afterSpacer = document.createElement('div'); |
| 1666 | this.afterSpacer.dataset.spacer = 'after'; | 1797 | this.afterSpacer.dataset.spacer = 'after'; |
| 1798 | + this.afterSpacer.style.willChange = 'height'; // GPU加速 | ||
| 1667 | } else if (!this.afterSpacer.parentNode) { | 1799 | } else if (!this.afterSpacer.parentNode) { |
| 1668 | this.afterSpacer = document.createElement('div'); | 1800 | this.afterSpacer = document.createElement('div'); |
| 1669 | this.afterSpacer.dataset.spacer = 'after'; | 1801 | this.afterSpacer.dataset.spacer = 'after'; |
| 1802 | + this.afterSpacer.style.willChange = 'height'; | ||
| 1670 | } | 1803 | } |
| 1671 | this.afterSpacer.style.height = `${afterHeight}px`; | 1804 | this.afterSpacer.style.height = `${afterHeight}px`; |
| 1672 | 1805 | ||
| 1673 | - // 使用DocumentFragment来减少DOM重绘 | 1806 | + // 【优化】使用DocumentFragment批量操作DOM |
| 1674 | const fragment = document.createDocumentFragment(); | 1807 | const fragment = document.createDocumentFragment(); |
| 1675 | 1808 | ||
| 1676 | - // 只更新可见区域的节点 | 1809 | + // 【优化】只更新可见区域的节点 |
| 1677 | for (let idx = start; idx < end; idx++) { | 1810 | for (let idx = start; idx < end; idx++) { |
| 1678 | const line = this.lines[idx]; | 1811 | const line = this.lines[idx]; |
| 1679 | const poolIdx = idx - start; | 1812 | const poolIdx = idx - start; |
| 1680 | const node = this.pool[poolIdx]; | 1813 | const node = this.pool[poolIdx]; |
| 1681 | if (!node) continue; | 1814 | if (!node) continue; |
| 1682 | 1815 | ||
| 1683 | - // 只在内容或类名变化时才更新节点 | 1816 | + // 【优化】只在内容或类名变化时才更新节点 |
| 1684 | if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) { | 1817 | if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) { |
| 1685 | node.className = line.className || 'console-line'; | 1818 | node.className = line.className || 'console-line'; |
| 1686 | node.textContent = line.text; | 1819 | node.textContent = line.text; |
| @@ -1688,33 +1821,36 @@ | @@ -1688,33 +1821,36 @@ | ||
| 1688 | fragment.appendChild(node); | 1821 | fragment.appendChild(node); |
| 1689 | } | 1822 | } |
| 1690 | 1823 | ||
| 1691 | - // 优化DOM更新:只在必要时清空容器 | ||
| 1692 | - // 检查容器是否需要重建(比如占位符丢失) | 1824 | + // 【优化】增量更新DOM,减少重绘 |
| 1693 | const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; | 1825 | const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; |
| 1826 | + | ||
| 1694 | if (needsRebuild) { | 1827 | if (needsRebuild) { |
| 1828 | + // 需要完全重建 | ||
| 1695 | this.container.innerHTML = ''; | 1829 | this.container.innerHTML = ''; |
| 1696 | this.container.appendChild(this.beforeSpacer); | 1830 | this.container.appendChild(this.beforeSpacer); |
| 1697 | this.container.appendChild(fragment); | 1831 | this.container.appendChild(fragment); |
| 1698 | this.container.appendChild(this.afterSpacer); | 1832 | this.container.appendChild(this.afterSpacer); |
| 1699 | } else { | 1833 | } else { |
| 1700 | - // 增量更新:只更新可见节点部分 | ||
| 1701 | - // 移除旧的可见节点 | 1834 | + // 【优化】只更新可见节点部分,使用更高效的方式 |
| 1702 | const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); | 1835 | const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); |
| 1703 | - existingNodes.forEach(node => { | ||
| 1704 | - if (node.parentNode === this.container) { | ||
| 1705 | - this.container.removeChild(node); | ||
| 1706 | - } | ||
| 1707 | - }); | 1836 | + |
| 1837 | + // 【优化】批量移除,减少重排 | ||
| 1838 | + if (existingNodes.length > 0) { | ||
| 1839 | + // 使用DocumentFragment收集要移除的节点 | ||
| 1840 | + existingNodes.forEach(node => { | ||
| 1841 | + if (node.parentNode === this.container) { | ||
| 1842 | + this.container.removeChild(node); | ||
| 1843 | + } | ||
| 1844 | + }); | ||
| 1845 | + } | ||
| 1846 | + | ||
| 1708 | // 在占位符之间插入新节点 | 1847 | // 在占位符之间插入新节点 |
| 1709 | this.container.insertBefore(fragment, this.afterSpacer); | 1848 | this.container.insertBefore(fragment, this.afterSpacer); |
| 1710 | } | 1849 | } |
| 1711 | 1850 | ||
| 1712 | - // 只在有标记且自动滚动启用时才滚动到底部 | 1851 | + // 【优化】如果需要滚动且自动滚动启用,立即滚动 |
| 1713 | if (this.needsScroll && this.autoScrollEnabled) { | 1852 | if (this.needsScroll && this.autoScrollEnabled) { |
| 1714 | - // 延迟执行滚动,确保 DOM 已经更新完毕 | ||
| 1715 | - requestAnimationFrame(() => { | ||
| 1716 | - this.scrollToBottom(); | ||
| 1717 | - }); | 1853 | + this.scrollToBottom(); |
| 1718 | } | 1854 | } |
| 1719 | } | 1855 | } |
| 1720 | } | 1856 | } |
| @@ -1836,6 +1972,17 @@ | @@ -1836,6 +1972,17 @@ | ||
| 1836 | // 启动所有定时器 | 1972 | // 启动所有定时器 |
| 1837 | startAllTimers(); | 1973 | startAllTimers(); |
| 1838 | 1974 | ||
| 1975 | + // 【新增】启动定期内存优化 | ||
| 1976 | + startMemoryOptimization(); | ||
| 1977 | + console.log('[性能优化] 已启动定期内存优化(每5分钟)'); | ||
| 1978 | + | ||
| 1979 | + // 【新增】将性能监控函数暴露到全局,方便调试 | ||
| 1980 | + window.getGlobalPerformanceStats = getGlobalPerformanceStats; | ||
| 1981 | + window.resetAllPerformanceStats = resetAllPerformanceStats; | ||
| 1982 | + console.log('[调试工具] 性能监控函数已挂载到window对象:'); | ||
| 1983 | + console.log(' - window.getGlobalPerformanceStats() : 查看所有渲染器性能统计'); | ||
| 1984 | + console.log(' - window.resetAllPerformanceStats() : 重置所有性能计数器'); | ||
| 1985 | + | ||
| 1839 | // 监听页面可见性变化 | 1986 | // 监听页面可见性变化 |
| 1840 | document.addEventListener('visibilitychange', handleVisibilityChange); | 1987 | document.addEventListener('visibilitychange', handleVisibilityChange); |
| 1841 | 1988 | ||
| @@ -2576,9 +2723,74 @@ | @@ -2576,9 +2723,74 @@ | ||
| 2576 | } | 2723 | } |
| 2577 | } | 2724 | } |
| 2578 | 2725 | ||
| 2726 | + // 【新增】全局性能监控函数 | ||
| 2727 | + function getGlobalPerformanceStats() { | ||
| 2728 | + console.log('=== 日志渲染器性能统计 ==='); | ||
| 2729 | + let totalMemory = 0; | ||
| 2730 | + let totalLines = 0; | ||
| 2731 | + | ||
| 2732 | + consoleLayerApps.forEach(app => { | ||
| 2733 | + const renderer = logRenderers[app]; | ||
| 2734 | + if (renderer) { | ||
| 2735 | + const stats = renderer.getPerformanceStats(); | ||
| 2736 | + console.log(`\n[${app.toUpperCase()}]:`); | ||
| 2737 | + console.log(` 总行数: ${stats.totalLines}`); | ||
| 2738 | + console.log(` 待处理行数: ${stats.pendingLines}`); | ||
| 2739 | + console.log(` 队列峰值: ${stats.pendingHighWaterMark}`); | ||
| 2740 | + console.log(` Flush次数: ${stats.flushCount}`); | ||
| 2741 | + console.log(` 上次渲染耗时: ${stats.lastRenderTime}`); | ||
| 2742 | + console.log(` 上次渲染行数: ${stats.lastRenderLineCount}`); | ||
| 2743 | + console.log(` DOM池大小: ${stats.poolSize}`); | ||
| 2744 | + console.log(` 内存估算: ${stats.memoryEstimate}`); | ||
| 2745 | + | ||
| 2746 | + totalLines += stats.totalLines; | ||
| 2747 | + // 简单累加(实际内存使用需要更精确的计算) | ||
| 2748 | + } | ||
| 2749 | + }); | ||
| 2750 | + | ||
| 2751 | + console.log(`\n=== 总计 ===`); | ||
| 2752 | + console.log(`总日志行数: ${totalLines}`); | ||
| 2753 | + console.log(`活跃渲染器: ${Object.keys(logRenderers).length}`); | ||
| 2754 | + } | ||
| 2755 | + | ||
| 2756 | + // 【新增】重置所有性能统计 | ||
| 2757 | + function resetAllPerformanceStats() { | ||
| 2758 | + consoleLayerApps.forEach(app => { | ||
| 2759 | + const renderer = logRenderers[app]; | ||
| 2760 | + if (renderer) { | ||
| 2761 | + renderer.resetPerformanceStats(); | ||
| 2762 | + } | ||
| 2763 | + }); | ||
| 2764 | + console.log('[性能统计] 已重置所有渲染器的性能统计'); | ||
| 2765 | + } | ||
| 2766 | + | ||
| 2767 | + // 【新增】定期内存优化(每5分钟检查一次) | ||
| 2768 | + function startMemoryOptimization() { | ||
| 2769 | + setInterval(() => { | ||
| 2770 | + consoleLayerApps.forEach(app => { | ||
| 2771 | + // 只优化非当前活跃的渲染器 | ||
| 2772 | + if (app !== currentApp) { | ||
| 2773 | + const renderer = logRenderers[app]; | ||
| 2774 | + if (renderer && renderer.lines.length > 0) { | ||
| 2775 | + // 如果非活跃渲染器有大量日志,进行trim | ||
| 2776 | + if (renderer.lines.length > renderer.maxLines * 0.8) { | ||
| 2777 | + const before = renderer.lines.length; | ||
| 2778 | + renderer.maybeTrim(); | ||
| 2779 | + const after = renderer.lines.length; | ||
| 2780 | + if (before > after) { | ||
| 2781 | + console.log(`[内存优化] 非活跃渲染器 ${app} 从 ${before} 行裁剪到 ${after} 行`); | ||
| 2782 | + } | ||
| 2783 | + } | ||
| 2784 | + } | ||
| 2785 | + } | ||
| 2786 | + }); | ||
| 2787 | + }, 5 * 60 * 1000); // 5分钟 | ||
| 2788 | + } | ||
| 2789 | + | ||
| 2579 | // 存储最后显示的行数,避免重复加载 | 2790 | // 存储最后显示的行数,避免重复加载 |
| 2580 | let lastLineCount = {}; | 2791 | let lastLineCount = {}; |
| 2581 | 2792 | ||
| 2793 | + | ||
| 2582 | function getConsoleContainer() { | 2794 | function getConsoleContainer() { |
| 2583 | return document.getElementById('consoleOutput'); | 2795 | return document.getElementById('consoleOutput'); |
| 2584 | } | 2796 | } |
| @@ -2604,8 +2816,9 @@ | @@ -2604,8 +2816,9 @@ | ||
| 2604 | consoleLayers[app] = layer; | 2816 | consoleLayers[app] = layer; |
| 2605 | logRenderers[app] = new LogVirtualList(layer); | 2817 | logRenderers[app] = new LogVirtualList(layer); |
| 2606 | 2818 | ||
| 2607 | - // 初始提示仅在渲染器内部渲染,不保留在 DOM | 2819 | + // 【FIX Bug #3】初始提示立即渲染,避免黑屏 |
| 2608 | logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`); | 2820 | logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`); |
| 2821 | + logRenderers[app].render(); // 立即同步渲染 | ||
| 2609 | }); | 2822 | }); |
| 2610 | 2823 | ||
| 2611 | // 不需要手动设置滚动位置,LogVirtualList会处理 | 2824 | // 不需要手动设置滚动位置,LogVirtualList会处理 |
| @@ -2661,10 +2874,22 @@ | @@ -2661,10 +2874,22 @@ | ||
| 2661 | // 触发一次渲染以确保内容正确显示 | 2874 | // 触发一次渲染以确保内容正确显示 |
| 2662 | const renderer = logRenderers[app]; | 2875 | const renderer = logRenderers[app]; |
| 2663 | if (renderer) { | 2876 | if (renderer) { |
| 2664 | - // 使用 requestAnimationFrame 确保在下一帧渲染,避免闪烁 | ||
| 2665 | - requestAnimationFrame(() => { | ||
| 2666 | - renderer.scheduleRender(true); | ||
| 2667 | - }); | 2877 | + // 【FIX Bug #1/#3】如果已有数据,立即同步渲染,避免黑屏 |
| 2878 | + if (renderer.lines.length > 0 || renderer.pending.length > 0) { | ||
| 2879 | + renderer.flush(); // 先将pending数据合并到lines | ||
| 2880 | + renderer.render(); // 立即同步渲染,不使用异步 | ||
| 2881 | + } else { | ||
| 2882 | + // 如果没有数据,显示加载提示(同步) | ||
| 2883 | + renderer.clear(`[系统] 正在加载 ${appNames[app] || app} 日志...`); | ||
| 2884 | + renderer.render(); // 立即渲染加载提示 | ||
| 2885 | + } | ||
| 2886 | + // 确保滚动到底部 | ||
| 2887 | + if (renderer.autoScrollEnabled) { | ||
| 2888 | + // 使用setTimeout确保DOM更新后再滚动 | ||
| 2889 | + setTimeout(() => { | ||
| 2890 | + renderer.scrollToBottom(); | ||
| 2891 | + }, 0); | ||
| 2892 | + } | ||
| 2668 | } | 2893 | } |
| 2669 | } | 2894 | } |
| 2670 | 2895 | ||
| @@ -2675,13 +2900,19 @@ | @@ -2675,13 +2900,19 @@ | ||
| 2675 | } | 2900 | } |
| 2676 | 2901 | ||
| 2677 | function appendConsoleTextLine(app, text, className = 'console-line') { | 2902 | function appendConsoleTextLine(app, text, className = 'console-line') { |
| 2903 | + // 【优化】添加空值检查 | ||
| 2904 | + if (!app || !text) return; | ||
| 2905 | + | ||
| 2678 | const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); | 2906 | const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); |
| 2679 | renderer.append(text, className); | 2907 | renderer.append(text, className); |
| 2680 | } | 2908 | } |
| 2681 | 2909 | ||
| 2682 | function appendConsoleElement(app, element) { | 2910 | function appendConsoleElement(app, element) { |
| 2911 | + // 【优化】添加空值检查 | ||
| 2912 | + if (!app || !element) return; | ||
| 2913 | + | ||
| 2683 | const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); | 2914 | const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); |
| 2684 | - if (!element || !renderer.container) return; | 2915 | + if (!renderer.container) return; |
| 2685 | 2916 | ||
| 2686 | // 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑 | 2917 | // 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑 |
| 2687 | const text = element.textContent || element.innerText || ''; | 2918 | const text = element.textContent || element.innerText || ''; |
| @@ -2700,29 +2931,56 @@ | @@ -2700,29 +2931,56 @@ | ||
| 2700 | loadForumLog(); | 2931 | loadForumLog(); |
| 2701 | return; | 2932 | return; |
| 2702 | } | 2933 | } |
| 2703 | - | 2934 | + |
| 2704 | if (app === 'report') { | 2935 | if (app === 'report') { |
| 2705 | loadReportLog(); | 2936 | loadReportLog(); |
| 2706 | return; | 2937 | return; |
| 2707 | } | 2938 | } |
| 2708 | - | 2939 | + |
| 2709 | fetch(`/api/output/${app}`) | 2940 | fetch(`/api/output/${app}`) |
| 2710 | .then(response => response.json()) | 2941 | .then(response => response.json()) |
| 2711 | .then(data => { | 2942 | .then(data => { |
| 2943 | + // 【FIX Bug #5】检查是否仍然是当前app,避免竞态条件 | ||
| 2944 | + // 如果用户已经切换到其他app,忽略这个响应 | ||
| 2945 | + if (currentApp !== app) { | ||
| 2946 | + console.log(`忽略${app}的日志响应(当前app是${currentApp})`); | ||
| 2947 | + return; | ||
| 2948 | + } | ||
| 2949 | + | ||
| 2712 | if (data.success && data.output.length > 0) { | 2950 | if (data.success && data.output.length > 0) { |
| 2713 | const lastCount = lastLineCount[app] || 0; | 2951 | const lastCount = lastLineCount[app] || 0; |
| 2714 | const newLines = data.output.slice(lastCount); | 2952 | const newLines = data.output.slice(lastCount); |
| 2715 | - | 2953 | + |
| 2716 | if (newLines.length > 0) { | 2954 | if (newLines.length > 0) { |
| 2717 | newLines.forEach(line => { | 2955 | newLines.forEach(line => { |
| 2718 | appendConsoleTextLine(app, line); | 2956 | appendConsoleTextLine(app, line); |
| 2719 | }); | 2957 | }); |
| 2720 | lastLineCount[app] = data.output.length; | 2958 | lastLineCount[app] = data.output.length; |
| 2959 | + | ||
| 2960 | + // 数据加载完成,更新加载提示为实际日志 | ||
| 2961 | + const renderer = logRenderers[app]; | ||
| 2962 | + if (renderer && renderer.lines.length > 0) { | ||
| 2963 | + // 移除"正在加载"提示(如果存在) | ||
| 2964 | + const firstLine = renderer.lines[0]; | ||
| 2965 | + if (firstLine && firstLine.text.includes('正在加载')) { | ||
| 2966 | + renderer.lines.shift(); // 移除第一行 | ||
| 2967 | + renderer.lastRenderHash = null; // 强制重新渲染 | ||
| 2968 | + renderer.scheduleRender(true); | ||
| 2969 | + } | ||
| 2970 | + } | ||
| 2721 | } | 2971 | } |
| 2722 | } | 2972 | } |
| 2723 | }) | 2973 | }) |
| 2724 | .catch(error => { | 2974 | .catch(error => { |
| 2725 | console.error('加载输出失败:', error); | 2975 | console.error('加载输出失败:', error); |
| 2976 | + // 加载失败时也显示错误提示 | ||
| 2977 | + if (currentApp === app) { | ||
| 2978 | + const renderer = logRenderers[app]; | ||
| 2979 | + if (renderer) { | ||
| 2980 | + renderer.clear(`[错误] 加载${appNames[app] || app}日志失败`); | ||
| 2981 | + renderer.render(); | ||
| 2982 | + } | ||
| 2983 | + } | ||
| 2726 | }); | 2984 | }); |
| 2727 | } | 2985 | } |
| 2728 | 2986 | ||
| @@ -3201,7 +3459,21 @@ | @@ -3201,7 +3459,21 @@ | ||
| 3201 | fetch('/api/forum/log') | 3459 | fetch('/api/forum/log') |
| 3202 | .then(response => response.json()) | 3460 | .then(response => response.json()) |
| 3203 | .then(data => { | 3461 | .then(data => { |
| 3204 | - if (!data.success) return; | 3462 | + // 【FIX Bug #5】检查是否仍然在forum页面 |
| 3463 | + if (currentApp !== 'forum') { | ||
| 3464 | + console.log('忽略forum日志响应(已切换到其他app)'); | ||
| 3465 | + return; | ||
| 3466 | + } | ||
| 3467 | + | ||
| 3468 | + if (!data.success) { | ||
| 3469 | + // 加载失败,显示错误 | ||
| 3470 | + const renderer = logRenderers['forum']; | ||
| 3471 | + if (renderer) { | ||
| 3472 | + renderer.clear('[错误] 加载Forum日志失败'); | ||
| 3473 | + renderer.render(); | ||
| 3474 | + } | ||
| 3475 | + return; | ||
| 3476 | + } | ||
| 3205 | 3477 | ||
| 3206 | const chatArea = document.getElementById('forumChatArea'); | 3478 | const chatArea = document.getElementById('forumChatArea'); |
| 3207 | if (chatArea) { | 3479 | if (chatArea) { |
| @@ -3213,6 +3485,7 @@ | @@ -3213,6 +3485,7 @@ | ||
| 3213 | 3485 | ||
| 3214 | if (logLines.length > 0) { | 3486 | if (logLines.length > 0) { |
| 3215 | clearConsoleLayer('forum', '[系统] Forum Engine 日志输出'); | 3487 | clearConsoleLayer('forum', '[系统] Forum Engine 日志输出'); |
| 3488 | + logRenderers['forum'].render(); // 立即渲染清空提示 | ||
| 3216 | logLines.forEach(line => appendConsoleTextLine('forum', line)); | 3489 | logLines.forEach(line => appendConsoleTextLine('forum', line)); |
| 3217 | } else { | 3490 | } else { |
| 3218 | forumLogLineCount = 0; | 3491 | forumLogLineCount = 0; |
| @@ -3226,6 +3499,14 @@ | @@ -3226,6 +3499,14 @@ | ||
| 3226 | }) | 3499 | }) |
| 3227 | .catch(error => { | 3500 | .catch(error => { |
| 3228 | console.error('加载论坛日志失败:', error); | 3501 | console.error('加载论坛日志失败:', error); |
| 3502 | + // 【优化】显示错误提示 | ||
| 3503 | + if (currentApp === 'forum') { | ||
| 3504 | + const renderer = logRenderers['forum']; | ||
| 3505 | + if (renderer) { | ||
| 3506 | + renderer.clear('[错误] 加载Forum日志失败: ' + error.message); | ||
| 3507 | + renderer.render(); | ||
| 3508 | + } | ||
| 3509 | + } | ||
| 3229 | }); | 3510 | }); |
| 3230 | } | 3511 | } |
| 3231 | 3512 | ||
| @@ -3341,9 +3622,16 @@ | @@ -3341,9 +3622,16 @@ | ||
| 3341 | fetch('/api/report/log') | 3622 | fetch('/api/report/log') |
| 3342 | .then(response => response.json()) | 3623 | .then(response => response.json()) |
| 3343 | .then(data => { | 3624 | .then(data => { |
| 3625 | + // 【FIX Bug #5】检查是否仍然在report页面 | ||
| 3626 | + if (currentApp !== 'report') { | ||
| 3627 | + console.log('忽略report日志响应(已切换到其他app)'); | ||
| 3628 | + return; | ||
| 3629 | + } | ||
| 3630 | + | ||
| 3344 | if (data.success) { | 3631 | if (data.success) { |
| 3345 | if (reportLogLineCount === 0) { | 3632 | if (reportLogLineCount === 0) { |
| 3346 | clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); | 3633 | clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); |
| 3634 | + logRenderers['report'].render(); // 立即渲染 | ||
| 3347 | } | 3635 | } |
| 3348 | 3636 | ||
| 3349 | if (data.log_lines && data.log_lines.length > 0) { | 3637 | if (data.log_lines && data.log_lines.length > 0) { |
| @@ -3353,17 +3641,43 @@ | @@ -3353,17 +3641,43 @@ | ||
| 3353 | linesToProcess.forEach(line => { | 3641 | linesToProcess.forEach(line => { |
| 3354 | appendConsoleTextLine('report', line); | 3642 | appendConsoleTextLine('report', line); |
| 3355 | }); | 3643 | }); |
| 3356 | - | 3644 | + |
| 3357 | // 重置计数器以确保后续消息能正确显示 | 3645 | // 重置计数器以确保后续消息能正确显示 |
| 3358 | reportLogLineCount = data.log_lines.length; | 3646 | reportLogLineCount = data.log_lines.length; |
| 3647 | + | ||
| 3648 | + // 移除"正在加载"提示(如果存在) | ||
| 3649 | + const renderer = logRenderers['report']; | ||
| 3650 | + if (renderer && renderer.lines.length > 0) { | ||
| 3651 | + const firstLine = renderer.lines[0]; | ||
| 3652 | + if (firstLine && firstLine.text.includes('正在加载')) { | ||
| 3653 | + renderer.lines.shift(); | ||
| 3654 | + renderer.lastRenderHash = null; | ||
| 3655 | + renderer.scheduleRender(true); | ||
| 3656 | + } | ||
| 3657 | + } | ||
| 3359 | } else { | 3658 | } else { |
| 3360 | // 如果没有日志,重置计数器 | 3659 | // 如果没有日志,重置计数器 |
| 3361 | reportLogLineCount = 0; | 3660 | reportLogLineCount = 0; |
| 3362 | } | 3661 | } |
| 3662 | + } else { | ||
| 3663 | + // 【优化】加载失败显示错误 | ||
| 3664 | + const renderer = logRenderers['report']; | ||
| 3665 | + if (renderer && currentApp === 'report') { | ||
| 3666 | + renderer.clear('[错误] 加载Report日志失败'); | ||
| 3667 | + renderer.render(); | ||
| 3668 | + } | ||
| 3363 | } | 3669 | } |
| 3364 | }) | 3670 | }) |
| 3365 | .catch(error => { | 3671 | .catch(error => { |
| 3366 | console.error('加载Report日志失败:', error); | 3672 | console.error('加载Report日志失败:', error); |
| 3673 | + // 【优化】显示错误提示 | ||
| 3674 | + if (currentApp === 'report') { | ||
| 3675 | + const renderer = logRenderers['report']; | ||
| 3676 | + if (renderer) { | ||
| 3677 | + renderer.clear('[错误] 加载Report日志失败: ' + error.message); | ||
| 3678 | + renderer.render(); | ||
| 3679 | + } | ||
| 3680 | + } | ||
| 3367 | }); | 3681 | }); |
| 3368 | } | 3682 | } |
| 3369 | 3683 |
-
Please register or login to post a comment