• 【摸鱼神器】UI库秒变低代码工具——表单篇(一)设计


    前面说了列表的低代码化的方法,本篇介绍一下表单的低代码化。

    内容摘要

    • 需求分析。
    • 定义 interface。
    • 定义表单控件的 props。
    • 定义 json 文件。
    • 基于 el-form 封装,实现依赖 json 渲染。
    • 实现多列、验证、分栏等功能。
    • 使用 slot 实现自定义扩展。
    • 自定义子控件。(下篇介绍)
    • 表单子控件的设计与实现。(下篇介绍)
    • 做个工具维护 json 文件。(下下篇介绍)

    需求分析

    表单是很常见的需求,各种网页、平台、后台管理等,都需要表单,有简单的、也有复杂的,但是目的一致:收集用户的数据,然后提交给后端。

    表单控件的基础需求:

    • 可以依赖 JSON 渲染。
    • 依赖 JSON 创建 model。
    • 便于用户输入数据。
    • 验证用户输入的数据。
    • 便于程序员实现功能。
    • 可以多列。
    • 可以分栏。
    • 可以自定义扩展。
    • 其他。

    el-form 实现了数据验证、自定义扩展等功能(还有漂亮的UI),我们可以直接拿过来封装,然后再补充点代码,实现多列、分栏、依赖 JSON 渲染等功能。

    设计 interface

    首先把表单控件需要的属性分为两大类:el-form 的属性、低代码需要的数据。

    表单控件需要的属性的分类

    整理一下做个脑图:

    表单控件需要的属性.png

    表单控件的接口

    我们转换为接口的形式,再做个脑图:

    表单控件的接口.png

    然后我们定义具体的 interface

    IFromProps:表单控件的接口 (包含所有属性,对应 json 文件)

    /**
     * 表单控件的属性
     */
    export interface IFromProps {
      /**
       * 表单的 model,对象,包含多个字段。
       */
      model: any,
      /**
       * 根据选项过滤后的 model,any
       */
      partModel?: any,
      /**
       * 表单控件需要的 meta
       */
      formMeta: IFromMeta,
      /**
       * 表单子控件的属性,IFormItem
       */
      itemMeta: IFormItemList,
      /**
       * 标签的后缀,string
       */
      labelSuffix: string,
      /**
      * 标签的宽度,string
      */
      labelWidth: string,
      /**
      * 控件的规格,ESize
      */
      size: ESize,
      /**
      * 其他扩展属性
      */
      [propName: string]: any
    
    }
    
    折叠
    • model:表单数据,可以依据 JSON 创建。
    • partModel:组件联动后,只保留可见组件对应的数据。
    • formMeta:低代码需要的属性集合。
    • itemMeta:表单子控件需要的属性集合。
    • 其他:el-table 组件需要的属性,可以使用 $attrs 进行扩展。

    本来想用这个接口约束组件的 props,但是有点小问题:

    • 如果用 Option API 的话,不支持这种形式的接口。
    • 如果使用 Composition API 的话,虽然支持,但是只能在组件内部定义 interface,暂时不支持从外部文件引入。

    接口文件应该可以在外部定义,然后引入组件。如果不能的话,那就尴尬了。

    所以只好暂时放弃对组件的 props 进行整体约束。

    IFromMeta:低代码需要的属性接口

    /**
     * 低代码的表单需要的 meta
     */
    export interface IFromMeta {
      /**
       * 模块编号,综合使用的时候需要
       */
      moduleId: number | string,
      /**
       * 表单编号,一个模块可以有多个表单
       */
      formId: number | string,
      /**
       * 表单字段的排序、显示依据,Array<number | string>,
       */
      colOrder: Array<number | string>,
      /**
       * 表单的列数,分为几列 number,
       */
      columnsNumber: number
       /**
       * 分栏的设置,ISubMeta
       */
      subMeta: ISubMeta,
      /**
       * 验证信息,IRuleMeta
       */
      ruleMeta: IRuleMeta,
      /**
       * 子控件的联动关系,ILinkageMeta
       */
      linkageMeta: ILinkageMeta
    }
    
    折叠
    • moduleId 模块编号,以后使用
    • formId 表单编号,一个模块可以有多个表单
    • colOrder 数组形式,表单里包含哪些字段?字段的先后顺序如何确定?就用这个数组。
    • columnsNumber 表单控件的列数,表单只能单列?太单调,支持多列才是王道。

    ISubMeta:分栏的接口

    /**
     * 分栏表单的设置
     */
    export interface ISubMeta {
      type: ESubType, // 分栏类型:card、tab、step、"" (不分栏)
      cols: Array<{ // 栏目信息
        title: string, // 栏目名称
        colIds:  Array<number> // 栏目里有哪些控件ID
      }>
    }
    

    UI库提供了 el-card、el-tab、el-step等组件,我们可以使用这几个组件来实现多种分栏的形式。

    IRule、IRuleMeta、:数据验证的接口

    el-form 采用 async-validator 实现数据验证,所以我们可以去官网(https://github.com/yiminghe/async-validator)看看可以有哪些属性,针对这些属性指定一个接口(IRule),然后定义一个【字段编号-验证数组】的接口(IRuleMeta)

    
    /**
     * 一条验证规则,一个控件可以有多条验证规则
     */
    export interface IRule {
      /**
       * 验证时机:blur、change、click、keyup
       */
      trigger?:  "blur" | "change" | "click" | "keyup",
      /**
       * 提示消息
       */
      message?: string,
      /**
       * 必填
       */
      required?: boolean,
      /**
       * 数据类型:any、date、url等
       */
      type?: string,
      /**
       * 长度
       */
      len?: number, // 长度
      /**
       * 最大值
       */
      max?: number,
      /**
       * 最小值
       */
      min?: number,
      /**
       * 正则
       */
      pattern?: string
    }
    
    /**
     * 表单的验证规则集合
     */
    export interface IRuleMeta {
      /**
       * 控件的ID作为key, 一个控件,可以有多条验证规则
       */
      [key: string | number]: Array<IRule>
    }
    
    折叠

    ILinkageMeta:组件联动的接口

    有时候需要根据用户的选择显示对应的一组组件,那么如何实现呢?其实也比较简单,还是做一个key-value ,字段值作为key,需要显示的字段ID集合作为value。这样就可以了。

    /**
     * 显示控件的联动设置
     */
    export interface ILinkageMeta {
      /**
       * 控件的ID作为key,每个控件值对应一个数组,数组里面是需要显示的控件ID。
       */
      [key: string | number]: {
        /**
         * 控件的值作为key,后面的数组里存放需要显示的控件ID
         */
        [id: string | number]: Array<number>
      }
    }
    
    • 根据选项,显示对应的组件

    联动的表单.png

    定义表单控件的 props。

    interface 都定义好了,我们来定义组件的 props(实现接口)。

    这里采用 Option API 的方式,因为可以从外部文件引入接口,也就是说,可以实现复用。

    import type { PropType } from 'vue'
    
    import type {
      IFromMeta // 表单控件需要的 meta
    } from '../types/30-form'
    
    import type { IFormItem, IFormItemList } from '../types/20-form-item'
    
    import type { ESize } from '../types/enum'
    import { ESize as size } from '../types/enum'
      
    /**
     * 表单控件需要的属性
     */
    export const formProps = {
      /**
       * 表单的完整的 model
       */
      model: {
        type: Object as PropType<any>,
        required: true
      },
      /**
       * 根据选项过滤后的 model
       */
      partModel: {
        type: Object as PropType<any>,
        default: () => { return {}}
      },
      /**
       * 表单控件的 meta
       */
      formMeta: {
        type: Object as PropType<IFromMeta>,
        default: () => { return {}}
      },
      /**
       * 表单控件的子控件的 meta 集合
       */
      itemMeta: {
        type: Object as PropType<IFormItemList>,
        default: () => { return {}}
      },
      /**
       * 标签的后缀
       */
      labelSuffix: {
        type: String,
        default: ':' 
      },
      /**
       * 标签的宽度
       */
      labelWidth: {
        type: String,
        default: '130px'
      },
      /**
       * 控件的规格
       */
      size: {
        type: Object as PropType<ESize>,
        default: size.small
      }
    }
    
    折叠

    在组件里的使用方式

    那么如何使用呢?很简单,用 import 导入,然后解构即可。

      // 表单控件的属性 
      import { formProps, formController } from '../map'
    
      export default defineComponent({
        name: 'nf-el-from-div',
        props: {
          ...formProps
          // 还可以设置其他属性
        },
        setup (props, context) {
          略。。。
        }
    })
    

    这样组件里的代码看起来也会很简洁。

    定义 json 文件

    我们做一个简单的 json 文件:

    {
      "formMeta": {
        "moduleId": 142,
        "formId": 14210,
        "columnsNumber": 2,
        "colOrder": [
          90,  101, 100,
          110, 111 
        ],
        "linkageMeta": {
          "90": {
            "1": [90, 101, 100],
            "2": [90, 110, 111] 
          }
        },
        "ruleMeta": {
          "101": [
            { "trigger": "blur", "message": "请输入活动名称", "required": true },
            { "trigger": "blur", "message": "长度在 3 到 5 个字符", "min": 3, "max": 5 }
          ]
        }
      },
      "itemMeta": {
        "90": {  
          "columnId": 90,
          "colName": "kind",
          "label": "分类",
          "controlType": 153,
          "isClear": false,
          "defValue": 0,
          "extend": {
            "placeholder": "分类",
            "title": "编号"
          },
          "optionList": [
            {"value": 1, "label": "文本类"},
            {"value": 2, "label": "数字类"}
          ],
          "colCount": 2
        },
        "100": {  
          "columnId": 100,
          "colName": "area",
          "label": "多行文本",
          "controlType": 100,
          "isClear": false,
          "defValue": 1000,
          "extend": {
            "placeholder": "多行文本",
            "title": "多行文本"
          },
          "colCount": 1
        },
        "101": {  
          "columnId": 101,
          "colName": "text",
          "label": "文本",
          "controlType": 101,
          "isClear": false,
          "defValue": "",
          "extend": {
            "placeholder": "文本",
            "title": "文本"
          },
          "colCount": 1
        },
        
        "110": {  
          "columnId": 110,
          "colName": "number1",
          "label": "数字",
          "controlType": 110,
          "isClear": false,
          "defValue": "",
          "extend": {
            "placeholder": "数字",
            "title": "数字"
          },
          "colCount": 1
        },
        "111": {  
          "columnId": 111,
          "colName": "number2",
          "label": "滑块",
          "controlType": 111,
          "isClear": false,
          "defValue": "",
          "extend": {
            "placeholder": "滑块",
            "title": "滑块"
          },
          "colCount": 1
        } 
      }
    }
    
    折叠

    温馨提示:JSON 文件不需要手撸哦。

    基于 el-form 封装,实现依赖 json 渲染。

    准备工作完毕,我们来二次封装 el-table 组件。

      <el-form
        :model="model"
        ref="formControl"
        :inline="false"
        class="demo-form-inline"
        :label-suffix="labelSuffix"
        :label-width="labelWidth"
        :size="size"
        v-bind="$attrs"
      >
        <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].label"
                :prop="itemMeta[ctrId].colName"
                :rules="ruleMeta[ctrId] ?? []"
                :label-width="itemMeta[ctrId].labelWidth??''"
                :size="size"
                v-show="showCol[ctrId]"
              >
                <component
                  :is="formItemKey[itemMeta[ctrId].controlType]"
                  :model="model"
                  v-bind="itemMeta[ctrId]"
                >
                </component>
              </el-form-item>
            </transition>
          </el-col>
        </el-row>
      </el-form>
    
    折叠
    • 通过 props 绑定 el-table 的属性
      props 里面定义的属性,直接绑定即可,比如 :label-suffix="labelSuffix"

    • 通过 $attrs 绑定 el-table 的属性
      props 里面没有定义的属性,会保存在 $attrs 里面,可以通过 v-bind="$attrs"的方式绑定,既方便又支持扩展。

    • 使用动态组件(component)加载表单子组件。

    • 实现数据验证,设置 rules 属性即可,:rules="ruleMeta[ctrId] ?? []"

    实现多列

    使用 el-row、el-col 实现多列的效果。

    el-col 分为了24个格子,通过一个字段占用多少个格子的方式实现多列,也就是说,最多支持 24列。当然肯定用不了这么多。

    所以,我们通过各种参数计算好 span 即可。篇幅有限,具体代码不介绍了,感兴趣的话可以看源码。

    • 单列表单

    单列的表单.png

    • 双列表单

    双列的表单.png

    • 三列表单

    三列的表单.png

    • 多列表单
      因为 el-col 的 span 最大是 24,所以最多支持24列。

    • 支持调整布局
      三列表单里面 URL组件就占用了一整行,这类的调整都是很方便实现的。

    分栏

    这里分为多个表单控件,以便于实现多种分栏方式,并不是在一个组件内部通过 v-if 来做各种判断,这也是我需要把 interface 写在单独文件里的原因。

      <el-form
        :model="model"
        ref="formControl"
        :inline="false"
        class="demo-form-inline"
        :label-suffix="labelSuffix"
        :label-width="labelWidth"
        :size="size"
        v-bind="$attrs"
      >
        <el-tabs
          v-model="tabIndex"
          type="border-card"
        >
          <el-tab-pane
            v-for="(item, index) in cardOrder"
            :key="'tabs_' + index"
            :label="item.title"
            :name="item.title"
          >
            <el-row :gutter="15">
              <el-col
                v-for="(ctrId, index) in item.colIds"
                :key="'form_' + ctrId + index"
                :span="formColSpan[ctrId]"
                v-show="showCol[ctrId]"
              >
                <transition name="el-zoom-in-top">
                  <el-form-item
                    :label="itemMeta[ctrId].label"
                    :prop="itemMeta[ctrId].colName"
                    v-show="showCol[ctrId]"
                  >
                    <component
                      :is="formItemKey[itemMeta[ctrId].controlType]"
                      :model="model"
                      v-bind="itemMeta[ctrId]"
                    >
                    </component>
                  </el-form-item>
                </transition>
              </el-col>
            </el-row>
          </el-tab-pane>
        </el-tabs>
      </el-form>
    
    折叠
    • 分栏的表单(el-card)

    card的表单.png

    • 分标签的表单(el-tabs)

    tab的表单.png

    • 分步骤的表单(el-steps)

    step的表单.png

    使用 slot 实现自定义扩展。

    虽然表单控件可以预设一些表单子控件,比如文本、数字、日期、选择等,但是客户的需求是千变万化的,固定的子控件肯定无法满足客户所有的需求,所以必须支持自定义扩展。

    比较简单的扩展就是使用 slot 插槽,el-table 里面的 el-form-item 其实就是以 slot 的形式加入到 el-table 内部的。

    所以我们也可以通过 slot 实现自定义的扩展:

         <nf-form
            v-form-drag="formMeta"
            :model="model"
            :partModel="model2"
            v-bind="formMeta"
          >
            <template v-slot:text>
              <h1>外部插槽 </h1>
              <input v-model="model.text"/>
            </template>
          </nf-form>
    

    nf-form 就是封装后的表单控件,设置属性和 model 后就可使用了,很方便。
    如果想扩展的话,可以使用 <template v-slot:text> 的方式,里面的 【text】 是字段名称(model 的属性)。

    也就是说,我们是依据字段名称来区分 slot 的。

    实现 interface 扩展子组件

    虽然使用 slot 可以扩展子组件,但是对于子组件的结构复杂的情况,每次都使用 slot 的话,明显不方便复用。

    既然都定义 interface 了,那么为何不实现接口制作组件,然后变成新的表单子组件呢?

    当然可以了,具体方法下次再介绍。

    关于 TypeScript

    • 为什么要定义 interface ?
      定义 interface 可以比较清晰的表明结构和意图,然后实现接口即可。避免过段时间自己都忘记含义。

    • JSON 文件导入后会自动解析为 js 的对象,那么还用 interface 做什么?
      这就比较尴尬了,也是我一直没有采用 TS 的原因之一。
      TS只能在编写代码、打包时做检查,但是在运行时就帮不上忙了,所以对于低代码的帮助有限。

    源码和演示

    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/

  • 相关阅读:
    ELK ----elasticsearch笔记增删改查等
    docker 安装 elasticsearch
    医院管理系统数据库,课程设计,SQLserver,纯代码设计
    电压参考芯片 LM285D-1.2 备忘
    `maven.test.skip` 和 `skipTests` 的区别
    DataBinding原理----双向绑定(4)
    算法通关村16关 | 滑动窗口最长字串专题
    Ubuntu上安装MySQL
    如何让iOS设备上App定时执行后台任务(上)
    【P53】基于TPS61093的典型单电源转双电源电路
  • 原文地址:https://www.cnblogs.com/jyk/p/16424726.html