配置段/环境变量、Launcher、健康检查 API、OpenAPI 与前后端文案同步;兼容探测旧 SimpleLite 进程名。 Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.9 KiB
Vue
179 lines
5.9 KiB
Vue
<template>
|
||
<PermissionGuard widget-id="ConfigCenter">
|
||
<slot
|
||
v-if="$slots.chrome"
|
||
name="chrome"
|
||
:payload="payload"
|
||
:update="update"
|
||
:save="save"
|
||
:reload="reload"
|
||
:saving="saving"
|
||
:loading="loading"
|
||
:envelope="envelope"
|
||
:relative-time="relativeTime"
|
||
/>
|
||
<el-card v-else 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
|
||
/** 加载完成后二次合并(如从 Simple3 拉 monitor) */
|
||
afterLoad?: (payload: T, update: (next: T) => void) => void | Promise<void>
|
||
/** 写入 Platform 之前先执行(如先落盘 Simple3 monitor) */
|
||
beforeSave?: (payload: T) => void | Promise<void>
|
||
/** 保存成功后副作用(如再次同步 Simple3) */
|
||
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(opts?: { toast?: boolean }): Promise<boolean> {
|
||
const showToast = typeof opts === 'object' && opts && 'toast' in opts ? opts.toast !== false : true
|
||
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 false
|
||
}
|
||
}
|
||
|
||
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 = `(平台配置已保存,但 Simple3 同步失败:${e instanceof Error ? e.message : String(e)})`
|
||
}
|
||
}
|
||
await reloadFromServer(true, false)
|
||
const extra = props.beforeSave || props.afterSave ? ',地图监控配置已写入 Simple3' : ''
|
||
if (showToast) ElMessage.success(`已保存 v${env.version}${extra}${monitorSyncWarn}`)
|
||
return true
|
||
} catch (e) {
|
||
ElMessage.error(`保存失败:${e instanceof Error ? e.message : String(e)}`)
|
||
return false
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
watch(() => props.section, () => reload())
|
||
|
||
onMounted(() => reload())
|
||
</script>
|
||
|
||
<style scoped>
|
||
.cpb-header { display: flex; align-items: center; gap: 8px; }
|
||
.cpb-title { font-weight: 600; font-size: 15px; color: var(--mg-text-light); }
|
||
.cpb-header .spacer { flex: 1; }
|
||
.cpb-desc {
|
||
color: var(--mg-text-muted);
|
||
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;
|
||
background: var(--mg-veil-2);
|
||
color: var(--mg-text-light);
|
||
max-height: 520px;
|
||
overflow: auto;
|
||
}
|
||
</style>
|