马一丁

Optimize Log Output Efficiency

@@ -372,49 +372,50 @@ def parse_forum_log_line(line): @@ -372,49 +372,50 @@ def parse_forum_log_line(line):
372 return None 372 return None
373 373
374 # Forum日志监听器 374 # Forum日志监听器
  375 +# 存储每个客户端的历史日志发送位置
  376 +forum_log_positions = {}
  377 +
375 def monitor_forum_log(): 378 def monitor_forum_log():
376 """监听forum.log文件变化并推送到前端""" 379 """监听forum.log文件变化并推送到前端"""
377 import time 380 import time
378 from pathlib import Path 381 from pathlib import Path
379 - 382 +
380 forum_log_file = LOG_DIR / "forum.log" 383 forum_log_file = LOG_DIR / "forum.log"
381 last_position = 0 384 last_position = 0
382 processed_lines = set() # 用于跟踪已处理的行,避免重复 385 processed_lines = set() # 用于跟踪已处理的行,避免重复
383 -  
384 - # 如果文件存在,获取初始位置 386 +
  387 + # 如果文件存在,获取初始位置但不跳过内容
385 if forum_log_file.exists(): 388 if forum_log_file.exists():
386 with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f: 389 with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
387 - # 初始化时读取所有现有行,避免重复处理  
388 - existing_lines = f.readlines()  
389 - for line in existing_lines:  
390 - line_hash = hash(line.strip())  
391 - processed_lines.add(line_hash) 390 + # 记录文件大小,但不添加到processed_lines
  391 + # 这样用户打开forum标签时可以获取历史
  392 + f.seek(0, 2) # 移到文件末尾
392 last_position = f.tell() 393 last_position = f.tell()
393 - 394 +
394 while True: 395 while True:
395 try: 396 try:
396 if forum_log_file.exists(): 397 if forum_log_file.exists():
397 with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f: 398 with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
398 f.seek(last_position) 399 f.seek(last_position)
399 new_lines = f.readlines() 400 new_lines = f.readlines()
400 - 401 +
401 if new_lines: 402 if new_lines:
402 for line in new_lines: 403 for line in new_lines:
403 line = line.rstrip('\n\r') 404 line = line.rstrip('\n\r')
404 if line.strip(): 405 if line.strip():
405 line_hash = hash(line.strip()) 406 line_hash = hash(line.strip())
406 - 407 +
407 # 避免重复处理同一行 408 # 避免重复处理同一行
408 if line_hash in processed_lines: 409 if line_hash in processed_lines:
409 continue 410 continue
410 - 411 +
411 processed_lines.add(line_hash) 412 processed_lines.add(line_hash)
412 - 413 +
413 # 解析日志行并发送forum消息 414 # 解析日志行并发送forum消息
414 parsed_message = parse_forum_log_line(line) 415 parsed_message = parse_forum_log_line(line)
415 if parsed_message: 416 if parsed_message:
416 socketio.emit('forum_message', parsed_message) 417 socketio.emit('forum_message', parsed_message)
417 - 418 +
418 # 只有在控制台显示forum时才发送控制台消息 419 # 只有在控制台显示forum时才发送控制台消息
419 timestamp = datetime.now().strftime('%H:%M:%S') 420 timestamp = datetime.now().strftime('%H:%M:%S')
420 formatted_line = f"[{timestamp}] {line}" 421 formatted_line = f"[{timestamp}] {line}"
@@ -422,13 +423,15 @@ def monitor_forum_log(): @@ -422,13 +423,15 @@ def monitor_forum_log():
422 'app': 'forum', 423 'app': 'forum',
423 'line': formatted_line 424 'line': formatted_line
424 }) 425 })
425 - 426 +
426 last_position = f.tell() 427 last_position = f.tell()
427 - 428 +
428 # 清理processed_lines集合,避免内存泄漏(保留最近1000行的哈希) 429 # 清理processed_lines集合,避免内存泄漏(保留最近1000行的哈希)
429 if len(processed_lines) > 1000: 430 if len(processed_lines) > 1000:
430 - processed_lines.clear()  
431 - 431 + # 保留最近500行的哈希
  432 + recent_hashes = list(processed_lines)[-500:]
  433 + processed_lines = set(recent_hashes)
  434 +
