Files
YschcmsFront/src/views/cms/components/SiteConfigForm.vue
T

186 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
import {
SITE_CONFIG_FIELD_DEFS,
SITE_CONFIG_GROUPS,
SiteConfigFieldDef,
SiteConfigFormModel,
SiteConfigGroup,
parseSiteConfig,
serializeSiteConfig,
validateFieldValue,
} from "@/utils/siteConfigForm";
import ImageAssetField from "@/components/ImageAssetField.vue";
import FooterPreview from "@/components/FooterPreview.vue";
/**
* 站点配置结构化表单(docs/09 §3.1;docs/16 §3.3 分区作用域;docs/20 F6 扩展参数区退役):
* v-model 绑定整页共享的 configJson 字符串(单一事实源)。
* props.groups 声明本分区渲染的分组。模型仍完整解析全部键(fields/preserved 双通道),
* 仅渲染限定分组——序列化时未渲染分区的值原样回写,保证多分区实例绑定同一 configJson 互不踩踏。
* 保存前由父级调用 validate()。
*/
const props = defineProps<{
modelValue: string;
/** 渲染的配置分组(缺省=全部分组,兼容旧整表单用法) */
groups?: SiteConfigGroup[];
/** 图片类字段上传归属站点(附件场景引用) */
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> }>({
fields: {},
});
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);
preserved = m.preserved;
}
watch(
() => props.modelValue,
v => {
// 自己 emit 出去的值回灌时不重解析,避免光标/输入被打断
if (v !== lastEmitted) parseIn(v || "");
},
{ immediate: true },
);
function emitUp() {
lastEmitted = serializeSiteConfig({
fields: { ...formModel.fields },
preserved,
});
emit("update:modelValue", lastEmitted);
}
/** 保存前校验:内置字段 Form 规则;通过返回 true */
async function validate(): Promise<boolean> {
return !!(await formRef.value?.validate());
}
/**
* F1(docs/20)Tab 独立保存:仅导出本实例渲染分组的字段值(空值剔除)。
* 父级把返回值合并进服务端事实源 configJson 后提交——其他 Tab 的未保存修改不会被连带提交。
*/
function ownFields(): Record<string, string> {
const out: Record<string, string> = {};
visibleGroups.forEach(g => {
defsOf(g.name).forEach(d => {
const v = (formModel.fields[d.key] || "").trim();
if (v) {
out[d.key] = v;
}
});
});
return out;
}
defineExpose({ validate, ownFields });
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 visibleGroups" :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)"
>
<ImageAssetField
v-if="def.kind === 'asset'"
v-model="formModel.fields[def.key]"
:site-id="siteId"
:placeholder="def.placeholder"
:aspect="def.aspect"
@update:model-value="emitUp"
/>
<Input
v-else-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>
<!-- F5(docs/20):页脚分组底部固定结构示意图(字段↔视觉位置一一对应,实时回显) -->
<FooterPreview
v-if="g.name === 'footer'"
:contact="formModel.fields.contact"
:footer-addresses="formModel.fields.footer_addresses"
:qr-wechat="formModel.fields.qr_wechat"
:qr-weibo="formModel.fields.qr_weibo"
:socials="[
{ label: '微信', value: formModel.fields.social_wechat },
{ label: '微博', value: formModel.fields.social_weibo },
{ label: '抖音', value: formModel.fields.social_douyin },
{ label: 'B站', value: formModel.fields.social_bilibili },
]"
:copyright="formModel.fields.copyright"
:beian="formModel.fields.beian"
/>
</template>
</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;
}
}
</style>