Showing
1 changed file
with
119 additions
and
24 deletions
| @@ -1226,6 +1226,111 @@ | @@ -1226,6 +1226,111 @@ | ||
| 1226 | const consoleLayers = {}; | 1226 | const consoleLayers = {}; |
| 1227 | const consoleLayerScrollPositions = {}; | 1227 | const consoleLayerScrollPositions = {}; |
| 1228 | let activeConsoleLayer = currentApp; | 1228 | let activeConsoleLayer = currentApp; |
| 1229 | + const logRenderers = {}; | ||
| 1230 | + | ||
| 1231 | + // 轻量日志虚拟渲染器:不限制总行数,使用可视窗口渲染 + 节流 | ||
| 1232 | + class LogVirtualList { | ||
| 1233 | + constructor(container) { | ||
| 1234 | + this.container = container; | ||
| 1235 | + this.lines = []; | ||
| 1236 | + this.pending = []; | ||
| 1237 | + this.pool = []; | ||
| 1238 | + this.lineHeight = 18; | ||
| 1239 | + this.maxVisible = 120; | ||
| 1240 | + this.rafId = null; | ||
| 1241 | + this.attachScroll(); | ||
| 1242 | + } | ||
| 1243 | + | ||
| 1244 | + attachScroll() { | ||
| 1245 | + if (!this.container) return; | ||
| 1246 | + this.container.addEventListener('scroll', () => this.scheduleRender()); | ||
| 1247 | + } | ||
| 1248 | + | ||
| 1249 | + setLineHeight(px) { | ||
| 1250 | + if (px > 0) this.lineHeight = px; | ||
| 1251 | + } | ||
| 1252 | + | ||
| 1253 | + append(text, className = 'console-line') { | ||
| 1254 | + this.pending.push({ text, className }); | ||
| 1255 | + if (this.pending.length > 200) { | ||
| 1256 | + this.flush(); | ||
| 1257 | + } | ||
| 1258 | + this.scheduleRender(); | ||
| 1259 | + } | ||
| 1260 | + | ||
| 1261 | + clear(message = null) { | ||
| 1262 | + this.lines = []; | ||
| 1263 | + this.pending = []; | ||
| 1264 | + this.pool = []; | ||
| 1265 | + if (message) { | ||
| 1266 | + this.lines.push({ text: message, className: 'console-line' }); | ||
| 1267 | + } | ||
| 1268 | + this.scheduleRender(true); | ||
| 1269 | + } | ||
| 1270 | + | ||
| 1271 | + flush() { | ||
| 1272 | + if (!this.pending.length) return; | ||
| 1273 | + this.lines.push(...this.pending); | ||
| 1274 | + this.pending = []; | ||
| 1275 | + } | ||
| 1276 | + | ||
| 1277 | + scheduleRender(force = false) { | ||
| 1278 | + if (!this.container) return; | ||
| 1279 | + if (!force && this.rafId) return; | ||
| 1280 | + this.rafId = requestAnimationFrame(() => { | ||
| 1281 | + this.rafId = null; | ||
| 1282 | + this.render(); | ||
| 1283 | + }); | ||
| 1284 | + } | ||
| 1285 | + | ||
| 1286 | + render() { | ||
| 1287 | + this.flush(); | ||
| 1288 | + const total = this.lines.length; | ||
| 1289 | + if (!total) { | ||
| 1290 | + this.container.innerHTML = ''; | ||
| 1291 | + return; | ||
| 1292 | + } | ||
| 1293 | + | ||
| 1294 | + const lh = this.lineHeight; | ||
| 1295 | + const viewport = this.container.clientHeight || 1; | ||
| 1296 | + const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); | ||
| 1297 | + | ||
| 1298 | + const start = Math.max(0, Math.floor(this.container.scrollTop / lh) - Math.floor(visible / 2)); | ||
| 1299 | + const end = Math.min(total, start + visible); | ||
| 1300 | + const beforeHeight = start * lh; | ||
| 1301 | + const afterHeight = (total - end) * lh; | ||
| 1302 | + | ||
| 1303 | + const needed = end - start; | ||
| 1304 | + while (this.pool.length < needed) { | ||
| 1305 | + const node = document.createElement('div'); | ||
| 1306 | + node.className = 'console-line'; | ||
| 1307 | + this.pool.push(node); | ||
| 1308 | + } | ||
| 1309 | + | ||
| 1310 | + const fragment = document.createDocumentFragment(); | ||
| 1311 | + for (let idx = start; idx < end; idx++) { | ||
| 1312 | + const line = this.lines[idx]; | ||
| 1313 | + const node = this.pool[idx - start]; | ||
| 1314 | + node.className = line.className || 'console-line'; | ||
| 1315 | + node.textContent = line.text; | ||
| 1316 | + fragment.appendChild(node); | ||
| 1317 | + } | ||
| 1318 | + | ||
| 1319 | + this.container.innerHTML = ''; | ||
| 1320 | + const beforeSpacer = document.createElement('div'); | ||
| 1321 | + beforeSpacer.style.height = `${beforeHeight}px`; | ||
| 1322 | + const afterSpacer = document.createElement('div'); | ||
| 1323 | + afterSpacer.style.height = `${afterHeight}px`; | ||
| 1324 | + this.container.appendChild(beforeSpacer); | ||
| 1325 | + this.container.appendChild(fragment); | ||
| 1326 | + this.container.appendChild(afterSpacer); | ||
| 1327 | + | ||
| 1328 | + const shouldStick = (this.container.scrollTop + this.container.clientHeight) >= (this.container.scrollHeight - lh * 2); | ||
| 1329 | + if (shouldStick) { | ||
| 1330 | + this.container.scrollTop = this.container.scrollHeight; | ||
| 1331 | + } | ||
| 1332 | + } | ||
| 1333 | + } | ||
| 1229 | 1334 | ||
| 1230 | const CONFIG_ENDPOINT = '/api/config'; | 1335 | const CONFIG_ENDPOINT = '/api/config'; |
| 1231 | const SYSTEM_STATUS_ENDPOINT = '/api/system/status'; | 1336 | const SYSTEM_STATUS_ENDPOINT = '/api/system/status'; |
| @@ -2109,6 +2214,7 @@ | @@ -2109,6 +2214,7 @@ | ||
| 2109 | placeholder.className = 'console-line'; | 2214 | placeholder.className = 'console-line'; |
| 2110 | placeholder.textContent = `[系统] ${appNames[app] || app} 日志就绪`; | 2215 | placeholder.textContent = `[系统] ${appNames[app] || app} 日志就绪`; |
| 2111 | layer.appendChild(placeholder); | 2216 | layer.appendChild(placeholder); |
| 2217 | + logRenderers[app] = new LogVirtualList(layer); | ||
| 2112 | 2218 | ||
| 2113 | container.appendChild(layer); | 2219 | container.appendChild(layer); |
| 2114 | consoleLayers[app] = layer; | 2220 | consoleLayers[app] = layer; |
| @@ -2136,6 +2242,7 @@ | @@ -2136,6 +2242,7 @@ | ||
| 2136 | 2242 | ||
| 2137 | container.appendChild(layer); | 2243 | container.appendChild(layer); |
| 2138 | consoleLayers[app] = layer; | 2244 | consoleLayers[app] = layer; |
| 2245 | + logRenderers[app] = new LogVirtualList(layer); | ||
| 2139 | return layer; | 2246 | return layer; |
| 2140 | } | 2247 | } |
| 2141 | 2248 | ||
| @@ -2169,40 +2276,28 @@ | @@ -2169,40 +2276,28 @@ | ||
| 2169 | return; | 2276 | return; |
| 2170 | } | 2277 | } |
| 2171 | 2278 | ||
| 2172 | - const container = getConsoleContainer(); | ||
| 2173 | - if (container) { | ||
| 2174 | - container.scrollTop = container.scrollHeight; | ||
| 2175 | - consoleLayerScrollPositions[app] = container.scrollTop; | 2279 | + const renderer = logRenderers[app]; |
| 2280 | + if (renderer && renderer.container) { | ||
| 2281 | + renderer.container.scrollTop = renderer.container.scrollHeight; | ||
| 2282 | + consoleLayerScrollPositions[app] = renderer.container.scrollTop; | ||
| 2176 | } | 2283 | } |
| 2177 | } | 2284 | } |
| 2178 | 2285 | ||
| 2179 | function appendConsoleTextLine(app, text, className = 'console-line') { | 2286 | function appendConsoleTextLine(app, text, className = 'console-line') { |
| 2180 | - const layer = getConsoleLayer(app); | ||
| 2181 | - if (!layer) return; | ||
| 2182 | - | ||
| 2183 | - const line = document.createElement('div'); | ||
| 2184 | - line.className = className; | ||
| 2185 | - line.textContent = text; | ||
| 2186 | - layer.appendChild(line); | ||
| 2187 | - syncConsoleScroll(app); | 2287 | + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); |
| 2288 | + renderer.append(text, className); | ||
| 2188 | } | 2289 | } |
| 2189 | 2290 | ||
| 2190 | function appendConsoleElement(app, element) { | 2291 | function appendConsoleElement(app, element) { |
| 2191 | - const layer = getConsoleLayer(app); | ||
| 2192 | - if (!layer || !element) return; | ||
| 2193 | - | ||
| 2194 | - layer.appendChild(element); | ||
| 2195 | - syncConsoleScroll(app); | 2292 | + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); |
| 2293 | + if (!element || !renderer.container) return; | ||
| 2294 | + renderer.container.appendChild(element); | ||
| 2295 | + renderer.scheduleRender(true); | ||
| 2196 | } | 2296 | } |
| 2197 | 2297 | ||
| 2198 | function clearConsoleLayer(app, message = null) { | 2298 | function clearConsoleLayer(app, message = null) { |
| 2199 | - const layer = getConsoleLayer(app); | ||
| 2200 | - if (!layer) return; | ||
| 2201 | - | ||
| 2202 | - layer.innerHTML = ''; | ||
| 2203 | - if (message) { | ||
| 2204 | - appendConsoleTextLine(app, message); | ||
| 2205 | - } | 2299 | + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); |
| 2300 | + renderer.clear(message); | ||
| 2206 | } | 2301 | } |
| 2207 | 2302 | ||
| 2208 | // 加载控制台输出 | 2303 | // 加载控制台输出 |
-
Please register or login to post a comment