• 基于 el-form 封装一个依赖 json 动态渲染的表单控件


    nf-form 表单控件的功能

    基于 el-form 封装了一个表单控件,包括表单的子控件。
    既然要封装,那么就要完善一些,把能想到的功能都要实现出来,不想留遗憾。
    毕竟UI库提供的功能都很强大了,不能浪费了对吧。

    • 依赖 json 动态创建表单
    • 可以多行多列
    • 可以调整布局
    • 可以自定义子控件(插槽和动态组件)
    • 可以扩展表单子控件
    • 数据验证
    • 数据联动
    • 组件联动
    • 依据 json 自动创建 model

    功能演示

    介绍代码之前先看看效果。

    • 单列表单
      这个比较基础,直接贴图。

    单列表单

    • 多列表单
      有时候需要双列或者三列的表单,这个也是要支持的。

    双列表单

    三列表单

    因为采用的是 el-col 实现的多列,所以理论上最多支持 24 列,当然要看屏幕的宽度了。

    • 调整布局
      看上面的图片,可以发现个问题,改变列数之后,表单页面变得不好看了,这时候需要我们做一些调整,比如让某个组件占用两份空间,调整一下组件的先后顺序。

    【单列中的合并】

    单列的合并组件位置

    调整之后,页面可以更紧凑。可以两个组件占一行,也可以三个组件占一行,具体看屏幕的宽度和一个组件的大小。

    【多列里的占一行】

    多列里面一个组件占用两个位置

    • 自定义子控件
      如果表单提供的子控件不能满足需求,那么怎么办?我们可以自己来定义一个子控件。
    1. 使用插槽
      使用插槽比较简单和灵活,可以在表单控件外部完全控制,适合临时的情况,插槽里可以有多个组件。

    用插槽实现自定义组件

    1. 使用动态组件
      插槽的方式虽然灵活,但是不便于复用,如果需要在多个地方使用的话,可以先做成一个组件,然后用动态组件的方式加入表单。

    动态组件实现自定义子控件

    这里使用动态组件的方式加入了 element 的穿梭控件,也可以加入其它各种组件。

    • 数据验证
      可以直接使用 el-form 提供的验证功能,在json里面设置好验证规则即可。

    表单验证

    • 数据联动
    1. 一个组件内的联动
      这个可以使用 el-cascader 来实现。

    2. 多个组件的联动
      可以用简单来实现。

    • 组件联动
      可以根据某个组件的值,设置其他组件是否显示。

    文本类

    选择类

    封装表单子控件

    表单控件需要很多子控件,所以要先封装一下子控件,然后才方便封装表单控件。

    定义接口,统一规范

    表单子控件有一个相同的需求,都需要实现属性和 v-model 数据交换,因为 element 把 value 给封装成了v-model,所以无法直接绑定组件的属性,必须建立一个内部变量来绑定。
    所以需要一个转换的方式,这里采用自定义ref来实现,顺便实现了一下防抖功能。

    虽然在表单控件里面并不需要防抖功能,但是查询的时候需要,而表单子控件是可以通用到查询控件里面的。

    定义一个 v-model 和 my-change

    // 自定义 ref 
    /**
     * 自定义的ref,实现属性和内部变量的数据转换
     * @param { reactive } props 组件的属性
     * @param { object } context 组件的上下文
     * @param { number } delay 延迟刷新的时间,单位:毫秒,默认:0
     * @param { string } name 要对应的属性名称,默认:modelValue
     * @returns 自定义的ref
     */
    export const debounceRef = (props, context, delay = 0, name = 'modelValue') => {
      let _value = props[name]
    
      // 计时器
      let timeout
      // 是否输入状态。输入时取 value;输入完毕取 modelValue 属性
      let isInput = false
      return customRef((track, trigger) => {
        return {
          get () {
            track()
            if (isInput) {
              // console.log(isInput)
              return _value
            } else {
              // console.log(isInput)
              return props[name]
            }
          },
          set (newValue) {
            isInput = true
            _value = newValue // 绑定值
            trigger() // 组件内部刷新模板
            clearTimeout(timeout) // 清掉上一次的计时
            timeout = setTimeout(() => {
              // 修改 modelValue 属性
              context.emit(`update:${name}`, newValue) // 提交给父组件
              // 用于区分是哪个组件触发的事件。
              context.emit('my-change', newValue, props.controlId, props.colName)
              isInput = false
            }, delay)
          }
        }
      })
    }
    

    封装各种表单子控件

    按照原子性原则,子控件封装的比较细,直接看图:

    表单子控件

    代码有点多,不一一介绍了,感兴趣的可以看源码。

    封装表单控件

    基础工作做好之后,我们就可以封装 el-form 了。

    定义属性

    依据 el-form 的属性我们定义几个关键性属性

    介绍属性
    /**
     * 表单控件需要的属性
     */
    export const formProps = {
      modelValue: Object, // 完整的model
      partModel: Object, // 根据选项过滤后的model
      miniModel: Object, // 精简的model
      /*
      * 自定义子控件 key:value形式
      * * key: 编号。1:插槽;100-200:保留编号
      * * value:string:标签;函数:异步组件,类似路由的设置
      */
      customerControl: { // 自定义的表单子组件
        type: Object,
        defaule: () => {}
      },
      colOrder: { // 表单字段的排序的依据
        type: Array,
        default: () => []
      },
      formColCount: { // 表单的列数
        type: Number,
        default: 1
      },
      reload: {
        type: Boolean, // 是否重新加载配置,需要来回取反
        default: false
      },
      itemMeta: {
        type: Object, // 表单子控件的属性
        default: () => {}
      },
      ruleMeta: { // 验证信息
        type: Object, 
        default: () => {}
      },
      formColShow: { // 数据变化,联动组件是否显示
        type: Object,
        default: () => {}
      } 
    }
    

    定义内部model

    一般一个 model 就可以,只是这里做了一个组件联动的,那么如果只需要获取可见的组件的值呢,于是做了局部model。

    model

    实现多行多列和布局调整

    采用 el-col 实现,通过控制 span 来实现多列,所以理论上最多支持24列,当然这个要看屏幕宽度了。

    /**
     * 处理一个字段占用几个td的需求
     * @param { object } props 表单组件的属性
     * @returns 
     */
    const getColSpan = (props) => {
      // 确定一个组件占用几个格子
      const formColSpan = reactive({})
      
      // 表单子控件的属性
      const formItemProps = props.itemMeta
    
      // 根据配置里面的colCount,设置 formColSpan
      const setFormColSpan = () => {
        const formColCount = props.formColCount // 列数
        const moreColSpan = 24 / formColCount // 一个格子占多少份
    
        if (formColCount === 1) {
        // 一列的情况
          for (const key in formItemProps) {
            const m = formItemProps[key]
            if (typeof m.colCount === 'undefined') {
              formColSpan[m.controlId] = moreColSpan
            } else {
              if (m.colCount >= 1) {
                // 单列,多占的也只有24格
                formColSpan[m.controlId] = moreColSpan
              } else if (m.colCount < 0) {
                // 挤一挤的情况, 24 除以 占的份数
                formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
              }
            }
          }
        } else {
          // 多列的情况
          for (const key in formItemProps) {
            const m = formItemProps[key]
            if (typeof m.colCount === 'undefined') {
              formColSpan[m.controlId] = moreColSpan
            } else {
              if (m.colCount < 0 || m.colCount === 1) {
                // 多列,挤一挤的占一份
                formColSpan[m.controlId] = moreColSpan
              } else if (m.colCount > 1) {
                // 多列,占的格子数 * 份数
                formColSpan[m.controlId] = moreColSpan * m.colCount
              }
            }
          }
        }
      }
    
      return {
        formColSpan,
        setFormColSpan
      }
    }
    

    首先计算一下一列要用多少个span,也就是用24除以列数。
    然后判断是不是单列,单列要处理多个组件占用一个位置的需求,多列要处理一个组件占用多个位置的需求。

    实现扩展

    表单子控件可以多种多样,无法完全封装进入表单控件,那么就需要表单控件支持子控件的扩展。
    这里要感谢 vue 的动态组件功能,让扩展子控件变得非常方便。

    我们使用 component 和动态组件来实现表单子控件的加载。

    <component
        :is="formItemListKey[getCtrMeta(ctrId).controlType]"
        v-model="formModel[getCtrMeta(ctrId).colName]"
        v-bind="getCtrMeta(ctrId)"
        @my-change="myChange">
      </component>
    
    export const formItemList = {
      // 文本类 defineComponent
      'el-form-text': defineAsyncComponent(() => import('./t-text.vue')),
      'el-form-area': defineAsyncComponent(() => import('./t-area.vue')),
      'el-form-url': defineAsyncComponent(() => import('./t-url.vue')),
      'el-form-password': defineAsyncComponent(() => import('./t-password.vue')),
      // 数字
      'el-form-number': defineAsyncComponent(() => import('./n-number.vue')),
      'el-form-range': defineAsyncComponent(() => import('./n-range.vue')),
      // 日期、时间
      'el-form-date': defineAsyncComponent(() => import('./d-date.vue')),
      'el-form-datetime': defineAsyncComponent(() => import('./d-datetime.vue')),
      'el-form-year': defineAsyncComponent(() => import('./d-year.vue')),
      'el-form-month': defineAsyncComponent(() => import('./d-month.vue')),
      'el-form-week': defineAsyncComponent(() => import('./d-week.vue')),
      'el-form-time-select': defineAsyncComponent(() => import('./d-time-select.vue')),
      'el-form-time-picker': defineAsyncComponent(() => import('./d-time-picker.vue')),
      // 选择、开关
      'el-form-checkbox': defineAsyncComponent(() => import('./s-checkbox.vue')),
      'el-form-switch': defineAsyncComponent(() => import('./s-switch.vue')),
      'el-form-checkboxs': defineAsyncComponent(() => import('./s-checkboxs.vue')),
      'el-form-radios': defineAsyncComponent(() => import('./s-radios.vue')),
      'el-form-select': defineAsyncComponent(() => import('./s-select.vue')),
      'el-form-selwrite': defineAsyncComponent(() => import('./s-selwrite.vue')),
      'el-form-select-cascader': defineAsyncComponent(() => import('./s-select-cascader.vue'))
    
    }
    
    /**
     * 动态组件的字典,便于v-for循环里面设置控件
     */
    export const formItemListKey = {
      // 文本类
      100: formItemList['el-form-area'], // 多行文本
      101: formItemList['el-form-text'], // 单行文本
      102: formItemList['el-form-password'], // 密码
      103: formItemList['el-form-text'], // 电话
      104: formItemList['el-form-text'], // 邮件
      105: formItemList['el-form-url'], // url
      106: formItemList['el-form-text'], // 搜索
      // 数字
      120: formItemList['el-form-number'], // 数字
      121: formItemList['el-form-range'], // 滑块
      // 日期、时间
      110: formItemList['el-form-date'], // 日期
      111: formItemList['el-form-datetime'], // 日期 + 时间
      112: formItemList['el-form-month'], // 年月
      113: formItemList['el-form-week'], // 年周
      114: formItemList['el-form-year'], // 年
      115: formItemList['el-form-time-picker'], // 任意时间
      116: formItemList['el-form-time-select'], // 选择固定时间
      // 选择、开关
      150: formItemList['el-form-checkbox'], // 勾选
      151: formItemList['el-form-switch'], // 开关
      152: formItemList['el-form-checkboxs'], // 多选组
      153: formItemList['el-form-radios'], // 单选组
      160: formItemList['el-form-select'], // 下拉
      161: formItemList['el-form-selwrite'], // 下拉多选
      162: formItemList['el-form-select-cascader'] // 下拉联动
    }
    

    需要扩展子控件的时候,我们只需要向字典(dict)里面添加需要的组件即可,然后设置一个新的编号。

      // 添加临时动态组件
      formProps.customerControl = {
        300: 'el-transfer'
      }
      // 设置表单字段
      childMeta.select.controlType = 300
    

    为啥用编号?虽然编号不易读,但是编号稳定,而且灵活。如果我们要基于ant design Vue 封装控件的话,我可以直接用编号,但是如果用名称的话,那么要不要区分 el- 和 a- 呢?

    实现数据联动

    联动分为数据联动,和组件联动,数据联动可以依赖UI库的组件来实现,或者依赖Vue的数据的响应性来实现。
    比如常见的省市区县联动,我们可以用 el-cascader。
    如果需要使用多个组件的话,我们可以监听组件的值的变化,然后获取数据绑定下一个组件的options。

    // 数据联动
      watch (() => model.provinces, (v1, v2) => {
        console.log('监听值的变化', v1)
        const arr = [
          {"value": 1 + v1, "label": "多选 选项一" + v1},
          {"value": 2 + v1, "label": "多选 选项二" + v1}
        ]
      
        childMeta.city.optionList.length = 0
        childMeta.city.optionList.push(...arr)
      })
    

    Vue 就是数据驱动的,所以联动的话也是直接监听value的改变即可,不用像以前那样要设置change事件了。

    实现组件联动

    组件联动,就是一个组件的值发生变化,影响其他组件的显示状态。

    企业用户

    个人用户

    比如在注册的时候,需要选择企业用户还是个人用户。
    如果是企业用户,需要添加企业名称(以及相关信息);
    如果是个人注册那么只需要填写个人姓名即可。

    这样表单里面显示的组件就要随之变化。

    对于这类的需求,我们可以配置一下 formColShow 属性。

        "formColShow": {
          "90": {  // 组件ID
            "1": [90, 101, 100, 102, 105],  // 组件值对应的需要显示的组件ID,下同
            "2": [90, 120, 121],
            "3": [90, 110, 114, 112, 113, 115, 116],
            "4": [90, 150, 151, 152, 153, 160, 162]
          }
        },
    

    配置好之后就可以实现了,表单控件内部代码会做一个 watch 监听:

      // 数据变化,联动组件的显示
      if (typeof props.formColShow !== 'undefined') {
        for (const key in props.formColShow) {
          const ctl = props.formColShow[key]
          const colName = props.itemMeta[key].colName
          // 监听组件的值,有变化就重新设置局部model
          watch(() => formModel[colName], (v1, v2) => {
            if (typeof ctl[v1] === 'undefined') {
              // 没有设定,显示默认组件
              setFormColSort()
            } else {
              // 按照设定显示组件
              setFormColSort(ctl[v1])
              // 设置部分的 model
              createPartModel(ctl[v1])
            }
          })
        }
    

    json格式

    整个表单是依据 json 动态渲染出来的,那么json格式是啥样的呢?分为两个部分,一个是表单控件自己需要的属性,另一个是表单子控件需要的属性,还有验证规则等。

    {
      "formTest": {
        "baseProps": { // 表单控件自己的属性
          "formColCount": 1, // 列数
          "colOrder": [ // 需要显示的组件的ID
            90,  101, 102,
            110, 111, 114, 112, 113, 115, 116,
            120, 121, 100, 
            150, 151, 152, 153,
            160, 162
          ]
        },
        "formColShow": { // 组件联动的信息
          "90": { // 触发的组件
            "1": [90, 101, 100, 102, 105], // 组件值对应的需要显示的组件的ID
            "2": [90, 120, 121],
            "3": [90, 110, 114, 112, 113, 115, 116],
            "4": [90, 150, 151, 153, 152, 160, 162]
          }
        },
        "ruleMeta": { // 验证规则
          "101": [ // 表单子控件的ID,下面是验证规则
            { "trigger": "blur", "message": "请输入活动名称", "required": true },
            { "trigger": "blur", "message": "长度在 3 到 5 个字符", "min": 3, "max": 5 }
          ]
        },
        "itemMeta": { // 表单子控件的属性
          "90": {  
            "controlId": 90,
            "colName": "kind",
            "label": "分类",
            "controlType": 153,
            "isClear": false,
            "defaultValue": "",
            "placeholder": "分类",
            "title": "编号",
            "optionList": [
              {"value": 1, "label": "文本类"},
              {"value": 2, "label": "数字类"},
              {"value": 3, "label": "日期类"},
              {"value": 4, "label": "选择类"}
            ],
            "colCount": 1
          },
          "100": {  
            "controlId": 100,
            "colName": "area",
            "label": "多行文本",
            "controlType": 100,
            "isClear": false,
            "defaultValue": 1000,
            "placeholder": "多行文本",
            "title": "多行文本",
            "colCount": 1
          },
          ...
        }
      }
    }
    

    遍历子控件

    因为子控件都封装好了,所以只需要简单遍历即可:

      <el-form
        :model="formModel"
        :rules="rules"
        ref="formControl"
        :inline="false"
        class="demo-form-inline"
        label-suffix=":"
        label-width="130px"
        size="mini"
      >
        <el-row>
          <!--不循环row,直接循环col,放不下会自动往下换行。-->
          <el-col
            v-for="(ctrId, index) in formColSort"
            :key="'form_'+index"
            :span="formColSpan[ctrId]"
          ><!--:prop="getCtrMeta(ctrId).colName"-->
            <el-form-item
              :label="getCtrMeta(ctrId).label"
              :prop="getCtrMeta(ctrId).colName"
            >
              <!--判断要不要加载插槽-->
              <template v-if="getCtrMeta(ctrId).controlType === 1">
                <!--<slot :name="ctrId">父组件没有设置插槽</slot>-->
                <slot :name="getCtrMeta(ctrId).colName">父组件没有设置插槽</slot>
              </template>
              <!--表单item组件,采用动态组件的方式-->
              <template v-else>
                <component
                  :is="dictControl[getCtrMeta(ctrId).controlType]"
                  v-model="formModel[getCtrMeta(ctrId).colName]"
                  v-bind="getCtrMeta(ctrId)"
                  @my-change="myChange">
                </component>
              </template>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    

    篇幅有限无法一一介绍,其他部分可以看源码。

    源码

    https://gitee.com/naturefw/nf-vite2-element

    自然框架/nf-vite2-element

  • 相关阅读:
    javascript中的几种遍历方法浅析
    实用的正则表达式
    关于git中的merge和rebase
    油猴脚本-3
    油猴脚本-2
    油猴脚本-1
    hadoop各个组件之间的通信
    mysql 表数据修改的方法,单标修改、多表修改--将一张表里面的其中一个字段的值赋值给另一张表
    kafka的副本同步机制(ISR)
    sql的over函数的作用和方法
  • 原文地址:https://www.cnblogs.com/jyk/p/14783548.html
Copyright © 2020-2023  润新知