diff --git a/xiaobai-datahub/admin/app.js b/xiaobai-datahub/admin/app.js index 00f8691..a71abbd 100644 --- a/xiaobai-datahub/admin/app.js +++ b/xiaobai-datahub/admin/app.js @@ -174,10 +174,20 @@ const FLASH_ON = 90, FLASH_GAP = 160, MAX_CLUSTER = 3; const GLOBAL_WINDOW = 1000, GLOBAL_CAP = 3; const globalFlashStarts = []; function reserveGlobalSlot(tNow) { + // 清掉确定已经过去(早于 tNow 一个窗口以上)的排队记录, + // 未来还会发生的排队起点必须保留,否则后续簇的计算会失去锚点。 while (globalFlashStarts.length && tNow - globalFlashStarts[0] > GLOBAL_WINDOW) globalFlashStarts.shift(); - if (globalFlashStarts.length < GLOBAL_CAP) { globalFlashStarts.push(tNow); return tNow; } - const wait = (globalFlashStarts[0] + GLOBAL_WINDOW - tNow) + 40 + Math.random() * 120; - const start = tNow + Math.max(30, wait); + const n = globalFlashStarts.length; + let start; + if (n < GLOBAL_CAP) { + // 队列未满:本簇可以立即起闪。 + start = tNow; + } else { + // 队列已排到 CAP:新起点必须晚于「倒数第 CAP 个已排队起点」一个窗口以上, + // 这样任何滚动 1 秒窗口内的新起簇数量都 ≤ CAP,而不是全都挤在同一段延迟里。 + const anchor = globalFlashStarts[n - GLOBAL_CAP]; + start = Math.max(tNow, anchor + GLOBAL_WINDOW + 40 + Math.random() * 120); + } globalFlashStarts.push(start); globalFlashStarts.sort((a, b) => a - b); return start; @@ -1245,6 +1255,9 @@ const Stage = (() => { el.querySelectorAll("[data-act]").forEach((btn, i) => { btn.onclick = c.actions[i].action; }); } function syncUI(idx, p) { + // 脏标记驱动:只在幕次真正切换时才重建 #cabin / 重新定位 rail, + // 不再每帧(60x/秒)重建,避免真实鼠标点击/输入所在节点被整体替换; + // 同幕内的数据更新交给下方 Views.onChange(真实数据变化时才刷新)。 if (idx !== cabinIdx) { const el = $("cabin"); el.classList.add("hide"); @@ -1255,10 +1268,7 @@ const Stage = (() => { $("crumb").textContent = `8766 · ${SCENES[idx].label} · 持续运转`; $("phase").textContent = `${idx + 1} / ${SCENES.length}`; state.scene = idx; - } else { - renderCabin(idx); } - if (Detail.openKey) Detail.reopen(); } Views.onChange(() => { if (running && cabinIdx >= 0) renderCabin(cabinIdx);