「摸鱼神器」ui库秒变lowcode工具——列表篇(一)设计与实现(idea摸鱼神器)-凯发体育官方app

  • 需求分析
  • 定义 interface
  • 定义 json 文件
  • 定义列表控件的 props
  • 基于 el-table 封装,实现依赖 json 渲染
  • 实现内置功能:选择行(单选、多选),格式化、锁定等。
  • 使用 slot 实现自定义扩展
  • 做个工具维护 json 文件(下篇介绍)

管理后台里面,列表是一个常用的功能,ui库提供了列表组件和分页组件实现功能。虽然功能强大,也很灵活,只是还不能称为低代码,不过没关系,我们可以写点代码让ui库变为摸鱼神器!

本篇介绍列表的设计思路和封装方式。

如果基于原生html来实现显示数据列表的功能的话,那么需考虑如何创建 table,如何设置css等。
如果直接使用ui库的话,那么可以简单很多,只需要设置各种属性,然后绑定数据即可。
以 el-table 为例:

设置好属性、记录集合,然后设置列(el-table-column)即可。
这样一个列表就搞定了,再加上 el-pagination 分页组件,编写一些代码即可实现分页的功能。

如果只是一个列表的话,这种方式没啥问题,但是管理后台项目,往往需要n个列表,而每个列表都大同小异,如果要一个一个手撸出来,那就有点麻烦了。

那么如何解决呢?我们可以参考低代码,基于 el-talbe 封装一个列表控件,
实现依赖 json 动态渲染列表,同时支持自定义扩展。

最近开始学习 typescript,发现了一个现象,如果可以先定义好类型,那么代码就可以更清晰的展现出来。

另外 vue3 的最新文档,也采用了通过 interface 来介绍api功能的方式,所以我们也可以借鉴一下。

vue3 的 props 有一套约束方式,这个似乎和ts的方式有点冲突,没想出了更好的方法(option api 和 script setup两种定义props的方式,都有不足 ),所以只好做两个 interface,一个用于定义组件的 props ,一个用于取值。

  • igridpropscomp:定义组件的 props

