总体思路:
1.把表单拆成多个可复用模块组件
2.父页面通过 v-if 按需装配
3.模块内部支持动态增删行
4.提交时用 ... 合并成一个对象
5.回显时用 Object.assign / splice 保留引用
6.子组件用 computed(() => props.modelValue),避免数据不回显
代码示例
父组件示例代码
javascript
<template>
<div class="p-2">
<el-card shadow="hover">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" :disabled="readonly">
<!-- 户籍信息(公共模块) -->
<el-divider content-position="left">户籍信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<HouseholdInfo v-model="form.household" prop-prefix="household" :disabled="readonly" />
</el-col>
</el-row>
<!-- 教育信息(公共模块) -->
<el-divider content-position="left">教育信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<EducationInfo v-model="form.education" prop-prefix="education" :disabled="readonly" />
</el-col>
</el-row>
<!-- 家庭信息(公共模块,本页可控制是否需要) -->
<template v-if="showFamily">
<el-divider content-position="left">家庭信息</el-divider>
<FamilyInfo v-model="form.family" prop-prefix="family" :disabled="readonly" />
</template>
<el-form-item v-if="!readonly" label=" ">
<el-button type="primary" :loading="buttonLoading" @click="submitForm">提 交</el-button>
<el-button type="warning" @click="loadMock">加载虚拟数据</el-button>
<el-button @click="cancel">清 空</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup name="Person">
import HouseholdInfo from '@/components/FormBlocks/HouseholdInfo.vue';
import EducationInfo from '@/components/FormBlocks/EducationInfo.vue';
import FamilyInfo from '@/components/FormBlocks/FamilyInfo.vue';
const { proxy } = getCurrentInstance();
const formRef = ref();
const buttonLoading = ref(false);
/** 是否只读(详情页用) */
const readonly = ref(false);
/** 是否显示家庭信息模块(其他页面按需关闭) */
const showFamily = ref(true);
/** 本页表单结构:分模块,子组件通过 v-model 绑定各模块 */
const createEmptyForm = () => ({
household: { householdType: undefined, address: undefined, moveInDate: undefined },
education: { school: undefined, degree: undefined, major: undefined, enrollDate: undefined },
family: { members: [] }
});
const form = reactive(createEmptyForm());
const rules = {
// 户籍模块的校验规则放在子组件里,这里只写本页特有的(本页没有特有字段,所以为空)
};
/** 提交:把分模块对象合并成扁平结构(后端要一个对象) */
const buildPayload = () => ({
...form.household,
...form.education,
members: form.family.members
});
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return;
buttonLoading.value = true;
try {
const payload = buildPayload();
// TODO: 实际项目改为调用后端接口
// if (payload.id) { await updatePerson(payload); } else { await addPerson(payload); }
console.log('提交数据(扁平结构)', payload);
proxy?.$modal.msgSuccess('操作成功,已打印到控制台');
} finally {
buttonLoading.value = false;
}
});
};
/** 回显:把后端扁平结构拆成各模块对象,逐个更新属性(保留引用,避免子组件持有的引用失效) */
const fillForm = (data) => {
Object.assign(form.household, {
householdType: data.householdType,
address: data.address,
moveInDate: data.moveInDate
});
Object.assign(form.education, {
school: data.school,
degree: data.degree,
major: data.major,
enrollDate: data.enrollDate
});
// members 是数组,保留数组引用,只替换元素
form.family.members.splice(0, form.family.members.length, ...(data.members ?? []));
formRef.value?.clearValidate();
};
/** 模拟后端返回的扁平结构 */
const loadMock = () => {
const mockData = {
id: Date.now(),
householdType: '1',
address: '北京市东城区',
moveInDate: '2020-01-01',
school: '清华大学',
degree: '本科',
major: '计算机科学',
enrollDate: '2018-09-01',
members: [
]
};
fillForm(mockData);
};
/** 清空表单 */
const cancel = () => {
formRef.value?.resetFields();
// 保留各子对象引用,只清空属性
Object.assign(form.household, { householdType: undefined, address: undefined, moveInDate: undefined });
Object.assign(form.education, { school: undefined, degree: undefined, major: undefined, enrollDate: undefined });
form.family.members.splice(0, form.family.members.length);
formRef.value?.clearValidate();
};
onMounted(() => {
// 进入页面自动加载虚拟数据,演示回显效果
loadMock();
});
</script>
HouseholdInfo 组件示例代码
javascript
<template>
<el-form-item label="户籍类型" :prop="`${propPrefix}.householdType`" :rules="rules.householdType">
<el-select v-model="model.householdType" placeholder="请选择户籍类型" :disabled="disabled" clearable>
<el-option label="城镇" value="1" />
<el-option label="农村" value="2" />
</el-select>
</el-form-item>
<el-form-item label="户口所在地" :prop="`${propPrefix}.address`" :rules="rules.address">
<el-input v-model="model.address" placeholder="请输入户口所在地" :disabled="disabled" maxlength="100" />
</el-form-item>
<el-form-item label="迁入日期" :prop="`${propPrefix}.moveInDate`" :rules="rules.moveInDate">
<el-date-picker
v-model="model.moveInDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择迁入日期"
:disabled="disabled"
class="!w-full"
/>
</el-form-item>
</template>
<script setup name="HouseholdInfo">
const props = defineProps({
/** 双向绑定的户籍信息对象 */
modelValue: { type: Object, required: true },
/** 是否禁用 */
disabled: { type: Boolean, default: false },
/** 校验路径前缀,嵌套对象时传 'household' */
propPrefix: { type: String, default: 'household' }
});
defineEmits(['update:modelValue']);
/**
* 关键:用 computed 保持对 props.modelValue 的响应式引用
* 不能写 const model = props.modelValue (一次性解构,父组件替换引用后会指向旧对象)
*/
const model = computed(() => props.modelValue);
const rules = {
householdType: [{ required: true, message: '请选择户籍类型', trigger: 'change' }],
address: [{ required: true, message: '请输入户口所在地', trigger: 'blur' }],
moveInDate: [{ required: false, message: '请选择迁入日期', trigger: 'change' }]
};
defineExpose({ rules });
</script>
EducationInfo组件示例代码:
javascript
<template>
<el-form-item label="毕业院校" :prop="`${propPrefix}.school`" :rules="rules.school">
<el-input v-model="model.school" placeholder="请输入毕业院校" :disabled="disabled" maxlength="50" />
</el-form-item>
<el-form-item label="学历" :prop="`${propPrefix}.degree`" :rules="rules.degree">
<el-select v-model="model.degree" placeholder="请选择学历" :disabled="disabled" clearable>
<el-option label="博士" value="博士" />
<el-option label="硕士" value="硕士" />
<el-option label="本科" value="本科" />
<el-option label="大专" value="大专" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="专业" :prop="`${propPrefix}.major`" :rules="rules.major">
<el-input v-model="model.major" placeholder="请输入专业" :disabled="disabled" maxlength="50" />
</el-form-item>
<el-form-item label="入学日期" :prop="`${propPrefix}.enrollDate`" :rules="rules.enrollDate">
<el-date-picker
v-model="model.enrollDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入学日期"
:disabled="disabled"
class="!w-full"
/>
</el-form-item>
</template>
<script setup name="EducationInfo">
const props = defineProps({
modelValue: { type: Object, required: true },
disabled: { type: Boolean, default: false },
propPrefix: { type: String, default: 'education' }
});
defineEmits(['update:modelValue']);
/** 用 computed 保持对 props.modelValue 的响应式引用 */
const model = computed(() => props.modelValue);
const rules = {
school: [{ required: true, message: '请输入毕业院校', trigger: 'blur' }],
degree: [{ required: true, message: '请选择学历', trigger: 'change' }],
major: [{ required: false, message: '请输入专业', trigger: 'blur' }],
enrollDate: [{ required: false, message: '请选择入学日期', trigger: 'change' }]
};
defineExpose({ rules });
</script>
FamilyInfo 组件示例代码:
javascript
<template>
<div v-for="(member, index) in model.members" :key="index" class="family-row">
<el-form-item
label="姓名"
:prop="`${propPrefix}.members.${index}.name`"
:rules="{ required: true, message: '请输入成员姓名', trigger: 'blur' }"
>
<el-input v-model="member.name" placeholder="请输入姓名" :disabled="disabled" maxlength="20" />
</el-form-item>
<el-form-item
label="关系"
:prop="`${propPrefix}.members.${index}.relation`"
:rules="{ required: true, message: '请选择关系', trigger: 'change' }"
>
<el-select v-model="member.relation" placeholder="请选择关系" :disabled="disabled" clearable>
<el-option label="父亲" value="父亲" />
<el-option label="母亲" value="母亲" />
<el-option label="配偶" value="配偶" />
<el-option label="子女" value="子女" />
<el-option label="兄弟姐妹" value="兄弟姐妹" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item
label="联系电话"
:prop="`${propPrefix}.members.${index}.phone`"
:rules="phoneRule"
>
<el-input v-model="member.phone" placeholder="请输入联系电话" :disabled="disabled" maxlength="11" />
</el-form-item>
<el-form-item v-if="!disabled" label=" ">
<el-button type="danger" plain icon="Delete" @click="removeMember(index)">删除</el-button>
</el-form-item>
</div>
<el-form-item v-if="!disabled" label=" ">
<el-button type="primary" plain icon="Plus" @click="addMember">添加家庭成员</el-button>
</el-form-item>
</template>
<script setup name="FamilyInfo">
const props = defineProps({
modelValue: { type: Object, required: true },
disabled: { type: Boolean, default: false },
propPrefix: { type: String, default: 'family' }
});
defineEmits(['update:modelValue']);
/** 用 computed 保持对 props.modelValue 的响应式引用 */
const model = computed(() => props.modelValue);
const phoneRule = [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }];
/** 添加一行 */
const addMember = () => {
props.modelValue.members.push({ name: undefined, relation: undefined, phone: undefined });
};
/** 删除一行 */
const removeMember = (index) => {
props.modelValue.members.splice(index, 1);
};
defineExpose({ addMember, removeMember });
</script>
<style scoped>
.family-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0 12px;
padding: 8px 0;
border-bottom: 1px dashed var(--el-border-color);
}
</style>