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