Vue技术每天学一点Vue3

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

2022-06-29  本文已影响0人  自然框架

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

内容摘要

需求分析

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

表单控件的基础需求:

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

}

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

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

所以只好暂时放弃对组件的 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
}

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>

实现多列

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

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

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

单列的表单.png 双列的表单.png 三列的表单.png

分栏

这里分为多个表单控件,以便于实现多种分栏方式,并不是在一个组件内部通过 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>
card的表单.png tab的表单.png 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

源码和演示

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/

上一篇 下一篇

猜你喜欢

热点阅读