init local code
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import {
|
||||
SITE_CONFIG_FIELD_DEFS,
|
||||
SITE_CONFIG_GROUPS,
|
||||
SiteConfigExtraRow,
|
||||
SiteConfigFieldDef,
|
||||
SiteConfigFormModel,
|
||||
extraRowError,
|
||||
firstExtraError,
|
||||
parseSiteConfig,
|
||||
serializeSiteConfig,
|
||||
validateFieldValue,
|
||||
} from "@/utils/siteConfigForm";
|
||||
|
||||
/**
|
||||
* 站点配置结构化表单(docs/09 §3.1):v-model 直接绑定 configJson 字符串。
|
||||
* 内部 parse/serialize 双向映射;已知键分组表单 + 扩展参数动态行;
|
||||
* 保存前由父级调用 validate()(含扩展行兜底校验)。
|
||||
*/
|
||||
const props = defineProps<{ modelValue: string }>();
|
||||
const emit = defineEmits<{ (e: "update:modelValue", v: string): void }>();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = reactive<{ fields: Record<string, string>; extras: SiteConfigExtraRow[] }>({
|
||||
fields: {},
|
||||
extras: [],
|
||||
});
|
||||
let preserved: Record<string, unknown> = {};
|
||||
let lastEmitted: string | null = null;
|
||||
|
||||
function parseIn(v: string) {
|
||||
const m: SiteConfigFormModel = parseSiteConfig(v);
|
||||
Object.keys(formModel.fields).forEach(k => delete formModel.fields[k]);
|
||||
Object.assign(formModel.fields, m.fields);
|
||||
formModel.extras.splice(0, formModel.extras.length, ...m.extras.map(r => ({ ...r })));
|
||||
preserved = m.preserved;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
v => {
|
||||
// 自己 emit 出去的值回灌时不重解析,避免光标/输入被打断
|
||||
if (v !== lastEmitted) parseIn(v || "");
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUp() {
|
||||
lastEmitted = serializeSiteConfig({
|
||||
fields: { ...formModel.fields },
|
||||
extras: formModel.extras.map(r => ({ ...r })),
|
||||
preserved,
|
||||
});
|
||||
emit("update:modelValue", lastEmitted);
|
||||
}
|
||||
|
||||
function addExtra() {
|
||||
formModel.extras.push({ key: "", value: "" });
|
||||
}
|
||||
|
||||
function removeExtra(i: number) {
|
||||
formModel.extras.splice(i, 1);
|
||||
emitUp();
|
||||
}
|
||||
|
||||
/** 保存前校验:内置字段 Form 规则 + 扩展行兜底;通过返回 true */
|
||||
async function validate(): Promise<boolean> {
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return false;
|
||||
const extraMsg = firstExtraError(formModel.extras);
|
||||
if (extraMsg) {
|
||||
Message.error(`扩展参数有误:${extraMsg}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defineExpose({ validate });
|
||||
|
||||
function rulesFor(def: SiteConfigFieldDef) {
|
||||
return [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, cb: (err?: Error) => void) => {
|
||||
const msg = validateFieldValue(def, value);
|
||||
cb(msg ? new Error(msg) : undefined);
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function defsOf(group: string): SiteConfigFieldDef[] {
|
||||
return SITE_CONFIG_FIELD_DEFS.filter(d => d.group === group);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-config-form">
|
||||
<Form ref="formRef" :model="formModel" :label-width="130">
|
||||
<template v-for="g in SITE_CONFIG_GROUPS" :key="g.name">
|
||||
<div class="group-title">{{ g.title }}</div>
|
||||
<FormItem
|
||||
v-for="def in defsOf(g.name)"
|
||||
:key="def.key"
|
||||
:label="def.label"
|
||||
:prop="`fields.${def.key}`"
|
||||
:rules="rulesFor(def)"
|
||||
>
|
||||
<Input
|
||||
v-if="!def.multiline"
|
||||
v-model="formModel.fields[def.key]"
|
||||
:placeholder="def.placeholder"
|
||||
@on-change="emitUp"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
v-model="formModel.fields[def.key]"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="def.placeholder"
|
||||
@on-change="emitUp"
|
||||
/>
|
||||
<p v-if="def.tip" class="cfg-tip">{{ def.tip }}</p>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<div class="group-title">扩展参数</div>
|
||||
<div v-for="(row, i) in formModel.extras" :key="i" class="extra-row">
|
||||
<Input v-model="row.key" placeholder="参数名" style="width: 200px" @on-change="emitUp" />
|
||||
<Input v-model="row.value" placeholder="参数值" style="flex: 1" @on-change="emitUp" />
|
||||
<Button type="text" class="extra-del" icon="md-trash" @click="removeExtra(i)" />
|
||||
<p v-if="extraRowError(formModel.extras, i)" class="cfg-error">
|
||||
{{ extraRowError(formModel.extras, i) }}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="dashed" long icon="md-add" class="extra-add" @click="addExtra">新增参数</Button>
|
||||
<p class="cfg-tip">
|
||||
面向后续扩展的预留配置项(键名仅字母/数字/下划线);系统托管参数(如 theme_params)不在此显示,保存时自动保留。
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.site-config-form {
|
||||
.group-title {
|
||||
font-weight: 600;
|
||||
color: #515a6e;
|
||||
font-size: 13px;
|
||||
margin: 8px 0 14px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #2d8cf0;
|
||||
line-height: 1.2;
|
||||
&:not(:first-child) {
|
||||
margin-top: 22px;
|
||||
}
|
||||
}
|
||||
.cfg-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cfg-error {
|
||||
color: #ed4014;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.extra-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
.extra-del {
|
||||
color: #999;
|
||||
&:hover {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
}
|
||||
.extra-add {
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user