432 time.sleep(1) # 每秒检查一次 435 time.sleep(1) # 每秒检查一次
433 except Exception as e: 436 except Exception as e:
434 logger.error(f"Forum日志监听错误: {e}") 437 logger.error(f"Forum日志监听错误: {e}")
@@ -903,6 +906,57 @@ def get_forum_log(): @@ -903,6 +906,57 @@ def get_forum_log():
903 except Exception as e: 906 except Exception as e:
904 return jsonify({'success': False, 'message': f'读取forum.log失败: {str(e)}'}) 907 return jsonify({'success': False, 'message': f'读取forum.log失败: {str(e)}'})
905 908
  909 +@app.route('/api/forum/log/history', methods=['POST'])
  910 +def get_forum_log_history():
  911 + """获取Forum历史日志(支持从指定位置开始)"""
  912 + try:
  913 + data = request.get_json()
  914 + start_position = data.get('position', 0) # 客户端上次接收的位置
  915 + max_lines = data.get('max_lines', 1000) # 最多返回的行数
  916 +
  917 + forum_log_file = LOG_DIR / "forum.log"
  918 + if not forum_log_file.exists():
  919 + return jsonify({
  920 + 'success': True,
  921 + 'log_lines': [],
  922 + 'position': 0,
  923 + 'has_more': False
  924 + })
  925 +
  926 + with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
  927 + # 从指定位置开始读取
  928 + f.seek(start_position)
  929 + lines = []
  930 + line_count = 0
  931 +
  932 + for line in f:
  933 + if line_count >= max_lines:
  934 + break
  935 + line = line.rstrip('\n\r')
  936 + if line.strip():
  937 + # 添加时间戳
  938 + timestamp = datetime.now().strftime('%H:%M:%S')
  939 + formatted_line = f"[{timestamp}] {line}"
  940 + lines.append(formatted_line)
  941 + line_count += 1
  942 +
  943 + # 记录当前位置
  944 + current_position = f.tell()
  945 +
  946 + # 检查是否还有更多内容
  947 + f.seek(0, 2) # 移到文件末尾
  948 + end_position = f.tell()
  949 + has_more = current_position < end_position
  950 +
  951 + return jsonify({
  952 + 'success': True,
  953 + 'log_lines': lines,
  954 + 'position': current_position,
  955 + 'has_more': has_more
  956 + })
  957 + except Exception as e:
  958 + return jsonify({'success': False, 'message': f'读取forum历史失败: {str(e)}'})
  959 +
906 @app.route('/api/search', methods=['POST']) 960 @app.route('/api/search', methods=['POST'])
907 def search(): 961 def search():
908 """统一搜索接口""" 962 """统一搜索接口"""
@@ -329,7 +329,7 @@ @@ -329,7 +329,7 @@
329 } 329 }
330 330
331 .console-layer { 331 .console-layer {
332 - visibility: hidden; /* 使用visibility替代display,避免重排 */ 332 + /* 【优化】使用transform代替visibility,GPU加速避免重绘 */
333 position: absolute; /* 相对于.console-output绝对定位 */ 333 position: absolute; /* 相对于.console-output绝对定位 */
334 top: 0; 334 top: 0;
335 left: 0; 335 left: 0;
@@ -338,20 +338,56 @@ @@ -338,20 +338,56 @@
338 padding: 15px; /* 图层内边距 */ 338 padding: 15px; /* 图层内边距 */
339 overflow-y: auto; /* 允许独立滚动 */ 339 overflow-y: auto; /* 允许独立滚动 */
340 overflow-x: hidden; 340 overflow-x: hidden;
341 - pointer-events: none; /* 隐藏层不响应交互 */  
342 box-sizing: border-box; /* 包含padding在width/height内 */ 341 box-sizing: border-box; /* 包含padding在width/height内 */
  342 + /* GPU加速优化 */
  343 + transform: translateX(100%); /* 默认移出视图 */
  344 + will-change: transform; /* 提示浏览器优化transform */
  345 + backface-visibility: hidden; /* 避免闪烁 */
  346 + -webkit-backface-visibility: hidden;
  347 + opacity: 0; /* 配合transform使用 */
  348 + pointer-events: none; /* 隐藏层不响应交互 */
  349 + /* 平滑切换 */
  350 + transition: transform 0.15s ease-out, opacity 0.15s ease-out;
343 } 351 }
344 352
345 .console-layer.active { 353 .console-layer.active {
346 - visibility: visible; /* 显示活动层 */  
347 - pointer-events: auto; /* 活动层响应交互 */  
348 - z-index: 1; /* 置顶显示 */ 354 + /* 【优化】活动层使用transform归位,高性能切换 */
  355 + transform: translateX(0); /* 移回视图 */
  356 + opacity: 1; /* 完全可见 */
  357 + pointer-events: auto; /* 活动层响应交互 */
  358 + z-index: 1; /* 置顶显示 */
349 } 359 }
350 360
351 .console-line { 361 .console-line {
352 margin-bottom: 2px; 362 margin-bottom: 2px;
353 } 363 }
354 364
  365 + /* 【新增】加载状态指示器样式 */
  366 + .console-line.loading-indicator {
  367 + color: #00ff00;
  368 + font-style: italic;
  369 + animation: pulse 1.5s ease-in-out infinite;
  370 + }
  371 +
  372 + .console-line.render-progress {
  373 + color: #00ff00;
  374 + background: linear-gradient(90deg, rgba(0,255,0,0.1) 0%, transparent 100%);
  375 + padding: 2px 5px;
  376 + border-left: 3px solid #00ff00;
  377 + font-weight: bold;
  378 + }
  379 +
  380 + @keyframes pulse {
  381 + 0%, 100% { opacity: 0.6; }
  382 + 50% { opacity: 1; }
  383 + }
  384 +
  385 + /* 渐进式渲染时的占位符 */
  386 + .console-line.placeholder {
  387 + opacity: 0.5;
  388 + font-style: italic;
  389 + }
  390 +
