Improves the Front-End Console Experience
This commit is contained in:
+287
-40
@@ -23,6 +23,10 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
--console-offset: 52px; /* app切换行 + 状态条的预留高度,运行时同步 */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100vw;
|
||||
height: 100vh; /* 固定高度为视口高度 */
|
||||
@@ -177,7 +181,7 @@
|
||||
.upload-button input[type="file"] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
top: 52px; /* 运行时再用JS同步为app-switcher的真实高度 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
@@ -238,6 +242,7 @@
|
||||
background-color: #ffffff;
|
||||
min-height: 0; /* 允许子元素缩小 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
position: relative; /* 让状态栏悬浮不占用布局空间 */
|
||||
}
|
||||
|
||||
/* 应用切换按钮 */
|
||||
@@ -330,7 +335,11 @@
|
||||
|
||||
/* 控制台状态栏 - 显示系统消息而不干扰日志查看 */
|
||||
.console-status-bar {
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 26px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(0, 120, 0, 0.95) 0%, rgba(0, 100, 0, 0.9) 100%);
|
||||
color: #00ff00;
|
||||
@@ -342,13 +351,17 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: height 0.3s ease, padding 0.3s ease;
|
||||
pointer-events: none; /* 不抢占滚动 */
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transform: translateY(-110%);
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.console-status-bar.visible {
|
||||
height: 26px;
|
||||
padding: 0 15px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.console-status-bar .status-message {
|
||||
@@ -372,7 +385,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; /* 填满整个黑色框 */
|
||||
padding: 15px; /* 图层内边距 */
|
||||
padding: var(--console-offset, 52px) 15px 15px; /* 顶部预留给状态栏与按钮,不影响滚动计算 */
|
||||
overflow-y: auto; /* 允许独立滚动 */
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box; /* 包含padding在width/height内 */
|
||||
@@ -1540,8 +1553,13 @@
|
||||
this.maxVisible = 150; // ↑ from 120(增加可见行数)
|
||||
this.maxLines = 50000; // ↑ from 10000(5倍提升,约5MB/app)
|
||||
this.trimTarget = 40000; // ↑ from 8000(保留更多历史)
|
||||
this.maxPoolSize = 300; // ↑ from 200(更大DOM池)
|
||||
this.preRenderBuffer = 90; // 新增:上下各预渲染90行,提升滚动流畅度
|
||||
this.minPoolSize = 220; // 保底DOM池,避免窗口渲染空白
|
||||
this.poolHardLimit = 800; // 池子硬上限,防止撑爆内存
|
||||
this.maxPoolSize = Math.max(
|
||||
this.minPoolSize,
|
||||
this.maxVisible + this.preRenderBuffer * 2 + 50
|
||||
); // 默认覆盖可视窗口+缓冲区,再留一些余量
|
||||
this.rafId = null;
|
||||
this.autoScrollEnabled = true;
|
||||
this.resumeDelay = 3000;
|
||||
@@ -1553,6 +1571,10 @@
|
||||
this.scrollHandler = null;
|
||||
this.beforeSpacer = null;
|
||||
this.afterSpacer = null;
|
||||
this.watchdogTimer = null;
|
||||
this.lastRenderAt = 0;
|
||||
this.idleTimer = null;
|
||||
this.idleTimeout = 3000; // 用户3秒不滚动后自动吸附
|
||||
|
||||
// 【优化吸附逻辑】记录用户滚动位置,用于智能恢复
|
||||
this.lastUserScrollPosition = 0; // 用户上次滚动的位置
|
||||
@@ -1605,6 +1627,90 @@
|
||||
}
|
||||
}
|
||||
|
||||
clearIdleTimer() {
|
||||
if (this.idleTimer) {
|
||||
clearTimeout(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
startIdleTimer() {
|
||||
this.clearIdleTimer();
|
||||
this.idleTimer = setTimeout(() => {
|
||||
this.autoScrollEnabled = true;
|
||||
this.needsScroll = true;
|
||||
this.clearResumeTimer();
|
||||
this.scrollToBottom();
|
||||
}, this.idleTimeout);
|
||||
}
|
||||
|
||||
// 保障DOM池容量,避免可视窗口比池子大导致空白
|
||||
ensurePoolCapacity(neededCount) {
|
||||
if (!Number.isFinite(neededCount)) return;
|
||||
|
||||
// 根据需求量和基线计算DOM池大小,避免窗口比池子大
|
||||
const baseline = Math.min(this.minPoolSize, neededCount + 80);
|
||||
const targetSize = Math.min(
|
||||
Math.max(neededCount, baseline, this.pool.length),
|
||||
this.poolHardLimit
|
||||
);
|
||||
|
||||
if (targetSize > this.maxPoolSize) {
|
||||
this.maxPoolSize = targetSize;
|
||||
}
|
||||
|
||||
const missing = Math.max(0, targetSize - this.pool.length);
|
||||
for (let i = 0; i < missing; i++) {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'console-line';
|
||||
this.pool.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// DOM意外空白时强制刷新
|
||||
forceRenderIfBlank() {
|
||||
if (!this.container) return false;
|
||||
if (this.lines.length === 0) return false;
|
||||
if (this.container.querySelector('.console-line')) return false;
|
||||
this.lastRenderHash = null;
|
||||
this.needsScroll = true;
|
||||
this.scheduleRender(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
startWatchdog() {
|
||||
if (this.watchdogTimer) return;
|
||||
this.watchdogTimer = setInterval(() => {
|
||||
if (!this.container) return;
|
||||
// 黑屏兜底:有数据但没有节点时强制渲染
|
||||
const hasLines = this.lines.length > 0;
|
||||
const hasNodes = !!this.container.querySelector('.console-line');
|
||||
if (hasLines && !hasNodes && !this.isRendering) {
|
||||
this.lastRenderHash = null;
|
||||
this.needsScroll = true;
|
||||
this.updateStatusBar('正在恢复显示...', '检测到渲染延迟', false, 1200);
|
||||
this.scheduleRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染长时间未推进时,提醒并触发一次刷新
|
||||
const now = performance.now();
|
||||
if (hasLines && this.pending.length > 0 && now - this.lastRenderAt > 1500 && !this.isRendering) {
|
||||
this.lastRenderHash = null;
|
||||
this.needsScroll = true;
|
||||
this.updateStatusBar('日志渲染中...', `${this.pending.length} 条待显示`, false, 1200);
|
||||
this.scheduleRender(true);
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
stopWatchdog() {
|
||||
if (this.watchdogTimer) {
|
||||
clearInterval(this.watchdogTimer);
|
||||
this.watchdogTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
attachScroll() {
|
||||
if (!this.scrollElement) return;
|
||||
if (this.scrollHandler) return; // 防止重复绑定
|
||||
@@ -1673,6 +1779,9 @@
|
||||
|
||||
// 显示状态栏
|
||||
this.statusBar.classList.add('visible');
|
||||
if (this.layoutCache) {
|
||||
this.layoutCache.invalidate();
|
||||
}
|
||||
|
||||
// 清除之前的自动隐藏定时器
|
||||
if (this.statusBarTimer) {
|
||||
@@ -1686,6 +1795,9 @@
|
||||
if (this.statusBar) {
|
||||
this.statusBar.classList.remove('visible');
|
||||
}
|
||||
if (this.layoutCache) {
|
||||
this.layoutCache.invalidate();
|
||||
}
|
||||
this.statusBarTimer = null;
|
||||
}, duration);
|
||||
}
|
||||
@@ -1699,6 +1811,9 @@
|
||||
this.statusBarTimer = null;
|
||||
}
|
||||
this.statusBar.classList.remove('visible');
|
||||
if (this.layoutCache) {
|
||||
this.layoutCache.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加清理方法
|
||||
@@ -1707,6 +1822,7 @@
|
||||
console.log('[性能统计] 最终统计:', this.getPerformanceStats());
|
||||
|
||||
// 清理定时器
|
||||
this.stopWatchdog();
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
@@ -1716,6 +1832,7 @@
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
this.clearIdleTimer();
|
||||
if (this.statusBarTimer) {
|
||||
clearTimeout(this.statusBarTimer);
|
||||
this.statusBarTimer = null;
|
||||
@@ -1779,7 +1896,7 @@
|
||||
|
||||
// 计算距离底部的距离
|
||||
const distanceFromBottom = scrollHeight - clientHeight - currentScrollTop;
|
||||
const atBottom = distanceFromBottom <= 50; // 50px阈值
|
||||
const atBottom = distanceFromBottom <= 80; // 更宽松的阈值,减少误判
|
||||
|
||||
if (atBottom) {
|
||||
// 用户主动滚动到底部,立即启用自动滚动
|
||||
@@ -1808,6 +1925,7 @@
|
||||
this.clearResumeTimer();
|
||||
const delay = this.calculateResumeDelay(distanceFromBottom);
|
||||
this.startResumeTimer(delay);
|
||||
this.startIdleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1815,10 +1933,12 @@
|
||||
console.log(`[自动吸附] 用户离开底部 ${Math.round(distanceFromBottom)}px,禁用自动滚动`);
|
||||
this.autoScrollEnabled = false;
|
||||
this.clearResumeTimer();
|
||||
this.clearIdleTimer();
|
||||
|
||||
// 智能计算恢复时间
|
||||
const delay = this.calculateResumeDelay(distanceFromBottom);
|
||||
this.startResumeTimer(delay);
|
||||
this.startIdleTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1847,6 +1967,7 @@
|
||||
console.log(`[自动吸附] ${delay}ms后恢复自动滚动,吸附到最新日志`);
|
||||
this.autoScrollEnabled = true;
|
||||
this.userScrollDistance = 0;
|
||||
this.needsScroll = true;
|
||||
|
||||
// 【优化】恢复时立即吸附到最新
|
||||
// 如果有新内容已经渲染完成,直接滚动到最新位置
|
||||
@@ -1891,10 +2012,11 @@
|
||||
}
|
||||
|
||||
// 【优化判断】智能阈值:
|
||||
// - 如果内容少(scrollHeight < 1000px),阈值30px(宽容)
|
||||
// - 如果内容多,阈值50px(标准)
|
||||
// 这样能更好地适应不同内容量的情况
|
||||
const threshold = scrollHeight < 1000 ? 30 : 50;
|
||||
// - 超小内容时使用30px
|
||||
// - 常规50px
|
||||
// - 当存在状态条/顶部padding时,再增加20px缓冲
|
||||
const baseThreshold = scrollHeight < 1000 ? 30 : 50;
|
||||
const threshold = baseThreshold + 20;
|
||||
const distanceFromBottom = scrollHeight - clientHeight - scrollTop;
|
||||
|
||||
return distanceFromBottom <= threshold;
|
||||
@@ -1915,9 +2037,42 @@
|
||||
return this.isNearBottom();
|
||||
}
|
||||
|
||||
// 将最新一行吸附到可视区域内,而不是盲目对齐到底部
|
||||
scrollLatestIntoView(margin = 12) {
|
||||
if (!this.scrollElement || this.scrollLocked) return false;
|
||||
const lastLine = this.scrollElement.querySelector('.console-line:last-of-type');
|
||||
if (!lastLine) return false;
|
||||
|
||||
const container = this.scrollElement;
|
||||
// 最新一行希望落在视口内并留出少量下边距
|
||||
const desiredBottom = container.clientHeight - margin;
|
||||
const delta = lastLine.offsetTop + lastLine.offsetHeight - desiredBottom;
|
||||
if (delta > 1 || delta < -1) {
|
||||
this.scrollLocked = true;
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = Math.max(0, container.scrollTop + delta);
|
||||
if (this.layoutCache) {
|
||||
this.layoutCache.invalidate();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scrollLocked = false;
|
||||
}, 80);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (!this.scrollElement) return;
|
||||
|
||||
// 优先将最新一行吸附到视口内,保留边距,避免“对齐页面底部”的跳动
|
||||
const snapped = this.scrollLatestIntoView(16);
|
||||
if (snapped) {
|
||||
this.needsScroll = false;
|
||||
this.startIdleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// 【修复黑屏】如果正在渲染,延迟滚动避免冲突
|
||||
if (this.isRendering) {
|
||||
requestAnimationFrame(() => this.scrollToBottom());
|
||||
@@ -1989,6 +2144,7 @@
|
||||
}
|
||||
}
|
||||
this.needsScroll = false;
|
||||
this.startIdleTimer();
|
||||
|
||||
// 解锁滚动(平滑滚动需要更长时间)
|
||||
setTimeout(() => {
|
||||
@@ -2016,9 +2172,12 @@
|
||||
this.isActive = active;
|
||||
|
||||
if (active) {
|
||||
this.startWatchdog();
|
||||
|
||||
// 【新增逻辑】切换引擎时,重置为自动吸附状态
|
||||
// 默认用户鼠标未滑动3秒以上
|
||||
this.autoScrollEnabled = true;
|
||||
this.needsScroll = true;
|
||||
this.clearResumeTimer();
|
||||
|
||||
// 【修复】窗口激活时,清除渲染哈希,确保强制渲染
|
||||
@@ -2112,6 +2271,17 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保证切换后立即同步最新内容
|
||||
this.scheduleRender(true);
|
||||
this.startIdleTimer();
|
||||
|
||||
if (this.lines.length > 0) {
|
||||
requestAnimationFrame(() => this.scrollToBottom());
|
||||
}
|
||||
} else {
|
||||
this.stopWatchdog();
|
||||
this.clearIdleTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2179,12 +2349,25 @@
|
||||
}
|
||||
|
||||
append(text, className = 'console-line') {
|
||||
const nearBottom = this.isNearBottom();
|
||||
|
||||
// 在添加内容前检查是否在底部,如果是则标记需要滚动
|
||||
if (this.autoScrollEnabled && this.isNearBottom()) {
|
||||
if (this.autoScrollEnabled && nearBottom) {
|
||||
this.needsScroll = true;
|
||||
} else if (!this.autoScrollEnabled && nearBottom) {
|
||||
// 用户并未真正离开底部,自动恢复吸附
|
||||
this.autoScrollEnabled = true;
|
||||
this.needsScroll = true;
|
||||
this.clearResumeTimer();
|
||||
}
|
||||
|
||||
this.pending.push({ text, className });
|
||||
this.pendingHighWaterMark = Math.max(this.pendingHighWaterMark, this.pending.length);
|
||||
|
||||
// 队列堆积时给出进度提示(仅当前活动窗口)
|
||||
if (this.isActive && this.pending.length > 150) {
|
||||
this.updateStatusBar('日志渲染中...', `${this.pending.length} 条排队`, false, 1200);
|
||||
}
|
||||
|
||||
// 【修复黑屏】统一批处理逻辑 - 所有窗口都实时渲染
|
||||
// 清除之前的定时器
|
||||
@@ -2206,6 +2389,9 @@
|
||||
}, this.batchDelay);
|
||||
}
|
||||
|
||||
// 如果容器当前是空的,强制渲染一次,避免“黑屏等待”
|
||||
this.forceRenderIfBlank();
|
||||
|
||||
this.maybeTrim();
|
||||
}
|
||||
|
||||
@@ -2332,6 +2518,7 @@
|
||||
this.beforeSpacer = null;
|
||||
this.afterSpacer = null;
|
||||
}
|
||||
this.lastRenderAt = performance.now();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2349,6 +2536,7 @@
|
||||
}
|
||||
this.lastRenderHash = contentHash;
|
||||
this.lastRenderLineCount = total;
|
||||
this.lastRenderAt = performance.now();
|
||||
|
||||
// 【优化】计算可见区域
|
||||
const lh = this.lineHeight;
|
||||
@@ -2374,14 +2562,33 @@
|
||||
const bufferStart = Math.max(0, rawStart - this.preRenderBuffer);
|
||||
const bufferEnd = Math.min(total, rawStart + visible + this.preRenderBuffer);
|
||||
|
||||
const start = bufferStart;
|
||||
const end = bufferEnd;
|
||||
let start = bufferStart;
|
||||
let end = bufferEnd;
|
||||
let needed = Math.max(0, end - start);
|
||||
|
||||
// 【修复白屏】先保证DOM池容量足够覆盖当前窗口
|
||||
this.ensurePoolCapacity(needed);
|
||||
|
||||
// 如果窗口仍超过池容量,收缩窗口贴近最新日志,防止中间缺口
|
||||
if (needed > this.pool.length) {
|
||||
const windowSize = this.pool.length || this.minPoolSize;
|
||||
end = Math.min(total, Math.max(end, windowSize));
|
||||
start = Math.max(0, end - windowSize);
|
||||
needed = Math.max(0, end - start);
|
||||
}
|
||||
|
||||
if (needed === 0 && total > 0) {
|
||||
// 极端情况下兜底展示最新日志,避免空白
|
||||
end = total;
|
||||
start = Math.max(0, end - Math.min(this.maxVisible, total));
|
||||
needed = end - start;
|
||||
this.ensurePoolCapacity(needed);
|
||||
}
|
||||
|
||||
const beforeHeight = start * lh;
|
||||
const afterHeight = (total - end) * lh;
|
||||
|
||||
const needed = Math.max(0, end - start);
|
||||
|
||||
// 【优化】限制DOM节点池大小
|
||||
// 【优化】限制DOM节点池大小(在更新池容量后执行)
|
||||
if (this.pool.length > this.maxPoolSize) {
|
||||
const excess = this.pool.length - this.maxPoolSize;
|
||||
this.pool.splice(this.maxPoolSize, excess).forEach(node => {
|
||||
@@ -2391,16 +2598,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 【优化】批量创建DOM节点,减少DOM操作
|
||||
const nodesToCreate = needed - this.pool.length;
|
||||
if (nodesToCreate > 0 && this.pool.length < this.maxPoolSize) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < Math.min(nodesToCreate, this.maxPoolSize - this.pool.length); i++) {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'console-line';
|
||||
this.pool.push(node);
|
||||
}
|
||||
}
|
||||
// 再次兜底,确保池容量与窗口一致
|
||||
this.ensurePoolCapacity(needed);
|
||||
|
||||
// 【优化】复用或创建占位符
|
||||
if (!this.beforeSpacer) {
|
||||
@@ -2479,6 +2678,8 @@
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
this.lastRenderAt = performance.now();
|
||||
});
|
||||
} else {
|
||||
// 【优化】增量更新:智能diff算法,只更新必要的节点
|
||||
@@ -2579,6 +2780,8 @@
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
this.lastRenderAt = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2607,6 +2810,7 @@
|
||||
const batchSize = 200; // 每批渲染200行
|
||||
let currentBatch = 0;
|
||||
const totalBatches = Math.ceil(total / batchSize);
|
||||
this.lastRenderAt = performance.now();
|
||||
|
||||
// 显示渲染进度
|
||||
const showProgress = () => {
|
||||
@@ -2654,6 +2858,7 @@
|
||||
|
||||
// 隐藏状态栏
|
||||
this.hideStatusBar();
|
||||
this.lastRenderAt = performance.now();
|
||||
|
||||
// 【优化吸附】渲染完成后的智能滚动
|
||||
// 1. 自动滚动已启用 -> 直接吸附
|
||||
@@ -2783,6 +2988,7 @@
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeConsoleLayers();
|
||||
syncStatusBarPosition();
|
||||
initializeSocket();
|
||||
initializeEventListeners();
|
||||
ensureSystemReadyOnLoad();
|
||||
@@ -2829,6 +3035,9 @@
|
||||
|
||||
// 连接探测定时器(保持运行)
|
||||
startConnectionProbe();
|
||||
|
||||
// 窗口尺寸变化时同步状态栏位置
|
||||
window.addEventListener('resize', syncStatusBarPosition);
|
||||
});
|
||||
|
||||
// Socket.IO连接
|
||||
@@ -3628,6 +3837,20 @@
|
||||
return document.getElementById('consoleOutput');
|
||||
}
|
||||
|
||||
// 同步状态栏位置,避免覆盖应用切换按钮
|
||||
function syncStatusBarPosition() {
|
||||
const bar = document.getElementById('consoleStatusBar');
|
||||
const switcher = document.querySelector('.app-switcher');
|
||||
if (!bar || !switcher) return;
|
||||
|
||||
const offset = switcher.offsetHeight || 0;
|
||||
const barHeight = bar.offsetHeight || 26;
|
||||
const totalOffset = offset + barHeight + 6; // 额外预留6px缓冲
|
||||
|
||||
bar.style.top = `${offset}px`;
|
||||
document.documentElement.style.setProperty('--console-offset', `${totalOffset}px`);
|
||||
}
|
||||
|
||||
function initializeConsoleLayers() {
|
||||
const container = getConsoleContainer();
|
||||
if (!container) return;
|
||||
@@ -3646,11 +3869,7 @@
|
||||
container.appendChild(layer);
|
||||
consoleLayers[app] = layer;
|
||||
logRenderers[app] = new LogVirtualList(layer);
|
||||
|
||||
// 【图层优化】标记活动窗口
|
||||
if (app === currentApp) {
|
||||
logRenderers[app].isActive = true;
|
||||
}
|
||||
logRenderers[app].setActive(app === currentApp);
|
||||
|
||||
// 【FIX Bug #3】初始提示立即渲染,避免黑屏
|
||||
logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`);
|
||||
@@ -3680,11 +3899,7 @@
|
||||
container.appendChild(layer);
|
||||
consoleLayers[app] = layer;
|
||||
logRenderers[app] = new LogVirtualList(layer);
|
||||
|
||||
// 【图层优化】标记活动窗口
|
||||
if (app === currentApp) {
|
||||
logRenderers[app].isActive = true;
|
||||
}
|
||||
logRenderers[app].setActive(app === currentApp);
|
||||
|
||||
return layer;
|
||||
}
|
||||
@@ -3718,6 +3933,8 @@
|
||||
const renderer = logRenderers[app];
|
||||
if (renderer) {
|
||||
renderer.setActive(true); // 会在内部异步渲染待处理内容
|
||||
renderer.needsScroll = true;
|
||||
renderer.scheduleRender(true);
|
||||
|
||||
// 确保滚动到底部
|
||||
if (renderer.autoScrollEnabled) {
|
||||
@@ -4485,6 +4702,28 @@
|
||||
// 创建全局日志管理器实例
|
||||
const reportLogManager = new ReportLogManager();
|
||||
|
||||
// 新任务时重置报告日志,避免残留历史输出
|
||||
function resetReportLogsForNewTask(taskId, reason = '开始新的报告任务,日志已重置') {
|
||||
if (!taskId) return;
|
||||
if (reportTaskId === taskId) return; // 已是同一任务,无需重复清空
|
||||
|
||||
// 停止当前流与轮询,防止旧日志混入
|
||||
safeCloseReportStream();
|
||||
reportLogManager.stop();
|
||||
reportLogManager.reset();
|
||||
|
||||
// 重置前端计数与缓存
|
||||
reportLogLineCount = 0;
|
||||
lastLineCount['report'] = 0;
|
||||
|
||||
clearConsoleLayer('report', `[系统] ${reason}`);
|
||||
resetReportStreamOutput('Report Engine 正在启动...');
|
||||
|
||||
// 重新启动轮询,确保新任务日志即时接入
|
||||
reportLogManager.start();
|
||||
reportTaskId = taskId;
|
||||
}
|
||||
|
||||
// 【调试】测试日志管理器
|
||||
window.testReportLogManager = function() {
|
||||
console.log('[测试] ===== 开始测试Report日志管理器 =====');
|
||||
@@ -5025,12 +5264,14 @@
|
||||
if (statusData.current_task) {
|
||||
updateTaskProgressStatus(statusData.current_task);
|
||||
if (statusData.current_task.status === 'running') {
|
||||
reportTaskId = statusData.current_task.task_id;
|
||||
const taskId = statusData.current_task.task_id;
|
||||
resetReportLogsForNewTask(taskId, '检测到正在运行的报告任务,日志已重新开始');
|
||||
reportTaskId = taskId;
|
||||
reportAutoPreviewLoaded = false;
|
||||
if (window.EventSource) {
|
||||
openReportStream(reportTaskId);
|
||||
} else {
|
||||
startProgressPolling(reportTaskId);
|
||||
startProgressPolling(taskId);
|
||||
}
|
||||
} else if (statusData.current_task.status === 'completed') {
|
||||
lastCompletedReportTask = statusData.current_task;
|
||||
@@ -5527,6 +5768,9 @@
|
||||
|
||||
// 更新进度显示(保持向后兼容)
|
||||
function updateProgressDisplay(task) {
|
||||
if (task && task.task_id && task.status === 'running') {
|
||||
resetReportLogsForNewTask(task.task_id, '检测到新的报告任务,日志已同步重置');
|
||||
}
|
||||
updateTaskProgressStatus(task);
|
||||
}
|
||||
|
||||
@@ -5735,6 +5979,9 @@
|
||||
const task = payload.task;
|
||||
|
||||
if (eventType === 'status' && task) {
|
||||
if (task.status === 'running') {
|
||||
resetReportLogsForNewTask(task.task_id, '收到流式状态事件,已重置日志');
|
||||
}
|
||||
updateTaskProgressStatus(task);
|
||||
reportTaskId = task.status === 'running' ? task.task_id : null;
|
||||
if (task.status === 'completed') {
|
||||
|
||||
Reference in New Issue
Block a user