perf(monitor): 监控配置加载缓存去重,避免重复全量反射扫描
- 新增 monitorConfigCache 进程内缓存(inflight 去重)与 carActionByTypeLookup - opsMonitorConfig 支持传入预加载快照与缓存根,保存后失效缓存 - 运维配置页/选中信息面板复用缓存,修复车型动作勾选与 model 不同步 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
// 地图监控信息与动作管理这三组多选写入 SimpleLite(simple.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>
|
||||
|
||||
Reference in New Issue
Block a user