355 /* 状态信息 */ 391 /* 状态信息 */
356 .status-bar { 392 .status-bar {
357 padding: 10px 20px; 393 padding: 10px 20px;
@@ -1379,8 +1415,8 @@ @@ -1379,8 +1415,8 @@
1379 this.pool = []; 1415 this.pool = [];
1380 this.lineHeight = 18; 1416 this.lineHeight = 18;
1381 this.maxVisible = 120; 1417 this.maxVisible = 120;
1382 - this.maxLines = 1000; // 【优化】增加到1000行,提高缓存能力  
1383 - this.trimTarget = 600; // 【优化】裁剪后保留600行 1418 + this.maxLines = 10000; // 【优化】保留10000行历史,平衡内存和使用体验
  1419 + this.trimTarget = 8000; // 【优化】裁剪后保留8000行,避免频繁触发trim
1384 this.maxPoolSize = 200; // 限制DOM节点池大小 1420 this.maxPoolSize = 200; // 限制DOM节点池大小
1385 this.rafId = null; 1421 this.rafId = null;
1386 this.autoScrollEnabled = true; 1422 this.autoScrollEnabled = true;
@@ -1394,9 +1430,9 @@ @@ -1394,9 +1430,9 @@
1394 this.beforeSpacer = null; 1430 this.beforeSpacer = null;
1395 this.afterSpacer = null; 1431 this.afterSpacer = null;
1396 1432
1397 - // 【新增】批处理优化参数  
1398 - this.batchThreshold = 200; // 累积200行才flush(原50行)  
1399 - this.batchDelay = 500; // 延迟500ms才flush(原200ms) 1433 + // 【优化】批处理参数 - 降低延迟提升响应速度
  1434 + this.batchThreshold = 50; // 累积50行就flush,减少延迟
  1435 + this.batchDelay = 100; // 延迟100ms就flush,大幅降低延迟
1400 this.lastFlushTime = 0; 1436 this.lastFlushTime = 0;
1401 this.flushCount = 0; 1437 this.flushCount = 0;
1402 1438
@@ -1560,39 +1596,40 @@ @@ -1560,39 +1596,40 @@
1560 scrollToBottom() { 1596 scrollToBottom() {
1561 if (!this.scrollElement) return; 1597 if (!this.scrollElement) return;
1562 1598
1563 - // 【FIX Bug #6】使用try-catch防止死锁  
1564 - try {  
1565 - // 使用锁防止重入  
1566 - if (this.scrollLocked) return;  
1567 - this.scrollLocked = true;  
1568 -  
1569 - // 【FIX Bug #2】不使用节流,确保每次调用都能滚动  
1570 - // 移除节流逻辑,因为scheduleRender已经有节流了 1599 + // 【优化】防抖机制,避免频繁滚动
  1600 + if (this.scrollTimer) {
  1601 + clearTimeout(this.scrollTimer);
  1602 + }
1571 1603
1572 - // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁  
1573 - requestAnimationFrame(() => {  
1574 - try {  
1575 - if (!this.scrollElement) {  
1576 - this.scrollLocked = false;  
1577 - return; 1604 + // 使用微任务延迟,减少layout thrashing
  1605 + this.scrollTimer = setTimeout(() => {
  1606 + if (!this.scrollElement) return;
  1607 +
  1608 + // 【优化】批量读取layout属性,减少重排
  1609 + const scrollData = {
  1610 + scrollHeight: this.scrollElement.scrollHeight,
  1611 + clientHeight: this.scrollElement.clientHeight,
  1612 + currentScroll: this.scrollElement.scrollTop
  1613 + };
  1614 +
  1615 + // 计算目标位置
  1616 + const targetScroll = scrollData.scrollHeight - scrollData.clientHeight;
  1617 +
  1618 + // 只在需要时滚动
  1619 + if (Math.abs(scrollData.currentScroll - targetScroll) > 1) {
  1620 + // 使用requestAnimationFrame确保在合适的时机滚动
  1621 + requestAnimationFrame(() => {
  1622 + if (this.scrollElement) {
  1623 + this.scrollElement.scrollTop = targetScroll;
1578 } 1624 }
1579 -  
1580 - // 直接滚动到底部,不使用平滑滚动以避免性能问题  
1581 - const targetScroll = this.scrollElement.scrollHeight;  
1582 - this.scrollElement.scrollTop = targetScroll;  
1583 -  
1584 - // 【FIX Bug #2】立即重置标志,不延迟  
1585 - this.scrollLocked = false;  
1586 this.needsScroll = false; 1625 this.needsScroll = false;
1587 - } catch (e) {  
1588 - console.error('滚动到底部失败:', e);  
1589 - this.scrollLocked = false; // 确保锁被释放  
1590 - }  
1591 - });  
1592 - } catch (e) {  
1593 - console.error('scrollToBottom失败:', e);  
1594 - this.scrollLocked = false; // 确保锁被释放  
1595 - } 1626 + });
  1627 + } else {
  1628 + this.needsScroll = false;
  1629 + }
  1630 +
  1631 + this.scrollTimer = null;
  1632 + }, 16); // 约1帧的时间,减少频繁触发
1596 } 1633 }
1597 1634
1598 setLineHeight(px) { 1635 setLineHeight(px) {
@@ -1600,20 +1637,67 @@ @@ -1600,20 +1637,67 @@
1600 } 1637 }
1601 1638
1602 /** 1639 /**
1603 - * 【图层优化】设置窗口激活状态 1640 + * 【优化】设置窗口激活状态,分批异步渲染积压内容
1604 * @param {boolean} active - 是否为活动窗口 1641 * @param {boolean} active - 是否为活动窗口
1605 */ 1642 */
1606 setActive(active) { 1643 setActive(active) {
  1644 + const wasInactive = !this.isActive;
1607 this.isActive = active; 1645 this.isActive = active;
1608 - if (active && this.needsRender) {  
1609 - // 窗口激活时,如果有待渲染内容,异步渲染  
1610 - requestIdleCallback(() => {  
1611 - if (this.pending.length > 0) {  
1612 - this.flush();  
1613 - }  
1614 - this.scheduleRender(true); 1646 +
  1647 + if (active) {
  1648 + // 【修复】窗口激活时,清除渲染哈希,确保强制渲染
  1649 + // 这解决了需要滚动才能显示内容的Bug
  1650 + if (wasInactive) {
  1651 + this.lastRenderHash = null; // 清除哈希,强制重新渲染
  1652 + console.log('[窗口激活] 清除渲染哈希,准备强制渲染');
  1653 + }
  1654 +
  1655 + // 处理积压的pending数据
  1656 + if (this.pending.length > 0) {
  1657 + // 窗口激活时,分批处理积压内容
  1658 + const batchSize = 100; // 每批处理100行
  1659 + const renderBatch = () => {
  1660 + if (this.pending.length > 0) {
  1661 + // 取出一批数据进行flush
  1662 + const batch = this.pending.splice(0, Math.min(batchSize, this.pending.length));
  1663 + this.lines.push(...batch);
  1664 + this.flushCount++;
  1665 + this.lastFlushTime = Date.now();
  1666 +
  1667 + // 如果还有剩余,继续下一批
  1668 + if (this.pending.length > 0) {
  1669 + requestAnimationFrame(renderBatch);
  1670 + } else {
  1671 + // 所有数据处理完,执行渲染
  1672 + this.needsRender = false;
  1673 + // 根据数据量选择渲染方式
  1674 + if (this.lines.length > 1000) {
  1675 + this.progressiveRender(); // 大量数据用渐进式渲染
  1676 + } else {
  1677 + this.scheduleRender(true); // 强制渲染
  1678 + }
  1679 + }
  1680 + }
  1681 + };
  1682 + requestAnimationFrame(renderBatch);
  1683 + } else if (this.needsRender || wasInactive) {
  1684 + // 【关键修复】即使needsRender为false,从非活动切换到活动也要渲染
1615 this.needsRender = false; 1685 this.needsRender = false;
1616 - }, { timeout: 50 }); 1686 +
  1687 + // 强制渲染一次,确保内容显示
  1688 + if (this.lines.length > 1000) {
  1689 + this.progressiveRender(); // 大量数据用渐进式渲染
  1690 + } else {
  1691 + this.scheduleRender(true); // 强制渲染
  1692 + }
  1693 +
  1694 + // 如果需要自动滚动到底部
  1695 + if (this.autoScrollEnabled && this.lines.length > 0) {
  1696 + requestAnimationFrame(() => {
  1697 + this.scrollToBottom();
  1698 + });
  1699 + }
  1700 + }
1617 } 1701 }
1618 } 1702 }
1619 1703
@@ -1630,15 +1714,23 @@ @@ -1630,15 +1714,23 @@
1630 this.pendingHighWaterMark = this.pending.length; 1714 this.pendingHighWaterMark = this.pending.length;
1631 } 1715 }
1632 1716
1633 - // 【图层优化】非活动窗口处理策略 1717 + // 【优化】非活动窗口延迟渲染策略
1634 if (!this.isActive) { 1718 if (!this.isActive) {
1635 - // 非活动窗口:只累积数据,不触发渲染  
1636 - // 设置队列上限,防止内存溢出  
1637 - if (this.pending.length >= 1000) {  
1638 - this.flush(); // 定期flush避免内存溢出 1719 + // 非活动窗口:延迟渲染,但不完全停止
  1720 + // 每500ms或累积100行就flush一次,保持数据流动
  1721 + if (this.pending.length >= 100 ||
  1722 + (this.pending.length > 0 && Date.now() - this.lastFlushTime > 500)) {
  1723 + this.flush(); // 定期flush,避免积压太多
1639 this.needsRender = true; // 标记需要渲染 1724 this.needsRender = true; // 标记需要渲染
1640 } 1725 }
1641 - return; // 跳过后续渲染逻辑 1726 + // 不立即渲染,但设置延迟渲染
  1727 + if (this.pending.length === 1 && !this.flushTimer) {
  1728 + this.flushTimer = setTimeout(() => {
  1729 + this.flush();
  1730 + this.needsRender = true;
  1731 + }, 500); // 非活动窗口500ms延迟
  1732 + }
  1733 + return; // 跳过立即渲染
1642 } 1734 }
1643 1735
1644 // 【优化】活动窗口:智能批处理策略 1736 // 【优化】活动窗口:智能批处理策略
@@ -1656,10 +1748,10 @@ @@ -1656,10 +1748,10 @@
1656 clearTimeout(this.flushTimer); 1748 clearTimeout(this.flushTimer);
1657 } 1749 }
1658 1750
1659 - // 【优化】自适应延迟:如果最近flush频繁,说明日志流量大,缩短延迟 1751 + // 【优化】自适应延迟:根据流量动态调整延迟,提升响应速度
1660 const adaptiveDelay = (timeSinceLastFlush < 1000) 1752 const adaptiveDelay = (timeSinceLastFlush < 1000)
1661 - ? Math.max(100, this.batchDelay / 2) // 高流量:缩短延迟  
1662 - : this.batchDelay; // 正常流量:使用标准延迟 1753 + ? Math.max(20, this.batchDelay / 4) // 高流量:大幅缩短延迟至20-25ms
  1754 + : this.batchDelay; // 正常流量:使用标准延迟100ms
