Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
马一丁
2025-11-18 12:58:40 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
eb036655a217e0e3f8f46951b8847d0d0dcdeb95
eb036655
1 parent
939fea26
Fix the Front-End Console Display Logic
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
149 additions
and
63 deletions
templates/index.html
templates/index.html
View file @
eb03665
...
...
@@ -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
`
;
const
fragment
=
document
.
createDocumentFragment
();
// 只更新可见区域的节点
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秒检查一次
// 定期刷新控制台输出
setInterval
(()
=>
{
refreshConsoleOutput
();
},
1000
);
// 定期刷新论坛对话(实时更新)
// 优化控制台刷新频率:从 1 秒改为 2 秒,减少不必要的 API 调用
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
()
{
...
...
Please
register
or
login
to post a comment