perf(monitor): 监控配置加载缓存去重,避免重复全量反射扫描
- 新增 monitorConfigCache 进程内缓存(inflight 去重)与 carActionByTypeLookup - opsMonitorConfig 支持传入预加载快照与缓存根,保存后失效缓存 - 运维配置页/选中信息面板复用缓存,修复车型动作勾选与 model 不同步 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+56
-30
@@ -211,6 +211,7 @@ import {
|
|||||||
import { OPS_WHITELIST, type OpsAction } from '@/types/ops'
|
import { OPS_WHITELIST, type OpsAction } from '@/types/ops'
|
||||||
import { executeOp } from '@/api/ops'
|
import { executeOp } from '@/api/ops'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { fetchMonitorConfigCached } from '@/utils/monitorConfigCache'
|
||||||
import type { Car } from '@/types/car'
|
import type { Car } from '@/types/car'
|
||||||
import type { Mission } from '@/types/mission'
|
import type { Mission } from '@/types/mission'
|
||||||
import type { SelectedObjectRef } from '@/types/workbench'
|
import type { SelectedObjectRef } from '@/types/workbench'
|
||||||
@@ -557,7 +558,7 @@ function selectionKey(sel: SelectedObjectRef | null): string {
|
|||||||
async function loadMonitorRuntimeConfig(force = false) {
|
async function loadMonitorRuntimeConfig(force = false) {
|
||||||
if (monitorConfigLoaded.value && !force) return
|
if (monitorConfigLoaded.value && !force) return
|
||||||
try {
|
try {
|
||||||
const mc = await reflectionApi.getMonitorConfig()
|
const mc = await fetchMonitorConfigCached(force)
|
||||||
carActionByType.value = { ...(mc.config.carActionByType ?? {}) }
|
carActionByType.value = { ...(mc.config.carActionByType ?? {}) }
|
||||||
siteActionKeys.value = [...(mc.config.site?.methods ?? [])]
|
siteActionKeys.value = [...(mc.config.site?.methods ?? [])]
|
||||||
trackActionKeys.value = [...(mc.config.track?.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() {
|
async function loadAll() {
|
||||||
if (!props.selection) {
|
if (!props.selection) {
|
||||||
bundleName.value = ''
|
bundleName.value = ''
|
||||||
@@ -584,7 +619,9 @@ async function loadAll() {
|
|||||||
if (!rk || !Number.isFinite(idNum)) return
|
if (!rk || !Number.isFinite(idNum)) return
|
||||||
|
|
||||||
const soft = hydrated.value
|
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
|
else actionsLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -592,34 +629,14 @@ async function loadAll() {
|
|||||||
// 运营端只读:只取 bundle 展示详情,不加载 reflection 动作/运营配置;
|
// 运营端只读:只取 bundle 展示详情,不加载 reflection 动作/运营配置;
|
||||||
// 失败时静默降级,由 findCarInList() 用 cars 列表数据兜底。
|
// 失败时静默降级,由 findCarInList() 用 cars 列表数据兜底。
|
||||||
const bundle = await reflectionApi.getBundle(rk, idNum)
|
const bundle = await reflectionApi.getBundle(rk, idNum)
|
||||||
bundleName.value = bundle.summary?.name ?? props.selection.name ?? ''
|
applyBundle(bundle, 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 = []
|
allMethods.value = []
|
||||||
hydrated.value = true
|
hydrated.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await loadMonitorRuntimeConfig(!soft)
|
await loadMonitorRuntimeConfig(false)
|
||||||
const [bundle, methods] = await Promise.all([
|
const bundle = await reflectionApi.getBundle(rk, idNum)
|
||||||
reflectionApi.getBundle(rk, idNum),
|
applyBundle(bundle, props.selection.name ?? '')
|
||||||
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
|
|
||||||
hydrated.value = true
|
hydrated.value = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (props.readOnly) {
|
if (props.readOnly) {
|
||||||
@@ -689,14 +706,23 @@ async function onSitePickOnMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let loadToken = 0
|
let loadToken = 0
|
||||||
|
let lastLoadedSelectionKey = ''
|
||||||
watch(
|
watch(
|
||||||
() => [selectionKey(props.selection), props.refreshKey] as const,
|
() => [selectionKey(props.selection), props.refreshKey] as const,
|
||||||
() => {
|
async (curr, prev) => {
|
||||||
const token = ++loadToken
|
const token = ++loadToken
|
||||||
void (async () => {
|
// refreshKey 单独变化(如运营维护保存动作白名单):只刷新配置,不重复拉 bundle。
|
||||||
await loadAll()
|
if (prev && curr[0] === prev[0] && curr[0] && curr[1] !== prev[1]) {
|
||||||
|
await loadMonitorRuntimeConfig(true)
|
||||||
if (token !== loadToken) return
|
if (token !== loadToken) return
|
||||||
})()
|
return
|
||||||
|
}
|
||||||
|
if (curr[0] !== lastLoadedSelectionKey) {
|
||||||
|
hydrated.value = false
|
||||||
|
lastLoadedSelectionKey = curr[0]
|
||||||
|
}
|
||||||
|
await loadAll()
|
||||||
|
if (token !== loadToken) return
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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 { DEFAULT_OPS } from '@/mock/data/configs'
|
||||||
import type { MonitorOpsPolicy, OpsConfig } from '@/types/config'
|
import type { MonitorOpsPolicy, OpsConfig } from '@/types/config'
|
||||||
|
|
||||||
|
export type MonitorConfigRoot = MonitorVisibilityMap & { carActionByType?: Record<string, string[]> }
|
||||||
|
|
||||||
export function defaultMonitorPolicy(): MonitorOpsPolicy {
|
export function defaultMonitorPolicy(): MonitorOpsPolicy {
|
||||||
return JSON.parse(JSON.stringify(DEFAULT_OPS.monitor)) as 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) {
|
function pickMonitorRoot(cfg: MonitorConfigRoot) {
|
||||||
return {
|
return {
|
||||||
carActionByType: { ...(cfg.carActionByType ?? {}) },
|
carActionByType: cloneCarActionByType(cfg.carActionByType),
|
||||||
siteActionKeys: [...(cfg.site?.methods ?? [])],
|
siteActionKeys: [...(cfg.site?.methods ?? [])],
|
||||||
trackActionKeys: [...(cfg.track?.methods ?? [])]
|
trackActionKeys: [...(cfg.track?.methods ?? [])]
|
||||||
}
|
}
|
||||||
@@ -41,35 +60,56 @@ function pickMonitorRoot(cfg: MonitorConfigRoot) {
|
|||||||
* 从 SimpleLite simple.json 加载地图监控配置(运行时唯一可信源)。
|
* 从 SimpleLite simple.json 加载地图监控配置(运行时唯一可信源)。
|
||||||
* Platform ops 里的 monitor 仅作备份;以 SimpleLite 为准覆盖表单。
|
* 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)
|
const next = normalizeOpsConfig(payload)
|
||||||
|
let cache: MonitorConfigRoot | null = null
|
||||||
try {
|
try {
|
||||||
const mc = await reflectionApi.getMonitorConfig()
|
const mc = preloaded ?? (await reflectionApi.getMonitorConfig())
|
||||||
const picked = pickMonitorRoot(mc.config as MonitorConfigRoot)
|
cache = mc.config as MonitorConfigRoot
|
||||||
|
const picked = pickMonitorRoot(cache)
|
||||||
next.monitor.carActionByType = picked.carActionByType
|
next.monitor.carActionByType = picked.carActionByType
|
||||||
next.monitor.site.actionKeys = picked.siteActionKeys
|
next.monitor.site.actionKeys = picked.siteActionKeys
|
||||||
next.monitor.track.actionKeys = picked.trackActionKeys
|
next.monitor.track.actionKeys = picked.trackActionKeys
|
||||||
} catch {
|
} catch {
|
||||||
// SimpleLite 未连接:退回 Platform payload 中已有的 monitor(若有)
|
// SimpleLite 未连接:退回 Platform payload 中已有的 monitor(若有)
|
||||||
}
|
}
|
||||||
return next
|
return { config: next, cache }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将运营维护表单中的地图监控配置写入 SimpleLite(simple.json)。
|
* 将运营维护表单中的地图监控配置写入 SimpleLite(simple.json)。
|
||||||
* 保留「地图监控配置」页面对 fields/status 的既有设置,只更新动作相关字段。
|
* 保留「地图监控配置」页面对 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
|
const monitor = normalizeOpsConfig(payload).monitor
|
||||||
let current: MonitorConfigRoot = emptyMonitorVisibilityMap()
|
let current: MonitorConfigRoot = options?.cachedRoot ?? emptyMonitorVisibilityMap()
|
||||||
try {
|
if (!options?.cachedRoot) {
|
||||||
const mc = await reflectionApi.getMonitorConfig()
|
try {
|
||||||
current = mc.config as MonitorConfigRoot
|
const mc = await reflectionApi.getMonitorConfig()
|
||||||
} catch {
|
current = mc.config as MonitorConfigRoot
|
||||||
// 无现有配置时用空壳,仍可写入动作白名单
|
} catch {
|
||||||
|
// 无现有配置时用空壳,仍可写入动作白名单
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await reflectionApi.saveMonitorConfig({
|
const saved = await reflectionApi.saveMonitorConfig({
|
||||||
car: {
|
car: {
|
||||||
fields: [...(current.car?.fields ?? [])],
|
fields: [...(current.car?.fields ?? [])],
|
||||||
status: [...(current.car?.status ?? [])],
|
status: [...(current.car?.status ?? [])],
|
||||||
@@ -85,8 +125,10 @@ export async function saveMonitorSettingsFromOps(payload: OpsConfig): Promise<vo
|
|||||||
status: [...(current.track?.status ?? [])],
|
status: [...(current.track?.status ?? [])],
|
||||||
methods: [...monitor.track.actionKeys]
|
methods: [...monitor.track.actionKeys]
|
||||||
},
|
},
|
||||||
carActionByType: { ...monitor.carActionByType }
|
carActionByType: cloneCarActionByType(monitor.carActionByType)
|
||||||
})
|
})
|
||||||
|
invalidateMonitorConfigCache()
|
||||||
|
return saved as MonitorConfigRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated 使用 loadMonitorSettingsIntoOps */
|
/** @deprecated 使用 loadMonitorSettingsIntoOps */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:defaults="DEFAULT_OPS"
|
:defaults="DEFAULT_OPS"
|
||||||
:normalize-payload="normalizeOpsConfig"
|
:normalize-payload="normalizeOpsConfig"
|
||||||
:after-load="onOpsConfigLoaded"
|
:after-load="onOpsConfigLoaded"
|
||||||
:before-save="saveMonitorSettingsFromOps">
|
:before-save="onBeforePlatformSave">
|
||||||
<template #default="{ payload, update }">
|
<template #default="{ payload, update }">
|
||||||
<el-form label-width="200px" :model="payload">
|
<el-form label-width="200px" :model="payload">
|
||||||
<el-divider content-position="left">调度回放</el-divider>
|
<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-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-form-item>
|
||||||
<el-divider content-position="left">地图监控信息与动作管理</el-divider>
|
<el-divider content-position="left">地图监控信息与动作管理</el-divider>
|
||||||
<el-alert
|
<div v-loading="monitorSectionLoading" class="monitor-section">
|
||||||
type="info"
|
<el-alert
|
||||||
:closable="false"
|
type="info"
|
||||||
show-icon
|
:closable="false"
|
||||||
:title="autoSaving ? '正在自动保存…' : '支持直接多选,修改后会自动保存到地图监控(simple.json),无需点击上方「保存」。若某组不勾选,表示该组在地图监控中显示全部。'"
|
show-icon
|
||||||
style="margin-bottom: 10px" />
|
:title="autoSaving ? '正在自动保存…' : '支持直接多选,修改后会自动保存到地图监控(simple.json),无需点击上方「保存」。若某组不勾选,表示该组在地图监控中显示全部。'"
|
||||||
<el-form-item label="按车型动作配置">
|
style="margin-bottom: 10px" />
|
||||||
<el-collapse class="type-actions">
|
<el-form-item label="按车型动作配置" v-loading="carTypesLoading">
|
||||||
<el-collapse-item
|
<el-collapse v-model="expandedCarTypes" class="type-actions">
|
||||||
v-for="row in carTypeActions"
|
<el-collapse-item
|
||||||
:key="row.typeKey"
|
v-for="row in carTypeActions"
|
||||||
:name="row.typeKey"
|
:key="row.typeKey"
|
||||||
:title="`${row.title}(${row.methods.length})`">
|
:name="row.typeKey"
|
||||||
<el-checkbox-group
|
:title="`${row.title}(${row.methods.length})`">
|
||||||
:model-value="getCarTypeActions(payload, row)"
|
<div v-show="expandedCarTypes.includes(row.typeKey)">
|
||||||
class="cfg-check-grid"
|
<el-checkbox-group
|
||||||
@update:model-value="(v) => updateCarTypeActions(update, payload, row, v as string[])">
|
:key="carCheckboxGroupKey(payload, row)"
|
||||||
<el-checkbox v-for="m in row.methods" :key="`${row.typeKey}-${m.methodName}`" :label="m.methodName">
|
:model-value="carActionsForRow(payload, row)"
|
||||||
{{ m.label }}
|
class="cfg-check-grid"
|
||||||
</el-checkbox>
|
@update:model-value="(v) => updateCarTypeActions(update, payload, row, v as string[])">
|
||||||
</el-checkbox-group>
|
<el-checkbox
|
||||||
</el-collapse-item>
|
v-for="m in row.methods"
|
||||||
</el-collapse>
|
:key="`${row.typeKey}-${m.methodName}`"
|
||||||
</el-form-item>
|
:label="m.methodName">
|
||||||
<el-form-item label="站点动作显示项">
|
{{ m.label }}
|
||||||
<el-checkbox-group
|
</el-checkbox>
|
||||||
:model-value="getMonitor(payload).site.actionKeys"
|
</el-checkbox-group>
|
||||||
class="cfg-check-grid"
|
</div>
|
||||||
@update:model-value="(v) => updateMonitorList(update, payload, 'site', 'actionKeys', v as string[])">
|
</el-collapse-item>
|
||||||
<el-checkbox v-for="m in siteMethods" :key="`sa-${m.methodName}`" :label="m.methodName">
|
</el-collapse>
|
||||||
{{ m.label }}
|
<p v-if="!monitorSectionLoading && !carTypesLoading && !carTypeActions.length" class="muted empty-hint">
|
||||||
</el-checkbox>
|
暂未扫描到带 MethodMember 的车型(请确认 SimpleLite 已连接并已加载插件)。
|
||||||
</el-checkbox-group>
|
</p>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="路径动作显示项">
|
<el-form-item label="站点动作显示项">
|
||||||
<el-checkbox-group
|
<el-checkbox-group
|
||||||
:model-value="getMonitor(payload).track.actionKeys"
|
:model-value="payload.monitor?.site?.actionKeys ?? []"
|
||||||
class="cfg-check-grid"
|
class="cfg-check-grid"
|
||||||
@update:model-value="(v) => updateMonitorList(update, payload, 'track', 'actionKeys', v as string[])">
|
@update:model-value="(v) => updateMonitorList(update, payload, 'site', 'actionKeys', v as string[])">
|
||||||
<el-checkbox v-for="m in trackMethods" :key="`ta-${m.methodName}`" :label="m.methodName">
|
<el-checkbox
|
||||||
{{ m.label }}
|
v-for="m in siteMethods"
|
||||||
</el-checkbox>
|
:key="`sa-${m.methodName}`"
|
||||||
</el-checkbox-group>
|
:label="m.methodName">
|
||||||
</el-form-item>
|
{{ 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>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
</ConfigPageBase>
|
</ConfigPageBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
import { onBeforeUnmount, shallowRef, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import ConfigPageBase from '@/components/ConfigPageBase.vue'
|
import ConfigPageBase from '@/components/ConfigPageBase.vue'
|
||||||
import { DEFAULT_OPS } from '@/mock/data/configs'
|
import { DEFAULT_OPS } from '@/mock/data/configs'
|
||||||
import { reflectionApi, emptyMonitorAvailableMap, type MonitorVisibilityMap } from '@/api/reflection'
|
import { reflectionApi } from '@/api/reflection'
|
||||||
import type { MonitorOpsPolicy, MonitorPanelPolicy, OpsConfig } from '@/types/config'
|
import type { MonitorPanelPolicy, OpsConfig } from '@/types/config'
|
||||||
|
import { lookupCarActionKeys } from '@/utils/carActionByTypeLookup'
|
||||||
|
import { fetchMonitorConfigCached } from '@/utils/monitorConfigCache'
|
||||||
import {
|
import {
|
||||||
|
cloneCarActionByType,
|
||||||
|
defaultMonitorPolicy,
|
||||||
loadMonitorSettingsIntoOps,
|
loadMonitorSettingsIntoOps,
|
||||||
normalizeOpsConfig,
|
normalizeOpsConfig,
|
||||||
saveMonitorSettingsFromOps
|
saveMonitorSettingsFromOps,
|
||||||
|
type MonitorConfigRoot
|
||||||
} from '@/utils/opsMonitorConfig'
|
} from '@/utils/opsMonitorConfig'
|
||||||
|
|
||||||
type MonitorKind = 'car' | 'site' | 'track'
|
type MonitorKind = 'car' | 'site' | 'track'
|
||||||
@@ -104,18 +126,21 @@ interface CarTypeActionRow {
|
|||||||
methods: MethodOption[]
|
methods: MethodOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = reactive<MonitorVisibilityMap>(emptyMonitorAvailableMap())
|
const CAR_METHODS_CACHE_KEY = 'simple-platform-methods-by-type-car-v1'
|
||||||
const carTypeActions = ref<CarTypeActionRow[]>([])
|
|
||||||
const siteMethods = ref<MethodOption[]>([])
|
const carTypeActions = shallowRef<CarTypeActionRow[]>([])
|
||||||
const trackMethods = ref<MethodOption[]>([])
|
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)
|
const autoSaving = ref(false)
|
||||||
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
|
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let pendingMonitorPayload: OpsConfig | null = null
|
let pendingMonitorPayload: OpsConfig | null = null
|
||||||
const AUTO_SAVE_DELAY_MS = 450
|
const AUTO_SAVE_DELAY_MS = 500
|
||||||
|
|
||||||
function scheduleMonitorAutoSave(next: OpsConfig) {
|
function scheduleMonitorAutoSave(next: OpsConfig) {
|
||||||
pendingMonitorPayload = next
|
pendingMonitorPayload = next
|
||||||
@@ -126,7 +151,6 @@ function scheduleMonitorAutoSave(next: OpsConfig) {
|
|||||||
async function flushMonitorAutoSave() {
|
async function flushMonitorAutoSave() {
|
||||||
autoSaveTimer = null
|
autoSaveTimer = null
|
||||||
if (autoSaving.value) {
|
if (autoSaving.value) {
|
||||||
// 上一次保存仍在进行:稍后重试,确保最新勾选最终落盘。
|
|
||||||
autoSaveTimer = setTimeout(() => { void flushMonitorAutoSave() }, 200)
|
autoSaveTimer = setTimeout(() => { void flushMonitorAutoSave() }, 200)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -135,7 +159,10 @@ async function flushMonitorAutoSave() {
|
|||||||
if (!target) return
|
if (!target) return
|
||||||
autoSaving.value = true
|
autoSaving.value = true
|
||||||
try {
|
try {
|
||||||
await saveMonitorSettingsFromOps(target)
|
const saved = await saveMonitorSettingsFromOps(target, {
|
||||||
|
cachedRoot: cachedMonitorRoot.value
|
||||||
|
})
|
||||||
|
cachedMonitorRoot.value = saved
|
||||||
ElMessage.success({ message: '地图监控配置已自动保存', duration: 1200, grouping: true })
|
ElMessage.success({ message: '地图监控配置已自动保存', duration: 1200, grouping: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(`地图监控配置自动保存失败:${e instanceof Error ? e.message : String(e)}`)
|
ElMessage.error(`地图监控配置自动保存失败:${e instanceof Error ? e.message : String(e)}`)
|
||||||
@@ -153,16 +180,88 @@ onBeforeUnmount(() => {
|
|||||||
if (pendingMonitorPayload && !autoSaving.value) void flushMonitorAutoSave()
|
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) {
|
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 {
|
async function onBeforePlatformSave(payload: OpsConfig) {
|
||||||
return normalizeMonitor(payload.monitor)
|
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(
|
function updateMonitorList(
|
||||||
@@ -172,7 +271,7 @@ function updateMonitorList(
|
|||||||
key: MonitorKey,
|
key: MonitorKey,
|
||||||
list: string[]
|
list: string[]
|
||||||
) {
|
) {
|
||||||
const monitor = getMonitor(payload)
|
const monitor = ensureMonitor(payload)
|
||||||
const next: OpsConfig = {
|
const next: OpsConfig = {
|
||||||
...payload,
|
...payload,
|
||||||
monitor: {
|
monitor: {
|
||||||
@@ -193,11 +292,9 @@ function updateCarTypeActions(
|
|||||||
row: CarTypeActionRow,
|
row: CarTypeActionRow,
|
||||||
methods: string[]
|
methods: string[]
|
||||||
) {
|
) {
|
||||||
const monitor = getMonitor(payload)
|
const monitor = ensureMonitor(payload)
|
||||||
const nextMap = {
|
const nextMap = cloneCarActionByType(monitor.carActionByType)
|
||||||
...monitor.carActionByType,
|
nextMap[row.typeKey] = [...methods]
|
||||||
[row.typeKey]: [...methods]
|
|
||||||
}
|
|
||||||
if (row.shortTypeName !== row.typeKey) {
|
if (row.shortTypeName !== row.typeKey) {
|
||||||
delete nextMap[row.shortTypeName]
|
delete nextMap[row.shortTypeName]
|
||||||
}
|
}
|
||||||
@@ -211,68 +308,12 @@ function updateCarTypeActions(
|
|||||||
update(next)
|
update(next)
|
||||||
scheduleMonitorAutoSave(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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.monitor-section {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
.cfg-check-grid {
|
.cfg-check-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
@@ -281,4 +322,11 @@ function dedupeMethodOptions(methods: Array<{ methodName: string; label?: string
|
|||||||
.type-actions {
|
.type-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.empty-hint {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: var(--mg-text-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user