1663 1755
1664 this.flushTimer = setTimeout(() => { 1756 this.flushTimer = setTimeout(() => {
1665 this.flush(); 1757 this.flush();
@@ -1715,7 +1807,7 @@ @@ -1715,7 +1807,7 @@
1715 } 1807 }
1716 1808
1717 maybeTrim() { 1809 maybeTrim() {
1718 - // 【优化】更积极的trim策略,但保留更多行 1810 + // 【优化】智能内存管理策略
1719 if (this.lines.length <= this.maxLines) return; 1811 if (this.lines.length <= this.maxLines) return;
1720 1812
1721 const toDrop = this.lines.length - this.trimTarget; 1813 const toDrop = this.lines.length - this.trimTarget;
@@ -1727,6 +1819,12 @@ @@ -1727,6 +1819,12 @@
1727 this.lastRenderHash = null; 1819 this.lastRenderHash = null;
1728 1820
1729 console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`); 1821 console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`);
  1822 +
  1823 + // 【优化】内存使用超过阈值时,强制垃圾回收提示
  1824 + const estimatedMemory = this.lines.length * 100 + this.pending.length * 100;
  1825 + if (estimatedMemory > 5 * 1024 * 1024) { // 超过5MB
  1826 + console.warn('[内存警告] 日志内存使用较高,建议刷新页面');
  1827 + }
1730 } 1828 }
1731 } 1829 }
1732 1830
@@ -1756,13 +1854,21 @@ @@ -1756,13 +1854,21 @@
1756 // 【优化】性能监控:记录渲染开始时间 1854 // 【优化】性能监控:记录渲染开始时间
1757 const renderStart = performance.now(); 1855 const renderStart = performance.now();
1758 1856
1759 - this.render(); 1857 + // 【优化】根据数据量选择渲染策略
  1858 + const totalLines = this.lines.length + this.pending.length;
  1859 + if (totalLines > 1000) {
  1860 + // 大量数据使用渐进式渲染
  1861 + this.progressiveRender();
  1862 + } else {
  1863 + // 少量数据直接渲染
  1864 + this.render();
  1865 + }
