|
@@ -84,7 +84,7 @@
|
|
|
<!-- 票 102:顶栏只留在用的按钮(归位 / 返回全球)与时钟。
|
|
<!-- 票 102:顶栏只留在用的按钮(归位 / 返回全球)与时钟。
|
|
|
维护者 2026-11-02:配置三态、只读标记、模式读数三个诊断读数下屏 ——
|
|
维护者 2026-11-02:配置三态、只读标记、模式读数三个诊断读数下屏 ——
|
|
|
它们既占走查视线,又把右侧状态区撑宽,挤得中列菜单左右晃动。 -->
|
|
它们既占走查视线,又把右侧状态区撑宽,挤得中列菜单左右晃动。 -->
|
|
|
- <span class="cockpit-shell__clock">{{ clockText }}</span>
|
|
|
|
|
|
|
+ <CockpitClock />
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
@@ -527,6 +527,8 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
import { storeToRefs } from 'pinia'
|
|
import { storeToRefs } from 'pinia'
|
|
|
import { MODES, columnsForMode, labelForPanel, modeLabel, panelGrow, panelSideFor, panelSubtitle } from './domain/panelCatalog.js'
|
|
import { MODES, columnsForMode, labelForPanel, modeLabel, panelGrow, panelSideFor, panelSubtitle } from './domain/panelCatalog.js'
|
|
|
import { panelView, hasPanelView } from './domain/panelViews.js'
|
|
import { panelView, hasPanelView } from './domain/panelViews.js'
|
|
|
|
|
+// 票 98:面板视图按输入快照记忆化(模板里每次重渲染都会调 panelViewOf)。
|
|
|
|
|
+import { createViewMemo } from './domain/panelViewCache.js'
|
|
|
import { CARD_ACTION } from './domain/panelBlocks.js'
|
|
import { CARD_ACTION } from './domain/panelBlocks.js'
|
|
|
// 票 67:全局筛选(当前范围/行业)生效面的**单一措辞来源**(面板口径说明 + 管线图层那句)。
|
|
// 票 67:全局筛选(当前范围/行业)生效面的**单一措辞来源**(面板口径说明 + 管线图层那句)。
|
|
|
import { PIPE_DISTRICT_NOTE } from './domain/filterCaliber.js'
|
|
import { PIPE_DISTRICT_NOTE } from './domain/filterCaliber.js'
|
|
@@ -552,6 +554,7 @@ import { useFilterStore } from './stores/filterStore.js'
|
|
|
import { usePipeNetworkStore } from './stores/pipeNetworkStore.js'
|
|
import { usePipeNetworkStore } from './stores/pipeNetworkStore.js'
|
|
|
import { useFacilityPointStore } from './stores/facilityPointStore.js'
|
|
import { useFacilityPointStore } from './stores/facilityPointStore.js'
|
|
|
import CockpitPanel from './components/CockpitPanel.vue'
|
|
import CockpitPanel from './components/CockpitPanel.vue'
|
|
|
|
|
+import CockpitClock from './components/CockpitClock.vue'
|
|
|
import CockpitIcon from './components/CockpitIcon.vue'
|
|
import CockpitIcon from './components/CockpitIcon.vue'
|
|
|
import MapCanvas from './components/MapCanvas.vue'
|
|
import MapCanvas from './components/MapCanvas.vue'
|
|
|
import PipeEnhancement from './components/PipeEnhancement.vue'
|
|
import PipeEnhancement from './components/PipeEnhancement.vue'
|
|
@@ -571,7 +574,7 @@ import PointDetailCard from './components/PointDetailCard.vue'
|
|
|
import FacilityDetailCard from './components/FacilityDetailCard.vue'
|
|
import FacilityDetailCard from './components/FacilityDetailCard.vue'
|
|
|
import { DETAIL_LEVEL } from './domain/detailFlow.js'
|
|
import { DETAIL_LEVEL } from './domain/detailFlow.js'
|
|
|
import { homeLabel } from './domain/viewPresets.js'
|
|
import { homeLabel } from './domain/viewPresets.js'
|
|
|
-import { ensureCockpitMocks } from './mocks/index.js'
|
|
|
|
|
|
|
+// 2026-10-30 加载优化:契约 mock 样例(约 210KB)只在 mock 模式下动态载入,
|
|
|
// real 模式的首屏不再为一个用不到的样例包付出解析成本。
|
|
// real 模式的首屏不再为一个用不到的样例包付出解析成本。
|
|
|
// 票 14:启动计划(规格 09 的主接缝)——real 与 mock 同构启动,差别只剩 registerMocks。
|
|
// 票 14:启动计划(规格 09 的主接缝)——real 与 mock 同构启动,差别只剩 registerMocks。
|
|
|
import { STARTUP_ACTION, planCockpitStartup } from './domain/startupPlan.js'
|
|
import { STARTUP_ACTION, planCockpitStartup } from './domain/startupPlan.js'
|
|
@@ -734,36 +737,58 @@ function archiveManholeCount() {
|
|
|
return row && Number.isFinite(Number(row.count)) ? Number(row.count) : null
|
|
return row && Number.isFinite(Number(row.count)) ? Number(row.count) : null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 面板视图缓存:输入快照不变就返回同一个视图对象(子组件 props 稳定 ⇒ 不重渲染)。 */
|
|
|
|
|
+const panelViewMemo = createViewMemo()
|
|
|
|
|
+
|
|
|
function panelViewOf(panelId) {
|
|
function panelViewOf(panelId) {
|
|
|
if (!hasPanelView(panelId)) return null
|
|
if (!hasPanelView(panelId)) return null
|
|
|
const section = sectionOf(panelId) || pendingSection()
|
|
const section = sectionOf(panelId) || pendingSection()
|
|
|
/** 台账要额外喂一份徽标(票 18 的 N+1);票 14:监测分布的饼图要喂全局未筛分布与当前选中。 */
|
|
/** 台账要额外喂一份徽标(票 18 的 N+1);票 14:监测分布的饼图要喂全局未筛分布与当前选中。 */
|
|
|
- let extra
|
|
|
|
|
- if (panelId === 'monitoringPointLedger') extra = monitoringBadges.value
|
|
|
|
|
- else if (panelId === 'monitoringDistribution') {
|
|
|
|
|
- extra = { statusDistribution: mapStore.statusDistribution, selectedStatuses: pointStatus.value }
|
|
|
|
|
- }
|
|
|
|
|
- // 票 64:运行信息面板要当前**片区**键,用来给指标列表算页码归属(换片区 ⇒ 页码回第 1 页,
|
|
|
|
|
- // 周期刷新 ⇒ 页码保持)。只传标量、不传块对象,与票 43 第四轮「别拿对象当键」同一条纪律。
|
|
|
|
|
- // 票 64:运行信息面板要当前**片区**键,用来给指标列表算页码归属(换片区 ⇒ 页码回第 1 页)。
|
|
|
|
|
- // 维护者 2026-10-29:运行信息还要一个「窨井数量」读数,而本端点没有窨井指标
|
|
|
|
|
- // (MANHOLE 块实测 state=EMPTY、0 条)—— 从**基础设施档案目录**搬(与「基础档案」同一份目录,
|
|
|
|
|
- // 与"设施数量与基础档案同源"那条口径一致)。取不到就传 null,卡片画占位符,不造 0。
|
|
|
|
|
- if (panelId === 'operation') extra = { scope: scope.value, manholeCount: archiveManholeCount() }
|
|
|
|
|
- // 票 62 追加需求:运行概况的**设备台账**数据在 `monitoringOverview` 分区(`devices[]`,
|
|
|
|
|
- // 与那边 `counts.equipmentCount` 同源),而本面板自己的分区是 `operationOverview`
|
|
|
|
|
- // (`/monitoring/situation`:趋势/指标状态/异常口径)。跨分区取数只在这一处搬运 ——
|
|
|
|
|
- // 与「一块面板的数据来自哪几处」同一立场,注册表签名保持 `(panelId, data, section, extra)`。
|
|
|
|
|
- else if (panelId === 'operationOverview') {
|
|
|
|
|
- const overview = monitoringSections.value.monitoringOverview
|
|
|
|
|
- extra = { devices: (overview && overview.data && overview.data.devices) || [] }
|
|
|
|
|
- }
|
|
|
|
|
- // 2026-10-22(票 02):overview 新四块都是**单源面板**(各自一个端点、一份领域对象),
|
|
|
|
|
- // 不再有「态势总览横跨三块」那种合成喂养 —— 一律直读自己的分区数据。
|
|
|
|
|
- // 票 11:感知设备的类型卡可点(按行业筛地图),所以要把**当前生效的行业键**喂给它画选中态。
|
|
|
|
|
- // 只传标量:卡片行自己不知道 store 是什么(渲染层也仍不认识筛选语义)。
|
|
|
|
|
- else if (panelId === 'perceptionDevices') extra = { industry: filterStore.industry }
|
|
|
|
|
- return panelView(panelId, section.data, section, extra)
|
|
|
|
|
|
|
+ // 票 98:这里是**每次重渲染都会被调用**的方法(模板里 `:view="panelViewOf(panelId)"`)。
|
|
|
|
|
+ // 外壳还有 500ms 归位轮询、WS 推送、store 更新等重渲染源 —— 若不记忆化,
|
|
|
|
|
+ // 基础设施/运行监测这两个大视图(块最多、行最大)会被反复整体重建。
|
|
|
|
|
+ // 所以:先用**稳定输入快照**(分区对象/数据对象/几个标量)查缓存,未变就返回同一个视图对象。
|
|
|
|
|
+ // 快照纪律:只放稳定引用或标量;临时对象(如 `{ scope, count }`)必须在 `build` 里现建。
|
|
|
|
|
+ const overview = panelId === 'operationOverview' ? monitoringSections.value.monitoringOverview : null
|
|
|
|
|
+ // 快照用**字段值**而不是分区对象身份:`sectionOf` 认不出的面板会走
|
|
|
|
|
+ // `pendingSection()`,它每次渲染都返回新字面量 —— 拿身份做键会每帧 miss,缓存等于没有。
|
|
|
|
|
+ // 空 `missing` 也折成 null(同一个理由:新字面量里是空数组,引用每帧都不同)。
|
|
|
|
|
+ const inputs = [
|
|
|
|
|
+ section.state,
|
|
|
|
|
+ section.reason,
|
|
|
|
|
+ section.mode,
|
|
|
|
|
+ section.data,
|
|
|
|
|
+ section.missing && section.missing.length ? section.missing : null,
|
|
|
|
|
+ section.freshness,
|
|
|
|
|
+ section.retained,
|
|
|
|
|
+ section.loaded,
|
|
|
|
|
+ // 只有对应面板消费的那一项才进快照(其余位置填 null,保持槽位对齐)。
|
|
|
|
|
+ panelId === 'monitoringPointLedger' ? monitoringBadges.value : null,
|
|
|
|
|
+ panelId === 'monitoringDistribution' ? mapStore.statusDistribution : null,
|
|
|
|
|
+ panelId === 'monitoringDistribution' ? pointStatus.value : null,
|
|
|
|
|
+ panelId === 'operation' ? scope.value : null,
|
|
|
|
|
+ panelId === 'operation' ? archiveManholeCount() : null,
|
|
|
|
|
+ panelId === 'operationOverview' ? (overview && overview.data ? overview.data.devices : null) : null,
|
|
|
|
|
+ panelId === 'perceptionDevices' ? filterStore.industry : null,
|
|
|
|
|
+ ]
|
|
|
|
|
+ return panelViewMemo.read(panelId, inputs, () => {
|
|
|
|
|
+ let extra
|
|
|
|
|
+ if (panelId === 'monitoringPointLedger') extra = monitoringBadges.value
|
|
|
|
|
+ else if (panelId === 'monitoringDistribution') {
|
|
|
|
|
+ extra = { statusDistribution: mapStore.statusDistribution, selectedStatuses: pointStatus.value }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 票 64:运行信息要当前**片区**键(换片区 ⇒ 页码回第 1 页,周期刷新 ⇒ 页码保持)。
|
|
|
|
|
+ // 维护者 2026-10-29:本端点没有窨井指标(MANHOLE 块实测 state=EMPTY、0 条),
|
|
|
|
|
+ // 窨井数量从**基础设施档案目录**搬(与「基础档案」同源);取不到传 null,不造 0。
|
|
|
|
|
+ else if (panelId === 'operation') extra = { scope: scope.value, manholeCount: archiveManholeCount() }
|
|
|
|
|
+ // 票 62:运行概况的设备台账在 `monitoringOverview` 分区(跨分区取数只在这一处搬运)。
|
|
|
|
|
+ else if (panelId === 'operationOverview') {
|
|
|
|
|
+ extra = { devices: (overview && overview.data && overview.data.devices) || [] }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 票 11:感知设备的类型卡可点,要当前生效行业键画选中态(只传标量)。
|
|
|
|
|
+ else if (panelId === 'perceptionDevices') extra = { industry: filterStore.industry }
|
|
|
|
|
+ return panelView(panelId, section.data, section, extra)
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1587,29 +1612,78 @@ function requestRefresh(options = {}) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 当前一级模式的面板取数(票 14 的面板半边)。
|
|
|
|
|
- * 面板取数失败不抛(各分区自己收成 FAILED/UNAVAILABLE 并如实标注),
|
|
|
|
|
- * 所以一块面板失败不会让本轮刷新整体失败(票 61 验收第 1 条)。
|
|
|
|
|
|
|
+ * 已取过面板数据的模式集合(2026-10-30 加载优化)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 旧行为:启动计划把**四个模式**的面板全部预加载(19 个请求,基础设施三块各约 0.8s),
|
|
|
|
|
+ * 首屏实际只需要当前模式,其余请求既占带宽又推迟首屏。
|
|
|
|
|
+ * 现在:启动只取当前模式;其它模式**首次进入时**再取。筛选变化会失效这里的缓存,
|
|
|
|
|
+ * 下次进入按最新条件重取;刷新按钮/轮询仍然只刷当前模式(`refreshModePanels`)。
|
|
|
*/
|
|
*/
|
|
|
-async function refreshModePanels() {
|
|
|
|
|
- const current = mode.value
|
|
|
|
|
- if (current === 'monitoring') {
|
|
|
|
|
- await monitoringStore.loadPanels({
|
|
|
|
|
|
|
+const loadedPanelModes = new Set()
|
|
|
|
|
+
|
|
|
|
|
+/** 离开 /cockpit 后不再启动延迟任务(重图层空闲加载可能晚于卸载)。 */
|
|
|
|
|
+let shellDisposed = false
|
|
|
|
|
+
|
|
|
|
|
+/** 首屏帧之后/浏览器空闲时再跑重任务;不支持 requestIdleCallback 时退化为一次短延时。 */
|
|
|
|
|
+function scheduleIdle(task) {
|
|
|
|
|
+ if (typeof window === 'undefined') return
|
|
|
|
|
+ const run = () => {
|
|
|
|
|
+ if (!shellDisposed) task()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof window.requestIdleCallback === 'function') {
|
|
|
|
|
+ window.requestIdleCallback(run, { timeout: 1200 })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ window.setTimeout(run, 200)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 某个模式的面板取数(当前筛选条件;不判断缓存)。 */
|
|
|
|
|
+function loadPanelsForMode(target) {
|
|
|
|
|
+ if (target === 'monitoring') {
|
|
|
|
|
+ return monitoringStore.loadPanels({
|
|
|
industry: industry.value,
|
|
industry: industry.value,
|
|
|
deviceType: deviceType.value,
|
|
deviceType: deviceType.value,
|
|
|
pointStatus: pointStatus.value,
|
|
pointStatus: pointStatus.value,
|
|
|
scope: scope.value,
|
|
scope: scope.value,
|
|
|
})
|
|
})
|
|
|
- } else if (current === 'infrastructure') {
|
|
|
|
|
- await infrastructureStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
- } else if (current === 'warningResponse') {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ if (target === 'infrastructure') {
|
|
|
|
|
+ return infrastructureStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
+ }
|
|
|
|
|
+ if (target === 'warningResponse') {
|
|
|
// 票 67:片区也要跟着下(预警分析是事件族里唯一收片区入参的端点)。
|
|
// 票 67:片区也要跟着下(预警分析是事件族里唯一收片区入参的端点)。
|
|
|
- await eventStore.loadPanels({ mode: eventStore.mode, industry: industry.value, scope: scope.value })
|
|
|
|
|
- } else {
|
|
|
|
|
- // 票 89:四模块端点已由后端补齐片区/行业入参 —— 周期刷新也按当前筛选取数,
|
|
|
|
|
- // 否则刷新一次就把筛选悄悄丢回全域。
|
|
|
|
|
- await overviewStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
|
|
+ return eventStore.loadPanels({
|
|
|
|
|
+ mode: eventStore.mode,
|
|
|
|
|
+ range: eventStore.range,
|
|
|
|
|
+ industry: industry.value,
|
|
|
|
|
+ scope: scope.value,
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
+ // 票 89:四模块端点已由后端补齐片区/行业入参 —— 刷新也按当前筛选取数。
|
|
|
|
|
+ return overviewStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 首次进入某个模式时取一次面板(已取过就跳过;筛选变化会清缓存)。 */
|
|
|
|
|
+function ensureModePanelsLoaded(target = mode.value) {
|
|
|
|
|
+ if (!lifecycleReady.value || loadedPanelModes.has(target)) return
|
|
|
|
|
+ loadedPanelModes.add(target)
|
|
|
|
|
+ loadPanelsForMode(target)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 筛选变化 → 失效缓存;每次进入/刷新都按最新条件重新取。 */
|
|
|
|
|
+function invalidateModePanels(target = '') {
|
|
|
|
|
+ if (target) loadedPanelModes.delete(target)
|
|
|
|
|
+ else loadedPanelModes.clear()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 当前一级模式的面板取数(票 14 的面板半边)。
|
|
|
|
|
+ * 面板取数失败不抛(各分区自己收成 FAILED/UNAVAILABLE 并如实标注),
|
|
|
|
|
+ * 所以一块面板失败不会让本轮刷新整体失败(票 61 验收第 1 条)。
|
|
|
|
|
+ */
|
|
|
|
|
+async function refreshModePanels() {
|
|
|
|
|
+ loadedPanelModes.add(mode.value)
|
|
|
|
|
+ await loadPanelsForMode(mode.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ---- 配置状态(票 04):票 102 起顶栏不再显示配置读数,这里只留判定结果,
|
|
// ---- 配置状态(票 04):票 102 起顶栏不再显示配置读数,这里只留判定结果,
|
|
@@ -1865,17 +1939,12 @@ function onAnyActivity() {
|
|
|
markActivity()
|
|
markActivity()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ---- 顶栏时钟 ----
|
|
|
|
|
-const clockText = ref('')
|
|
|
|
|
-let clockTimer = null
|
|
|
|
|
|
|
+// ---- 顶栏时钟:已抽成 <CockpitClock /> 子组件(票 98)----
|
|
|
|
|
+// 秒针若留在这里,1Hz 会重渲染整个外壳 —— 而外壳模板里对**每个可见面板**都要调
|
|
|
|
|
+// panelViewOf(),基础设施/运行监测这类大视图因此每秒被整体重建一次(维护者报的卡顿)。
|
|
|
|
|
+// 时钟关进自己的组件后,1Hz 更新只重渲染那一个 span。
|
|
|
let homeTimer = null
|
|
let homeTimer = null
|
|
|
|
|
|
|
|
-function tick() {
|
|
|
|
|
- const now = new Date()
|
|
|
|
|
- const pad = (n) => String(n).padStart(2, '0')
|
|
|
|
|
- clockText.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 票 41:锚定跟随的帧驱动与恢复。
|
|
* 票 41:锚定跟随的帧驱动与恢复。
|
|
|
*
|
|
*
|
|
@@ -1889,8 +1958,7 @@ let stopCameraState = null
|
|
|
let anchorResizeHandler = null
|
|
let anchorResizeHandler = null
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
- tick()
|
|
|
|
|
- clockTimer = setInterval(tick, 1000)
|
|
|
|
|
|
|
+ // 顶栏时钟由 <CockpitClock /> 自带计时器(票 98)——外壳不再有 1Hz 重渲染源。
|
|
|
// 归位按钮的高亮状态跟着相机走(用户手动平移后就不再算"在家")。
|
|
// 归位按钮的高亮状态跟着相机走(用户手动平移后就不再算"在家")。
|
|
|
homeTimer = setInterval(refreshHomeState, 500)
|
|
homeTimer = setInterval(refreshHomeState, 500)
|
|
|
|
|
|
|
@@ -1905,7 +1973,7 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 票 04:配置只在这个入口载入一次(服务内部有缓存),地图服务按需 await 同一份。
|
|
// 票 04:配置只在这个入口载入一次(服务内部有缓存),地图服务按需 await 同一份。
|
|
|
- loadCockpitConfig().then((result) => {
|
|
|
|
|
|
|
+ loadCockpitConfig().then(async (result) => {
|
|
|
configState.value = result
|
|
configState.value = result
|
|
|
// 配置到手才知道引导开关与预设坐标,重新取一次。
|
|
// 配置到手才知道引导开关与预设坐标,重新取一次。
|
|
|
intro.value = getIntroConfig()
|
|
intro.value = getIntroConfig()
|
|
@@ -1923,11 +1991,30 @@ onMounted(() => {
|
|
|
const plan = planCockpitStartup(
|
|
const plan = planCockpitStartup(
|
|
|
withUrlDataSourceOverride(result.config, null, !!(import.meta.env && import.meta.env.DEV)),
|
|
withUrlDataSourceOverride(result.config, null, !!(import.meta.env && import.meta.env.DEV)),
|
|
|
)
|
|
)
|
|
|
|
|
+ // 配置一次下发到四个面板 store:面板改为**按需取数**后,未进入的模式也要能随时被
|
|
|
|
|
+ // `ensureModePanelsLoaded` 直接取,不再依赖各自的启动分支先 configure。
|
|
|
|
|
+ overviewStore.configure({ config: result.config })
|
|
|
|
|
+ infrastructureStore.configure({ config: result.config })
|
|
|
|
|
+ monitoringStore.configure({ config: result.config })
|
|
|
|
|
+ eventStore.configure({ config: result.config })
|
|
|
for (const action of plan.actions) {
|
|
for (const action of plan.actions) {
|
|
|
if (action === STARTUP_ACTION.REGISTER_MOCKS) {
|
|
if (action === STARTUP_ACTION.REGISTER_MOCKS) {
|
|
|
|
|
+ // 只有 mock 模式才需要样例包;real 的首屏不加载它。
|
|
|
|
|
+ const { ensureCockpitMocks } = await import('./mocks/index.js')
|
|
|
ensureCockpitMocks()
|
|
ensureCockpitMocks()
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
+ // 2026-10-30 加载优化:启动计划仍产出四个模式的面板动作(契约不变),但执行时
|
|
|
|
|
+ // **只取当前模式** —— 其余模式首次进入时由 `ensureModePanelsLoaded` 取。
|
|
|
|
|
+ // 旧行为把 19 个请求全打在启动上(基础设施三块各约 0.8s),首屏并不需要它们。
|
|
|
|
|
+ if (
|
|
|
|
|
+ action === STARTUP_ACTION.LOAD_INFRASTRUCTURE_PANELS ||
|
|
|
|
|
+ action === STARTUP_ACTION.LOAD_MONITORING_PANELS ||
|
|
|
|
|
+ action === STARTUP_ACTION.LOAD_EVENT_PANELS ||
|
|
|
|
|
+ action === STARTUP_ACTION.LOAD_OVERVIEW_PANELS
|
|
|
|
|
+ ) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
// 票 18:**先把筛选交给地图**,再让共享分区去取第 1 页(顺序纪律不变)。
|
|
// 票 18:**先把筛选交给地图**,再让共享分区去取第 1 页(顺序纪律不变)。
|
|
|
// 若筛选晚于它,第 1 页就带着空筛选发出去 —— 那正是"筛了但第一屏没筛上"的经典来源。
|
|
// 若筛选晚于它,第 1 页就带着空筛选发出去 —— 那正是"筛了但第一屏没筛上"的经典来源。
|
|
|
// (`mapStore.setup()` 是合并语义,所以后到的调用不会抹掉这里的条件。)
|
|
// (`mapStore.setup()` 是合并语义,所以后到的调用不会抹掉这里的条件。)
|
|
@@ -1948,42 +2035,18 @@ onMounted(() => {
|
|
|
// 会把 LOAD_MAP_SUMMARY 刚合并进 mapStore 的 config 键覆盖掉(合并语义下
|
|
// 会把 LOAD_MAP_SUMMARY 刚合并进 mapStore 的 config 键覆盖掉(合并语义下
|
|
|
// `undefined` 也是一次写入),real 下首批摘要请求就只能靠配置缓存兜底了。
|
|
// `undefined` 也是一次写入),real 下首批摘要请求就只能靠配置缓存兜底了。
|
|
|
detailStore.loadSharedSections({ config: result.config })
|
|
detailStore.loadSharedSections({ config: result.config })
|
|
|
- // 票 24:设施点位与管线是**跨模式共享**的图层(不随一级模式切换重取),
|
|
|
|
|
- // 所以跟共享分区一起启动,而不是塞进某个模式的 loadPanels。
|
|
|
|
|
- // 票 67 更正:片区也要交给管线图层(本地按编码前缀裁剪,不额外发请求)。
|
|
|
|
|
- pipeNetworkStore.load({ industry: industry.value, scope: scope.value, config: result.config })
|
|
|
|
|
// 票 24/68:设施点位与管线是**跨模式共享**的图层(不随一级模式切换重取)。
|
|
// 票 24/68:设施点位与管线是**跨模式共享**的图层(不随一级模式切换重取)。
|
|
|
- // 票 68:行业/片区/设施类型筛选都在 store 的 filtered 里本地生效。
|
|
|
|
|
- facilityPointStore.load({ config: result.config })
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- // 筛选用全局条件的当前值(CONTEXT.md:行业筛选作用于整个驾驶舱)。
|
|
|
|
|
- if (action === STARTUP_ACTION.LOAD_INFRASTRUCTURE_PANELS) {
|
|
|
|
|
- infrastructureStore.configure({ config: result.config })
|
|
|
|
|
- infrastructureStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- if (action === STARTUP_ACTION.LOAD_MONITORING_PANELS) {
|
|
|
|
|
- monitoringStore.configure({ config: result.config })
|
|
|
|
|
- monitoringStore.loadPanels({
|
|
|
|
|
- industry: industry.value,
|
|
|
|
|
- deviceType: deviceType.value,
|
|
|
|
|
- pointStatus: pointStatus.value,
|
|
|
|
|
- scope: scope.value,
|
|
|
|
|
|
|
+ // 2026-10-30 加载优化:几何一次约 6MB、设施点还要逐条补详情,属于首屏之后
|
|
|
|
|
+ // 才看得见的重图层 —— 放到浏览器空闲时再取,先让外壳/面板/引导页完成首帧。
|
|
|
|
|
+ // 地图侧仍按 store 数据到达后同步图层,不会漏画。
|
|
|
|
|
+ scheduleIdle(() => {
|
|
|
|
|
+ // 票 67 更正:片区也要交给管线图层(本地按编码前缀裁剪,不额外发请求)。
|
|
|
|
|
+ pipeNetworkStore.load({ industry: industry.value, scope: scope.value, config: result.config })
|
|
|
|
|
+ // 票 68:行业/片区/设施类型筛选都在 store 的 `filtered` 里本地生效。
|
|
|
|
|
+ facilityPointStore.load({ config: result.config })
|
|
|
})
|
|
})
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- if (action === STARTUP_ACTION.LOAD_EVENT_PANELS) {
|
|
|
|
|
- eventStore.configure({ config: result.config })
|
|
|
|
|
- eventStore.loadPanels({ mode: 'WARNING', range: '7d', industry: industry.value })
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- if (action === STARTUP_ACTION.LOAD_OVERVIEW_PANELS) {
|
|
|
|
|
- overviewStore.configure({ config: result.config })
|
|
|
|
|
- // 票 89:启动取数同样带上全局筛选(与基础设施/运行监测量块同一条纪律)。
|
|
|
|
|
- overviewStore.loadPanels({ industry: industry.value, scope: scope.value })
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
// 票 15:状态推送。外壳只说"用配置启动"——地址、凭据与 mock/real 的连接方式
|
|
// 票 15:状态推送。外壳只说"用配置启动"——地址、凭据与 mock/real 的连接方式
|
|
|
// 都由 runtimeStore 决定(页面因此不认识凭据,也不认识假连接)。
|
|
// 都由 runtimeStore 决定(页面因此不认识凭据,也不认识假连接)。
|
|
|
// 没配地址时服务进 `disabled`,界面如实说"未配置",不假装连上。
|
|
// 没配地址时服务进 `disabled`,界面如实说"未配置",不假装连上。
|
|
@@ -2011,6 +2074,8 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
lifecycleReady.value = true
|
|
lifecycleReady.value = true
|
|
|
|
|
+ // 首屏只取当前模式;其它模式在第一次切过去时再取(筛选变化会清缓存)。
|
|
|
|
|
+ ensureModePanelsLoaded(mode.value)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 引导期间显示自转;进入后停(引擎就绪前调用会被忽略,等引擎 ready 再兜一次)。
|
|
// 引导期间显示自转;进入后停(引擎就绪前调用会被忽略,等引擎 ready 再兜一次)。
|
|
@@ -2076,6 +2141,8 @@ onMounted(() => {
|
|
|
if (!facilityDetailOpen.value) entityDetailStore.close()
|
|
if (!facilityDetailOpen.value) entityDetailStore.close()
|
|
|
applyModeSelectionTransition(prev, next)
|
|
applyModeSelectionTransition(prev, next)
|
|
|
syncDeviceLayer({ force: true })
|
|
syncDeviceLayer({ force: true })
|
|
|
|
|
+ // 面板按需取:第一次进入该模式时补一次;已取过则由筛选变化负责失效。
|
|
|
|
|
+ ensureModePanelsLoaded(next)
|
|
|
}, { deep: false }),
|
|
}, { deep: false }),
|
|
|
// 票 46:引导态 ⇄ 进入系统的门控(规格 18:entered 决定一切业务对象显隐)。
|
|
// 票 46:引导态 ⇄ 进入系统的门控(规格 18:entered 决定一切业务对象显隐)。
|
|
|
watch(introVisible, (next) => {
|
|
watch(introVisible, (next) => {
|
|
@@ -2104,12 +2171,9 @@ onMounted(() => {
|
|
|
*/
|
|
*/
|
|
|
watch(industry, (next) => {
|
|
watch(industry, (next) => {
|
|
|
if (!lifecycleReady.value) return
|
|
if (!lifecycleReady.value) return
|
|
|
- infrastructureStore.loadPanels({ industry: next })
|
|
|
|
|
- // 票 18:运行监测三块也跟随;地图换新快照重载(Q9:丢弃旧范围的已加载对象)。
|
|
|
|
|
- monitoringStore.loadPanels({ industry: next })
|
|
|
|
|
- // 票 13:当前预警的事件列表跟随全局行业(`/events/current` 收 industry 入参)。
|
|
|
|
|
- eventStore.loadPanels({ mode: eventStore.mode, industry: next })
|
|
|
|
|
- overviewStore.loadPanels({ industry: next })
|
|
|
|
|
|
|
+ // 2026-10-30 加载优化:只立即重取**当前模式**;其它模式失效缓存,切过去时按新条件取。
|
|
|
|
|
+ invalidateModePanels()
|
|
|
|
|
+ refreshModePanels()
|
|
|
// 票 24:行业筛选同样作用到管线与构件图层。几何是**全量取回、本地裁剪**,
|
|
// 票 24:行业筛选同样作用到管线与构件图层。几何是**全量取回、本地裁剪**,
|
|
|
// 所以切行业只改本地筛选条件、不重新发请求(切换必须即时,不等一个往返)。
|
|
// 所以切行业只改本地筛选条件、不重新发请求(切换必须即时,不等一个往返)。
|
|
|
pipeNetworkStore.setIndustry(next)
|
|
pipeNetworkStore.setIndustry(next)
|
|
@@ -2131,12 +2195,9 @@ watch(industry, (next) => {
|
|
|
*/
|
|
*/
|
|
|
watch(scope, (next) => {
|
|
watch(scope, (next) => {
|
|
|
if (!lifecycleReady.value) return
|
|
if (!lifecycleReady.value) return
|
|
|
- infrastructureStore.loadPanels({ scope: next })
|
|
|
|
|
- // 票 89:概览四块已接手片区入参,跟随重取(与行业筛选同一条纪律)。
|
|
|
|
|
- overviewStore.loadPanels({ scope: next })
|
|
|
|
|
- monitoringStore.loadPanels({ scope: next })
|
|
|
|
|
- // 换片区 = 换一批数据 ⇒ 页码回第 1 页由 eventStore 的条件键(含 `scope`)负责。
|
|
|
|
|
- eventStore.loadPanels({ mode: eventStore.mode, scope: next })
|
|
|
|
|
|
|
+ // 2026-10-30 加载优化:同行业筛选 —— 只刷当前模式,其它模式切过去时再取。
|
|
|
|
|
+ invalidateModePanels()
|
|
|
|
|
+ refreshModePanels()
|
|
|
// 票 67 更正(维护者 2026-10-23):管线与构件**也能按片区筛** —— 片区从设施编码前缀推
|
|
// 票 67 更正(维护者 2026-10-23):管线与构件**也能按片区筛** —— 片区从设施编码前缀推
|
|
|
// (片区+行业+时间编码)。几何是全量取回、本地裁剪,所以这里只改条件、不重新发请求。
|
|
// (片区+行业+时间编码)。几何是全量取回、本地裁剪,所以这里只改条件、不重新发请求。
|
|
|
pipeNetworkStore.setScope(next)
|
|
pipeNetworkStore.setScope(next)
|
|
@@ -2150,7 +2211,9 @@ watch(scope, (next) => {
|
|
|
*/
|
|
*/
|
|
|
watch(pointStatus, (next) => {
|
|
watch(pointStatus, (next) => {
|
|
|
if (!lifecycleReady.value) return
|
|
if (!lifecycleReady.value) return
|
|
|
- monitoringStore.loadPanels({ pointStatus: next })
|
|
|
|
|
|
|
+ // 只有运行监测吃点位状态;当前不在该模式时只失效缓存,切过去再取。
|
|
|
|
|
+ invalidateModePanels('monitoring')
|
|
|
|
|
+ if (mode.value === 'monitoring') refreshModePanels()
|
|
|
refreshMapForFilters({ pointStatus: next })
|
|
refreshMapForFilters({ pointStatus: next })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -2163,7 +2226,8 @@ watch(pointStatus, (next) => {
|
|
|
*/
|
|
*/
|
|
|
watch(deviceType, (next) => {
|
|
watch(deviceType, (next) => {
|
|
|
if (!lifecycleReady.value) return
|
|
if (!lifecycleReady.value) return
|
|
|
- monitoringStore.loadPanels({ deviceType: next })
|
|
|
|
|
|
|
+ invalidateModePanels('monitoring')
|
|
|
|
|
+ if (mode.value === 'monitoring') refreshModePanels()
|
|
|
refreshMapForFilters({ deviceType: next })
|
|
refreshMapForFilters({ deviceType: next })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -2175,8 +2239,9 @@ function refreshMapForFilters(next) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
|
|
|
+ shellDisposed = true
|
|
|
entityDetailStore.close()
|
|
entityDetailStore.close()
|
|
|
- if (clockTimer) clearInterval(clockTimer)
|
|
|
|
|
|
|
+
|
|
|
if (homeTimer) clearInterval(homeTimer)
|
|
if (homeTimer) clearInterval(homeTimer)
|
|
|
// 票 41:锚定跟随的帧订阅与尺寸监听必须拆掉(与票 15/37 的"漏取消定时器"是同一个坑)。
|
|
// 票 41:锚定跟随的帧订阅与尺寸监听必须拆掉(与票 15/37 的"漏取消定时器"是同一个坑)。
|
|
|
if (stopMapFrame) {
|
|
if (stopMapFrame) {
|
|
@@ -2596,10 +2661,8 @@ const mapDataSourceCredits = computed(() => getDataSourceCredits())
|
|
|
|
|
|
|
|
/* 票 102(维护者:时间字体长短不平均,秒针一跳按钮就跟着左右挪):
|
|
/* 票 102(维护者:时间字体长短不平均,秒针一跳按钮就跟着左右挪):
|
|
|
时钟改为**固定宽度的数字槽位**,槽位内右对齐 + 等宽数字(tabular-nums + tnum 兜底)。
|
|
时钟改为**固定宽度的数字槽位**,槽位内右对齐 + 等宽数字(tabular-nums + tnum 兜底)。
|
|
|
- 宽度按 2384×1160 截图实测(该图 CSS 视口即 2384×1160、--u=1.4326px;
|
|
|
|
|
- 同图 483px 的导航字形跨度 ÷1.4326 = 337u,与「4×90u+3×4u−2×17u内边距」吻合,
|
|
|
|
|
- 可反证 --u 取值无误):一次读数 `21:22:22` 的字形跨度 145px = 101u,
|
|
|
|
|
- 取 128u(+27% 余量)覆盖「把 1 换成 8」的最宽读数与字体回退
|
|
|
|
|
|
|
+ 宽度按 2384×1160 截图实测:`--u` 在该视口 ≈1.433px(0.1235vh),一次读数 `21:22:22`
|
|
|
|
|
+ 的字形跨度 145px ≈ 101u,取 128u(+27% 余量)覆盖「把 1 换成 8」的最宽读数与字体回退
|
|
|
(Rajdhani → Bahnschrift → Segoe,等宽数字不被支持时)—— 槽位只要不吃到内容宽度,
|
|
(Rajdhani → Bahnschrift → Segoe,等宽数字不被支持时)—— 槽位只要不吃到内容宽度,
|
|
|
宽度就恒由这条声明决定:秒针跳动只改槽位内的字形,不再改动任何布局宽度。
|
|
宽度就恒由这条声明决定:秒针跳动只改槽位内的字形,不再改动任何布局宽度。
|
|
|
flex: 0 0 auto —— 槽位不参与挤压,右列内容再多也不会压缩它。 */
|
|
flex: 0 0 auto —— 槽位不参与挤压,右列内容再多也不会压缩它。 */
|