配置数据构建复杂form表单
一、为何需要 fieldConfig
前端开发人员是否经常遇到以下问题?
-
💡后端同学告诉你请求字段为 null,导致访问时报错
-
💡表单项过多,每个字段都写一堆 if 判空,重复又容易漏
-
💡数据展示、下拉枚举、编辑权限、默认值、字段联动... 各种逻辑写死在业务DOM里?
-
💡复杂业务表单涉及模块较多导致本地变量过多,新人接手成本很高
-
😅**谁写的功能,谁负责后期维护,如何减少后期维护成本,或者提高维护效率?**😅
-
🦛我们可以通过特定的数据结构配置规避掉这些问题 于是,fieldConfig思维应运而生。
二、什么是 fieldConfig?
- fieldConfig 指的是前端使用一份集中数据模型配置,来描述特定业务对象例如:
export class OrderBtn {
/** 按钮标识 */
key: string | null;
/** 按钮文字 */
title: string | null;
/** 按钮优先度 */
priority: number | null;
/** 按钮是否显示 */
show: boolean | null;
/** 按钮是否禁用 */
disabled: boolean | null;
/** css的类 */
class: string | null;
/** 按钮是否加载中 */
loading: boolean | null;
/** 样式 */
style: string | null;
}
export class PropertyConfig {
/** 属性名称(必须配置) */
name?: string | null;
/** 属性字段(必须配置) */
field: string;
/** 默认值(必须配置,如果没有配置,默认为'') */
defaultValue?: IDefaultValue | null;
/** 元素展示类型(必须配置) */
elementType?: FE_ROOM_LAYOUT_ELEMENT_TYPE | null;
/** 是否独占一行(默认否) */
aloneLine?: boolean | null;
/** 属性校验规则(可选) */
rule?: IDynamicChangeField[] | null;
/** 提示信息(可选) */
placeholder?: string | null;
/** 列数 */
column?: number | null;
/** 下拉选项(可选) */
options?: ISelectOption[] | null;
/** 是否有提示图标 */
hasTipsIcon?: boolean | null;
/** 提示语 */
tipsWords?: ISelectOption[] | null;
/** 表单项附加的备注信息 */
fromItemRemark?: string | null;
/** 额外的字段(上传的参数,url等) */
extraFiled?: IDynamicChangeField | null;
/** 是否禁用 */
disable?: boolean | null;
/** 是否影藏 */
hidden?: boolean | null;
}
- 具体的表单配置项,可以参考以下代码,这里就不一一列举了。
export const doorOne = [
{
elementType: FE_ROOM_LAYOUT_ELEMENT_TYPE.Select,
name: '材料基材',
field: 'surfaceTreatment',
placeholder: '请选择',
options: [
{ text: '平板', value: '平板' },
{ text: '造型', value: '造型' },
{ text: '其他', value: '其他' },
{ text: '无', value: '无' },
],
rule: [
{
required: true,
trigger: 'change',
},
],
disable: false,
hidden: false,
},
- 通过抽象model层面的模型,你可以让「特殊逻辑、展示、编辑、默认值、联动、」全部脱离 if 判断,变成结构化驱动的系统。
三、合理的模块划,多端共用代码节省开发成本
✅ 1. 模块划分
- 将表单拆分成多个模块,每个模块负责一部分表单的渲染,这样不仅可以减少页面的DOM绘制,还可以提高组件的复用性。最终将多个模块统一给Form组件进行管理和渲染。
✅ 2. 作用域插槽构建模块骨架
- 实现展开折叠等模块动画效果,以及图标,模块标题,模块内容,模块操作按钮等功能。
- 可以在不同的表单中复用这些组件,减少代码的重复性。
四、fieldConfig 应用思路
✅ 1. 字段枚举展示(enumMap)
- 使用关键枚举替代每个字段包裹form-item:
// signed logistic afterSale againBuy
OrderBtn.key
// Check = 'el-checkbox',
// Radio = 'el-radio',
// InputNumber = 'uni-easyinput-number',
// Upload = 'el-upload',
// OnlyShow = 'el-text-area',
PropertyConfig.elementType
- 逻辑判断:
<el-Input
v-if="config.elementType === FE_ROOM_LAYOUT_ELEMENT_TYPE.Input"
:placeholder="config.placeholder"
:disabled="true"
v-model="localFormModel[config.field]"
/>
✅ 2. 表单默认值(defaultValue)
- 用于页面初始化规避掉给后端传入null值:
form.defaultValue = PropertyConfig.defaultValue;
✅ 3. 表单校验(rules)
- 用于表单提交校验:
form.rules = PropertyConfig.rules;
✅ 4. 动态 label / placeholder / tooltip
config[field]?.label
config[field]?.description
config[field]?.placeholder
config[field]?.disable
✅ 5. 表单隐藏(hidden)
- 替代
if-else用于表单隐藏:
form.hidden = PropertyConfig.hidden;
✅ 6. 表单禁用(disable)
- 用于表单禁用:
form.disable = PropertyConfig.disable;
- 用 fieldConfig配置数据,统一提取表单label、规则、字段:
- 统一管理所有表单校验信息,避免重复编写
protected generateFormModelAndRules(formFields:Array): any {
const rules: any = {};
const formModel: any = {};
formFields.forEach((config: any) => {
// 生成ruleForm
formModel[config.field] = config.defaultValue || '';
// 生成rules,统一管理form校验规则
if (config?.rule) {
const ruleValue = {
rules: [
{
required: true,
trigger: 'change',
errorMessage: `${config.placeholder || '请输入'}${config.name || ''} `,
},
],
};
rules[config.field] = ruleValue;
}
});
return {
rules,
formModel,
};
}
✅ 7. 控制字段行为权限
form.hidden = PropertyConfig.hidden;
form.edit = PropertyConfig.edit;
- 你可以统一控制哪些状态下字段是可编辑的、只读的、禁用的。
✅ 8. 字段联动 / 特殊判断
- 以“酒店是否双包”为例:是和否,会影响,“安装”,"酒店协作方”等字段的显示。
- 传统写法都有类似
v-if="serviceCode === 'turnkeyProject'"的判断这样的写法法值写死在业务代码中,不易读,也难以维护。 - 使用配置数据,利用函数可以统一管理关联的字段管理,并且可以动态控制显示和隐藏。
// 是否为双包
@Watch('localFormModel.turnkeyProject', { deep: true, immediate: false })
turnkeyProjectsChange(newVal: string): void {
this.projectRequirements = ProjectDetail.judgeFormItemStatus({
hiddenCondition: newVal === 'N',
relativeFields: ['installName', 'projectCoordination'],
formVariable: this.projectRequirements,
originFormVariable: this.originProjectRequirements,
});
}
- 具体函数很简单就利用配置数据中的hidden和filter函数,判断是否显示。然后统一封装判断逻辑
/** 判断状态 */
static judgeFormItemStatus(options: {
hiddenCondition: boolean;
relativeFields: string[];
formVariable: IPropertyConfig[];
originFormVariable: IPropertyConfig[];
}): IPropertyConfig[] {
options.relativeFields.forEach((field) => {
const propertyConfig = options.originFormVariable.find((item) =>
item.field.startsWith(field),
) as IPropertyConfig;
if (propertyConfig) {
propertyConfig.hidden = options.hiddenCondition;
}
});
options.formVariable = options.originFormVariable.filter((item) => !item.hidden);
return options.formVariable;
}
- 表单数据全程监控自动更新,无需手动更新,每一项数据更改都会及时触达提交表单的配置数据,从而实现表单配置数据与表单数据的同步。
// 监听localFormModel变化
@Watch('localFormModel', { deep: true, immediate: true })
localFormChange() {
this.latestFormData = this.generateFormModelAndRules().formConfigs;
}
表单数据检验和错误定位
- 不再是只弹框提示用户,必填项未填,而是直接在表单上标红,并滑动到必填项为止提示用户未填项。
(this.$refs.combineForm as any)
.validate()
.then((res: string) => {
console.log('表单数据:', res, this.localFormModel);
})
.catch((err: { errorMessage: string }[]) => {
const message = err[0]?.errorMessage || '';
const errorElement = document.querySelector(`.msg--active`);
if (message) {
uni.showToast({
title: message,
duration: 2000,
icon: 'none',
});
if (errorElement) {
errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
});
思考:为什么这是更好的做法?
- 把行为语义化:所见即所得,表单配置数据就是表单行为
- 将字段→行为的映射集中管理
- 一套配置数据管理多个客户端(PC,小程序,企微h5)
- 未来新增表单项(如 isKinlong,isComfort),只需配置数据新增一个字段,无需修改页面代码
- 未来新增表单模块(舒适家定制),新增一个模块数组,无需修改页面代码
这是一种「结构即页面」的思维方式。
五、关键步骤
- 抽象操作对象,先从枚举字段入手定义不同表单项,
- 减少多个本地变量共同维护表单特定字段交给Watch,
- 逐步建立 judgeFormItemStatus 等通用方法,统一赋值行为;
- 对所有字段建立行为描述统一表(fieldConfig) ,再生成表单;
- 推荐封装组件如
<form-item />,由 fieldConfig 决定类型、枚举、只读,显示等行为。
六、总结:fieldConfig 是你组件解耦、逻辑统一的利器
-
让字段自己“说”该怎么被使用
-
让 if else 变成结构化配置
-
摆脱人为判断去兜底,通过结构设计让DOM保持清爽
-
让代码更像配置,配置更像代码
-
一旦用上,你会发现整个页面“安静了”,逻辑清晰了,页面神清气爽。