1760 1866
1761 // 【优化】记录渲染耗时 1867 // 【优化】记录渲染耗时
1762 this.renderTime = performance.now() - renderStart; 1868 this.renderTime = performance.now() - renderStart;
1763 1869
1764 // 【性能警告】如果渲染耗时超过16ms(一帧),输出警告 1870 // 【性能警告】如果渲染耗时超过16ms(一帧),输出警告
1765 - if (this.renderTime > 16) { 1871 + if (this.renderTime > 16 && totalLines < 1000) {
1766 console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`); 1872 console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`);
1767 } 1873 }
1768 }); 1874 });
@@ -1783,15 +1889,16 @@ @@ -1783,15 +1889,16 @@
1783 return; 1889 return;
1784 } 1890 }
1785 1891
1786 - // 【优化】改进内容哈希:使用总行数+最后一行文本 1892 + // 【优化】改进内容哈希:包含活动状态,确保窗口切换时渲染
1787 const lastLine = this.lines[total - 1]; 1893 const lastLine = this.lines[total - 1];
1788 - const contentHash = `${total}-${lastLine ? lastLine.text : ''}`; 1894 + const contentHash = `${total}-${lastLine ? lastLine.text : ''}-${this.isActive}`;
1789 1895
1790 - // 如果需要滚动,强制渲染  
1791 - const forceRender = this.needsScroll && this.autoScrollEnabled; 1896 + // 检查是否需要强制渲染
  1897 + const forceRender = (this.needsScroll && this.autoScrollEnabled) ||
  1898 + !this.container.querySelector('.console-line'); // DOM为空时强制渲染
1792 1899
1793 if (this.lastRenderHash === contentHash && !forceRender) { 1900 if (this.lastRenderHash === contentHash && !forceRender) {
1794 - // 内容没有变化且不需要滚动,跳过渲染 1901 + // 内容没有变化且不需要强制渲染,跳过
1795 return; 1902 return;
1796 } 1903 }
1797 this.lastRenderHash = contentHash; 1904 this.lastRenderHash = contentHash;
@@ -1802,7 +1909,17 @@ @@ -1802,7 +1909,17 @@
1802 const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; 1909 const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1;
1803 const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); 1910 const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
1804 1911
1805 - const scrollTop = (this.scrollElement && this.scrollElement.scrollTop) || 0; 1912 + let scrollTop = (this.scrollElement && this.scrollElement.scrollTop) || 0;
  1913 +
  1914 + // 【优化】初始渲染时,如果需要自动滚动,从底部开始显示
  1915 + // 这样用户能看到最新的日志而不是最旧的
  1916 + if (scrollTop === 0 && this.autoScrollEnabled && total > visible) {
  1917 + // 模拟滚动到底部的scrollTop值
  1918 + scrollTop = Math.max(0, (total - visible) * lh);
  1919 + // 标记需要实际滚动
  1920 + this.needsScroll = true;
  1921 + }
  1922 +
