perf(monitor): 监控配置加载缓存去重,避免重复全量反射扫描

- 新增 monitorConfigCache 进程内缓存(inflight 去重)与 carActionByTypeLookup
- opsMonitorConfig 支持传入预加载快照与缓存根,保存后失效缓存
- 运维配置页/选中信息面板复用缓存,修复车型动作勾选与 model 不同步

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-05-31 00:16:49 +08:00
co-authored by Cursor
parent 061acad9de
commit e6c49a0d78
5 changed files with 336 additions and 177 deletions
@@ -6,7 +6,7 @@
:defaults="DEFAULT_OPS"
:normalize-payload="normalizeOpsConfig"
:after-load="onOpsConfigLoaded"
:before-save="saveMonitorSettingsFromOps">
:before-save="onBeforePlatformSave">
<template #default="{ payload, update }">
<el-form label-width="200px" :model="payload">
<el-divider content-position="left">调度回放</el-divider>
@@ -28,66 +28,88 @@
<el-input-number :model-value="payload.version.keepReleases" :min="1" :max="100" @update:model-value="(v: number | undefined) => update({ ...payload, version: { keepReleases: v ?? 0 } })" />
</el-form-item>
<el-divider content-position="left">地图监控信息与动作管理</el-divider>
<el-alert
type="info"
:closable="false"
show-icon
:title="autoSaving ? '正在自动保存…' : '支持直接多选,修改后会自动保存到地图监控(simple.json),无需点击上方「保存」。若某组不勾选,表示该组在地图监控中显示全部。'"
style="margin-bottom: 10px" />
<el-form-item label="按车型动作配置">
<el-collapse class="type-actions">
<el-collapse-item
v-for="row in carTypeActions"
:key="row.typeKey"
:name="row.typeKey"
:title="`${row.title}${row.methods.length}`">
<el-checkbox-group
:model-value="getCarTypeActions(payload, row)"
class="cfg-check-grid"
@update:model-value="(v) => updateCarTypeActions(update, payload, row, v as string[])">
<el-checkbox v-for="m in row.methods" :key="`${row.typeKey}-${m.methodName}`" :label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</el-collapse-item>
</el-collapse>
</el-form-item>
<el-form-item label="站点动作显示项">
<el-checkbox-group
:model-value="getMonitor(payload).site.actionKeys"
class="cfg-check-grid"
@update:model-value="(v) => updateMonitorList(update, payload, 'site', 'actionKeys', v as string[])">
<el-checkbox v-for="m in siteMethods" :key="`sa-${m.methodName}`" :label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="路径动作显示项">
<el-checkbox-group
:model-value="getMonitor(payload).track.actionKeys"
class="cfg-check-grid"
@update:model-value="(v) => updateMonitorList(update, payload, 'track', 'actionKeys', v as string[])">
<el-checkbox v-for="m in trackMethods" :key="`ta-${m.methodName}`" :label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<div v-loading="monitorSectionLoading" class="monitor-section">
<el-alert
type="info"
:closable="false"
show-icon
:title="autoSaving ? '正在自动保存…' : '支持直接多选,修改后会自动保存到地图监控(simple.json),无需点击上方「保存」。若某组不勾选,表示该组在地图监控中显示全部。'"
style="margin-bottom: 10px" />
<el-form-item label="按车型动作配置" v-loading="carTypesLoading">
<el-collapse v-model="expandedCarTypes" class="type-actions">
<el-collapse-item
v-for="row in carTypeActions"
:key="row.typeKey"
:name="row.typeKey"
:title="`${row.title}${row.methods.length}`">
<div v-show="expandedCarTypes.includes(row.typeKey)">
<el-checkbox-group
:key="carCheckboxGroupKey(payload, row)"
:model-value="carActionsForRow(payload, row)"
class="cfg-check-grid"
@update:model-value="(v) => updateCarTypeActions(update, payload, row, v as string[])">
<el-checkbox
v-for="m in row.methods"
:key="`${row.typeKey}-${m.methodName}`"
:label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-collapse-item>
</el-collapse>
<p v-if="!monitorSectionLoading && !carTypesLoading && !carTypeActions.length" class="muted empty-hint">
暂未扫描到带 MethodMember 的车型(请确认 SimpleLite 已连接并已加载插件)。
</p>
</el-form-item>
<el-form-item label="站点动作显示项">
<el-checkbox-group
:model-value="payload.monitor?.site?.actionKeys ?? []"
class="cfg-check-grid"
@update:model-value="(v) => updateMonitorList(update, payload, 'site', 'actionKeys', v as string[])">
<el-checkbox
v-for="m in siteMethods"
:key="`sa-${m.methodName}`"
:label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="路径动作显示项">
<el-checkbox-group
:model-value="payload.monitor?.track?.actionKeys ?? []"
class="cfg-check-grid"
@update:model-value="(v) => updateMonitorList(update, payload, 'track', 'actionKeys', v as string[])">
<el-checkbox
v-for="m in trackMethods"
:key="`ta-${m.methodName}`"
:label="m.methodName">
{{ m.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</div>
</el-form>
</template>
</ConfigPageBase>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { onBeforeUnmount, shallowRef, ref } from 'vue'
import { ElMessage } from 'element-plus'
import ConfigPageBase from '@/components/ConfigPageBase.vue'
import { DEFAULT_OPS } from '@/mock/data/configs'
import { reflectionApi, emptyMonitorAvailableMap, type MonitorVisibilityMap } from '@/api/reflection'
import type { MonitorOpsPolicy, MonitorPanelPolicy, OpsConfig } from '@/types/config'
import { reflectionApi } from '@/api/reflection'
import type { MonitorPanelPolicy, OpsConfig } from '@/types/config'
import { lookupCarActionKeys } from '@/utils/carActionByTypeLookup'
import { fetchMonitorConfigCached } from '@/utils/monitorConfigCache'
import {
cloneCarActionByType,
defaultMonitorPolicy,
loadMonitorSettingsIntoOps,
normalizeOpsConfig,
saveMonitorSettingsFromOps
saveMonitorSettingsFromOps,
type MonitorConfigRoot
} from '@/utils/opsMonitorConfig'
type MonitorKind = 'car' | 'site' | 'track'
@@ -104,18 +126,21 @@ interface CarTypeActionRow {
methods: MethodOption[]
}
const available = reactive<MonitorVisibilityMap>(emptyMonitorAvailableMap())
const carTypeActions = ref<CarTypeActionRow[]>([])
const siteMethods = ref<MethodOption[]>([])
const trackMethods = ref<MethodOption[]>([])
const CAR_METHODS_CACHE_KEY = 'simple-platform-methods-by-type-car-v1'
const carTypeActions = shallowRef<CarTypeActionRow[]>([])
const siteMethods = shallowRef<MethodOption[]>([])
const trackMethods = shallowRef<MethodOption[]>([])
const monitorSectionLoading = ref(false)
const carTypesLoading = ref(false)
const expandedCarTypes = ref<string[]>([])
const cachedMonitorRoot = ref<MonitorConfigRoot | null>(null)
// 地图监控信息与动作管理这三组多选写入 SimpleLitesimple.json),与上方「保存」按钮走的
// Platform ops 配置是两条独立链路。这里做勾选即自动保存:防抖合并连续勾选,串行避免并发
// getMonitorConfig/saveMonitorConfig 竞态,卸载时把窗口内未落盘的改动补存。
const autoSaving = ref(false)
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
let pendingMonitorPayload: OpsConfig | null = null
const AUTO_SAVE_DELAY_MS = 450
const AUTO_SAVE_DELAY_MS = 500
function scheduleMonitorAutoSave(next: OpsConfig) {
pendingMonitorPayload = next
@@ -126,7 +151,6 @@ function scheduleMonitorAutoSave(next: OpsConfig) {
async function flushMonitorAutoSave() {
autoSaveTimer = null
if (autoSaving.value) {
// 上一次保存仍在进行:稍后重试,确保最新勾选最终落盘。
autoSaveTimer = setTimeout(() => { void flushMonitorAutoSave() }, 200)
return
}
@@ -135,7 +159,10 @@ async function flushMonitorAutoSave() {
if (!target) return
autoSaving.value = true
try {
await saveMonitorSettingsFromOps(target)
const saved = await saveMonitorSettingsFromOps(target, {
cachedRoot: cachedMonitorRoot.value
})
cachedMonitorRoot.value = saved
ElMessage.success({ message: '地图监控配置已自动保存', duration: 1200, grouping: true })
} catch (e) {
ElMessage.error(`地图监控配置自动保存失败:${e instanceof Error ? e.message : String(e)}`)
@@ -153,16 +180,88 @@ onBeforeUnmount(() => {
if (pendingMonitorPayload && !autoSaving.value) void flushMonitorAutoSave()
})
function normalizeMonitor(monitor?: MonitorOpsPolicy): MonitorOpsPolicy {
return normalizeOpsConfig({ monitor } as OpsConfig).monitor
}
async function onOpsConfigLoaded(payload: OpsConfig, update: (next: OpsConfig) => void) {
update(await loadMonitorSettingsIntoOps(payload))
monitorSectionLoading.value = true
carTypesLoading.value = true
try {
const mc = await fetchMonitorConfigCached()
const [{ config, cache }, _] = await Promise.all([
loadMonitorSettingsIntoOps(payload, mc),
loadCarTypeActions()
])
cachedMonitorRoot.value = cache
siteMethods.value = methodOptionsFromKeys(mc.available.site.methods)
trackMethods.value = methodOptionsFromKeys(mc.available.track.methods)
update(config)
} catch {
update(normalizeOpsConfig(payload))
siteMethods.value = []
trackMethods.value = []
await loadCarTypeActions()
} finally {
monitorSectionLoading.value = false
carTypesLoading.value = false
}
}
function getMonitor(payload: OpsConfig): MonitorOpsPolicy {
return normalizeMonitor(payload.monitor)
async function onBeforePlatformSave(payload: OpsConfig) {
cachedMonitorRoot.value = await saveMonitorSettingsFromOps(payload, {
cachedRoot: cachedMonitorRoot.value
})
}
async function loadCarTypeActions() {
try {
const raw = sessionStorage.getItem(CAR_METHODS_CACHE_KEY)
if (raw) {
const parsed = JSON.parse(raw) as CarTypeActionRow[]
if (Array.isArray(parsed) && parsed.length) {
carTypeActions.value = parsed
return
}
}
} catch { /* ignore */ }
try {
const rows = await reflectionApi.listMethodsByType('car')
const mapped = rows
.map((r) => ({
typeKey: r.fullTypeName ?? r.typeName,
shortTypeName: r.typeName,
title: `${r.typeLabel ?? r.typeName} / ${r.fullTypeName ?? r.typeName} [${r.assemblyName}]`,
methods: r.methods
.map((m) => ({ methodName: m.methodName, label: m.label || m.methodName }))
.filter((m) => !!m.methodName)
}))
.filter((r) => r.methods.length > 0)
.sort((a, b) => a.title.localeCompare(b.title))
carTypeActions.value = mapped
try {
sessionStorage.setItem(CAR_METHODS_CACHE_KEY, JSON.stringify(mapped))
} catch { /* quota */ }
} catch {
carTypeActions.value = []
}
}
function methodOptionsFromKeys(keys?: string[] | null): MethodOption[] {
if (!keys?.length) return []
return keys.map((methodName) => ({ methodName, label: methodName }))
}
function ensureMonitor(payload: OpsConfig) {
return payload.monitor ?? defaultMonitorPolicy()
}
function carActionsForRow(payload: OpsConfig, row: CarTypeActionRow): string[] {
const map = ensureMonitor(payload).carActionByType
return lookupCarActionKeys(map, row.typeKey, row.shortTypeName)
}
/** 展开/已选变化时强制 checkbox-group 重挂载,修复 Element Plus 勾选与 model 不同步。 */
function carCheckboxGroupKey(payload: OpsConfig, row: CarTypeActionRow): string {
const selected = carActionsForRow(payload, row).slice().sort().join('\0')
return `${row.typeKey}\0${selected}`
}
function updateMonitorList(
@@ -172,7 +271,7 @@ function updateMonitorList(
key: MonitorKey,
list: string[]
) {
const monitor = getMonitor(payload)
const monitor = ensureMonitor(payload)
const next: OpsConfig = {
...payload,
monitor: {
@@ -193,11 +292,9 @@ function updateCarTypeActions(
row: CarTypeActionRow,
methods: string[]
) {
const monitor = getMonitor(payload)
const nextMap = {
...monitor.carActionByType,
[row.typeKey]: [...methods]
}
const monitor = ensureMonitor(payload)
const nextMap = cloneCarActionByType(monitor.carActionByType)
nextMap[row.typeKey] = [...methods]
if (row.shortTypeName !== row.typeKey) {
delete nextMap[row.shortTypeName]
}
@@ -211,68 +308,12 @@ function updateCarTypeActions(
update(next)
scheduleMonitorAutoSave(next)
}
function getCarTypeActions(payload: OpsConfig, row: CarTypeActionRow): string[] {
const monitor = getMonitor(payload)
return monitor.carActionByType[row.typeKey]
?? monitor.carActionByType[row.shortTypeName]
?? []
}
onMounted(async () => {
try {
const mc = await reflectionApi.getMonitorConfig()
available.car = mc.available.car
available.site = mc.available.site
available.track = mc.available.track
} catch {
available.car = { fields: [], status: [], methods: [] }
available.site = { fields: [], status: [], methods: [] }
available.track = { fields: [], status: [], methods: [] }
}
try {
const rows = await reflectionApi.listMethodsByType('car')
carTypeActions.value = rows
.map((r) => ({
typeKey: r.fullTypeName ?? r.typeName,
shortTypeName: r.typeName,
title: `${r.typeLabel ?? r.typeName} / ${r.fullTypeName ?? r.typeName} [${r.assemblyName}]`,
methods: r.methods
.map((m) => ({ methodName: m.methodName, label: m.label || m.methodName }))
.filter((m) => !!m.methodName)
}))
.filter((r) => r.methods.length > 0)
.sort((a, b) => a.title.localeCompare(b.title))
} catch {
carTypeActions.value = []
}
try {
const rows = await reflectionApi.listMethodsByType('site')
siteMethods.value = dedupeMethodOptions(rows.flatMap((r) => r.methods))
} catch {
siteMethods.value = available.site.methods.map((m) => ({ methodName: m, label: m }))
}
try {
const rows = await reflectionApi.listMethodsByType('track')
trackMethods.value = dedupeMethodOptions(rows.flatMap((r) => r.methods))
} catch {
trackMethods.value = available.track.methods.map((m) => ({ methodName: m, label: m }))
}
})
function dedupeMethodOptions(methods: Array<{ methodName: string; label?: string | null }>): MethodOption[] {
const map = new Map<string, MethodOption>()
for (const m of methods) {
if (!m.methodName) continue
if (!map.has(m.methodName)) {
map.set(m.methodName, { methodName: m.methodName, label: m.label || m.methodName })
}
}
return [...map.values()].sort((a, b) => a.label.localeCompare(b.label))
}
</script>
<style scoped>
.monitor-section {
min-height: 80px;
}
.cfg-check-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
@@ -281,4 +322,11 @@ function dedupeMethodOptions(methods: Array<{ methodName: string; label?: string
.type-actions {
width: 100%;
}
.empty-hint {
margin: 4px 0 0;
font-size: 12px;
}
.muted {
color: var(--mg-text-muted);
}
</style>