8

【摸鱼神器】UI库秒变低代码工具——表单篇(二)子控件 - 金色海洋(jyk)

 2 years ago
source link: https://www.cnblogs.com/jyk/p/16485920.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

上一篇介绍了表单控件,这一篇介绍一下表单里面的各种子控件的封装方式。

  • 子控件的分类
  • 子控件属性的分类
  • 定义 interface。
  • 定义子控件的的 props。
  • 定义 json 文件。
  • 基于 UI库 进行二次封装,实现依赖 json 渲染。
  • 通过 slot 、 “字典”,实现自定义子控件。
  • 做个工具维护 json 文件。(下篇介绍)

表单里面需要各种各样的子控件,像文本、数字、选择、日期等常见的需求,可以由内部提供组件解决,但是其他各种“奇奇怪怪”的需求怎么办呢?

如果还是由“内部”提供组件的话,那肯定是行不通的,因为以往的经验教训告诉我们,内部不断扩充子控件的结果,必然会导致内部代码越来越臃肿,以至后期无法维护,最终崩盘!

所以必须支持自定义扩展!感谢 Vue 和 UI库,提供基础的技术支持,让扩展变得非常容易。

我们先对表单子控件进行一下分类,然后为其设计一套接口,即定义一套规则,这样才好方便做长期维护。

子控件的分类

我们对常见的组件进行分析,得到了下面的分类:

表单子控件的分类

上图涵盖了一些常用控件,但是很显然并不全面,比如没有金额类的控件,输入金额也是需要一些辅助的,比如金额的大小写的切换等,不过这些应该用扩展的方式实现。

属性的分类

组件的分类可以做的“规整”一些,但是组件的属性的分类,就比较有难度了,我们可以把组件需要的属性分为三个主要部分:代码里需要的、共用的、扩展的。

  • 低代码需要的属性
    需要在代码里面使用的属性,比如字段名称、控件类型、默认值、防抖延迟等,集中在一起,通过 props 的方式传递。

  • 共用属性
    各个组件(或者大部分组件)都需要的属性,比如浮动提示、size、是否显示清空按钮等,作为一级属性,通过 props 的方式传递。

  • 扩展属性
    某个组件需要的属性,比如数字组件需要 max、min、step等。通过 $attrs 的方式传递。

100表单子控件的属性.png

其中扩展属性最为复杂,如果按照面向对象的方式来设计的话,结构就会非常复杂,会复杂到什么程度呢?可以参考当初 asp.net 里面 webform 的继承结构:

十三年前做的一张图

(controll是控件(组件)的意思,下面分出来WebControll和 repeater 两个子类,然后又,,,算了不说了,是不是看着就很累的样子?)

现在是 JS 环境,我们没有必要生搬硬套,而是可以利用JS的灵活性来做简洁设计:

表单子控件的接口

我们给表单子控件的 props 定义一个interface:(虽然暂时用不上)

  • IFormItemProps
/**
 * 表单控件的子控件的 props。
 */
export interface IFormItemProps {
  /**
   * 低代码需要的数据
   */
  formItemMeta: IFormItemMeta,
  /**
   * 子控件备选项,一级或者多级
   */
  optionList: Array<IOptionList | IOptionTree>,
  /**
   * 表单的 model,含义多个属性
   */
  model: any,
  /**
   * 是否显示清空的按钮
   */
  clearable: boolean,
  /**
   * 浮动提示信息
   */
  title: string,
  /**
   * 子控件的扩展属性
   */
  [key: string]: any
}
  • IFormItemMeta 的定义:
/**
 * 子控件的低代码需要的数据
 */
export interface IFormItemMeta {
  /**
   * -- 字段ID、控件ID
   */
  columnId?: number | string,
  /**
   * -- 字段名称
   */
  colName: string,
  /**
   * -- 字段的中文名称,标签
   */
  label?: string,
  /**
   * -- 子控件类型,number,EControlType
   */
  controlType: EControlType | number,
  /**
   * 子控件的默认值
   */
  defValue: any,
  /**
   * -- 一个控件占据的空间份数。
   */
  colCount?: number,
  /**
   * 访问后端API的配置信息,有备选项的控件需要
   */
  webapi?: IWebAPI,
  /**
   * -- 防抖延迟时间,0:不延迟
   */
  delay: number,
  /**
   * 防抖相关的事件
   */
  events?: IEventDebounce,
}