1806 const halfVisible = Math.floor(visible / 2); 1923 const halfVisible = Math.floor(visible / 2);
1807 const rawStart = Math.floor(scrollTop / lh) - halfVisible; 1924 const rawStart = Math.floor(scrollTop / lh) - halfVisible;
1808 const start = Math.max(0, Math.min(total, rawStart)); 1925 const start = Math.max(0, Math.min(total, rawStart));
@@ -1878,27 +1995,128 @@ @@ -1878,27 +1995,128 @@
1878 const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; 1995 const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode;
1879 1996
1880 if (needsRebuild) { 1997 if (needsRebuild) {
1881 - // 需要完全重建  
1882 - this.container.innerHTML = '';  
1883 - this.container.appendChild(this.beforeSpacer);  
1884 - this.container.appendChild(fragment);  
1885 - this.container.appendChild(this.afterSpacer); 1998 + // 【优化】双缓冲渲染 - 避免黑屏空窗期
  1999 + // 先准备新内容,再一次性替换,保持界面始终有内容显示
  2000 +
  2001 + // 如果容器有内容且是大量日志,显示加载提示
  2002 + if (this.container.childNodes.length > 0 && total > 500) {
  2003 + // 创建加载提示
  2004 + const loadingDiv = document.createElement('div');
  2005 + loadingDiv.className = 'console-line loading-indicator';
  2006 + loadingDiv.textContent = `[系统] 正在渲染 ${total} 行日志,请稍候...`;
  2007 + loadingDiv.style.opacity = '0.7';
  2008 +
  2009 + // 只在容器为空或没有加载提示时添加
  2010 + if (!this.container.querySelector('.loading-indicator')) {
  2011 + this.container.insertBefore(loadingDiv, this.container.firstChild);
  2012 + }
  2013 + }
  2014 +
  2015 + // 使用requestAnimationFrame确保加载提示显示
  2016 + requestAnimationFrame(() => {
  2017 + // 创建新的内容容器
  2018 + const newContent = document.createDocumentFragment();
  2019 +
  2020 + // 添加beforeSpacer
  2021 + newContent.appendChild(this.beforeSpacer);
  2022 +
  2023 + // 添加可见内容
  2024 + newContent.appendChild(fragment);
  2025 +
  2026 + // 添加afterSpacer
  2027 + newContent.appendChild(this.afterSpacer);
  2028 +
  2029 + // 一次性替换所有子节点,避免闪烁
  2030 + // replaceChildren 是原子操作,比 innerHTML = '' 更高效
  2031 + this.container.replaceChildren(...newContent.childNodes);
  2032 +
  2033 + // 如果需要滚动到底部,延迟执行避免影响渲染
  2034 + if (this.needsScroll && this.autoScrollEnabled) {
  2035 + requestAnimationFrame(() => {
  2036 + this.scrollToBottom();
  2037 + });
  2038 + }
  2039 + });
1886 } else { 2040 } else {
1887 - // 【优化】只更新可见节点部分,使用更高效的方式 2041 + // 【优化】增量更新:智能diff算法,只更新必要的节点
1888 const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); 2042 const existingNodes = Array.from(this.container.querySelectorAll('.console-line'));
1889 2043
1890 - // 【优化】批量移除,减少重排  
1891 - if (existingNodes.length > 0) {  
1892 - // 使用DocumentFragment收集要移除的节点  
1893 - existingNodes.forEach(node => {  
1894 - if (node.parentNode === this.container) { 2044 + // 计算需要的节点数量
  2045 + const needCount = end - start;
  2046 + const existCount = existingNodes.length;
  2047 +
  2048 + if (existCount === needCount) {
  2049 + // 节点数量相同,直接替换内容
  2050 + let i = 0;
  2051 + for (let idx = start; idx < end; idx++) {
  2052 + const line = this.lines[idx];
  2053 + const node = existingNodes[i];
  2054 + if (node) {
  2055 + // 只在内容变化时更新
  2056 + if (node.textContent !== line.text) {
  2057 + node.textContent = line.text;
  2058 + }
  2059 + if (node.className !== (line.className || 'console-line')) {
  2060 + node.className = line.className || 'console-line';
  2061 + }
  2062 + }
  2063 + i++;
  2064 + }
  2065 + } else if (existCount > needCount) {
  2066 + // 节点过多,移除多余的
  2067 + for (let i = needCount; i < existCount; i++) {
  2068 + const node = existingNodes[i];
  2069 + if (node && node.parentNode === this.container) {
1895 this.container.removeChild(node); 2070 this.container.removeChild(node);
1896 } 2071 }
1897 - }); 2072 + }
  2073 + // 更新保留的节点内容
  2074 + let i = 0;
  2075 + for (let idx = start; idx < end && i < needCount; idx++) {
  2076 + const line = this.lines[idx];
  2077 + const node = existingNodes[i];
  2078 + if (node) {
  2079 + if (node.textContent !== line.text) {
  2080 + node.textContent = line.text;
  2081 + }
  2082 + if (node.className !== (line.className || 'console-line')) {
  2083 + node.className = line.className || 'console-line';
  2084 + }
  2085 + }
  2086 + i++;
  2087 + }
  2088 + } else {
  2089 + // 节点不足,复用现有的并添加新的
  2090 + // 先更新现有节点
  2091 + let i = 0;
  2092 + for (; i < existCount; i++) {
  2093 + const line = this.lines[start + i];
  2094 + const node = existingNodes[i];
  2095 + if (node && line) {
  2096 + if (node.textContent !== line.text) {
  2097 + node.textContent = line.text;
  2098 + }
  2099 + if (node.className !== (line.className || 'console-line')) {
  2100 + node.className = line.className || 'console-line';
  2101 + }
  2102 + }
  2103 + }
  2104 + // 添加不足的节点
  2105 + const newFragment = document.createDocumentFragment();
  2106 + for (let idx = start + existCount; idx < end; idx++) {
  2107 + const line = this.lines[idx];
  2108 + const poolIdx = idx - start;
  2109 + const node = this.pool[poolIdx];
  2110 + if (node) {
  2111 + node.className = line.className || 'console-line';
  2112 + node.textContent = line.text;
  2113 + newFragment.appendChild(node);
  2114 + }
  2115 + }
  2116 + if (newFragment.childNodes.length > 0) {
  2117 + this.container.insertBefore(newFragment, this.afterSpacer);
  2118 + }
1898 } 2119 }
1899 -  
1900 - // 在占位符之间插入新节点  
1901 - this.container.insertBefore(fragment, this.afterSpacer);  
1902 } 2120 }
1903 2121
1904 // 【优化】如果需要滚动且自动滚动启用,立即滚动 2122 // 【优化】如果需要滚动且自动滚动启用,立即滚动
@@ -1906,6 +2124,104 @@ @@ -1906,6 +2124,104 @@
1906 this.scrollToBottom(); 2124 this.scrollToBottom();
1907 } 2125 }
1908 } 2126 }
  2127 +
  2128 + /**
  2129 + * 【新增】渐进式渲染方法 - 处理大量日志时分批渲染
  2130 + * 避免一次性渲染大量内容导致的卡顿和空窗期
  2131 + */
  2132 + progressiveRender() {
  2133 + if (!this.container || this.isProgressiveRendering) return;
  2134 +
  2135 + this.isProgressiveRendering = true;
  2136 + const total = this.lines.length;
  2137 +
  2138 + // 只对大量日志启用渐进式渲染
  2139 + if (total <= 500) {
  2140 + this.render();
  2141 + this.isProgressiveRendering = false;
  2142 + return;
  2143 + }
  2144 +
  2145 + console.log(`[渐进式渲染] 开始渲染 ${total} 行日志`);
  2146 +
  2147 + // 分批参数
  2148 + const batchSize = 200; // 每批渲染200行
  2149 + let currentBatch = 0;
  2150 + const totalBatches = Math.ceil(total / batchSize);
  2151 +
  2152 + // 显示渲染进度
  2153 + const showProgress = () => {
  2154 + const progress = Math.round((currentBatch / totalBatches) * 100);
  2155 + const progressDiv = this.container.querySelector('.render-progress');
  2156 + if (progressDiv) {
  2157 + progressDiv.textContent = `[系统] 渲染进度: ${progress}% (${Math.min(currentBatch * batchSize, total)}/${total} 行)`;
  2158 + } else {
  2159 + const newProgressDiv = document.createElement('div');
  2160 + newProgressDiv.className = 'console-line render-progress';
  2161 + newProgressDiv.style.color = '#00ff00';
  2162 + newProgressDiv.textContent = `[系统] 渲染进度: ${progress}%`;
  2163 + if (this.container.firstChild) {
  2164 + this.container.insertBefore(newProgressDiv, this.container.firstChild);
  2165 + }
  2166 + }
  2167 + };
  2168 +
  2169 + // 渲染一批数据
  2170 + const renderBatch = () => {
  2171 + const startIdx = currentBatch * batchSize;
  2172 + const endIdx = Math.min((currentBatch + 1) * batchSize, total);
  2173 +
  2174 + // 创建批量fragment
  2175 + const batchFragment = document.createDocumentFragment();
  2176 + for (let i = startIdx; i < endIdx; i++) {
  2177 + const line = this.lines[i];
  2178 + const node = document.createElement('div');
  2179 + node.className = line.className || 'console-line';
  2180 + node.textContent = line.text;
  2181 + batchFragment.appendChild(node);
  2182 + }
  2183 +
  2184 + // 如果是第一批,清理旧内容
  2185 + if (currentBatch === 0) {
  2186 + // 保留一个提示,避免完全空白
  2187 + const placeholder = document.createElement('div');
  2188 + placeholder.className = 'console-line';
  2189 + placeholder.textContent = '[系统] 正在加载日志...';
  2190 + placeholder.style.opacity = '0.5';
  2191 + this.container.replaceChildren(placeholder);
  2192 + }
  2193 +
  2194 + // 追加新批次
  2195 + this.container.appendChild(batchFragment);
  2196 +
  2197 + currentBatch++;
  2198 + showProgress();
  2199 +
  2200 + // 继续下一批或完成
  2201 + if (currentBatch < totalBatches) {
  2202 + requestAnimationFrame(renderBatch);
  2203 + } else {
  2204 + // 渲染完成,清理进度提示
  2205 + const progressDiv = this.container.querySelector('.render-progress');
  2206 + if (progressDiv) {
  2207 + progressDiv.remove();
  2208 + }
  2209 + console.log(`[渐进式渲染] 完成,共渲染 ${total} 行`);
  2210 + this.isProgressiveRendering = false;
  2211 +
  2212 + // 完成后触发滚动
  2213 + if (this.autoScrollEnabled) {
  2214 + requestAnimationFrame(() => {
  2215 + this.scrollToBottom();
  2216 + });
  2217 + }
  2218 + }
  2219 + };
  2220 +
  2221 + // 开始渲染第一批
  2222 + showProgress();
  2223 + requestAnimationFrame(renderBatch);
  2224 + }