/** * 列表控件的属性的描述,基于el-table */export interface igridpropscomp { /** * 模块id,number | string */ moduleid: ipropsvalidation, /** * 主键字段的名称 string,对应 row-key */ idname: ipropsvalidation, /** * table的高度, number */ height: ipropsvalidation, /** * 列(字段)显示的顺序 array */ colorder: ipropsvalidation, /** * 斑马纹,boolean */ stripe: ipropsvalidation, /** * 纵向边框,boolean */ border: ipropsvalidation, /** * 列的宽度是否自撑开,boolean */ fit: ipropsvalidation, /** * 要高亮当前行,boolean */ highlightcurrentrow: ipropsvalidation, /** * 锁定的列数 number,设置到 el-table-column 的 fixed */ fixedindex: ipropsvalidation, /** * table的列的 igriditem * * id: number | string, * * colname: string, 字段名称 * * label: string, 列的标签、标题 * * width: number, 列的宽度 * * align: ealign, 内容对齐方式 * * headeralign: ealign 列标题对齐方式 */ itemmeta: ipropsvalidation, // /** * 记录选择的行:igridselection * * dataid: '', 单选id number 、string * * row: {}, 单选的数据对象 {} * * dataids: [], 多选id [] * * rows: [] 多选的数据对象 [] */ selection: ipropsvalidation, /** * 绑定的数据 array, 对应 data */ datalist: ipropsvalidation // 其他扩展属性 [propname: string]: ipropsvalidation}

  • moduleid:模块id,一个模块菜单只能有一个列表,菜单可以嵌套。
  • itemmeta:列的属性集合,记录列表的列的属性。
  • selection:记录列表的单选、多选的 row。
  • datalist:显示的数据,对应 el-table 的 data
  • 其他:对应 el-table 的属性

igridpropscomp 的作用是,约束列表控件需要设置哪些属性,属性的具体类型,就无法在这里约束了。

  • ipropsvalidation (不知道vue内部有没有这样的 interface)

/** * vue 的 props 的验证的类型约束 */export interface ipropsvalidation { /** * 属性的类型,比较灵活,可以是 string、number 等,也可以是数组、class等 */ type: array | any, /** * 是否必须传递属性 */ required?: boolean, /** * 自定义类型校验函数(箭头函数),value:属性值 */ validator?: (value: any) => boolean, /** * 默认值,可以是值,也可以是函数(箭头函数) */ default?: any}

igridpropscomp 无法约束属性的具体类型,所以只好再做一个 interface。

  • igridprops

/** * 列表控件的属性的类型,基于el-table */ export interface igridprops { /** * 模块id,number | string */ moduleid: number | string, /** * 主键字段的名称 string,对应 row-key */ idname: string, /** * table的高度, number */ height: number, /** * 列(字段)显示的顺序 array */ colorder: array, /** * 斑马纹,boolean */ stripe: boolean, /** * 纵向边框,boolean */ border: boolean, /** * 列的宽度是否自撑开,boolean */ fit: boolean, /** * 要高亮当前行,boolean */ highlightcurrentrow: boolean, /** * 锁定的列数 number,设置到 el-table-column 的 fixed */ fixedindex: number, /** * table的列的 object< igriditem > * * id: number | string, * * colname: string, 字段名称 * * label: string, 列的标签、标题 * * width: number, 列的宽度 * * align: ealign, 内容对齐方式 * * headeralign: ealign 列标题对齐方式 */ itemmeta: { [key:string | number]: igriditem }, // /** * 选择行的情况:igridselection * * dataid: '', 单选id number 、string * * row: {}, 单选的数据对象 {} * * dataids: [], 多选id [] * * rows: [] 多选的数据对象 [] */ selection: igridselection, /** * 绑定的数据 array, 对应 data */ datalist: array // 其他扩展属性 [propname: string]: any}

对比一下就会发现,属性的类型不一样。因为定义 props 需要使用一套特定的对象格式,而使用 props 的时候需要的是属性自己的类型。

理想情况下,应该可以在 script setup 里面,引入外部文件 定义的 interface ,然后设置给组件的 props,但是到目前为止还不支持,只能在( script setup方式的)组件内部定义 props。希望早日支持,支持了就不会这么纠结和痛苦了。

  • igriditem:列表里面列的属性

/** * 列的属性,基于 el-table-column */export interface igriditem { /** * 字段id、列id */ id: number | string, /** * 字段名称 */ colname: string, /** * 列的标签、标题 */ label: string, /** * 列的宽度 */ width: number, /** * 内容对齐方式 ealign */ align: ealign, /** * 列标题对齐方式 */ headeralign: ealign, // 其他扩展属性 [propname: string]: any}

还是需要扩展属性的,因为这里只是列出来目前需要的属性,el-table-column 的其他属性、方法还有很多,而且以后也可能会新增。

这个属性不是直接设置给组件的 props,所以不用定义两套了。

枚举可以理解为常量,定义之后可以避免低级错误,避免手滑。

  • ealign

export const enum ealign { left = 'left', center = 'center', right = 'right'}

列表可以单选也可以多选,el-table 在默认情况下似乎是二选一,觉得有点不方便,为啥不能都要?

  • 单选:鼠标单一任意一行就是单选;(清空其他已选项)
  • 多选:单击第一列的(多个)复选框,就是多选;

这样用户就可以愉快的想单选就单选,想多选就多选了。

  • igridselection

/** * 列表里选择的数据 */export interface igridselection { /** * 单选id number 、string */ dataid: number | string, /** * 单选的数据对象 {} */ row: any, /** * 多选id [] */ dataids: array, /** * 多选的数据对象 [] */ rows: array}

其实我觉得只记录id即可,不过既然 el-talble 提供的 row,那么还是都记录下来吧。

接口定义好之后,我们可以依据 interface 编写 json 文件:

{ "moduleid": 142, "height": 400, "idname": "id", "colorder": [ 90, 100, 101 ], "stripe": true, "border": true, "fit": true, "highlightcurrentrow": true, "highlight-current-row": true, "itemmeta": { "90": { "id": 90, "colname": "kind", "label": "分类", "width": 140, "title": "分类", "align": "center", "header-align": "center" }, "100": { "id": 100, "colname": "area", "label": "多行文本", "width": 140, "title": "多行文本", "align": "center", "header-align": "center" }, "101": { "id": 101, "colname": "text", "label": "文本", "width": 140, "title": "文本", "align": "center", "header-align": "center" } }}

  • 为什么直接设置 json 文件而不是 js 对象呢?
    因为对象会比较长,如果是代码形式的话,那还不如直接使用ui库组件来的方便呢。
  • 你可能又会问了,既然直接用 json文件,为啥还要设计 interface 呢?
    当然是为了明确各种类型,interface 可以当做文档使用,另外封装ui库的组件的时候,也可以用到这些 interface。使用列表控件的时候也可以使用这些 interface。

其实json文件不用手动编写,而是通过工具来编写和维护。

封装组件之前需要先定义一下组件需要的 props:

  • props-grid.ts

import type { proptype } from 'vue'import type { igridprops, igriditem, igridselection} from '../types/50-grid'/** * 表单控件需要的属性propsform */export const gridprops: igridprops = { /** * 模块id,number | string */ moduleid: { type: number, required: true }, /** * 主键字段的名称 */ idname: { type: string, default: 'id' }, /** * 字段显示的顺序 */ colorder: { type: array as proptype>, default: () => [] }, /** * 锁定的列数 */ fixedindex: { type: number, default: 0 }, /** * table的列的 meta */ itemmeta: { type: object as proptype<{ [key:string | number]: igriditem }> }, /** * 选择的情况 igridselection */ selection: { type: object as proptype, default: () => { return { dataid: '', // 单选id number 、string row: {}, // 单选的数据对象 {} dataids: [], // 多选id [] rows: [] // 多选的数据对象 [] } } }, /** * 绑定的数据 */ datalist: { type: array as proptype>, default: () => [] }, 其他略。。。}

按照 option api 的方式设置 props 的定义,这样便于共用属性的定义(好吧似乎也没有需要共用的地方,不过我还是喜欢把 props 的定义写在一个单独的文件里)。

定义好 json 、props之后,我们基于 el-table 封装列表控件:

  • template 模板

设置 type="selection"列,实现多选的功能。
使用 v-for 的方式,遍历出动态列。
设置 :fixed="index < fixedindex",实现锁定左面列的功能。

  • js 代码

import { definecomponent, ref } from 'vue' // 列表控件的属性 import { gridprops } from '../map' /** * 普通列表控件 */ export default definecomponent({ name: 'nf-elp-grid-list', inheritattrs: false, props: { ...gridprops // 解构共用属性 }, setup (props, ctx) { // 获取 el-table const gridref = ref(null) return { gridref } } })

把 props 的定义放在单独的 ts文件 里面,组件内部的代码就可以简洁很多。

可以按照自己的喜好,设置一些内部功能,比如单选/多选的功能,格式化的功能等。

  • 定义控制函数 controller.ts

import type { eltable } from 'element-plus'// 列表控件的属性 import type { igridprops } from '../map'export interface irow { [key: string | number]: any}/*** 列表的单选和多选的事件* @param props 列表组件的 props* @param gridref el-table 的 $ref*/export default function choicemanageextends igridprops, v extends typeof eltable>(props: t, gridref: v) { // 是否单选触发 let iscurrenting = false // 是否多选触发 let ismoring = false // 单选 const currentchange = (row: irow) => { if (ismoring) return // 多选代码触发 if (!row) return // 清空 if (gridref.value) { iscurrenting = true gridref.value.clearselection() // 清空多选 gridref.value.togglerowselection(row) // 设置复选框 gridref.value.setcurrentrow(row) // 设置单选 // 记录 props.selection.dataid = row[props.idname] props.selection.dataids = [ row[props.idname] ] props.selection.row = row props.selection.rows = [ row ] iscurrenting = false } } // 多选 const selectionchange = (rows: array) => { if (iscurrenting) return // 记录 if (typeof props.selection.dataids === 'undefined') { props.selection.dataids = [] } props.selection.dataids.length = 0 // 清空 // 设置多选 rows.foreach((item: irow) => { if (typeof item !== 'undefined' && item !== null) { props.selection.dataids.push(item[props.idname]) } }) props.selection.rows = rows // 设置单选 switch (rows.length) { case 0: // 清掉单选 gridref.value.setcurrentrow() props.selection.dataid = '' props.selection.row = {} break case 1: ismoring = true // 设置新单选 gridref.value.setcurrentrow(rows[0]) ismoring = false props.selection.row = rows[0] props.selection.dataid = rows[0][props.idname] break default: // 去掉单选 gridref.value.setcurrentrow() props.selection.row = rows[rows.length - 1] props.selection.dataid = props.selection.row[props.idname] } } return { currentchange, // 单选 selectionchange // 多选 }}

  • 列表控件的 setup 里调用

setup (props, ctx) { // 获取 el-table const gridref = ref>() // 列表选项的事件 const { currentchange, // 单选 selectionchange // 多选 } = choicemanage(props, gridref) return { selectionchange, // 多选 currentchange, // 单选 gridref // table 的 dom }}

  • el-table 完全通过 slot 的方式实现各种功能,这种方法的特点是:非常灵活,可以各种组合;缺点是比较繁琐。
    而我们需要寻找到一个适合的“折中点”,显然这个折中点很难统一,这也是过渡封装带来的问题。
  • 不能遇到新的需求,就增加内部功能,这样就陷入了《人月神话》里说的“焦油坑”,进去了就很难出来。

这也是低代码被诟病的因素。

那么如何找到这个折中点呢?可以按照 “开闭原则”,按照不同的需求,设置多个不同功能的列表控件,使用 slot 实现扩展功能。或者干脆改为直接使用 el-table 的方式。(要灵活,不要一刀切)

比如简单需求,不需要扩展功能的情况,设置一个基础列表控件:nf-grid。
需要扩展列的情况,设置一个可以扩展的列表控件:nf-grid-slot。

如果需要多表头、树形数据等需求,可以设置一个新的列表控件,不过需要先想想,是不是直接用 el-table 更方便。

要不要新增一个控件,不要惯性思维,而要多方面全局考虑。

这里介绍一下支持 slot 扩展的列表控件的封装方式:

模板部分,首先判断一下是否需要使用 slot,做一个分支。
需要使用 slot 的列,通过

网站地图