• 【摸鱼神器】UI库秒变低代码工具——表单篇(二)子控件


    上一篇介绍了表单控件,这一篇介绍一下表单里面的各种子控件的封装方式。

    主要内容

    • 需求分析
    • 子控件的分类
    • 子控件属性的分类
    • 定义 interface。
    • 定义子控件的的 props。
    • 定义 json 文件。
    • 基于 UI库 进行二次封装,实现依赖 json 渲染。
    • 通过 slot 、 “字典”,实现自定义子控件。
    • 做个工具维护 json 文件。(下篇介绍)

    需求分析

    表单里面需要各种各样的子控件,像文本、数字、选择、日期等常见的需求,可以由内部提供组件解决,但是其他各种“奇奇怪怪”的需求怎么办呢?

    如果还是由“内部”提供组件的话,那肯定是行不通的,因为以往的经验教训告诉我们,内部不断扩充子控件的结果,必然会导致内部代码越来越臃肿,以至后期无法维护,最终崩盘!

    所以必须支持自定义扩展!感谢 Vue 和 UI库,提供基础的技术支持,让扩展变得非常容易。

    我们先对表单子控件进行一下分类,然后为其设计一套接口,即定义一套规则,这样才好方便做长期维护。

    子控件的分类

    我们对常见的组件进行分析,得到了下面的分类:

    表单子控件的分类

    上图涵盖了一些常用控件,但是很显然并不全面,比如没有金额类的控件,输入金额也是需要一些辅助的,比如金额的大小写的切换等,不过这些应该用扩展的方式实现。

    属性的分类

    组件的分类可以做的“规整”一些,但是组件的属性的分类,就比较有难度了,我们可以把组件需要的属性分为三个主要部分:代码里需要的、共用的、扩展的。

    • 低代码需要的属性
      需要在代码里面使用的属性,比如字段名称、控件类型、默认值、防抖延迟等,集中在一起,通过 props 的方式传递。

    • 共用属性
      各个组件(或者大部分组件)都需要的属性,比如浮动提示、size、是否显示清空按钮等,作为一级属性,通过 props 的方式传递。

    • 扩展属性
      某个组件需要的属性,比如数字组件需要 max、min、step等。通过 $attrs 的方式传递。

    100表单子控件的属性.png

    其中扩展属性最为复杂,如果按照面向对象的方式来设计的话,结构就会非常复杂,会复杂到什么程度呢?可以参考当初 asp.net 里面 webform 的继承结构:

    十三年前做的一张图

    (controll是控件(组件)的意思,下面分出来WebControll和 repeater 两个子类,然后又,,,算了不说了,是不是看着就很累的样子?)

    定义接口

    现在是 JS 环境,我们没有必要生搬硬套,而是可以利用JS的灵活性来做简洁设计:

    表单子控件的接口

    我们给表单子控件的 props 定义一个interface:(虽然暂时用不上)

    • IFormItemProps
    /**
     * 表单控件的子控件的 props。
     */
    export interface IFormItemProps {
      /**
       * 低代码需要的数据
       */
      formItemMeta: IFormItemMeta,
      /**
       * 子控件备选项,一级或者多级
       */
      optionList: Array<IOptionList | IOptionTree>,
      /**
       * 表单的 model,含义多个属性
       */
      model: any,
      /**
       * 是否显示清空的按钮
       */
      clearable: boolean,
      /**
       * 浮动提示信息
       */
      title: string,
      /**
       * 子控件的扩展属性
       */
      [key: string]: any
    }
    
    折叠
    • IFormItemMeta 的定义:
    /**
     * 子控件的低代码需要的数据
     */
    export interface IFormItemMeta {
      /**
       * -- 字段ID、控件ID
       */
      columnId?: number | string,
      /**
       * -- 字段名称
       */
      colName: string,
      /**
       * -- 字段的中文名称,标签
       */
      label?: string,
      /**
       * -- 子控件类型,number,EControlType
       */
      controlType: EControlType | number,
      /**
       * 子控件的默认值
       */
      defValue: any,
      /**
       * -- 一个控件占据的空间份数。
       */
      colCount?: number,
      /**
       * 访问后端API的配置信息,有备选项的控件需要
       */
      webapi?: IWebAPI,
      /**
       * -- 防抖延迟时间,0:不延迟
       */
      delay: number,
      /**
       * 防抖相关的事件
       */
      events?: IEventDebounce,
    }
    
    折叠

    规则定义之后呢,总会发现有特例的属性,比如 select 的 option。代码里面需要使用 option 去绑定组件,应该放在“低代码需要的属性”里面。

    但是实际使用的时候发现,放在“共用属性”里面会更方便。

    然后在做“维护JSON的小工具”的时候,发现需要放在“扩展属性”里面维护,这样维护代码更容易实现。

    综合考虑之后,就出现了一个不符合规则的属性 —— optionList。

    定义组件的 props。

    按照接口实现一下 props 的定义。

    import type { PropType } from 'vue'
    
    import type {
      IOptionList,
      IOptionTree,
      IFormItemProps
    } from '../types/20-form-item'
    
    /**
     * 基础控件的共用属性,即表单子控件的基础属性
     */
    export const itemProps = {
      formItemMeta: {
        type: Object as PropType<IFormItemProps>,
        default: () =>  {return {}}
      },
      /**
       * optionList:IOptionList | IOptionTree,控件的备选项,单选、多选、等控件需要
       */
      optionList: {
        type: Object as PropType<Array<IOptionList | IOptionTree>>,
        default: () =>  {return []}
      },
      /**
       * 表单的 model,整体传入,便于子控件维护字段值。
       */
      model: {
        type: Object
      },
      /**
       * 是否显示可清空的按钮,默认显示
       */
      clearable: {
        type: Boolean,
        default: true
      },
      /**
       * 浮动的提示信息,部分控件支持
       */
      title: {
        type: String,
        default: ''
      }
    }
    
    折叠

    其他属性以及扩展属性,可以通过 $attrs 传递和绑定,这样可以方便各种扩展。

    定义 json 文件。

    我们来定义一个示例用的 json文件。

    {
        "formItemMeta": {
          "columnId": 90,
          "colName": "kind",
          "label": "分类",
          "controlType": 107,
          "isClear": false,
          "defValue": 0,
          "colCount": 7
        },
        "placeholder": "分类",
        "title": "编号",
        "optionList": [
          {"value": 1, "label": "文本类"},
          {"value": 2, "label": "数字类"},
          {"value": 3, "label": "日期类"},
          {"value": 4, "label": "时间类"},
          {"value": 5, "label": "选择类"},
          {"value": 6, "label": "下拉类"}
        ]
    }
    

    基于 UI库 封装,实现依赖 json 渲染。

    首先要感谢强大的UI库,实现了大部分的功能,我们只需要再稍微封装一下即可,只有少数几个组件需要我们补充点代码。

    文本类

    • template
      <el-input
        v-model="value"
        v-bind="$attrs"
        :id="'c' + formItemMeta.columnId"
        :name="'c' + formItemMeta.columnId"
        :title="title"
        :clearable="clearable"
        @blur="run"
        @change="run"
        @clear="run"
        @keydown="clear"
      >
      el-input>
    

    使用 v-bind="$attrs" 绑定扩展属性

    • ts
      import { defineComponent } from 'vue'
      import { ElInput } from 'element-plus'
      // 引入组件需要的属性、控制类
      import { itemProps, itemController } from '@naturefw/ui-elp'
    
      export default defineComponent({
        name: 'nf-el-form-item-text',
        inheritAttrs: false,
        components: {
          ElInput
        },
        props: {
          modelValue: [String, Number],
          ...itemProps // 基础属性
        },
        emits: ['update:modelValue'],
        setup (props, context) {
          const {
            value,
            run,
            clear
          } = itemController(props, context.emit)
    
          return {
            value,
            run,
            clear
          }
        }
      })
    
    
    折叠

    使用 ...itemProps 定义属性。

    是不是很简单。

    可能你会问了,这不是封装了个寂寞吗,你看看里面空荡荡的,完全没有封装的必要嘛。

    确实,对于文本这类简单的组件,确实没有封装的必要,直接使用UI库提供的组件即可。

    那么为啥好要封装一下呢?

    首先为了统一风格,不管是简单的,还是复杂的,都按照统一方式封装一下,这样便于维护和扩展。

    日期类

    • template
      <el-date-picker
        ref="domDate"
        v-model="value"
        v-bind="$attrs"
        :type="dateType"
        :name="'c' + formItemMeta.columnId"
        :format="format"
        :value-format="valueFormat"
        :title="title"
        :clearable="clearable"
      >
      el-date-picker>
    
    • ts
      import { defineComponent } from 'vue'
      // 引入组件需要的属性 引入表单子控件的管理类
      import { itemProps, itemController } from '@naturefw/ui-elp'
      
      /**
       * 日期
       */
      export default defineComponent({
        name: 'nf-el-from-item-date',
        inheritAttrs: false,
        props: {
          ...itemProps, // 基础属性
          format: {
            type: String,
            default: 'YYYY-MM-DD'
          },
          'value-format': {
            type: String,
            default: 'YYYY-MM-DD'
          },
          modelValue: [String, Date, Number, Array]
        },
        emits: ['update:modelValue'],
        setup (props, context) {
          const { value } = itemController(props, context.emit)
    
          // 根据类型判断是否为数组,判断是否 使用范围。
          let dateType = 'date'
          if (props.formItemMeta.controlType == '125' ) {
            dateType = 'daterange'
            if (!Array.isArray(value.value)) {
              value.value = []
            }
          } else {
            if (Array.isArray(value.value)) {
              value.value = ''
            }
          }
    
          return {
            dateType, // 控件类型
            value // 控件值
          }
        }
      })
    
    折叠

    可以增设属性,然后根据需求设置默认值,这样方便统一风格。

    选择类

    • template
      <el-select
        v-model="value"
        v-bind="$attrs"
        :id="'c' + formItemMeta.columnId"
        :name="'c' + formItemMeta.columnId"
        :clearable="clearable"
        :multiple="multiple"
        :collapse-tags="collapseTags"
        :collapse-tags-tooltip="collapseTagsTooltip"
      >
        <el-option
          v-for="item in optionList"
          :key="'select' + item.value"
          :label="item.label"
          :value="item.value"
          :disabled="item.disabled"
        >
        el-option>
      el-select>
    
    • ts
      import { defineComponent, computed } from 'vue'
      // 引入组件需要的属性 引入表单子控件的管理类
      import { itemProps, itemController } from '@naturefw/ui-elp'
    
      export default defineComponent({
        name: 'nf-el-from-select',
        inheritAttrs: false,
        props: {
          ...itemProps, // 基础属性
          'collapse-tags': {
            type: Boolean,
            default: true
          },
          'collapse-tags-tooltip': {
            type: Boolean,
            default: true
          },
          modelValue: [String, Number, Array]
        },
        emits: ['update:modelValue'],
        setup (props, context) {
          const multiple = computed (() => props.formItemMeta.controlType === 161)
      
          return {
            ...itemController(props, context.emit)
          }
        }
      })
    
    折叠

    template 里面增加了 el-option 部分,通过对 optionList 的遍历,实现了选项的渲染。

    其他组件也是一样的方式进行封装,就不一一介绍了。

    封装 el-form-item

    el-table 通过 el-form-item 来加载子组件,所以我们也可以封装一下:

      <el-row :gutter="15">
        <el-col
          v-for="(ctrId, index) in colOrder"
          :key="'form_' + ctrId + '_' + index"
          :span="formColSpan[ctrId]"
          v-show="showCol[ctrId]"
        >
          
          <transition name="el-zoom-in-top">
            <el-form-item
              :label="itemMeta[ctrId].formItemMeta.label"
              :prop="itemMeta[ctrId].formItemMeta.colName"
              :rules="ruleMeta[ctrId] ?? []"
              :label-width="itemMeta[ctrId].formItemMeta.labelWidth??''"
              :size="size"
              v-show="showCol[ctrId]"
            >
              <component
                :is="formItemKey[itemMeta[ctrId].formItemMeta.controlType]"
                :model="model"
                v-bind="itemMeta[ctrId]"
              >
              component>
            el-form-item>
          transition>
        el-col>
      el-row>
    
    折叠
    • el-row、el-col:实现多列
    • transition:组件联动的时候的动画效果
    • component:动态加载子控件
    • formItemKey 子控件的字典,key-value形式,key就是控件编号,value是组件。这样就可以根据控件的编号加载对应的子控件了。

    使用 slot 和 字典 实现扩展自定义子控件。

    这里要感谢强大的 vue3,提供了插槽这种很灵活的扩展方式。以及组件的形成管理代码。

    说到扩展,想必大家想到的是插槽,我们也支持使用插槽的扩展方式,不过我觉得,既然定义了接口,那么不用的话,是不是有点浪费。

    我们可以定义组件实现接口,然后并入字典(formItemKey),这样表单控件就可以从字典里面加载我们自己定义的组件了,更便于管理和扩展。

    源码和演示

    core:https://gitee.com/naturefw-code/nf-rollup-ui-controller

    二次封装: https://gitee.com/naturefw-code/nf-rollup-ui-element-plus

    演示: https://naturefw-code.gitee.io/nf-rollup-ui-element-plus/

  • 相关阅读:
    【C++编程语言】之string容器 基本概念 构造函数 赋值 拼接 查找 替换比较 插入 删除 子串获取
    6、Linux:一起玩转vi/vim编辑命令
    【2022新生学习】第三周要点
    一文带你深入理解【Java基础】· 注解
    【强化学习】07——规划与学习(Dyna-Q)
    企业经营异常查询API:保障供应链的可靠性与稳定性
    测试进阶知识之零日攻击的发现和防御
    [DOM]获取元素:根据ID、标签名、HTML5新增的方法、特殊元素获取
    React项目实战之租房app项目(五)顶部导航组件封装&CSS Modules解决样式覆盖&地图找房模块封装
    【Azure 媒体服务】Media Service的编码示例 -- 创建缩略图子画面的.NET代码调试问题
  • 原文地址:https://www.cnblogs.com/jyk/p/16485920.html