Fix the Front-End Console Display Logic
This commit is contained in:
@@ -1242,21 +1242,30 @@
|
||||
this.trimTarget = 1500; // 裁剪后保留的目标行数,避免频繁裁剪
|
||||
this.rafId = null;
|
||||
this.autoScrollEnabled = true;
|
||||
this.resumeDelay = 10000; // 手动滚动后重新自动滚动的延迟
|
||||
this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒)
|
||||
this.resumeTimer = null;
|
||||
this.lastRenderHash = null; // 用于检测内容是否真正变化
|
||||
this.scrollLocked = false; // 防止滚动冲突的锁
|
||||
this.needsScroll = false; // 标记是否需要滚动
|
||||
this.lastScrollTime = 0; // 上次滚动时间,用于节流
|
||||
this.scrollThrottle = 100; // 滚动节流时间(毫秒)
|
||||
this.attachScroll();
|
||||
}
|
||||
|
||||
attachScroll() {
|
||||
if (!this.scrollElement) return;
|
||||
let scrollTimer = null;
|
||||
this.scrollElement.addEventListener('scroll', () => {
|
||||
this.handleUserScroll();
|
||||
this.scheduleRender();
|
||||
});
|
||||
// 防抖处理,避免频繁触发
|
||||
if (scrollTimer) clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(() => {
|
||||
this.handleUserScroll();
|
||||
}, 100);
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
handleUserScroll() {
|
||||
if (!this.scrollElement) return;
|
||||
if (!this.scrollElement || this.scrollLocked) return;
|
||||
const atBottom = this.isNearBottom();
|
||||
if (atBottom) {
|
||||
this.autoScrollEnabled = true;
|
||||
@@ -1264,8 +1273,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户主动向上滚动,禁用自动滚动
|
||||
this.autoScrollEnabled = false;
|
||||
this.clearResumeTimer();
|
||||
// 设置定时器,在用户停止滚动一段时间后自动恢复吸底
|
||||
this.resumeTimer = setTimeout(() => {
|
||||
this.autoScrollEnabled = true;
|
||||
this.scrollToBottom();
|
||||
@@ -1282,12 +1293,52 @@
|
||||
isNearBottom() {
|
||||
if (!this.scrollElement) return true;
|
||||
const { scrollTop, clientHeight, scrollHeight } = this.scrollElement;
|
||||
return (scrollTop + clientHeight) >= (scrollHeight - this.lineHeight * 2);
|
||||
// 增加阈值到 50px,使吸底判断更宽容
|
||||
return (scrollTop + clientHeight) >= (scrollHeight - 50);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (!this.scrollElement) return;
|
||||
this.scrollElement.scrollTop = this.scrollElement.scrollHeight;
|
||||
|
||||
// 节流:如果上次滚动时间距离现在不到 scrollThrottle 毫秒,则跳过
|
||||
const now = Date.now();
|
||||
if (now - this.lastScrollTime < this.scrollThrottle) {
|
||||
return;
|
||||
}
|
||||
this.lastScrollTime = now;
|
||||
|
||||
// 使用锁防止重入
|
||||
if (this.scrollLocked) return;
|
||||
this.scrollLocked = true;
|
||||
|
||||
// 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.scrollElement) {
|
||||
this.scrollLocked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 平滑滚动到底部,避免突然跳跃
|
||||
const targetScroll = this.scrollElement.scrollHeight;
|
||||
const currentScroll = this.scrollElement.scrollTop;
|
||||
|
||||
// 如果已经在底部附近,直接设置,否则平滑滚动
|
||||
if (Math.abs(targetScroll - currentScroll) < 100) {
|
||||
this.scrollElement.scrollTop = targetScroll;
|
||||
} else {
|
||||
// 使用平滑滚动
|
||||
this.scrollElement.scrollTo({
|
||||
top: targetScroll,
|
||||
behavior: 'auto' // 使用 auto 而不是 smooth,避免性能问题
|
||||
});
|
||||
}
|
||||
|
||||
// 延迟解锁,避免立即触发 scroll 事件导致循环
|
||||
setTimeout(() => {
|
||||
this.scrollLocked = false;
|
||||
this.needsScroll = false; // 滚动完成后重置标志
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
setLineHeight(px) {
|
||||
@@ -1295,8 +1346,14 @@
|
||||
}
|
||||
|
||||
append(text, className = 'console-line') {
|
||||
// 在添加内容前检查是否在底部,如果是则标记需要滚动
|
||||
if (this.autoScrollEnabled && this.isNearBottom()) {
|
||||
this.needsScroll = true;
|
||||
}
|
||||
|
||||
this.pending.push({ text, className });
|
||||
if (this.pending.length > 200) {
|
||||
// 降低批处理阈值到 50,更快响应
|
||||
if (this.pending.length > 50) {
|
||||
this.flush();
|
||||
}
|
||||
this.maybeTrim();
|
||||
@@ -1310,6 +1367,8 @@
|
||||
if (message) {
|
||||
this.lines.push({ text: message, className: 'console-line' });
|
||||
}
|
||||
this.lastRenderHash = null;
|
||||
this.needsScroll = true; // 清空后需要滚动到底部
|
||||
this.scheduleRender(true);
|
||||
}
|
||||
|
||||
@@ -1327,16 +1386,17 @@
|
||||
const toDrop = this.lines.length - this.trimTarget;
|
||||
if (toDrop > 0) {
|
||||
this.lines.splice(0, toDrop);
|
||||
// 调整滚动位置使得视觉保持在底部附近
|
||||
if (this.scrollElement && !this.autoScrollEnabled) {
|
||||
this.scrollElement.scrollTop = Math.max(0, this.scrollElement.scrollTop - toDrop * this.lineHeight);
|
||||
}
|
||||
// 不调整滚动位置,让用户保持当前位置或自动吸底
|
||||
}
|
||||
}
|
||||
|
||||
scheduleRender(force = false) {
|
||||
if (!this.container) return;
|
||||
if (!force && this.rafId) return;
|
||||
// 取消之前的请求,使用节流
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.rafId = null;
|
||||
this.render();
|
||||
@@ -1347,10 +1407,23 @@
|
||||
this.flush();
|
||||
const total = this.lines.length;
|
||||
if (!total) {
|
||||
this.container.innerHTML = '';
|
||||
if (this.container.innerHTML !== '') {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算内容哈希,只在内容真正变化时才更新 DOM
|
||||
const contentHash = `${total}-${this.lines[total - 1].text}`;
|
||||
if (this.lastRenderHash === contentHash) {
|
||||
// 内容没有变化,只需要处理滚动(如果需要的话)
|
||||
if (this.needsScroll && this.autoScrollEnabled) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.lastRenderHash = contentHash;
|
||||
|
||||
const lh = this.lineHeight;
|
||||
const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1;
|
||||
const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
|
||||
@@ -1364,39 +1437,60 @@
|
||||
const afterHeight = (total - end) * lh;
|
||||
|
||||
const needed = Math.max(0, end - start);
|
||||
|
||||
// 复用现有的 DOM 节点池
|
||||
while (this.pool.length < needed) {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'console-line';
|
||||
this.pool.push(node);
|
||||
}
|
||||
|
||||
// 截断池中过期结点,减少 DOM 引用
|
||||
if (needed && this.pool.length > needed * 2) {
|
||||
this.pool.length = needed * 2;
|
||||
}
|
||||
|
||||
// 不要完全清空容器,而是更新现有节点
|
||||
const existingChildren = Array.from(this.container.children);
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// 更新或创建前置占位符
|
||||
let beforeSpacer = existingChildren.find(el => el.dataset.spacer === 'before');
|
||||
if (!beforeSpacer) {
|
||||
beforeSpacer = document.createElement('div');
|
||||
beforeSpacer.dataset.spacer = 'before';
|
||||
}
|
||||
beforeSpacer.style.height = `${beforeHeight}px`;
|
||||
|
||||
// 更新或创建后置占位符
|
||||
let afterSpacer = existingChildren.find(el => el.dataset.spacer === 'after');
|
||||
if (!afterSpacer) {
|
||||
afterSpacer = document.createElement('div');
|
||||
afterSpacer.dataset.spacer = 'after';
|
||||
}
|
||||
afterSpacer.style.height = `${afterHeight}px`;
|
||||
|
||||
// 只更新可见区域的节点
|
||||
for (let idx = start; idx < end; idx++) {
|
||||
const line = this.lines[idx];
|
||||
const node = this.pool[idx - start];
|
||||
if (!node) continue; // 防御性避免越界
|
||||
node.className = line.className || 'console-line';
|
||||
node.textContent = line.text;
|
||||
if (!node) continue;
|
||||
|
||||
// 只在内容或类名变化时才更新节点
|
||||
if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) {
|
||||
node.className = line.className || 'console-line';
|
||||
node.textContent = line.text;
|
||||
}
|
||||
fragment.appendChild(node);
|
||||
}
|
||||
|
||||
// 一次性更新 DOM
|
||||
this.container.innerHTML = '';
|
||||
const beforeSpacer = document.createElement('div');
|
||||
beforeSpacer.style.height = `${beforeHeight}px`;
|
||||
const afterSpacer = document.createElement('div');
|
||||
afterSpacer.style.height = `${afterHeight}px`;
|
||||
this.container.appendChild(beforeSpacer);
|
||||
this.container.appendChild(fragment);
|
||||
this.container.appendChild(afterSpacer);
|
||||
|
||||
const shouldStick = this.autoScrollEnabled || this.isNearBottom();
|
||||
if (shouldStick) {
|
||||
this.scrollToBottom();
|
||||
// 只在有标记且自动滚动启用时才滚动到底部
|
||||
if (this.needsScroll && this.autoScrollEnabled) {
|
||||
// 延迟执行滚动,确保 DOM 已经更新完毕
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1521,16 +1615,20 @@
|
||||
// 初始化Report Engine锁定状态检查
|
||||
checkReportLockStatus();
|
||||
reportLockCheckInterval = setInterval(checkReportLockStatus, 10000); // 每10秒检查一次
|
||||
|
||||
// 定期刷新控制台输出
|
||||
|
||||
// 优化控制台刷新频率:从 1 秒改为 2 秒,减少不必要的 API 调用
|
||||
setInterval(() => {
|
||||
refreshConsoleOutput();
|
||||
}, 1000);
|
||||
|
||||
// 定期刷新论坛对话(实时更新)
|
||||
setInterval(() => {
|
||||
refreshForumMessages();
|
||||
if (appStatus[currentApp] === 'running' || appStatus[currentApp] === 'starting') {
|
||||
refreshConsoleOutput();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// 优化论坛对话刷新频率:从 2 秒改为 3 秒
|
||||
setInterval(() => {
|
||||
if (currentApp === 'forum' || appStatus.forum === 'running') {
|
||||
refreshForumMessages();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// 初始化论坛相关功能
|
||||
initializeForum();
|
||||
@@ -2339,15 +2437,9 @@
|
||||
}
|
||||
|
||||
function syncConsoleScroll(app) {
|
||||
if (app !== currentApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = logRenderers[app];
|
||||
if (renderer && renderer.container) {
|
||||
renderer.container.scrollTop = renderer.container.scrollHeight;
|
||||
consoleLayerScrollPositions[app] = renderer.container.scrollTop;
|
||||
}
|
||||
// 这个函数已经不需要了,因为 LogVirtualList 内部已经处理了滚动
|
||||
// 保留函数签名以避免破坏现有调用,但不执行任何操作
|
||||
return;
|
||||
}
|
||||
|
||||
function appendConsoleTextLine(app, text, className = 'console-line') {
|
||||
@@ -2358,8 +2450,11 @@
|
||||
function appendConsoleElement(app, element) {
|
||||
const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
|
||||
if (!element || !renderer.container) return;
|
||||
renderer.container.appendChild(element);
|
||||
renderer.scheduleRender(true);
|
||||
|
||||
// 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑
|
||||
const text = element.textContent || element.innerText || '';
|
||||
const className = element.className || 'console-line';
|
||||
renderer.append(text, className);
|
||||
}
|
||||
|
||||
function clearConsoleLayer(app, message = null) {
|
||||
@@ -3708,27 +3803,18 @@
|
||||
return; // 章节内容流式写入不再逐条输出
|
||||
}
|
||||
|
||||
const line = document.createElement('div');
|
||||
line.className = `console-line report-stream-line ${level}`;
|
||||
|
||||
const timestampSpan = document.createElement('span');
|
||||
timestampSpan.className = 'timestamp';
|
||||
timestampSpan.textContent = new Date().toLocaleTimeString('zh-CN');
|
||||
line.appendChild(timestampSpan);
|
||||
// 格式化时间戳
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN');
|
||||
|
||||
// 构建文本内容而不是 DOM 元素
|
||||
let textContent = `[${timestamp}]`;
|
||||
if (options.badge) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'stream-badge';
|
||||
badge.textContent = options.badge;
|
||||
line.appendChild(badge);
|
||||
textContent += ` [${options.badge}]`;
|
||||
}
|
||||
textContent += ` ${message}`;
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.className = 'line-text';
|
||||
textSpan.textContent = message;
|
||||
line.appendChild(textSpan);
|
||||
|
||||
appendConsoleElement('report', line);
|
||||
// 使用统一的文本添加方法,避免直接操作 DOM
|
||||
appendConsoleTextLine('report', textContent, `console-line report-stream-line ${level}`);
|
||||
}
|
||||
|
||||
function startStreamHeartbeat() {
|
||||
|
||||
Reference in New Issue
Block a user