概述
实际项目中,基本上离不开表单,一般都是直接使用组件库form组件进行编写,直接使用对应组件其实也没什么大问题,就是复用起来比较麻烦,每次都需要手动复制一个个的表单item,当表单很多,联动较多的时候就会占大篇幅的代码,并且不易阅读和统一维护。
配置式表单demon

传统的表单组件格式
demon
js
<template>
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="Approved by">
<el-input v-model="formInline.user" placeholder="Approved by" clearable />
</el-form-item>
<el-form-item label="Activity zone">
<el-select
v-model="formInline.region"
placeholder="Activity zone"
clearable
>
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity time">
<el-date-picker
v-model="formInline.date"
type="date"
placeholder="Pick a date"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">Query</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
const formInline = reactive({
user: '',
region: '',
date: '',
})
const onSubmit = () => {
console.log('submit!')
}
</script>
<style>
.demo-form-inline .el-input {
--el-input-width: 220px;
}
.demo-form-inline .el-select {
--el-select-width: 220px;
}
</style>
缺点
- 大量的el-form-item组件重复
- 随着组件越来越复杂,不方便统一维护
- 阅读负担
- 不方便扩展(每次新增表单可能都需要改很多地方)
配置式表单
组件源码仓库地址:github.com/vgnip/vue3-...

dmeon
js
<template>
<MyForm @register="register">
<template #selectA="{ model, field }">
<ElSelect v-model="model[field]" collapse-tags multiple placeholder="Select">
<ElOption v-for="item in TestData.colors" :key="item.value" :label="item.label" :value="item.value">
<div class="ls-flex ls-items-center">
<el-tag :color="item.value" style="margin-right: 8px" size="small" class="ls-h-[20px] ls-w-[20px]" />
<span :style="{ color: item.value }">{{ item.label }}</span>
</div>
</ElOption>
<template #tag>
<el-tag v-for="color in model[field]" :key="color" :color="color" size="small" class="ls-h-[20px] ls-w-[20px]" />
</template>
</ElSelect>
</template>
<template #DatePickerA="{ model, field }">
<ElDatePicker
v-model="model[field]" type="date" placeholder="slot选择日期" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" style="width: 100%" @change="pickerChange"
/>
</template>
</MyForm>
</template>
<script lang="ts" setup>
import { useForm } from '@/components/form/hooks/UseForm'
import MyForm from '@/components/form/index.vue'
//表单渲染数据项
const schemas: FormSchema[] = [
{
field: 'field1',
changeEvent: 'input',
component: ComponentType.Input,
label: 'field1',
helpMessage: ['天王盖地虎', '小鸡炖蘑菇'],
labelLength: 7,
required: true,
componentProps: () => {
return {
placeholder: '自定义placeholder',
onChange: (e: any) => {
console.log(e)
},
}
},
renderComponentContent: () => {
return {
suffix: () => h('span', { style: 'color: red;' }, 'suffix插槽'),
}
},
},
{
field: 'field2',
component: ComponentType.InputNumber,
label: 'field2',
required: true,
rules: [{ trigger: 'blur', validator: TestData.customVaildate }],
componentProps: {
style: {
width: '100%',
},
},
colProps: {
span: 8,
},
},
{
field: 'field3',
changeEvent: 'input',
component: ComponentType.Input,
label: 'field3',
defaultValue: '默认值',
// colProps: {
// span: 8,
// },
},
// el-select组件需要配合el-option使用,但是底层formItem只会渲染一层组件,如果想直接使用el-select组件,有三种方法.1.基于el-select二次封装一次组件.2.使用schema中的render参数,自定义渲染.3.使用slot插槽,在模板中实现.
{
field: 'select1',
component: ComponentType.Select,
label: 'select1',
defaultValue: '',
componentProps: {
options: TestData.optionsA.value,
multiLang: true,
},
colProps: {
span: 8,
},
// 也可用直接写布尔值,这里的两个区别一个是v-if,一个是v-show
ifShow: ({ model, field }) => {
return model[field] !== '1'
},
show: ({ model, field }) => {
return model[field] !== '1'
},
},
{
field: 'select2',
component: ComponentType.Select,
label: 'select2 ',
defaultValue: '',
render: ({ model, field }) => {
return renderElSelect({ model, field })
},
},
{
field: 'select3',
component: ComponentType.Select,
label: 'select3',
slot: 'selectA',
colProps: {
span: 8,
},
itemProps: {
labelWidth: 100,
},
},
{
field: 'switch1',
component: ComponentType.Switch,
label: 'switch1',
componentProps: ({ formModel, formEvents, schema }) => {
return {
onChange: (val: boolean) => {
formEvents.setFieldsValue({ field1: 'switch1事件触发' })
console.log('Switch1Change:', val, formModel, formEvents, schema)
},
}
},
},
{
field: 'DatePicker1',
component: ComponentType.DatePicker,
label: 'DatePicker1',
slot: 'DatePickerA',
colProps: {
span: 8,
},
},
{
field: 'DatePicker2',
component: ComponentType.DatePicker,
label: 'DatePicker2',
render: ({ model, field }) => {
return renderElDatePicker({ model, field })
},
},
{
field: 'DatePicker3',
component: ComponentType.DatePicker,
label: 'DatePicker3',
componentProps: {
style: {
width: '100%',
},
type: 'date',
placeholder: '选择日期',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
onChange: (val: any) => {
console.log(val)
},
onVisibleChange: (val: any) => {
console.log(val)
},
},
colProps: {
span: 8,
},
},
{
field: 'Slider1',
component: ComponentType.Slider,
label: 'Slider1',
componentProps: {
min: 0,
max: 100,
range: true,
marks: {
20: '20°C',
60: '60°C',
},
},
colProps: {
span: 8,
},
},
{
field: 'TreeSelect1',
component: ComponentType.TreeSelect,
label: 'TreeSelect1',
componentProps: {
data: TestData.treeData,
renderAfterExpand: false,
},
},
{
field: 'AutoComplete1',
component: ComponentType.AutoComplete,
label: 'AutoComplete1',
componentProps: {
fetchSuggestions: querySearch,
clearable: true,
style: {
width: '100%',
},
},
colProps: {
span: 8,
},
},
{
field: 'Divider1',
component: ComponentType.Divider,
label: '',
colProps: {
span: 24,
},
},
{
field: 'TimePicker1',
component: ComponentType.TimePicker,
label: 'TimePicker',
colProps: {
span: 24,
},
},
{
field: 'name',
component: ComponentType.MultilingualInput,
label: 'MultilingualInput',
componentProps: ({ formModel }) => {
return {
inputType: 'textarea',
defaultValue: { ...formModel.name },
maxlength: 100, // 默认100
rules: [{ required: true, message: '请输入内容', trigger: 'blur' }],
}
},
rules: [{
required: true,
message: '请输入内容',
trigger: 'blur',
validator: (_rule: any, model) => {
return !isEmpty(model[currentLanguage])
},
}],
colProps: {
span: 8,
},
},
{
field: 'tip',
component: ComponentType.Input,
label: '提示',
helpMessage: '提示icon',
colProps: {
span: 8,
},
},
]
// form表单数据
const formModel = reactive({
field1: '1',
name: {
en: '默认英文',
cn: '默认中文',
},
})
const formProps: FormProps = reactive({
modelValue: formModel,
labelPosition: LabelPosition.right,
rowProps: {
gutter: 10,
justify: 'start',
align: 'top',
tag: 'div',
},
labelWidth: 160,
autoSetPlaceHolder: true,
// baseRowStyle: {
// background: 'red',
// padding: '10px',
// },
baseColProps: {
span: 7, // 栅格占据的列数
offset: 1, // 栅格左侧的间隔格数
// push: 1, // 栅格向右移动格数
pull: 1, // 栅格向左移动格数
},
disabled: false,
rulesMessageJoinLabel: true,
schemas,
labelSuffix: ':',
})
//初始化表单钩子
const [register, method] = useForm(formProps)
</script>
优点
- 表单数据视图渲染分离
- 大量减少重复代码量
- 动态可配置,只需要提供配置项即可
- 方便团队统一规范维护
- 大大提高开发效率
- 表单项可随意定制(样式、联动、数据等等)
- 支持jsx和模版语法
- 可根据实际情况改写多语言形式
配置式表单组件使用和配置
使用方法
按照上述的配置式demon即可配置一个表单组件
表单配置项类型
类型
源码类型位置:\src\components\form\types
js
export interface FormProps {
// 表单数据收集对线
modelValue?: Recordable
//对齐方式
labelPosition?: LabelPosition
// 整个表单的行配置
rowProps?: RowProps
// 整个表单中所有项目的宽度
labelWidth?: number | string
// 整个表单的label后缀
labelSuffix?: string
// 自动根据szchema中的label设置placeholder
autoSetPlaceHolder?: boolean
// 表单的内部组件尺寸
size?: '' | 'large' | 'default' | 'small'
// row的自定义样式
baseRowStyle?: CSSProperties
// col的自定义样式
baseColProps?: Partial<ColEx>
// 表单配置规则
schemas?: FormSchema[]
// 是否禁用
disabled?: boolean
// 检查标签中是否添加了信息
rulesMessageJoinLabel?: boolean
// 自定义重置函数
resetFunc?: () => Promise<void>
// 是否详情展示
isView?: boolean
}
使用
js
const formProps: FormProps = reactive({
modelValue: formModel,
labelPosition: LabelPosition.right,
rowProps: {
gutter: 10,
justify: 'start',
align: 'top',
tag: 'div',
},
labelWidth: 160,
autoSetPlaceHolder: true,
// baseRowStyle: {
// background: 'red',
// padding: '10px',
// },
baseColProps: {
span: 7, // 栅格占据的列数
offset: 1, // 栅格左侧的间隔格数
// push: 1, // 栅格向右移动格数
pull: 1, // 栅格向左移动格数
},
disabled: false,
rulesMessageJoinLabel: true,
schemas,
labelSuffix: ':',
})
表单项配置
类型
源码类型位置:\src\components\form\types
js
export interface FormSchema {
// 表单值
field: string
// 表单值变化的监听事件名 默认为change
changeEvent?: string
// v-model双向绑定的值的字段,默认为model-value
valueField?: string
component: ComponentType
label: string | (() => string)
// 组件入参
componentProps?: ((opt: { schema: FormSchema, formEvents: FormEvents, formModel: Recordable }) => Recordable) | object
// 栅格配置
colProps?: any
// 默认值
defaultValue?: any
// 是否校验
required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
// 校验规则
rules?: Arrayable<Rules>
// 是否自动拼接label和校验信息
rulesMessageJoinLabel?: boolean
// js控制显隐
ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
// css控制显隐
show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean)
// el-form-item的参数
itemProps?: any
// 自定义插槽, 插入到el-form-item中
slot?: string
// 类似插槽方式,不过是通过函数来实现dom结构,而不是像插槽那样写到template中
render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string
// 也是自定义插槽, 不过需要自己写el-form-item
itemSlot?: string
// 同render 和 itemSlot
renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string
// 自定义渲染组件内部的slot
renderComponentContent?: ((renderCallbackParams: RenderCallbackParams) => any) | VNode | VNode[] | string
// 标签右侧提示icon
helpMessage?: string | string[] | ((renderCallbackParams: RenderCallbackParams) => string | string[])
suffix?: string | number | ((values: RenderCallbackParams) => string | number)
subLabel?: string
labelLength?: number
commonRules?: boolean
}
使用
一个formItem就是一个个数组项,只需要配置数据项即可,组件类型有内置的,也可以灵活控制自己渲染想要的类型。
js
const schemas: FormSchema[] = [
{
field: 'field1',
changeEvent: 'input',
component: ComponentType.Input,
label: 'field1',
helpMessage: ['天王盖地虎', '小鸡炖蘑菇'],
labelLength: 7,
required: true,
componentProps: () => {
return {
placeholder: '自定义placeholder',
onChange: (e: any) => {
console.log(e)
},
}
},
renderComponentContent: () => {
return {
suffix: () => h('span', { style: 'color: red;' }, 'suffix插槽'),
}
},
},
{
field: 'field2',
component: ComponentType.InputNumber,
label: 'field2',
required: true,
rules: [{ trigger: 'blur', validator: TestData.customVaildate }],
componentProps: {
style: {
width: '100%',
},
},
colProps: {
span: 8,
},
},
{
field: 'field3',
changeEvent: 'input',
component: ComponentType.Input,
label: 'field3',
defaultValue: '默认值',
// colProps: {
// span: 8,
// },
},
{
field: 'select1',
component: ComponentType.Select,
label: 'select1',
defaultValue: '',
componentProps: {
options: TestData.optionsA.value,
multiLang: true,
},
colProps: {
span: 8,
},
// 也可用直接写布尔值,这里的两个区别一个是v-if,一个是v-show
ifShow: ({ model, field }) => {
return model[field] !== '1'
},
show: ({ model, field }) => {
return model[field] !== '1'
},
},
]
配置式表单常用API
动态修改对齐方式
js
const changeLabelPosition = () => {
const labelPositions: any = ['left', 'right', 'top']
const currentIndex = labelPositions.indexOf(formProps.labelPosition)
const nextIndex = (currentIndex + 1) % labelPositions.length
formProps.labelPosition = labelPositions[nextIndex]
}
禁用表单
js
const disabledForm = () => {
formProps.disabled = !formProps.disabled
}
校验表单
js
const validate = async () => {
const valid = (valid: any) => {
console.log(valid)
}
const result = await formEvents.validate(valid)
const msg = result ? '校验成功' : '校验失败'
ElMessage(msg)
}
自定义校验
js
const validateField = async (field: string) => {
const valid = (valid: any) => {
console.log(valid)
}
const result = await formEvents.validateField(field, valid)
const msg = result ? '校验成功' : '校验失败'
ElMessage(msg)
}
移除特定表单项校验
js
method.clearValidate('field2')
重置表单
js
method.resetFields
获取表单值
js
const getFieldsValue = () => {
console.log('表单值========>', formEvents.getFieldsValue())
ElMessage(JSON.stringify(formEvents.getFieldsValue()))
}
#### 设置特项定表单值
```js
formEvents.setFieldsValue({ field1: '设置的值' })
新增表单项
js
const appendSchemaByField = (schema: FormSchema, prefixField?: string | undefined, first?: boolean | undefined) => {
formEvents.appendSchemaByField(schema, prefixField, first)
}
const newSchemas: FormSchema = {
field: 'newField',
changeEvent: 'input',
component: ComponentType.Input,
label: '新增出来的表单项',
defaultValue: '新增出来的表单项',
colProps: {
span: 24,
},
}
appendSchemaByField(newSchemas, 'field2')
删除表单项
js
const removeSchemaByFiled = (field: string | string[]) => {
formEvents.removeSchemaByFiled(field)
}
removeSchemaByFiled('field1')
更新表单项
js
const updateSchema = (schema: Partial<FormSchema> | Partial<FormSchema>[]) => {
formEvents.updateSchema(schema)
}
const field1NewSchema: FormSchema = {
field: 'field1',
component: ComponentType.Input,
label: '更新字段1的label',
}
updateSchema(field1NewSchema)
重置表单项
js
const resetSchema = (schema: Partial<FormSchema> | Partial<FormSchema>[]) => {
formEvents.resetSchema(schema)
}
设置为详情展示
js
method.setProps({ isView: true })
总结
上面的配饰和表单组件是vite+vue3搭建的,总体思路通过配置式数据进行开发,省去传统表单开发一个个去复制的麻烦过程,思路类似于低代码平台开发配置式数据渲染页面一样的道理(JSON schema),上面示例基本上遇到的表单都能完成,更加复杂的情况可以通过JSX实现,上述组件内置了接口。