5

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

 2 years ago
source link: https://www.cnblogs.com/jyk/p/16424726.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【摸鱼神器】UI库秒变低代码工具——表单篇(一)设计 - 金色海洋(jyk) - 博客园代码改变世界

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

2022-06-29 19:43  金色海洋(jyk)  阅读(191)  评论(0)  编辑  收藏  举报

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

  • 需求分析。
  • 定义 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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK