关于表单:前端配置化表单组件设计方法-京东云技术团队

42次阅读

共计 5502 个字符,预计需要花费 14 分钟才能阅读完成。

一、背景

前端开发中波及表单的页面十分多,看似性能简略,开发疾速,实则占去了很大一部分工夫。当某个表单蕴含元素过多时还会导致 html 代码过多,vue 文件过大。从而不容易查找、批改和保护。为了进步开发效率及升高保护老本,上面介绍表单配置化组件的封装原理与封装办法。

二、技术计划

如上图所示,封装表单配置化组件的关键点有三个一是如何解决表单元素排布的行列问题,二是表单数据的绑定问题,三是表单元素的参数配置校验等问题。上面别离介绍这三个问题的解决办法。

•配置化表单组件的入参及阐明

参数 阐明 类型 可选值 默认值
labelWidth 表单元素 label 所占宽度 String —— 150px
columnList 表单元素所组成的配置,是一个数组 Array —— []
formData 表单元素值的汇合 Object —— {}
columnSpan 表单排布分栏 Number —— 24
size 表单元素尺寸 String medium / small / mini medium

•计算配置化表单的行数,本表单通过根底的 24 分栏计算表单最终的行数和列数,通过上面办法最终失去一个对于行列的二维数组

newColumnList() {const newColumnList= []
  const row = Math.floor(24 / this.columnSpan)
  let newColumnItem = []
  for(let i=0; i< this.columnList.length; i++) {newColumnItem.push(this.columnList[i])
    if(newColumnItem.length === row || i === this.columnList.length-1) {newColumnList.push(newColumnItem)
      newColumnItem = []}
  }
  return newColumnList
}

•通过下面失去的二维数组进行循环渲染,首先循环渲染行,其次循环渲染列。本计划采纳 element 中的表单,当然也能够用其余组件库或者原生表单进行渲染,其原理通用。最终将会依据参数 column.type 决定加载哪一个具体的表单元素。

<el-form ref="form" :model="formData" :label-width="labelWidth" :size="size">
    <el-row :gutter="20" v-for="(element,index) in newColumnList" :key="index+'formRow'">
      <template v-for="(item, index) in element" >
        <column
          :key="index +'formView'":columnSpan="columnSpan":column="item":formData="formData"
        />
      </template>
    </el-row>
</el-form>

•column 组件最终依据 type 加载具体的表单元素。上面展现 column 组件的入参及其阐明,通过 component 加载不同的表单元素

参数 阐明 类型 可选值 默认值
column 表单元素的具体配置 Object —— {}
formData 表单元素值的汇合 Object —— {}
columnSpan 表单排布分栏 Number —— 24
<el-col :span="columnSpan">
     <component
      :is="column.type +'View'":column="column":formData="formData"v-model="formData[column.name]":columnSpan="columnSpan"/>
 </el-col>

•这里次要以 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 —— ——
<el-form-item :label="column.title +':'":prop="column.name":rules="rules">
    <el-select
      v-model="val"
      clearable
      :multiple="column.multiple"
      :filterable="column.filterable"
      :placeholder="'请抉择' + column.title":disabled="column.disabled"style="width: 100%"@change="onChange"@clear="onClear">
      <el-option v-for="item in column.dictionary" :key="item.code" :label="item.name" :value="item.code">
      </el-option>
    </el-select>
</el-form-item>
rules:  [
    {
      required: this.column.required,
      message: this.column.placeholder placeholder ? this.column.placeholder : ` 请输出 ${this.column.title}`,
      trigger: 'change'
    },
    ...this.column.rules
 ]
onChange(){this.$emit('input',this.val)
  if(this.column && this.column.changeFunction){this.column.changeFunction(this.val)
  }
},
onClear(){this.onChange()
}

三、我的项目实际

•配置化表单为 bs-form,在页面中引入 bs-form 表单组件

<bs-form ref="formDemo"
     :columnList="columnList"
     :formData="formData"
     :columnSpan="columnSpan"
     labelWidth="120px">
</bs-form>
<el-row style="text-align: center;">
  <el-button type="primary"
             @click="onSave"> 保留 </el-button>
  <el-button @click="onCancel"> 勾销 </el-button>
</el-row>

•formData 参数

formData: {
    name: '',
    yearIncome: '', // 业务类型
    goodsCategoryId: '', // 托寄物品类 id
    projectManagerErp: '', // 项目经理 erp
    projectName: '', // 项目名称
    projectStage: '', // 我的项目阶段编码
    projectStandardName: '', // 规范名称
    projectYear: 2023, // 年份
    startRegionId: '', // 始发区域 id
    startBattleId: '', // 始发战区 id
    address: [], // 省市
    category: null, // 图文类型
    range: [] // 公布范畴}

•分栏参数

columnSpan: 6

•表单配置参数

columnList(){
  const self = this
  return [
    {
      type: 'text',
      name: 'name',
      title: '项目名称',
      required: true,
      maxlength: 20,
      showwordlimit: true,
      placeholder: '请输出'
    },
    {
      name: 'category',
      type: 'radio',
      dictionary: [
        {
          code: 1,
          name: '类型一'
        },
        {
          code: 2,
          name: '类型二'
        }
      ],
      title: '图文类型',
      required: true
    },
    {
      name: 'range',
      type: 'checkbox',
      title: '公布范畴',
      dictionary: [
        {
          code: 1,
          name: '范畴一'
        },
        {
          code: 2,
          name: '范畴二'
        }
      ],
      required: true
    },
    {
      type: 'text',  // 字段类型文本框
      name: 'yearIncome',  // 与后盾对接字段
      title: '年均收入',  // 前端展现字段
      required: true, // 必填项设置
      maxlength: 50,  // 字符串长度限度
      showwordlimit: true, // 是否显示字符串长度
      placeholder: '请输出', // 占位文本提醒
      rules: [{ pattern: /(^[1-9]([0-9]+)?(.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9].[0-9]([0-9])?$)/, message: '请输出数字最多两位小数' }
      ],
    },
    {
      type: 'select',
      name: 'goodsCategoryId',
      title: '托寄物品类',
      required: true,
      filterable: true,
      placeholder: '请抉择',
      dictionary: [{
        name: '苹果',
        code: '1'
      },{
        name: '手机',
        code: '2'
      },{
        name: '测试',
        code: '3'
      },{
        name: '樱桃',
        code: '7'
      },{
        name: '荸荠',
        code: '9'
      }]
    },
    {
      type: 'select',
      name: 'startRegionId',
      title: '区域',
      required: true,
      placeholder: '请抉择',
      dictionary: [{
        name: '销售 - 华北区域',
        code: '1'
      },{
        name: '销售 - 华东区域',
        code: '2'
      },{
        name: '销售 - 华南区域',
        code: '3'
      },{
        name: '销售 - 东北区域',
        code: '4'
      },{
        name: '销售 - 华中区域',
        code: '5'
      },{
        name: '销售 - 西南区域',
        code: '6'
      }],
      // 点击下来触发切换联动的事件,为一个函数
      changeFunction: function (val) {}}, {
      type: 'select',
      name: 'startBattleId',
      title: '战区',
      required: true,
      placeholder: '请抉择',
      dictionary: this.battleByRegionList
    }, {
      type: 'select',
      name: 'projectStage',
      title: '我的项目阶段',
      required: true,
      placeholder: '请抉择',
      dictionary: [{
        name: '我的项目发动阶段',
        code: '10'
      },{
        name: '我的项目调研阶段',
        code: '20'
      },{
        name: '可行性分析阶段',
        code: '30'
      },{
        name: '立项阶段',
        code: '40'
      }]
    }, {
      type: 'text',
      name: 'projectStandardName',
      title: '规范名称',
      required: true,
      placeholder: '请输出',
      append: '.com',  // 文本框后置内容
    }, {
      type: 'text',
      name: 'projectManagerErp',
      title: '项目经理',
      required: true,
      placeholder: '请输出'
    },{
      type: 'cascader',  // 字段类型下拉框
      name: 'address',   // 与后盾对接字段
      title: '省市区',  // 前端展现字段
      required: true, // 必填项设置
      placeholder:'请抉择',  // 占位文本提醒
      dictionary: [{
        value: 'shanxi',
        label: '陕西省',
        children: [{
          value: 'xian',
          label: '西安市',
          children: [{
            value: 'yanta',
            label: '雁塔区'
          }, {
            value: 'beilin',
            label: '碑林区'
          }, {
            value: 'xincheng',
            label: '新城区'
          }, {
            value: 'weiyang',
            label: '未央区'
          }]
        }]
      }],
      // 点击下来触发切换联动的事件,为一个函数
      changeFunction: function(){}
    },{
      type: 'static',
      name: 'projectYear',
      title: '年份'
    }
  ]
}

•表单保留

// 保留
async onSave() {const valid = await this.$refs.formDemo.onValidate()
  if(valid) {this.$message.success('校验通过')
  }else {this.$message.error('校验失败')
  }
}

四、成绩展现

作者:京东物流 田雷雷

正文完
 0