规则定义之后呢,总会发现有特例的属性,比如 select 的 option。代码里面需要使用 option 去绑定组件,应该放在“低代码需要的属性”里面。

但是实际使用的时候发现,放在“共用属性”里面会更方便。

然后在做“维护JSON的小工具”的时候,发现需要放在“扩展属性”里面维护,这样维护代码更容易实现。

综合考虑之后,就出现了一个不符合规则的属性 —— optionList。

定义组件的 props。

按照接口实现一下 props 的定义。

import type { PropType } from 'vue'

import type {
  IOptionList,
  IOptionTree,
  IFormItemProps
} from '../types/20-form-item'

/**
 * 基础控件的共用属性,即表单子控件的基础属性
 */
export const itemProps = {
  formItemMeta: {
    type: Object as PropType<IFormItemProps>,
    default: () =>  {return {}}
  },
  /**
   * optionList:IOptionList | IOptionTree,控件的备选项,单选、多选、等控件需要
   */
  optionList: {
    type: Object as PropType<Array<IOptionList | IOptionTree>>,
    default: () =>  {return []}
  },
  /**
   * 表单的 model,整体传入,便于子控件维护字段值。
   */
  model: {
    type: Object
  },
  /**
   * 是否显示可清空的按钮,默认显示
   */
  clearable: {
    type: Boolean,
    default: true
  },
  /**
   * 浮动的提示信息,部分控件支持
   */
  title: {
    type: String,
    default: ''
  }
}

其他属性以及扩展属性,可以通过 $attrs 传递和绑定,这样可以方便各种扩展。

定义 json 文件。

我们来定义一个示例用的 json文件。

{
    "formItemMeta": {
      "columnId": 90,
      "colName": "kind",
      "label": "分类",
      "controlType": 107,
      "isClear": false,
      "defValue": 0,
      "colCount": 7
    },
    "placeholder": "分类",
    "title": "编号",
    "optionList": [
      {"value": 1, "label": "文本类"},
      {"value": 2, "label": "数字类"},
      {"value": 3, "label": "日期类"},
      {"value": 4, "label": "时间类"},
      {"value": 5, "label": "选择类"},
      {"value": 6, "label": "下拉类"}
    ]
}

基于 UI库 封装,实现依赖 json 渲染。

首先要感谢强大的UI库,实现了大部分的功能,我们只需要再稍微封装一下即可,只有少数几个组件需要我们补充点代码。

  • template
  <el-input
    v-model="value"
    v-bind="$attrs"
    :id="'c' + formItemMeta.columnId"
    :name="'c' + formItemMeta.columnId"
    :title="title"
    :clearable="clearable"
    @blur="run"
    @change="run"
    @clear="run"
    @keydown="clear"
  >
  </el-input>

使用 v-bind="$attrs" 绑定扩展属性

  import { defineComponent } from 'vue'
  import { ElInput } from 'element-plus'
  // 引入组件需要的属性、控制类
  import { itemProps, itemController } from '@naturefw/ui-elp'

  export default defineComponent({
    name: 'nf-el-form-item-text',
    inheritAttrs: false,
    components: {
      ElInput
    },
    props: {
      modelValue: [String, Number],
      ...itemProps // 基础属性
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const {
        value,
        run,
        clear
      } = itemController(props, context.emit)

      return {
        value,
        run,
        clear
      }
    }
  })

使用 ...itemProps 定义属性。

是不是很简单。

可能你会问了,这不是封装了个寂寞吗,你看看里面空荡荡的,完全没有封装的必要嘛。

确实,对于文本这类简单的组件,确实没有封装的必要,直接使用UI库提供的组件即可。

那么为啥好要封装一下呢?