1909 } 2225 }
1910 2226
1911 const CONFIG_ENDPOINT = '/api/config'; 2227 const CONFIG_ENDPOINT = '/api/config';
@@ -3507,8 +3823,20 @@ @@ -3507,8 +3823,20 @@
3507 } 3823 }
3508 3824
3509 // 加载论坛日志 3825 // 加载论坛日志
  3826 + let forumLogPosition = 0; // 记录已接收的日志位置
  3827 +
3510 function loadForumLog() { 3828 function loadForumLog() {
3511 - fetch('/api/forum/log') 3829 + // 【优化】使用历史API获取完整日志
  3830 + fetch('/api/forum/log/history', {
  3831 + method: 'POST',
  3832 + headers: {
  3833 + 'Content-Type': 'application/json',
  3834 + },
  3835 + body: JSON.stringify({
  3836 + position: 0, // 从头开始获取所有历史
  3837 + max_lines: 5000 // 获取最近5000行历史
  3838 + })
  3839 + })
3512 .then(response => response.json()) 3840 .then(response => response.json())
3513 .then(data => { 3841 .then(data => {
3514 // 【FIX Bug #5】检查是否仍然在forum页面 3842 // 【FIX Bug #5】检查是否仍然在forum页面
@@ -3527,35 +3855,59 @@ @@ -3527,35 +3855,59 @@
3527 return; 3855 return;
3528 } 3856 }
3529 3857
3530 - const chatArea = document.getElementById('forumChatArea');  
3531 - if (chatArea) {  
3532 - chatArea.innerHTML = '';  
3533 - }  
3534 -  
3535 const logLines = data.log_lines || []; 3858 const logLines = data.log_lines || [];
3536 - const parsedMessages = data.parsed_messages || []; 3859 + forumLogPosition = data.position || 0; // 记录当前位置
3537 3860
  3861 + // 清空并重新加载日志
3538 if (logLines.length > 0) { 3862 if (logLines.length > 0) {
3539 - clearConsoleLayer('forum', '[系统] Forum Engine 日志输出'); 3863 + clearConsoleLayer('forum', '[系统] Forum Engine 历史日志');
3540 logRenderers['forum'].render(); // 立即渲染清空提示 3864 logRenderers['forum'].render(); // 立即渲染清空提示
3541 - logLines.forEach(line => appendConsoleTextLine('forum', line)); 3865 +
  3866 + // 批量添加历史日志,避免卡顿
  3867 + const batchSize = 100;
  3868 + let index = 0;
  3869 +
  3870 + function addBatch() {
  3871 + const batch = logLines.slice(index, index + batchSize);
  3872 + batch.forEach(line => appendConsoleTextLine('forum', line));
  3873 + index += batchSize;
  3874 +
  3875 + if (index < logLines.length && currentApp === 'forum') {
  3876 + requestAnimationFrame(addBatch);
  3877 + }
  3878 + }
  3879 +
  3880 + addBatch();
3542 } else { 3881 } else {
3543 - forumLogLineCount = 0; 3882 + clearConsoleLayer('forum', '[系统] Forum Engine 暂无日志');
3544 } 3883 }
3545 3884
3546 - if (parsedMessages.length > 0) {  
3547 - parsedMessages.forEach(message => addForumMessage(message));  
3548 - } 3885 + // 同时获取解析的消息(用于聊天区域)
  3886 + fetch('/api/forum/log')
  3887 + .then(response => response.json())
  3888 + .then(data => {
  3889 + if (!data.success) return;
3549 3890
3550 - forumLogLineCount = logLines.length; 3891 + const chatArea = document.getElementById('forumChatArea');
  3892 + if (chatArea) {
  3893 + chatArea.innerHTML = '';
  3894 + }
  3895 +
  3896 + const parsedMessages = data.parsed_messages || [];
  3897 + if (parsedMessages.length > 0) {
  3898 + parsedMessages.forEach(message => addForumMessage(message));
  3899 + }
  3900 +
  3901 + forumLogLineCount = data.log_lines ? data.log_lines.length : 0;
  3902 + });
3551 }) 3903 })
3552 .catch(error => { 3904 .catch(error => {
3553 - console.error('加载论坛日志失败:', error); 3905 + console.error('加载论坛历史日志失败:', error);
3554 // 【优化】显示错误提示 3906 // 【优化】显示错误提示
3555 if (currentApp === 'forum') { 3907 if (currentApp === 'forum') {
3556 const renderer = logRenderers['forum']; 3908 const renderer = logRenderers['forum'];
3557 if (renderer) { 3909 if (renderer) {
3558 - renderer.clear('[错误] 加载Forum日志失败: ' + error.message); 3910 + renderer.clear('[错误] 加载Forum历史日志失败: ' + error.message);
3559 renderer.render(); 3911 renderer.render();
3560 } 3912 }
3561 } 3913 }