表单是我们前端开发工作中经常打交道的业务组件,特别是当表单元素数量比较大的时候十分头疼,因此如何优雅的处理表单元素至关重要,我们知道表单元素一般都分为几种常见类型:输入框,日期选择框,下拉选择框,附件上传,textarea等等,我们可以根据不同类型去构造一个含各种表单元素属性的json数据,然后循环该数据将表单渲染出来。下面是我针对我司业务按照上述思路封装的一个动态渲染表单组件。
表单组件DynamicForm.vue
基于element-ui组件库
<template>
<el-form :model="formData" ref="formRef" :disabled="isOnlyShow" :label-width="labelWidth+'px'" v-loading="loading">
<el-row type="flex" :gutter="10">
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="item.span || getColSpan(item)"
:key="item.field"
v-for="item in showFieldList"
>
<el-form-item :label="item.label+':'" :prop="item.field" :rules="item.rules">
<el-input v-model.trim="formData[item.field]" type="text" v-bind="item.props" v-if="item.showType=='text'"/>
<el-select v-model="formData[item.field]" v-bind="item.props" v-else-if="item.showType=='select'" style="width:100%">
<el-option
v-for="{label,value} in item.options"
:key="value"
:label="label"
:value="value"
>
</el-option>
</el-select>
<!-- commonsSelect是基础的业务选择组件,可以根据不同的selectType渲染不同的下拉数据 -->
<CommonSelect v-model="formData[item.field]" :type="item.selectType" @change="(data)=>changeCommonSelect(data,item)" v-bind="item.props" v-else-if="item.showType=='commonSelect'" />
<el-input v-model.trim="formData[item.field]" type="textarea" v-bind="item.props" v-else-if="item.showType=='textarea'"/>
<!--hasCallBack表单元素是否有触发回调方法 -->
<el-input v-model.trim="formData[item.field]" type="number" @blur="item.hasCallBack && $emit(item.field,formData[item.field])" v-bind="item.props" v-else-if="item.showType=='number'"/>
<date-picker
v-else-if="item.showType == 'date'"
v-model="formData[item.field]"
type="date"
value-type="YYYY-MM-DD"
style="width: 100%"
v-bind="item.props"
size="mini"
></date-picker>
<DateRangePicker v-model="formData[item.field]" v-bind="item.props" style="width: 100%" v-else-if="item.showType=='dateRangePicker'"></DateRangePicker>
<!-- 地址选择 -->
<ChooseAddress ref="addressRef" :isDisabled="isOnlyShow" :defaultAddress="formData[item.field] || {}" v-else-if="item.showType=='address'"/>
<!-- 籍贯选择 -->
<bs-address v-model="formData[item.field]" v-bind="item.props" v-else-if="item.showType=='origin'" @change="(data)=>changeBsAddress(data,item.fieldName)"></bs-address>
<!-- 附件选择 -->
<Upload
list-type="new-type"
:file-list="formData[item.field]"
v-bind="item.props"
:disabled="item.uploadFileLoading"
:accept="item.props.accept || 'doc,docx,xls,xlsx,pdf,jpg,png,jpeg'"
:progressUpload="()=>item.uploadFileLoading=true"
:on-remove="(file)=>handleFileRemove(file,item.field)"
@afterUpload="(data)=>handleAfterUpload(data,item)"
v-else-if="item.showType=='file'"
>
<el-button :loading="item.uploadFileLoading">点击上传</el-button>
</Upload>
</el-form-item>
</el-col>
</el-row>
<!-- 提供一个插槽供外部调用传入其他元素 -->
<slot></slot>
</el-form>
</template>
<script>
import {delete_attachments} from '@/api/account'
import {formatOptions} from '@/utils/utils'
import ChooseAddress from './ChooseAddress.vue';
export default {
name:'DynamicForm',
components:{ChooseAddress},
props:{
fieldList:{
type:Array,
default:()=>[]
},
defaultData:{
type:Object,
default:()=>{}
},
labelWidth:{
type:Number,
default:110
},
isOnlyShow:{
type:Boolean,
default:false
}
},
data(){
return {
showFieldList:[],
loading:false,
formData:{},
}
},
async mounted(){
for (const item of this.showFieldList) {
this.initFormData(item)
item.remote && await this.getFiledOptions(item)
}
this.setDefaultData(this.defaultData)
},
watch:{
fieldList:{
handler(val){
this.showFieldList=val
},
immediate:true,
deep:true
},
},
methods: {
getColSpan() {
const { length } = this.showFieldList;
if (length === 1) return 24;
return 8;
},
// 需要请求接口渲染的下拉选项
getOptions(item){
this.loading=true
this.$emit('loading',this.loading)
const {url,labelKey,valueKey,params}=item.remote || {}
return new Promise((resolve)=>{
url(params).then(res=>{
if(res.code==200){
const list=formatOptions(res.data,labelKey,valueKey)
this.$set(item,'options',list)
resolve()
}
}).finally(()=>{
this.loading=false
this.$emit('loading',this.loading)
})
})
},
changeCommonSelect(data,item){
const {fieldName,props}=item || {}
if(fieldName){
if(props.multiple){
this.formData[fieldName]=data.map(item=>item.label)
}else{
this.formData[fieldName]=data?.label || ''
}
}
},
async getFiledOptions(item){
await this.getOptions(item)
},
initFormData(item){
if(item.showType=='file' || item.showType=='origin'){
this.$set(this.formData,item.field,[])
}else if(item.props && item.props.multiple){
this.$set(this.formData,item.field,[])
}else{
this.$set(this.formData,item.field,'')
}
},
// 表单数据回显
setDefaultData(val){
if(val && Object.keys(val).length){
Object.keys(val).forEach(key=>{
this.$set(this.formData,key,val[key] ?? '')
})
}
},
changeBsAddress(data,fieldName){
this.formData[fieldName]={
bsProvince:data[0]?.label || '',
bsProvinceCode:data[0]?.value || '',
bsCity:data[1]?.label || '',
bsCityCode:data[1]?.value || '',
bsCountry:data[2]?.label || '',
bsCountryCode:data[2]?.value || '',
}
},
handleFileRemove(file,field) {
const fileId = file.id;
const files = this.formData[field].filter((item) => item.id !== fileId);
this.formData[field] = files;
if (fileId) {
delete_attachments({ id: fileId });
}
},
handleAfterUpload(data,item){
this.formData[item.field].push(data)
item.uploadFileLoading=false
},
// 表单校验
async validateForm(){
return new Promise((resolve,reject)=>{
this.$refs.formRef.validate((valid) => {
if (valid) {
resolve(true)
}else{
resolve(false)
}
})
})
},
// 表单元素数据获取,供外部保存提交
getFormData(){
let addressData={}
if(this.$refs.addressRef){
addressData=this.$refs.addressRef[0].getAddressInfo()
}
return {
...this.formData,
...addressData
}
},
},
}
</script>
<style lang="scss" scoped>
/deep/ .mx-input{
height:28px!important
}
</style>
渲染表单的json数据
这是外部传入的json数据可根据需求自己构造
import {isMobilePhone,isIdentity,isEmail} from '@/utils/validate'
import {query_employee_config} from '@/api/system'
export const handleFieldList=[
{
showType:'text', //要渲染的表单元素类型
label:'姓名', // 字段名
field:'userName', // 字段
rules:{required:true,message:'请填写姓名',trigger:['change','blur']}, // 校验规则
props:{ // 表单元素的属性
disabled:false,
maxlength:20,
placeholder:'姓名',
clearable:true
}
},
{
showType:'number',
label:'手机号',
field:'mobile',
hasCallBack:true,
rules:[
{required:true,message:'请填写手机号',trigger:['change','blur']},
{validator:isMobilePhone,trigger:'change'}
],
props:{
disabled:false,
maxlength:11,
placeholder:'手机号',
clearable:true
}
},
{
showType:'commonSelect',
label:'部门',
field:'departmentId',
fieldName:'departmentName',
options:[],
selectType:'department',
rules:{required:true,message:'请选择部门',trigger:['change']},
props:{
placeholder:'部门',
clearable:true,
multiple:false,
}
},
{
showType:'commonSelect',
label:'所属本方',
field:'ownCompanyId',
fieldName:'ownCompanyName',
options:[],
selectType:'ownCompany',
rules:{required:true,message:'请选择所属本方',trigger:['change']},
props:{
placeholder:'所属本方',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'职务',
field:'duty',
options:[],
remote:{
url:query_employee_config, // 需请求接口渲染下拉选项的下拉组件
params:{ // 接口请求的入参
type:'职务',
disabled:0,
status:0
},
labelKey:'name', // 接口返回的list的key
valueKey:'id'//接口返回的list的value
},
props:{
placeholder:'职务',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'职务类别',
field:'dutyType',
options:[],
remote:{
url:query_employee_config,
params:{
type:'职务类别',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'职务类别',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'性别',
field:'sex',
options:[{label:'男',value:2},{label:'女',value:1}], //静态下拉选项
props:{
placeholder:'性别',
clearable:true,
multiple:false,
}
},
{
showType:'number',
label:'身份证号',
field:'cardNo',
rules:[
{validator:isIdentity,trigger:'change'}
],
props:{
disabled:false,
maxlength:18,
placeholder:'身份证号',
clearable:true
}
},
{
showType:'origin',
label:'籍贯',
field:'origin',
fieldName:'originAddress',
props:{
// area:false
}
},
{
showType:'select',
label:'政治面貌',
field:'politicalOutlook',
options:[],
remote:{
url:query_employee_config,
params:{
type:'政治面貌',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'政治面貌',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'专业',
field:'speciality',
options:[],
remote:{
url:query_employee_config,
params:{
type:'专业',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'专业',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'职称',
field:'positionalTitles',
options:[],
remote:{
url:query_employee_config,
params:{
type:'职称',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'职称',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'学历',
field:'education',
options:[],
remote:{
url:query_employee_config,
params:{
type:'学历',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'学历',
clearable:true,
multiple:false,
}
},
{
showType:'select',
label:'民族',
field:'nation',
options:[],
remote:{
url:query_employee_config,
params:{
type:'民族',
disabled:0,
status:0
},
labelKey:'name',
valueKey:'id'
},
props:{
placeholder:'民族',
clearable:true,
multiple:false,
}
},
{
showType:'text',
label:'开户行',
field:'bank',
props:{
disabled:false,
maxlength:50,
placeholder:'开户行',
clearable:true
}
},
{
showType:'text',
label:'账号',
field:'accountNo',
props:{
disabled:false,
maxlength:50,
placeholder:'账号',
clearable:true
}
},
{
showType:'text',
label:'邮箱',
field:'email',
rules:[
{validator:isEmail,trigger:'change'}
],
props:{
disabled:false,
maxlength:50,
placeholder:'邮箱',
clearable:true
}
},
{
showType:'select',
label:'婚姻',
field:'marry',
options:[{label:'已婚',value:1},{label:'未婚',value:0}],
props:{
placeholder:'婚姻',
clearable:true,
multiple:false,
}
},
{
showType:'date',
label:'出生时间',
field:'birthday',
props:{
placeholder:'出生时间',
clearable:true,
}
},
{
showType:'date',
label:'进入公司时间',
field:'entryDate',
props:{
placeholder:'进入公司时间',
clearable:true,
}
},
{
showType:'address',
label:'住址',
field:'fullAddress',
},
{
showType:'textarea',
label:'备注',
field:'remark',
props:{
disabled:false,
maxlength:200,
placeholder:'备注',
clearable:true
}
},
{
showType:'file',
label:'个人签名',
field:'signature',
uploadFileLoading:false,
props:{
maxSize:20,
limit:1,
showSize:false,
multiple:false,
accept:'jpg,png,jpeg'
}
},
{
showType:'file',
label:'劳动合同',
field:'laborContract',
uploadFileLoading:false,
props:{
maxSize:20,
limit:20,
showSize:false,
multiple:false
}
},
{
showType:'file',
label:'其他附件',
field:'fileList',
uploadFileLoading:false,
props:{
maxSize:20,
limit:20,
showSize:false,
multiple:false
}
},
]
外部调用
// fieldList是上面构造的JSON数据,staffForm是表单回显的数据
<DynamicForm :fieldList="fieldList" :defaultData="staffForm" @loading="val=>isLoading=val" @mobile="changeName" ref="dynamicFormRef" v-if="dialogStaffVisible">
<el-form-item label="手机验证码:" prop="mobileCheckCode" v-if="Codes && openVerify" :rules="{required:true,message:'请输入验证码',trigger:'change'}">
<el-row>
<el-col :span="12" :lg="17" :xl="17">
<el-input v-model="staffForm.mobileCheckCode" @change="changeMobileCheckCoe" style="width: 95%" placeholder="请输入"></el-input>
</el-col>
<el-col :span="12" :lg="6" :xl="6">
<el-button class="diyBtn" type="search" v-if="countDown < 1" @click="showCheckDialog">
获取验证码
</el-button>
<el-button style="width: 96px" :loading="isMessageBtnDisabled" type="search" v-else>
{{ countDown + '秒' }}
</el-button>
</el-col>
</el-row>
</el-form-item>
</DynamicForm>
最终效果
欢迎评论区交流