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

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

内容摘要

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

需求分析

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

表单控件的基础需求:

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

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

设计 interface

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

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

整理一下做个脑图:

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

表单控件的接口

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

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

然后我们定义具体的 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>   } } 
  • 根据选项,显示对应的组件

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

定义表单控件的 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 即可。篇幅有限,具体代码不介绍了,感兴趣的话可以看源码。

  • 单列表单

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

  • 双列表单

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

  • 三列表单

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

  • 多列表单
    因为 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)

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

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

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

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

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

使用 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/

发表评论

相关文章