2026-05-29 18:16:34 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<PermissionGuard widget-id="ConfigCenter">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="cpb-header">
|
|
|
|
|
|
<span class="cpb-title">{{ title }}</span>
|
|
|
|
|
|
<el-tag size="small" effect="plain">section={{ section }}</el-tag>
|
|
|
|
|
|
<el-tag size="small" type="info" effect="plain">v{{ envelope?.version ?? '-' }}</el-tag>
|
|
|
|
|
|
<el-tag size="small" type="success" effect="plain" v-if="envelope?.updatedAt">{{ relativeTime }}</el-tag>
|
|
|
|
|
|
<div class="spacer" />
|
|
|
|
|
|
<el-button size="small" :icon="Refresh" :loading="loading" @click="reload(true)">重载</el-button>
|
|
|
|
|
|
<el-button size="small" type="primary" :icon="Check" :loading="saving" @click="save">保存</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="description" class="cpb-desc">{{ description }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="cpb-body">
|
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
|
<el-col :span="14">
|
|
|
|
|
|
<el-card shadow="never" body-style="padding: 12px">
|
|
|
|
|
|
<template #header>表单</template>
|
|
|
|
|
|
<slot :payload="payload" :update="update" />
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="10">
|
|
|
|
|
|
<el-card shadow="never" body-style="padding: 0">
|
|
|
|
|
|
<template #header>JSON 预览(保存即下发占位)</template>
|
|
|
|
|
|
<pre class="cpb-json">{{ jsonText }}</pre>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</PermissionGuard>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts" generic="T extends object">
|
|
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
|
|
|
|
import { Refresh, Check } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
import PermissionGuard from '@/components/PermissionGuard.vue'
|
|
|
|
|
|
import { useConfigStore } from '@/stores/config'
|
|
|
|
|
|
import type { ConfigEnvelope, ConfigSection } from '@/types/config'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
section: ConfigSection
|
|
|
|
|
|
title: string
|
|
|
|
|
|
description?: string
|
|
|
|
|
|
defaults: T
|
|
|
|
|
|
/** 加载/保存前规范化 payload(如 ops 补全 monitor 字段) */
|
|
|
|
|
|
normalizePayload?: (payload: T) => T
|
|
|
|
|
|
/** 加载完成后二次合并(如从 SimpleLite 拉 monitor) */
|
|
|
|
|
|
afterLoad?: (payload: T, update: (next: T) => void) => void | Promise<void>
|
|
|
|
|
|
/** 写入 Platform 之前先执行(如先落盘 SimpleLite monitor) */
|
|
|
|
|
|
beforeSave?: (payload: T) => void | Promise<void>
|
|
|
|
|
|
/** 保存成功后副作用(如再次同步 SimpleLite) */
|
|
|
|
|
|
afterSave?: (payload: T) => void | Promise<void>
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const store = useConfigStore()
|
|
|
|
|
|
const envelope = ref<ConfigEnvelope<T> | null>(null)
|
|
|
|
|
|
const payload = ref<T>(JSON.parse(JSON.stringify(props.defaults)) as T)
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const jsonText = computed(() => JSON.stringify(payload.value, null, 2))
|
|
|
|
|
|
const relativeTime = computed(() => envelope.value?.updatedAt
|
|
|
|
|
|
? new Date(envelope.value.updatedAt).toLocaleString('zh-CN')
|
|
|
|
|
|
: '—')
|
|
|
|
|
|
|
|
|
|
|
|
function update(next: T) { payload.value = next }
|
|
|
|
|
|
|
|
|
|
|
|
function applyPayload(raw: T | undefined | null) {
|
|
|
|
|
|
let next = raw
|
|
|
|
|
|
? (JSON.parse(JSON.stringify(raw)) as T)
|
|
|
|
|
|
: (JSON.parse(JSON.stringify(props.defaults)) as T)
|
|
|
|
|
|
if (props.normalizePayload) next = props.normalizePayload(next)
|
|
|
|
|
|
payload.value = next
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function reloadFromServer(force: boolean, toastOnSuccess: boolean) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const env = await store.load<T>(props.section, force)
|
|
|
|
|
|
envelope.value = env
|
|
|
|
|
|
applyPayload(env.payload as T)
|
|
|
|
|
|
if (props.afterLoad) await props.afterLoad(payload.value, update)
|
|
|
|
|
|
if (toastOnSuccess) ElMessage.success('已重载')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
ElMessage.error(`加载失败:${e instanceof Error ? e.message : String(e)}`)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function reload(force = false) {
|
|
|
|
|
|
await reloadFromServer(force, force)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function save() {
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
let body = payload.value
|
|
|
|
|
|
if (props.normalizePayload) body = props.normalizePayload(body)
|
|
|
|
|
|
|
|
|
|
|
|
if (props.beforeSave) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await props.beforeSave(body)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
ElMessage.error(`地图监控配置保存失败:${e instanceof Error ? e.message : String(e)}`)
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let monitorSyncWarn = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const env = await store.save<T>(props.section, body)
|
|
|
|
|
|
envelope.value = env
|
|
|
|
|
|
payload.value = body
|
|
|
|
|
|
if (props.afterSave) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await props.afterSave(body)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
monitorSyncWarn = `(平台配置已保存,但 SimpleLite 同步失败:${e instanceof Error ? e.message : String(e)})`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await reloadFromServer(true, false)
|
|
|
|
|
|
const extra = props.beforeSave || props.afterSave ? ',地图监控配置已写入 SimpleLite' : ''
|
|
|
|
|
|
ElMessage.success(`已保存 v${env.version}${extra}${monitorSyncWarn}`)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
ElMessage.error(`保存失败:${e instanceof Error ? e.message : String(e)}`)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => props.section, () => reload())
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => reload())
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.cpb-header { display: flex; align-items: center; gap: 8px; }
|
2026-05-29 23:52:42 +08:00
|
|
|
|
.cpb-title { font-weight: 600; font-size: 15px; color: var(--mg-text-light); }
|
2026-05-29 18:16:34 +08:00
|
|
|
|
.cpb-header .spacer { flex: 1; }
|
|
|
|
|
|
.cpb-desc {
|
2026-05-29 23:52:42 +08:00
|
|
|
|
color: var(--mg-text-muted);
|
2026-05-29 18:16:34 +08:00
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
margin: 0 0 14px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.cpb-body { margin-top: 4px; }
|
|
|
|
|
|
.cpb-json {
|
|
|
|
|
|
margin: 0; padding: 14px;
|
|
|
|
|
|
font-size: 12px; line-height: 1.55;
|
|
|
|
|
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
2026-05-29 23:52:42 +08:00
|
|
|
|
background: var(--mg-veil-2);
|
|
|
|
|
|
color: var(--mg-text-light);
|
2026-05-29 18:16:34 +08:00
|
|
|
|
max-height: 520px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|