一、 引言:中后台业务生态下的动态表单诉求
在 Vue 中后台项目中,开发者在表单上耗费大量时间重复编写相似的 <el-form-item> 代码,导致开发效率低且难以维护。面对频繁变动的业务需求,因此出现了 "配置驱动" 的动态表单方案,通过 JSON 配置自动生成表单界面。
二:目标
本方案旨在设计一个高拓展性、易于维护的Vue动态表单组件:
- 动态生成:通过声明式的JSON配置,自动渲染完整的表单界面,免除编写重复的模板代码。
- 复杂交互 :支持字段间的条件显示、值联动、规则校验等,让表单能灵活响应动态业务逻辑。
- 布局统一 :提供统一的表单布局风格(如默认一行两列),并支持通过配置自定义行列占比与栅格规则,兼顾一致性与灵活性。
- 集成扩展 :允许轻松接入自定义表单控件,确保方案能适应多样化的业务组件需求。
三:基础实现
1.外部定义一个schema表
go
const schema = [
{
// 字段的唯一标识,用于表单数据绑定和校验规则匹配,如 formData.age
prop: 'age',
// 字段的初始值,此值将作为 v-model 绑定的初始数据
initValue: 18,
// 显示在表单控件前的标签文本
label: '年龄',
// 指定渲染该字段所使用的表单组件,实际会映射到一个已注册的 Vue 组件(如 Element Plus 的 ElInput)
component: 'Input',
// 传递给组件的额外属性对象,这些属性将被透传绑定到组件的 props 上
componentProps: {
// 设置 Input 组件的 type 属性为 'number',这将渲染一个数字输入框
type: 'number'
}
}
]
2.将schema表传递给动态表单组件

3.表单组件内部接收schema,完成初始化数据并渲染视图
typescript
// 1.组件内部监听schema表
watch(
() => props.schema,
(arr = []) => {
formData.value = initModel(arr, formData.value)
console.log('formData.value根据schema拿到的值', formData.value)
schema.value = cloneDeep(arr) || []
},
{
immediate: true,
deep: true
}
)
// 2.初始表单数据
const initModel = (schema: FormOptions[], model: Object) => {
const clone_model: any = cloneDeep(model)
schema.forEach((v) => {
// 表单里面是否有值了
const hasField = Reflect.has(model, v.prop)
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
// 只更新新加的schema
clone_model[v.prop] = hasField
? clone_model[v.prop]
: v.initValue !== void 0
? v.initValue
: ''
})
return clone_model
}
// 3.组件渲染
<ElForm
ref="formRef"
:model="formData"
:validate-on-rule-change="false"
label-suffix=":"
label-position="right"
// attrs属性透传绑定
v-bind="attrs"
>
<component :is="createRow()"></component>
</ElForm>
// 4.createRow函数
const createRow = () => {
return (
<>
{schema.value.map((item: FormOptions, index: number) =>
createFormItem(item, index)
)}
</>
)
}
// 5.createFormItem函数
const createFormItem = (item: FormOptions, index: number) => {
return (
<ElFormItem
key={index}
label={item?.label}
prop={item.prop}
>
{createFormItemDom(item, index)}
</ElFormItem>
)
}
import { ElInput } from 'element-plus'
export const comMap = {
'Input': ElInput
}
// 6.createFormItemDom函数
const createFormItemDom = (item: FormOptions, index: number) => {
const Com = comMap[item?.component]
return (
<Com
v-model={formData.value[item.prop]}
></Com>
)
}
4.视图效果
接下来运行项目,会发现表单被正常渲染,表单项的初始值也正常回显,这样我们一个最基本的动态表单就实现了。(相信大家也发现了,每一步都抽离了一个函数,为什么要这样做呢?往后看会有答案)

