Showing
2 changed files
with
522 additions
and
116 deletions
| @@ -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 | } |
-
Please register or login to post a comment