feat(cms): docs/16 站点设置整页化+首页布局编辑器- SiteSetting.vue整页7分区(基本信息/皮肤与外观/首页布局/基础展示/页脚信息/SEO与域名/扩展参数)锚点导航sticky+IntersectionObserver+分区卡片三段式每分区独立保存(configJson单一事实源+submitSection统一提交);创建模式仅①②可编辑其余锁定,创建成功原地replace到site/setting/:id;SiteWizard三步向导退役删除(门户预览收编portalPreviewUrl);SiteManage瘦身表格+行内编辑跳整页(adminPathOf子站带站点段);ImageAssetField图片双通道(上传+路径粘贴折叠);SiteConfigForm支持groups/showExtras/siteId+asset字段接ImageAssetField+三旧键表单不再展示(ABSORBED_LEGACY_KEYS静默保留);homeLayout三件套(homeLayout.ts模型parse/serialize/validate/genFromCategories一键生成+blockTypes.ts注册表showIf条件字段+HomeLayoutEditor.vue序列编辑增删显隐上移下移);routes site/new+site/setting/:id+legacy redirect;CategoryManage home_style提示改口指向首页布局;新增siteSetting.spec+homeLayout.spec,重写siteManage.spec,117用例全绿
build / build (push) Successful in 2m19s

This commit is contained in:
chenxing
2026-09-10 15:09:10 +08:00
parent 7c925cd4f2
commit 34733ec280
16 changed files with 2399 additions and 840 deletions
+51 -24
View File
@@ -7,21 +7,38 @@ import {
SiteConfigExtraRow,
SiteConfigFieldDef,
SiteConfigFormModel,
SiteConfigGroup,
extraRowError,
firstExtraError,
parseSiteConfig,
serializeSiteConfig,
validateFieldValue,
} from "@/utils/siteConfigForm";
import ImageAssetField from "@/components/ImageAssetField.vue";
/**
* 站点配置结构化表单(docs/09 §3.1):v-model 直接绑定 configJson 字符串。
* 内部 parse/serialize 双向映射;已知键分组表单 + 扩展参数动态行;
* 站点配置结构化表单(docs/09 §3.1;docs/16 §3.3 分区作用域扩展):
* v-model 绑定整页共享的 configJson 字符串(单一事实源)。
* props.groups 声明本分区渲染的分组;props.showExtras 控制扩展参数编辑区(每页仅一处)。
* 模型仍完整解析全部键(fields/extras/preserved 三通道),仅渲染限定分组——
* 序列化时未渲染分区的值原样回写,保证多分区实例绑定同一 configJson 互不踩踏。
* 保存前由父级调用 validate()(含扩展行兜底校验)。
*/
const props = defineProps<{ modelValue: string }>();
const props = defineProps<{
modelValue: string;
/** 渲染的配置分组(缺省=全部分组,兼容旧整表单用法) */
groups?: SiteConfigGroup[];
/** 是否渲染扩展参数编辑区(docs/16:仅「扩展参数」分区开启) */
showExtras?: boolean;
/** 图片类字段上传归属站点(附件场景引用) */
siteId?: number;
}>();
const emit = defineEmits<{ (e: "update:modelValue", v: string): void }>();
/** 本实例渲染的分组集合(空=全部) */
const groupSet = (() => new Set(props.groups || SITE_CONFIG_GROUPS.map(g => g.name)))();
const visibleGroups = SITE_CONFIG_GROUPS.filter(g => groupSet.has(g.name));
const formRef = ref();
const formModel = reactive<{ fields: Record<string, string>; extras: SiteConfigExtraRow[] }>({
fields: {},
@@ -65,14 +82,16 @@ function removeExtra(i: number) {
emitUp();
}
/** 保存前校验:内置字段 Form 规则 + 扩展行兜底;通过返回 true */
/** 保存前校验:内置字段 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;
if (props.showExtras) {
const extraMsg = firstExtraError(formModel.extras);
if (extraMsg) {
Message.error(`扩展参数有误:${extraMsg}`);
return false;
}
}
return true;
}
@@ -93,13 +112,12 @@ function rulesFor(def: SiteConfigFieldDef) {
function defsOf(group: string): SiteConfigFieldDef[] {
return SITE_CONFIG_FIELD_DEFS.filter(d => d.group === group);
}
</script>
}</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">
<template v-for="g in visibleGroups" :key="g.name">
<div class="group-title">{{ g.title }}</div>
<FormItem
v-for="def in defsOf(g.name)"
@@ -108,8 +126,15 @@ function defsOf(group: string): SiteConfigFieldDef[] {
:prop="`fields.${def.key}`"
:rules="rulesFor(def)"
>
<ImageAssetField
v-if="def.kind === 'asset'"
v-model="formModel.fields[def.key]"
:site-id="siteId"
:placeholder="def.placeholder"
@update:model-value="emitUp"
/>
<Input
v-if="!def.multiline"
v-else-if="!def.multiline"
v-model="formModel.fields[def.key]"
:placeholder="def.placeholder"
@on-change="emitUp"
@@ -126,19 +151,21 @@ function defsOf(group: string): SiteConfigFieldDef[] {
</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) }}
<template v-if="showExtras">
<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>
</div>
<Button type="dashed" long icon="md-add" class="extra-add" @click="addExtra">新增参数</Button>
<p class="cfg-tip">
面向后续扩展的预留配置项(键名仅字母/数字/下划线);系统托管参数(如 theme_params)不在此显示,保存时自动保留。
</p>
</template>
</Form>
</div>
</template>