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
@@ -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<ReturnType<typeof reflectionApi.getBundle>>
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 () => {
// 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 }
)
@@ -0,0 +1,19 @@
/** 按车型 key 查找已保存的动作白名单(兼容 fullTypeName / shortTypeName / 后缀匹配)。 */
export function lookupCarActionKeys(
map: Record<string, string[] | undefined> | 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 []
}
@@ -0,0 +1,24 @@
import { reflectionApi, type MonitorConfigPayload } from '@/api/reflection'
/** 进程内缓存:避免每次选中车辆都 GET /monitor-config(全量反射扫描,极慢)。 */
let cached: MonitorConfigPayload | null = null
let inflight: Promise<MonitorConfigPayload> | null = null
export function invalidateMonitorConfigCache(): void {
cached = null
inflight = null
}
export async function fetchMonitorConfigCached(force = false): Promise<MonitorConfigPayload> {
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
}
@@ -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<string, string[]> }
export function defaultMonitorPolicy(): MonitorOpsPolicy {
return JSON.parse(JSON.stringify(DEFAULT_OPS.monitor)) as MonitorOpsPolicy
}
@@ -27,11 +35,22 @@ export function normalizeOpsConfig(raw?: Partial<OpsConfig> | null): OpsConfig {
}
}
type MonitorConfigRoot = MonitorVisibilityMap & { carActionByType?: Record<string, string[]> }
/** 深拷贝车型动作表,避免与 SimpleLite 返回对象共享引用导致勾选 UI 不同步。 */
export function cloneCarActionByType(
src?: Record<string, string[] | readonly string[]> | null
): Record<string, string[]> {
const out: Record<string, string[]> = {}
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<OpsConfig> {
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<LoadMonitorSettingsResult> {
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 }
}
/**
* 将运营维护表单中的地图监控配置写入 SimpleLitesimple.json)。
* 保留「地图监控配置」页面对 fields/status 的既有设置,只更新动作相关字段。
*/
export async function saveMonitorSettingsFromOps(payload: OpsConfig): Promise<void> {
export interface SaveMonitorSettingsOptions {
/** 页面加载时缓存的 monitor-config,避免勾选自动保存时重复 GET(该接口会全量反射扫描)。 */
cachedRoot?: MonitorConfigRoot | null
}
export async function saveMonitorSettingsFromOps(
payload: OpsConfig,
options?: SaveMonitorSettingsOptions
): Promise<MonitorConfigRoot> {
const monitor = normalizeOpsConfig(payload).monitor
let current: MonitorConfigRoot = emptyMonitorVisibilityMap()
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<vo
status: [...(current.track?.status ?? [])],
methods: [...monitor.track.actionKeys]
},
carActionByType: { ...monitor.carActionByType }
carActionByType: cloneCarActionByType(monitor.carActionByType)
})
invalidateMonitorConfigCache()
return saved as MonitorConfigRoot
}
/** @deprecated 使用 loadMonitorSettingsIntoOps */
@@ -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>
<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="按车型动作配置">
<el-collapse class="type-actions">
<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
:model-value="getCarTypeActions(payload, row)"
: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">
<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="getMonitor(payload).site.actionKeys"
: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">
<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"
: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">
<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>