四:扩展
可是仅仅实现上面的效果,投入业务使用是远远不够的,常见业务需求:
1.控制字段显隐
2.校验规则、表单事件绑定、自定义内容及插槽渲染
3.表单字段联动
4.统一表单风格
5.表单基本方法
...
接下来我们一一实现:
4.1 实现字段显隐控制
实现字段的显隐控制非常简单,给每一个表单字段加一个hidden属性,渲染的表结构从schema变成过滤掉之后的filteredSchema就可以了。
typescript
const filteredSchema = computed(() => {
//过滤掉隐藏的表单项 -- 追踪
return schema.value.filter((v: any) => !v?.hidden)
})
// 修改createRow函数,渲染filteredSchema
修改前 :
const createRow = () => {
return (
<>
{schema.value.map((item: FormOptions, index: number) =>
createFormItem(item, index)
)}
</>
)
}
修改后 :
const createRow = () => {
return (
<>
{filteredSchema.value.map((item: FormOptions, index: number) =>
createFormItem(item, index)
)}
</>
)
}
效果演示:
go
const schema = [
{
prop: 'age',
initValue: 18,
// 隐藏字段
hidden: true,
label: '年龄',
component: 'Input',
componentProps: {
type: 'number'
}
},
{
prop: 'text',
initValue: '数学试卷一张,语文试卷一张。',
label: '作业',
component: 'Input',
componentProps: {
type: 'text'
}
}
]

这样就实现了表单字段的显示与隐藏。
4.2 表单校验规则、表单事件绑定、自定义内容及插槽渲染
接下来我们继续完善动态表单的业务功能 :
4.2.1 校验规则添加
我们先回顾一下之前的createFormItem函数:
typescript
// 修改前:
const createFormItem = (item: FormOptions, index: number) => {
return (
<ElFormItem
key={index}
label={item?.label}
prop={item.prop}
>
{createFormItemDom(item, index)}
</ElFormItem>
)
}
目前只绑定了基本的prop和label,此时我们给它加上一个rules属性,
就加上了表单项的校验规则:
const createFormItem = (item: FormOptions, index: number) => {
return (
<ElFormItem
key={index}
label={item?.label}
prop={item.prop}
rules={item?.rules}
>
{createFormItemDom(item, index)}
</ElFormItem>
)
}
这里我们要考虑扩展性的问题,每一个属性都要声明一次 ,
组件的改动次数和扩展性就会大打折扣,所有我们借用"透传"的思想,
透传一个formItemProps对象,所有关于formItem的属性都可以经过这个对象传递:
// 修改后:
const createFormItem = (item: FormOptions, index: number) => {
return (
<ElFormItem
key={index}
label={item?.label}
prop={item.prop}
rules={item?.rules}
// 透传formItem的相关属性
{...(item?.formItemProps || {})}
>
{createFormItemDom(item, index)}
</ElFormItem>
)
}
效果演示:
yaml
const schema = [
{
prop: 'age',
initValue: 18,
hidden: true,
label: '年龄',
component: 'Input',
componentProps: {
type: 'number'
}
},
{
prop: 'text',
initValue: '数学试卷一张,语文试卷一张。',
label: '作业',
component: 'Input',
// 添加必填校验
rules: [
{
required: true,
message: '请输入作业',
trigger: 'blur'
}
],
componentProps: {
type: 'text'
}
}
]

这样表单的校验规则已经生效,对于其他的自定义规则可以自行尝试。
4.2.2 表单事件绑定
我们先回顾一下之前的createFormItem函数:
typescript
// 修改前:
const createFormItemDom = (item: FormOptions, index: number) => {
const Com = comMap[item?.component]
const componentProps = item?.componentProps || {}
return (
<Com
v-model={formData.value[item.prop]}
>
</Com>
)
}
// 目前只是做了一个表单字段的v-model绑定,并没有传入事件,
// 同样考虑到扩展性的问题,我们将传递给组件的属性和事件都放进一个对象
// componentProps,完成组件属性的透传
// 修改后:
const createFormItemDom = (item: FormOptions, index: number) => {
const Com = comMap[item?.component]
const componentProps = item?.componentProps || {}
return (
<Com
v-model={formData.value[item.prop]}
// 透传组件的相关属性和事件
{...componentProps}
>
</Com>
)
}
演示效果:
typescript
const schema = [
{
prop: 'age',
initValue: 18,
hidden: true,
label: '年龄',
component: 'Input',
componentProps: {
type: 'number'
}
},
{
prop: 'text',
initValue: '数学试卷一张,语文试卷一张。',
label: '作业',
component: 'Input',
rules: [
{
required: true,
message: '请输入作业',
trigger: 'blur'
}
],
componentProps: {
type: 'text',
// 增加表单事件绑定
onBlur: (val: any) => {
console.log(val, '失去焦点')
}
}
}
]

