From e6c49a0d78adb3be5ac8874e9ee2e569455c4cca Mon Sep 17 00:00:00 2001 From: "zhaowei.huang" <228127304@qq.com> Date: Sun, 31 May 2026 00:16:49 +0800 Subject: [PATCH] =?UTF-8?q?perf(monitor):=20=E7=9B=91=E6=8E=A7=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=A0=E8=BD=BD=E7=BC=93=E5=AD=98=E5=8E=BB=E9=87=8D?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E5=8F=8D=E5=B0=84=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 monitorConfigCache 进程内缓存(inflight 去重)与 carActionByTypeLookup - opsMonitorConfig 支持传入预加载快照与缓存根,保存后失效缓存 - 运维配置页/选中信息面板复用缓存,修复车型动作勾选与 model 不同步 Co-authored-by: Cursor --- .../workbench/MonitorSelectionPanel.vue | 86 +++-- .../src/utils/carActionByTypeLookup.ts | 19 ++ .../src/utils/monitorConfigCache.ts | 24 ++ .../src/utils/opsMonitorConfig.ts | 74 ++++- .../src/views/admin/config/OpsConfigView.vue | 310 ++++++++++-------- 5 files changed, 336 insertions(+), 177 deletions(-) create mode 100644 frontends/apps/simple-platform-vue/src/utils/carActionByTypeLookup.ts create mode 100644 frontends/apps/simple-platform-vue/src/utils/monitorConfigCache.ts diff --git a/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue b/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue index 101f19e..6b5528c 100644 --- a/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue +++ b/frontends/apps/simple-platform-vue/src/components/workbench/MonitorSelectionPanel.vue @@ -211,6 +211,7 @@ import { import { OPS_WHITELIST, type OpsAction } from '@/types/ops' import { executeOp } from '@/api/ops' import { useAuthStore } from '@/stores/auth' +import { fetchMonitorConfigCached } from '@/utils/monitorConfigCache' import type { Car } from '@/types/car' import type { Mission } from '@/types/mission' import type { SelectedObjectRef } from '@/types/workbench' @@ -557,7 +558,7 @@ function selectionKey(sel: SelectedObjectRef | null): string { async function loadMonitorRuntimeConfig(force = false) { if (monitorConfigLoaded.value && !force) return try { - const mc = await reflectionApi.getMonitorConfig() + const mc = await fetchMonitorConfigCached(force) carActionByType.value = { ...(mc.config.carActionByType ?? {}) } siteActionKeys.value = [...(mc.config.site?.methods ?? [])] trackActionKeys.value = [...(mc.config.track?.methods ?? [])] @@ -570,6 +571,40 @@ async function loadMonitorRuntimeConfig(force = false) { } } +type BundlePayload = Awaited> + +function applyBundle(bundle: BundlePayload, fallbackName?: string) { + bundleName.value = bundle.summary?.name ?? fallbackName ?? '' + bundleTypeName.value = bundle.typeName ?? '' + bundleFullTypeName.value = + ('fullTypeName' in bundle && bundle.fullTypeName) ? bundle.fullTypeName : bundle.typeName ?? '' + fieldMap.value = { ...bundle.fields } + for (const f of bundle.fieldList ?? []) { + fieldMap.value[f.key] = f.value + } + statusRows.value = bundle.status ?? [] + allMethods.value = bundle.methods ?? [] +} + +/** 车辆列表已有投影数据时先展示,bundle 返回后再补齐。 */ +function applyCarListPreview(): boolean { + const car = findCarInList() + if (!car) return false + bundleName.value = car.name + bundleTypeName.value = car.typeName ?? '' + bundleFullTypeName.value = car.typeName ?? '' + const pct = Math.round(car.batterySoc <= 1 ? car.batterySoc * 100 : car.batterySoc) + fieldMap.value = { + x: String(car.x), + y: String(car.y), + th: String(car.theta), + batterySoc: String(pct) + } + statusRows.value = [] + allMethods.value = [] + return true +} + async function loadAll() { if (!props.selection) { bundleName.value = '' @@ -584,7 +619,9 @@ async function loadAll() { if (!rk || !Number.isFinite(idNum)) return const soft = hydrated.value - if (!soft) initialLoading.value = true + const listPreview = !soft && viewKind.value === 'vehicle' && applyCarListPreview() + if (listPreview) hydrated.value = true + if (!soft) initialLoading.value = !listPreview else actionsLoading.value = true try { @@ -592,34 +629,14 @@ async function loadAll() { // 运营端只读:只取 bundle 展示详情,不加载 reflection 动作/运营配置; // 失败时静默降级,由 findCarInList() 用 cars 列表数据兜底。 const bundle = await reflectionApi.getBundle(rk, idNum) - bundleName.value = bundle.summary?.name ?? props.selection.name ?? '' - bundleTypeName.value = bundle.typeName ?? '' - bundleFullTypeName.value = - ('fullTypeName' in bundle && bundle.fullTypeName) ? bundle.fullTypeName : bundle.typeName ?? '' - fieldMap.value = { ...bundle.fields } - for (const f of bundle.fieldList ?? []) { - fieldMap.value[f.key] = f.value - } - statusRows.value = bundle.status ?? [] + applyBundle(bundle, props.selection.name ?? '') allMethods.value = [] hydrated.value = true return } - await loadMonitorRuntimeConfig(!soft) - const [bundle, methods] = await Promise.all([ - reflectionApi.getBundle(rk, idNum), - reflectionApi.listMethods(rk, idNum) - ]) - bundleName.value = bundle.summary?.name ?? props.selection.name ?? '' - bundleTypeName.value = bundle.typeName ?? '' - bundleFullTypeName.value = - ('fullTypeName' in bundle && bundle.fullTypeName) ? bundle.fullTypeName : bundle.typeName ?? '' - fieldMap.value = { ...bundle.fields } - for (const f of bundle.fieldList ?? []) { - fieldMap.value[f.key] = f.value - } - statusRows.value = bundle.status ?? [] - allMethods.value = methods + await loadMonitorRuntimeConfig(false) + const bundle = await reflectionApi.getBundle(rk, idNum) + applyBundle(bundle, props.selection.name ?? '') hydrated.value = true } catch (e) { if (props.readOnly) { @@ -689,14 +706,23 @@ async function onSitePickOnMap() { } let loadToken = 0 +let lastLoadedSelectionKey = '' watch( () => [selectionKey(props.selection), props.refreshKey] as const, - () => { + async (curr, prev) => { const token = ++loadToken - void (async () => { - await loadAll() + // refreshKey 单独变化(如运营维护保存动作白名单):只刷新配置,不重复拉 bundle。 + if (prev && curr[0] === prev[0] && curr[0] && curr[1] !== prev[1]) { + await loadMonitorRuntimeConfig(true) if (token !== loadToken) return - })() + return + } + if (curr[0] !== lastLoadedSelectionKey) { + hydrated.value = false + lastLoadedSelectionKey = curr[0] + } + await loadAll() + if (token !== loadToken) return }, { immediate: true } ) diff --git a/frontends/apps/simple-platform-vue/src/utils/carActionByTypeLookup.ts b/frontends/apps/simple-platform-vue/src/utils/carActionByTypeLookup.ts new file mode 100644 index 0000000..0b7104e --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/utils/carActionByTypeLookup.ts @@ -0,0 +1,19 @@ +/** 按车型 key 查找已保存的动作白名单(兼容 fullTypeName / shortTypeName / 后缀匹配)。 */ +export function lookupCarActionKeys( + map: Record | null | undefined, + typeKey: string, + shortTypeName: string +): string[] { + if (!map) return [] + const direct = map[typeKey] ?? map[shortTypeName] + if (direct?.length) return [...direct] + for (const [k, v] of Object.entries(map)) { + if (!v?.length) continue + if (k === typeKey || k === shortTypeName) return [...v] + if (typeKey.endsWith('.' + k) || k.endsWith('.' + shortTypeName)) return [...v] + if (typeKey.endsWith('.' + shortTypeName) && (k === shortTypeName || k.endsWith('.' + shortTypeName))) { + return [...v] + } + } + return [] +} diff --git a/frontends/apps/simple-platform-vue/src/utils/monitorConfigCache.ts b/frontends/apps/simple-platform-vue/src/utils/monitorConfigCache.ts new file mode 100644 index 0000000..bd2bc33 --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/utils/monitorConfigCache.ts @@ -0,0 +1,24 @@ +import { reflectionApi, type MonitorConfigPayload } from '@/api/reflection' + +/** 进程内缓存:避免每次选中车辆都 GET /monitor-config(全量反射扫描,极慢)。 */ +let cached: MonitorConfigPayload | null = null +let inflight: Promise | null = null + +export function invalidateMonitorConfigCache(): void { + cached = null + inflight = null +} + +export async function fetchMonitorConfigCached(force = false): Promise { + if (!force && cached) return cached + if (!force && inflight) return inflight + inflight = reflectionApi.getMonitorConfig().then((mc) => { + cached = mc + inflight = null + return mc + }).catch((e) => { + inflight = null + throw e + }) + return inflight +} diff --git a/frontends/apps/simple-platform-vue/src/utils/opsMonitorConfig.ts b/frontends/apps/simple-platform-vue/src/utils/opsMonitorConfig.ts index a9d417f..6c38fc7 100644 --- a/frontends/apps/simple-platform-vue/src/utils/opsMonitorConfig.ts +++ b/frontends/apps/simple-platform-vue/src/utils/opsMonitorConfig.ts @@ -1,7 +1,15 @@ -import { reflectionApi, emptyMonitorVisibilityMap, type MonitorVisibilityMap } from '@/api/reflection' +import { + reflectionApi, + emptyMonitorVisibilityMap, + type MonitorConfigPayload, + type MonitorVisibilityMap +} from '@/api/reflection' +import { invalidateMonitorConfigCache } from '@/utils/monitorConfigCache' import { DEFAULT_OPS } from '@/mock/data/configs' import type { MonitorOpsPolicy, OpsConfig } from '@/types/config' +export type MonitorConfigRoot = MonitorVisibilityMap & { carActionByType?: Record } + export function defaultMonitorPolicy(): MonitorOpsPolicy { return JSON.parse(JSON.stringify(DEFAULT_OPS.monitor)) as MonitorOpsPolicy } @@ -27,11 +35,22 @@ export function normalizeOpsConfig(raw?: Partial | null): OpsConfig { } } -type MonitorConfigRoot = MonitorVisibilityMap & { carActionByType?: Record } +/** 深拷贝车型动作表,避免与 SimpleLite 返回对象共享引用导致勾选 UI 不同步。 */ +export function cloneCarActionByType( + src?: Record | null +): Record { + const out: Record = {} + if (!src) return out + for (const [k, v] of Object.entries(src)) { + if (!k || !Array.isArray(v)) continue + out[k] = [...v] + } + return out +} function pickMonitorRoot(cfg: MonitorConfigRoot) { return { - carActionByType: { ...(cfg.carActionByType ?? {}) }, + carActionByType: cloneCarActionByType(cfg.carActionByType), siteActionKeys: [...(cfg.site?.methods ?? [])], trackActionKeys: [...(cfg.track?.methods ?? [])] } @@ -41,35 +60,56 @@ function pickMonitorRoot(cfg: MonitorConfigRoot) { * 从 SimpleLite simple.json 加载地图监控配置(运行时唯一可信源)。 * Platform ops 里的 monitor 仅作备份;以 SimpleLite 为准覆盖表单。 */ -export async function loadMonitorSettingsIntoOps(payload: OpsConfig): Promise { +export interface LoadMonitorSettingsResult { + config: OpsConfig + /** 与 SimpleLite 对齐的 monitor-config 快照,供保存时合并 fields/status,避免每次 POST 前再 GET。 */ + cache: MonitorConfigRoot | null +} + +export async function loadMonitorSettingsIntoOps( + payload: OpsConfig, + preloaded?: MonitorConfigPayload | null +): Promise { const next = normalizeOpsConfig(payload) + let cache: MonitorConfigRoot | null = null try { - const mc = await reflectionApi.getMonitorConfig() - const picked = pickMonitorRoot(mc.config as MonitorConfigRoot) + const mc = preloaded ?? (await reflectionApi.getMonitorConfig()) + cache = mc.config as MonitorConfigRoot + const picked = pickMonitorRoot(cache) next.monitor.carActionByType = picked.carActionByType next.monitor.site.actionKeys = picked.siteActionKeys next.monitor.track.actionKeys = picked.trackActionKeys } catch { // SimpleLite 未连接:退回 Platform payload 中已有的 monitor(若有) } - return next + return { config: next, cache } } /** * 将运营维护表单中的地图监控配置写入 SimpleLite(simple.json)。 * 保留「地图监控配置」页面对 fields/status 的既有设置,只更新动作相关字段。 */ -export async function saveMonitorSettingsFromOps(payload: OpsConfig): Promise { +export interface SaveMonitorSettingsOptions { + /** 页面加载时缓存的 monitor-config,避免勾选自动保存时重复 GET(该接口会全量反射扫描)。 */ + cachedRoot?: MonitorConfigRoot | null +} + +export async function saveMonitorSettingsFromOps( + payload: OpsConfig, + options?: SaveMonitorSettingsOptions +): Promise { const monitor = normalizeOpsConfig(payload).monitor - let current: MonitorConfigRoot = emptyMonitorVisibilityMap() - try { - const mc = await reflectionApi.getMonitorConfig() - current = mc.config as MonitorConfigRoot - } catch { - // 无现有配置时用空壳,仍可写入动作白名单 + let current: MonitorConfigRoot = options?.cachedRoot ?? emptyMonitorVisibilityMap() + if (!options?.cachedRoot) { + try { + const mc = await reflectionApi.getMonitorConfig() + current = mc.config as MonitorConfigRoot + } catch { + // 无现有配置时用空壳,仍可写入动作白名单 + } } - await reflectionApi.saveMonitorConfig({ + const saved = await reflectionApi.saveMonitorConfig({ car: { fields: [...(current.car?.fields ?? [])], status: [...(current.car?.status ?? [])], @@ -85,8 +125,10 @@ export async function saveMonitorSettingsFromOps(payload: OpsConfig): Promise + :before-save="onBeforePlatformSave">