一、背景
前端开发中涉及表单的页面非常多,看似功能简单,开发快速,实则占去了很大一部分时间。当某个表单包含元素过多时还会导致html代码过多,vue文件过大。从而不容易查找、修改和维护。为了提高开发效率及降低维护成本,下面介绍表单配置化组件的封装原理与封装方法。
二、技术方案
如上图所示,封装表单配置化组件的关键点有三个一是如何解决表单元素排布的行列问题,二是表单数据的绑定问题,三是表单元素的参数配置校验等问题。下面分别介绍这三个问题的解决方法。
•配置化表单组件的入参及说明
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| labelWidth | 表单元素label所占宽度 | String | —— | 150px |
| columnList | 表单元素所组成的配置,是一个数组 | Array | —— | [] |
| formData | 表单元素值的集合 | Object | —— | {} |
| columnSpan | 表单排布分栏 | Number | —— | 24 |
| size | 表单元素尺寸 | String | medium / small / mini | medium |
•计算配置化表单的行数,本表单通过基础的24分栏计算表单最终的行数和列数,通过下面方法最终得到一个关于行列的二维数组
1newColumnList() { 2 const newColumnList= [] 3 const row = Math.floor(24 / this.columnSpan) 4 let newColumnItem = [] 5 for(let i=0; i< this.columnList.length; i++) { 6 newColumnItem.push(this.columnList[i]) 7 if(newColumnItem.length === row || i === this.columnList.length-1) { 8 newColumnList.push(newColumnItem) 9 newColumnItem = [] 10 } 11 } 12 return newColumnList 13} 14
•通过上面得到的二维数组进行循环渲染,首先循环渲染行,其次循环渲染列。本方案采用element中的表单,当然也可以用其他组件库或者原生表单进行渲染,其原理通用。最终将会根据参数column.type决定加载哪一个具体的表单元素。
1<el-form ref="form" :model="formData" :label-width="labelWidth" :size="size"> 2 <el-row :gutter="20" v-for="(element,index) in newColumnList" :key="index+'formRow'"> 3 <template v-for="(item, index) in element" > 4 <column 5 :key="index + 'formView'" 6 :columnSpan="columnSpan" 7 :column="item" 8 :formData="formData" 9 /> 10 </template> 11 </el-row> 12</el-form> 13
•column组件最终根据type加载具体的表单元素。下面展示column组件的入参及其说明,通过component加载不同的表单元素
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| column | 表单元素的具体配置 | Object | —— | {} |
| formData | 表单元素值的集合 | Object | —— | {} |
| columnSpan | 表单排布分栏 | Number | —— | 24 |
1<el-col :span="columnSpan"> 2 <component 3 :is="column.type + 'View'" 4 :column="column" 5 :formData="formData" 6 v-model="formData[column.name]" 7 :columnSpan="columnSpan"/> 8 </el-col> 9
•这里主要以select表单元素为例进行说明,表单元素的双向绑定、校验以及值更新等问题
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| column | 表单元素的具体配置 | Object | —— | {} |
| value | 表单元素值 | Number/String/Array | —— | —— |
•column参数
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| placeholder | 空值说明 | String | —— | —— |
| required | 是否必填 | Boolean | —— | —— |
| rules | 校验规则 | Array | —— | —— |
| title | 表单元素label | String | —— | —— |
| name | 表单元素值名称 | String | —— | —— |
| multiple | 是否多选 | Boolean | —— | —— |
| filterable | 是否过滤 | Boolean | —— | —— |
| disabled | 是否禁用 | Boolean | —— | —— |
| dictionary | 下拉选项枚举 | Array | —— | —— |
| changeFunction | 值改变时的回调函数 | Function | —— | —— |
1<el-form-item :label="column.title + ':'" :prop="column.name" :rules="rules"> 2 <el-select 3 v-model="val" 4 clearable 5 :multiple="column.multiple" 6 :filterable="column.filterable" 7 :placeholder="'请选择' + column.title" 8 :disabled="column.disabled" 9 style="width: 100%" 10 @change="onChange" 11 @clear="onClear"> 12 <el-option v-for="item in column.dictionary" :key="item.code" :label="item.name" :value="item.code"> 13 </el-option> 14 </el-select> 15</el-form-item> 16
1rules: [ 2 { 3 required: this.column.required, 4 message: this.column.placeholder placeholder ? this.column.placeholder : `请输入${this.column.title}`, 5 trigger: 'change' 6 }, 7 ...this.column.rules 8 ] 9
1onChange(){ 2 this.$emit('input',this.val) 3 if(this.column && this.column.changeFunction){ 4 this.column.changeFunction(this.val) 5 } 6}, 7onClear(){ 8 this.onChange() 9} 10
三、项目实践
•配置化表单为bs-form,在页面中引入bs-form表单组件
1<bs-form ref="formDemo" 2 :columnList="columnList" 3 :formData="formData" 4 :columnSpan="columnSpan" 5 labelWidth="120px"> 6</bs-form> 7<el-row style="text-align: center;"> 8 <el-button type="primary" 9 @click="onSave">保存</el-button> 10 <el-button @click="onCancel">取消</el-button> 11</el-row> 12
•formData参数
1formData: { 2 name: '', 3 yearIncome: '', // 业务类型 4 goodsCategoryId: '', // 托寄物品类id 5 projectManagerErp: '', // 项目经理erp 6 projectName: '', // 项目名称 7 projectStage: '', // 项目阶段编码 8 projectStandardName: '', // 标准名称 9 projectYear: 2023, // 年份 10 startRegionId: '', // 始发区域id 11 startBattleId: '', // 始发战区id 12 address: [], // 省市 13 category: null, //图文类型 14 range: [] //发布范围 15 } 16
•分栏参数
columnSpan: 6
•表单配置参数
1columnList(){ 2 const self = this 3 return [ 4 { 5 type: 'text', 6 name: 'name', 7 title: '项目名称', 8 required: true, 9 maxlength: 20, 10 showwordlimit: true, 11 placeholder: '请输入' 12 }, 13 { 14 name: 'category', 15 type: 'radio', 16 dictionary: [ 17 { 18 code: 1, 19 name: '类型一' 20 }, 21 { 22 code: 2, 23 name: '类型二' 24 } 25 ], 26 title: '图文类型', 27 required: true 28 }, 29 { 30 name: 'range', 31 type: 'checkbox', 32 title: '发布范围', 33 dictionary: [ 34 { 35 code: 1, 36 name: '范围一' 37 }, 38 { 39 code: 2, 40 name: '范围二' 41 } 42 ], 43 required: true 44 }, 45 { 46 type: 'text', // 字段类型文本框 47 name: 'yearIncome', //与后台对接字段 48 title: '年均收入', // 前端展示字段 49 required: true, // 必填项设置 50 maxlength: 50, // 字符串长度限制 51 showwordlimit: true, // 是否显示字符串长度 52 placeholder: '请输入', // 占位文本提示 53 rules: [ 54 { pattern: /(^[1-9]([0-9]+)?(.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9].[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } 55 ], 56 }, 57 { 58 type: 'select', 59 name: 'goodsCategoryId', 60 title: '托寄物品类', 61 required: true, 62 filterable: true, 63 placeholder: '请选择', 64 dictionary: [{ 65 name: '苹果', 66 code: '1' 67 },{ 68 name: '手机', 69 code: '2' 70 },{ 71 name: '测试', 72 code: '3' 73 },{ 74 name: '樱桃', 75 code: '7' 76 },{ 77 name: '荸荠', 78 code: '9' 79 }] 80 }, 81 { 82 type: 'select', 83 name: 'startRegionId', 84 title: '区域', 85 required: true, 86 placeholder: '请选择', 87 dictionary: [{ 88 name: '销售-华北区域', 89 code: '1' 90 },{ 91 name: '销售-华东区域', 92 code: '2' 93 },{ 94 name: '销售-华南区域', 95 code: '3' 96 },{ 97 name: '销售-西南区域', 98 code: '4' 99 },{ 100 name: '销售-华中区域', 101 code: '5' 102 },{ 103 name: '销售-东北区域', 104 code: '6' 105 }], 106 // 点击下来触发切换联动的事件,为一个函数 107 changeFunction: function (val) { 108 } 109 }, { 110 type: 'select', 111 name: 'startBattleId', 112 title: '战区', 113 required: true, 114 placeholder: '请选择', 115 dictionary: this.battleByRegionList 116 }, { 117 type: 'select', 118 name: 'projectStage', 119 title: '项目阶段', 120 required: true, 121 placeholder: '请选择', 122 dictionary: [{ 123 name: '项目发起阶段', 124 code: '10' 125 },{ 126 name: '项目调研阶段', 127 code: '20' 128 },{ 129 name: '可行性分析阶段', 130 code: '30' 131 },{ 132 name: '立项阶段', 133 code: '40' 134 }] 135 }, { 136 type: 'text', 137 name: 'projectStandardName', 138 title: '标准名称', 139 required: true, 140 placeholder: '请输入', 141 append: '.com', // 文本框后置内容 142 }, { 143 type: 'text', 144 name: 'projectManagerErp', 145 title: '项目经理', 146 required: true, 147 placeholder: '请输入' 148 },{ 149 type: 'cascader', // 字段类型下拉框 150 name: 'address', //与后台对接字段 151 title: '省市区', // 前端展示字段 152 required: true, // 必填项设置 153 placeholder:'请选择', // 占位文本提示 154 dictionary: [{ 155 value: 'shanxi', 156 label: '陕西省', 157 children: [{ 158 value: 'xian', 159 label: '西安市', 160 children: [{ 161 value: 'yanta', 162 label: '雁塔区' 163 }, { 164 value: 'beilin', 165 label: '碑林区' 166 }, { 167 value: 'xincheng', 168 label: '新城区' 169 }, { 170 value: 'weiyang', 171 label: '未央区' 172 }] 173 }] 174 }], 175 // 点击下来触发切换联动的事件,为一个函数 176 changeFunction: function(){} 177 },{ 178 type: 'static', 179 name: 'projectYear', 180 title: '年份' 181 } 182 ] 183} 184
•表单保存
1// 保存 2async onSave() { 3 const valid = await this.$refs.formDemo.onValidate() 4 if(valid) { 5 this.$message.success('校验通过') 6 }else { 7 this.$message.error('校验失败') 8 } 9} 10
四、成果展示
作者:京东物流 田雷雷
