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


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

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

    内容摘要

    • 需求分析。
    • 定义 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
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 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,
     */
      colOrder: Array,
     /**
     * 表单的列数,分为几列 number,
     */
     columnsNumber: number
     /**
     * 分栏的设置,ISubMeta
     */
     subMeta: ISubMeta,
     /**
     * 验证信息,IRuleMeta
     */
     ruleMeta: IRuleMeta,
     /**
     * 子控件的联动关系,ILinkageMeta
     */
     linkageMeta: ILinkageMeta
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • moduleId 模块编号,以后使用
    • formId 表单编号,一个模块可以有多个表单
    • colOrder 数组形式,表单里包含哪些字段?字段的先后顺序如何确定?就用这个数组。
    • columnsNumber 表单控件的列数,表单只能单列?太单调,支持多列才是王道。

    ISubMeta:分栏的接口

    /**
     * 分栏表单的设置
     */
    export interface ISubMeta {
      type: ESubType, // 分栏类型:card、tab、step、"" (不分栏)
      cols: Array<{ // 栏目信息
        title: string, // 栏目名称
        colIds:  Array // 栏目里有哪些控件ID
     }>
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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>
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    ILinkageMeta:组件联动的接口

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

    /**
     * 显示控件的联动设置
     */
    export interface ILinkageMeta {
      /**
     * 控件的ID作为key,每个控件值对应一个数组,数组里面是需要显示的控件ID。
     */
      [key: string | number]: {
        /**
     * 控件的值作为key,后面的数组里存放需要显示的控件ID
     */
        [id: string | number]: Array
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 根据选项,显示对应的组件

    联动的表单.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,
     required: true
     },
     /**
     * 根据选项过滤后的 model
     */
     partModel: {
     type: Object as PropType,
     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
     }
    }
    折叠 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    在组件里的使用方式

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

      // 表单控件的属性 
      import { formProps, formController } from '../map'
    
      export default defineComponent({
        name: 'nf-el-from-div',
        props: {
          ...formProps
          // 还可以设置其他属性
        },
        setup (props, context) {
          略。。。
        }
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    定义 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
        } 
      }
    }
    折叠 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    温馨提示: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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 通过 props 绑定 el-table 的属性
      props 里面定义的属性,直接绑定即可,比如 :label-suffix="labelSuffix"
    • 通过 $attrs 绑定 el-table 的属性
      props 里面没有定义的属性,会保存在 a t t r s 里 面 , 可 以 通 过 ‘ v − b i n d = " attrs 里面,可以通过 `v-bind=" attrsvbind="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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 分栏的表单(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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    nf-form 就是封装后的表单控件,设置属性和 model 后就可使用了,很方便。
    如果想扩展的话,可以使用 的方式,里面的 【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/

  • 相关阅读:
    vue-element-switch用法
    关于对于Java中Entity以及VO,以及DTO中Request对象序列化的学习
    跨国制造商部署SD-WAN提升全球业务案例分析
    技术管理进阶——把控基建与业务的比例和节奏
    路面坑洼检测中的视觉算法
    pytorch.数据结构Tensor
    spark学习笔记(十二)——sparkStreaming-RDD队列/自定义数据源/kafka数据源/DStream转换/DStream输出/优雅关闭
    Android 架构模式如何选择
    解决vue 跳转相同路由携带不同参数,页面不渲染问题
    MLOps是什么,为什么重要,以及如何实现?
  • 原文地址:https://blog.csdn.net/m0_56069948/article/details/125532434