这样我们就完成了表单事件的绑定。
4.2.3 自定义内容及插槽渲染
在这里,我们先回顾一下之前的reateFormItemDom函数:
typescript
// 修改前:
const createFormItemDom = (item: FormOptions, index: number) => {
const Com = comMap[item?.component]
const componentProps = item?.componentProps || {}
return (
<Com
v-model={formData.value[item.prop]}
{...componentProps}
>
</Com>
)
}
// 这里就能看见,对于渲染内容的选择,已经被固定在了comMap的映射组件中,
// 如果想渲染一段文本或者其他自定义内容,这里显然是不满足的,以及这里的Com组件,并没有渲染自己的子组件,
// 像ElSelect 需要与 ElOptions搭配使用,所有我们也需要考虑这个情况
// 获取所有插槽
const slots = useSlots()
// 渲染ElOption子选项
const rendSelectOptions = (item: FormOptions) => {
// console.log('渲染selecr选项', item)
return item.componentProps?.options?.map((option: any) => {
return (
<ElOption
key={option[item.componentProps?.valueField || 'value']}
label={option[item.componentProps?.labelField || 'label']}
value={option[item.componentProps?.valueField || 'value']}
></ElOption>
)
})
}
修改后:
const createFormItemDom = (item: FormOptions, index: number) => {
// 如果指定插槽,直接渲染插槽内容
if (item.slot && slots[item.prop]) {
return slots[item.prop]!(item, formData.value)
}
const Com = comMap[item?.component]
const componentProps = item?.componentProps || {}
return (
<Com
style={{ ...(item?.comStyle || {}), width: '100%' }}
v-model={formData.value[item.prop]}
{...componentProps}
>
{{
// 使用默认插槽(default slot)渲染 options
default: () => (
<>
{item?.component === 'Select' &&
item.componentProps?.options &&
rendSelectOptions(item)}
</>
)
}}
</Com>
)
}
演示效果:
css
<CodeForm ref="codeFormRef" :schema="schema">
<template #desc>
<div>这是我的一段描述-仅展示作用</div>
</template>
</CodeForm>
const schema = [
{
prop: 'age',
initValue: 18,
hidden: true,
label: '年龄',
component: 'Input',
componentProps: {
type: 'number'
}
},
{
prop: 'text',
initValue: '数学试卷一张,语文试卷一张。',
label: '作业',
component: 'Input',
rules: [
{
required: true,
message: '请输入作业',
trigger: 'blur'
}
],
componentProps: {
type: 'text',
onBlur: (val: any) => {
console.log(val, '失去焦点')
}
}
},
{
prop: 'desc',
label: '描述',
slot: true
},
{
prop: 'csa',
label: '阶段',
component: 'Select',
componentProps: {
options: [
{
label: '儿童',
value: 1
},
{
label: '青年',
value: 2
},
{
label: '老年',
value: 3
}
]
}
},
]

截止到这里,我们就实现了表单校验规则、表单事件绑定、自定义内容及插槽渲染的相关工作,接下来我们继续往下看,已经满足了一些简单系统的业务需求。
4.3 表单字段联动、表单数据获取
对于相对来说比较复杂的业务,常常也会伴随着表单字段的联动,接下来模拟一个简单的业务联动需求:
1.表单的阶段字段不可填,根据年龄自动映射;
2.输入年龄:
0-5岁,作业字段置空,且为禁用选项,并去除必填校验;阶段变为儿童;
6-22岁,作业字段默认赋值 : '一张试卷' ,并支持修改;阶段变为青年;
...(其他数字暂不做处理)
要实现这个需求,仅仅靠上面的实现肯定是不够的,上面所有的实现仅限于固定内容渲染,而不能 满足上面所提到的**"表单动态联动"、"表单动态赋值" ** ,这时给组件增加两个方法:
setSchemas :用于除初次渲染外,改变表单渲染结构。
typescript
import { set } from 'lodash'
const setSchemas = (arr: SetColumns[]) => {
// 在这里不要尝试修改value值 -- 也不要尝试在这新增schema项
// 如果想修改 -- 请第一次就加上,设置了hidden的表单项,不会显示出来
// 想要修改值,请使用setValues方法
arr.forEach((item) => {
const { path, prop, value } = item
const isEext = schema.value.find((v: any) => v.prop === prop)
// 如果路径不存在,无效操作
if (!isEext) {
console.warn(
`[setSchemas] 属性 "${prop}" 不存在,跳过更新(请确保在初始化时声明)`
)
return
}
set(isEext, path, value)
})
}
setValues :用于除初次渲染外,改变表单值
javascript
const setValues = (value: Object) => {
Object.assign(formData.value, value)
}
有了这两个方法的加入,我们就可以实现上面的业务效果了,演示效果:
yaml
const schema = [
{
prop: 'age',
label: '年龄',
component: 'Input',
componentProps: {
type: 'number',
onChange: (val: any) => {
// 改变表单状态
codeFormRef.value?.setSchemas([
{
prop: 'text',
path: 'componentProps.disabled',
value: val <= 5
}
])
// 改变表单数据
codeFormRef.value.setValues({
csa: val <= 5 ? 1 : val >= 6 && val <= 22 ? 2 : 3,
text:
val <= 5 ? '' : val >= 6 && val <= 22 ? '试卷一张' : ''
})
}
}
},
{
prop: 'text',
initValue: '数学试卷一张,语文试卷一张。',
label: '作业',
component: 'Input',
rules: [
{
required: true,
message: '请输入作业',
trigger: 'blur'
}
],
componentProps: {
type: 'text',
onBlur: (val: any) => {
console.log(val, '失去焦点')
}
}
},
{
prop: 'desc',
label: '描述',
slot: true
},
{
prop: 'csa',
label: '阶段',
component: 'Select',
componentProps: {
options: [
{
label: '儿童',
value: 1
},
{
label: '青年',
value: 2
},
{
label: '老年',
value: 3
}
]
}
},
]