首先为了统一风格,不管是简单的,还是复杂的,都按照统一方式封装一下,这样便于维护和扩展。

  • template
  <el-date-picker
    ref="domDate"
    v-model="value"
    v-bind="$attrs"
    :type="dateType"
    :name="'c' + formItemMeta.columnId"
    :format="format"
    :value-format="valueFormat"
    :title="title"
    :clearable="clearable"
  >
  </el-date-picker>
  import { defineComponent } from 'vue'
  // 引入组件需要的属性 引入表单子控件的管理类
  import { itemProps, itemController } from '@naturefw/ui-elp'
  
  /**
   * 日期
   */
  export default defineComponent({
    name: 'nf-el-from-item-date',
    inheritAttrs: false,
    props: {
      ...itemProps, // 基础属性
      format: {
        type: String,
        default: 'YYYY-MM-DD'
      },
      'value-format': {
        type: String,
        default: 'YYYY-MM-DD'
      },
      modelValue: [String, Date, Number, Array]
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const { value } = itemController(props, context.emit)

      // 根据类型判断是否为数组,判断是否 使用范围。
      let dateType = 'date'
      if (props.formItemMeta.controlType == '125' ) {
        dateType = 'daterange'
        if (!Array.isArray(value.value)) {
          value.value = []
        }
      } else {
        if (Array.isArray(value.value)) {
          value.value = ''
        }
      }

      return {
        dateType, // 控件类型
        value // 控件值
      }
    }
  })

可以增设属性,然后根据需求设置默认值,这样方便统一风格。

  • template
  <el-select
    v-model="value"
    v-bind="$attrs"
    :id="'c' + formItemMeta.columnId"
    :name="'c' + formItemMeta.columnId"
    :clearable="clearable"
    :multiple="multiple"
    :collapse-tags="collapseTags"
    :collapse-tags-tooltip="collapseTagsTooltip"
  >
    <el-option
      v-for="item in optionList"
      :key="'select' + item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
  import { defineComponent, computed } from 'vue'
  // 引入组件需要的属性 引入表单子控件的管理类
  import { itemProps, itemController } from '@naturefw/ui-elp'

  export default defineComponent({
    name: 'nf-el-from-select',
    inheritAttrs: false,
    props: {
      ...itemProps, // 基础属性
      'collapse-tags': {
        type: Boolean,
        default: true
      },
      'collapse-tags-tooltip': {
        type: Boolean,
        default: true
      },
      modelValue: [String, Number, Array]
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const multiple = computed (() => props.formItemMeta.controlType === 161)
  
      return {
        ...itemController(props, context.emit)
      }
    }
  })

template 里面增加了 el-option 部分,通过对 optionList 的遍历,实现了选项的渲染。

其他组件也是一样的方式进行封装,就不一一介绍了。

封装 el-form-item

el-table 通过 el-form-item 来加载子组件,所以我们也可以封装一下:

  <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].formItemMeta.label"
          :prop="itemMeta[ctrId].formItemMeta.colName"
          :rules="ruleMeta[ctrId] ?? []"
          :label-width="itemMeta[ctrId].formItemMeta.labelWidth??''"
          :size="size"
          v-show="showCol[ctrId]"
        >
          <component
            :is="formItemKey[itemMeta[ctrId].formItemMeta.controlType]"
            :model="model"
            v-bind="itemMeta[ctrId]"
          >
          </component>
        </el-form-item>
      </transition>
    </el-col>
  </el-row>
  • el-row、el-col:实现多列
  • transition:组件联动的时候的动画效果
  • component:动态加载子控件
  • formItemKey 子控件的字典,key-value形式,key就是控件编号,value是组件。这样就可以根据控件的编号加载对应的子控件了。

使用 slot 和 字典 实现扩展自定义子控件。

这里要感谢强大的 vue3,提供了插槽这种很灵活的扩展方式。以及组件的形成管理代码。

说到扩展,想必大家想到的是插槽,我们也支持使用插槽的扩展方式,不过我觉得,既然定义了接口,那么不用的话,是不是有点浪费。

我们可以定义组件实现接口,然后并入字典(formItemKey),这样表单控件就可以从字典里面加载我们自己定义的组件了,更便于管理和扩展。

源码和演示

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