背景
经常开发管理系统的小伙伴们肯定或多或少都遇到过表单需求,对于一个系统而言,动辄就是十几,几十个表单;如果每个表单都按照传统模式编写的话,简直要把前端累死,看着一段段大同小异的代码,也是提不上一点劲,甚至看着这些它懂你,你不想懂它
的代码就犯恶心。
本着偷懒的精神,我就想能否封装一个动态表单,实现思路大致就是通过JSON配置,动态生成表单页面
,于是说干就干,咱玩的就是真实对吧。开撸,开撸.....
项目地址:github地址
数据接口设计
废话不多说,代码敬上
咋眼一看,代码有点多哈,别着急,注释已安排上。
js
type TreeItem = {
value: string
label: string
children?: TreeItem[]
}
export type FormListItem = {
// 栅格占据的列数
colSpan?: number
placeholder?: string
// 表单元素特有的属性
props?: {
defaultValue?: unknown // 绑定的默认值
clearable?: boolean
disabled?: boolean | ((data: { [key: string]: any }) => boolean)
size?: 'large' | 'default' | 'small'
group?: unknown // 父级特有属性,针对嵌套组件 Select、Checkbox、Radio
child?: unknown // 子级特有属性,针对嵌套组件 Select、Checkbox、Radio
[key: string]: unknown
}
// 表单元素特有的插槽
slots?: {
name: string
content: unknown
}[]
// 组件类型
typeName?: 'input' | 'select' | 'date-picker' | 'time-picker' | 'switch' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'radio-group' | 'radio-button' | 'input-number' | 'tree-select' | 'upload' | 'slider'
// 表单元素特有的样式
styles?: {
[key: string]: number | string
}
// select options 替换字段
replaceField?: { value: string; label: string }
// 列表项
options?: {
value?: string | number | boolean | object
label?: string | number
disabled?: ((data: { [key: string]: any }) => boolean) | boolean
[key: string]: unknown
}[]
// <el-form-item> 独有属性,同 FormItem Attributes
formItem: Partial<FormItemProps & { class: string }>
// 嵌套<el-form-item>
children?: FormListItem[]
// 树形选择器数据
treeData?: TreeItem[] // 只针对 'tree-select'组件
// 组件显示条件
isShow?: ((data: { [key: string]: any }) => boolean) | boolean
}
export type FConfig = {
form: Partial<InstanceType<typeof ElForm>> // Form Attributes 与Element属性一致
configs: FormListItem[] // 表单主体配置
}
常见表单需求
- 如何控制某个组件的显示隐藏
实现思路,提供一个
isShow
方法,将方法绑定在对应的组件上,从而组件显示隐藏条件
js
isShow: (data = {}) => {
return model.value.region == 'shanghai'
}
....
<el-form-item v-if="isShow(model)" v-bind="item.formItem">
- 目标组件是否禁用,需要根据某个组件是否有值来判断
js
disabled: (data = {}) => {
return !model.value.date1
}
....
<component :disabled="disabled(model)"></component>
- 组件之间相互赋值,
A组件
的值赋值给B组件
,B组件
的值赋值给A组件
- 表单验证
js
formItem: {
prop: 'name',
label: 'Activity name',
rules: [
{
required: true,
message: 'Please enter content',
trigger: 'blur'
}
]
}
组件封装
1. 输入框组件
js
<template>
<el-input v-bind="attrs.props"
ref="elInputRef"
:style="attrs.styles">
<template v-for="item in attrs.slots"
#[item.name]
:key="item.name">
<component :is="item.content"></component>
</template>
</el-input>
</template>
2. 下拉选择器组件
js
<template>
<el-select v-bind="attrs.props?.group"
ref="elSelectRef"
:style="attrs.styles">
<el-option v-for="item in attrs.options"
v-bind="attrs.props?.child"
:key="item[attrs.replaceField?.value || 'value']"
:label="item[attrs.replaceField?.label || 'label']"
:value="item[attrs.replaceField?.value || 'value']"
:disabled="item.disabled"></el-option>
</el-select>
</template>
3. 日期选择器组件
js
<template>
<el-date-picker v-bind="attrs.props"
ref="elDatePickerRef"
:style="attrs.styles"></el-date-picker>
</template>
封装方法都一致,还有很多组件,这里就不一个个列出来,具体大家就移步源码查看哈
项目路径 src/components/Form
附上完整配置
js
const config = ref<FConfig>({
form: {
labelWidth: '140px'
},
configs: [
// 输入框
{
colSpan: 12,
typeName: 'input',
placeholder: 'Please enter content',
props: {
defaultValue: '',
clearable: true
},
slots: [
{
name: 'suffix',
content: () => (
<ElIcon class="el-input__icon">
<Search />
</ElIcon>
)
}
],
formItem: {
prop: 'name',
label: 'Activity name',
rules: [
{
required: true,
message: 'Please enter content',
trigger: 'blur'
}
]
}
},
// 选择器
{
colSpan: 12,
typeName: 'select',
placeholder: 'Please select content',
props: {
defaultValue: undefined,
group: {
clearable: true,
onChange: events.changeSelect
},
child: {}
},
replaceField: { value: 'key', label: 'title' },
options: [
{ key: 'shanghai', title: 'Zone one' },
{ key: 'beijing', title: 'Zone two' }
],
styles: {
width: '100%'
},
formItem: {
prop: 'region',
label: 'Activity zone',
rules: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change'
}
]
}
},
{
colSpan: 24,
formItem: {
required: true,
label: 'Activity time'
},
children: [
// 日期选择器
{
colSpan: 12,
typeName: 'date-picker',
placeholder: 'Pick a day',
props: {
type: 'datetime',
clearable: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss'
},
styles: { width: '100%' },
formItem: {
prop: 'date1',
rules: [
{
type: 'date',
required: true,
message: 'Please pick a date',
trigger: 'change'
}
]
}
},
// 时间选择器
{
colSpan: 12,
typeName: 'time-picker',
placeholder: 'Pick a time',
props: {
disabled: (data = {}) => {
return !model.value.date1
},
clearable: true
},
styles: { width: '100%' },
formItem: {
prop: 'date2',
rules: [
{
type: 'date',
required: true,
message: 'Please pick a time',
trigger: 'change'
}
]
}
}
]
},
// 开关
{
colSpan: 24,
typeName: 'switch',
props: {
defaultValue: false
},
formItem: {
prop: 'delivery',
label: 'Instant delivery'
}
},
// 多选框
{
colSpan: 12,
typeName: 'checkbox-group',
props: {
group: {},
child: {}
},
formItem: {
prop: 'type',
label: 'Activity type',
rules: [
{
type: 'array',
required: true,
message: 'Please select at least one activity type',
trigger: 'change'
}
]
},
// replaceField: { value: 'value', label: 'label' },
options: [
{ value: 'shanghai', label: 'Zone one' },
{ value: 'beijing', label: 'Zone two' }
]
},
// 多选按钮框
{
colSpan: 12,
typeName: 'checkbox-button',
props: {
group: {},
child: {}
},
formItem: {
prop: 'button',
label: 'Activity button',
rules: [
{
type: 'array',
required: true,
message: 'Please select at least one activity type',
trigger: 'change'
}
]
},
// replaceField: { value: 'value', label: 'label' },
options: [
{ value: 'shanghai', label: 'Zone one' },
{ value: 'beijing', label: 'Zone two' }
]
},
// 单选框
{
colSpan: 12,
typeName: 'radio-group',
props: {},
formItem: {
prop: 'resource',
label: 'Resources',
rules: [
{
required: true,
message: 'Please select activity resource',
trigger: 'change'
}
]
},
options: [
{ value: 'shanghai', label: 'Sponsorship' },
{ value: 'beijing', label: 'Venue' }
]
},
// 单选按钮框
{
colSpan: 12,
typeName: 'radio-button',
props: {},
formItem: {
prop: 'resourceButton',
label: 'Resources button',
rules: [
{
required: true,
message: 'Please select activity resource',
trigger: 'change'
}
]
},
options: [
{ value: 'shanghai', label: 'Sponsorship' },
{ value: 'beijing', label: 'Venue' }
]
},
// 文本域
{
colSpan: 24,
typeName: 'input',
placeholder: 'Please enter content',
formItem: {
prop: 'desc',
label: 'Activity form'
},
props: {
rows: 5,
type: 'textarea',
clearable: true
},
isShow: (data = {}) => {
return model.value.region == 'shanghai'
}
},
// 文件上传
{
colSpan: 24,
typeName: 'upload',
formItem: {
prop: 'fileName',
label: 'Upload File',
rules: [
{
required: true,
message: 'Please select at least one activity type',
trigger: 'change'
}
]
},
props: {
httpRequest: events.httpRequest
},
slots: [
{
name: 'default',
content: () => <ElButton type="primary">上传</ElButton>
},
{
name: 'tip',
content: () => <span style="margin-left:10px">jpg/png files with a size less than 500KB</span>
}
]
},
// 滑块
{
colSpan: 16,
typeName: 'slider',
props: {
onChange: (val: number) => {
model.value.number = val
}
},
formItem: {
label: 'Activity slider',
prop: 'slider',
rules: [
{
required: true,
message: 'Please enter content',
trigger: 'change'
}
]
}
},
// 数字输入框
{
colSpan: 8,
typeName: 'input-number',
formItem: {
prop: 'number',
label: 'Activity number'
},
props: {
min: 1,
max: 100,
onChange: (val: number) => {
model.value.slider = val
}
}
},
// 树形选择器
{
colSpan: 24,
typeName: 'tree-select',
placeholder: 'Please select content',
formItem: {
prop: 'tree',
label: 'Activity tree'
},
styles: { width: '100%' },
props: {
multiple: true,
showCheckbox: true
},
treeData: []
}
]
})
实现效果
详细的实现逻辑,就委屈大家移步到项目中查看了。
最后
文章暂时就写到这,如果本文对您有什么帮助,别忘了动动手指点个赞❤️。 本文如果有错误和不足之处,欢迎大家在评论区指出,多多提出您宝贵的意见!
最后分享项目地址:github地址