到这里,我们就实现了一个基本满足业务需求的动态表单。
4.4 统一表单风格
我们还需要统一一下表单风格,这里设计了两种风格:
- 居中展示,每行展示一个表单项
ini
<CodeForm
ref="codeFormRef"
:schema="schema"
:isCenter="true"
:labelWidth="'100px'"
>
<template #desc>
<div>这是我的一段描述-仅展示作用</div>
</template>
</CodeForm>

2.每行展示多个表单项(默认一行两个表单项)
ini
<CodeForm
ref="codeFormRef"
:schema="schema"
:labelWidth="'100px'"
>
<template #desc>
<div>这是我的一段描述-仅展示作用</div>
</template>
</CodeForm>

实现代码:
javascript
// 修改前 :
const createRow = () => {
return (
<>
{schema.value.map((item: FormOptions, index: number) =>
createFormItem(item, index)
)}
</>
)
}
const calculateOffset = (item: FormOptions) => {
// 默认 span 为 12(占一半宽度)
const span = item?.colAttrs?.span || 12
// 计算偏移量:(24 - span) / 2
return Math.max(0, (24 - span) / 2)
}
// 修改后 :
const createRow = () => {
if (props.layoutType === 'cell') {
return (
<ElRow
{...(props.rowAttrs || {})}
gutter={props.isCenter ? 0 : 10} // 非居中模式时才设置间隔
>
{filteredSchema.value.map(
(item: FormOptions, index: number) => (
<ElCol
key={item.prop} // 建议用 item.prop 作为 key
span={item?.colAttrs?.span || 12} // 优先使用 item 的配置
offset={props.isCenter ? calculateOffset(item) : 0} // 动态计算偏移量
{...(item?.colAttrs || {})}
>
{createFormItem(item, index)}
</ElCol>
)
)}
</ElRow>
)
} else {
return (
<>
{filteredSchema.value.map(
(item: FormOptions, index: number) =>
createFormItem(item, index)
)}
</>
)
}
}
4.5 表单基本方法
下面总结了一些基本的表单方法,暴露在外给调用的组件使用:
getData :获取表单数据
javascript
const getData = (isReset = false) => {
return new Promise((resolve) => {
//只要设置过的值,全部让其在外面组件可以拿到
resolve(formData.value)
})
}
resetFields :用于除初次渲染外,改变表单值
ini
const resetFields = () => {
formRef.value?.resetFields()
}
validate :用于除初次渲染外,改变表单值
typescript
const validate = () => {
return new Promise((resolve) => {
formRef.value?.validate((valid: any) => {
resolve(valid)
})
})
}
cleanField :用于除初次渲染外,改变表单值
typescript
const cleanField = (field: string) => {
formRef.value.clearValidate(field)
}

五:结言
以上内容就是此次实践动态表单的全部内容,各位快去试试吧!
组件库官网文档 : element-plus.org/