《我是这样处理多元素表单的》

213 阅读4分钟

表单是我们前端开发工作中经常打交道的业务组件,特别是当表单元素数量比较大的时候十分头疼,因此如何优雅的处理表单元素至关重要,我们知道表单元素一般都分为几种常见类型:输入框,日期选择框,下拉选择框,附件上传,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>

最终效果

微信截图_20240901102409.png

欢迎评论区交流