【手撸低代码工具】二次封装UI库(七)封装列表

列表组件,说简单也简单,说复杂那也确实有点麻烦的地方。

列表组件,主要不是封装 el-table,而是 el-table-column,因为前者设置好各种属性即可,而后者却需要各种思考。

还有就是封装风格,像 el-table 那样,提供一个组件,然后用slot 实现其他各种需求,还是依据需求,封装多个不同功能的组件?

目前采用的方案是 —— 封装多个不同功能的组件:

  • 基础功能列表:可以锁定行列、多选、单选、隔行颜色、高亮等
  • 可以使用 slot 的列表:使用 slot 设置操作按钮。
  • 行内编辑的列表
    • 一起编辑:都是 input 上来就可以改。
    • 一次只编辑一行:先选一行才能改,可以选择保存或者取消。

大概四种吧。目的是:使用的时候可以简洁一点,参考一下开闭原则,做好功能管理。

el-table-column

这是列表的基础,原生组件需要我们手动一个一个设置,这样做非常灵活可以实现各种需求,但是用起来有点麻烦,所以我们先对他下手。

el-table-column 的 Interface

去官网看了一下 el-table-column 的属性,那叫一个多,不过我们不用都设置到 Interface 里面,只需要挑我们需要的即可,其他可以通过 $attrs 传入。

export interface IGridItem {
  id: number | string,
  prop: string,
  label: string,
  width: number,
  align: EAlign,
  headerAlign: EAlign
}
  • id:字段ID、列ID
  • prop:字段名称,相当于表单里的 colName
  • label:列的标签、标题
  • width:列的宽度
  • align:内容对齐方式
  • headerAlign:列标题对齐方式

做一个 enum 设置左中右:

/**
 * 横向对齐方式,左、中、右
 */
export const enum EAlign {
  left = 'left',
  center = 'center',
  right = 'right'
}

json

根据 Interface,我们来做一份json。

itemMeta: {
 "90": {
      "id": 90,
      "colName": "kind",
      "label": "分类",
      "width": 140,
      "title": "分类",
      "align": "center",
      "header-align": "center"
    },
  // 其他列 
}  

这个ID是非常重要的,虽然现在看不出来,其他的就是我们常用的属性了。

el-table

这个很简单,设置好属性即可,我们按照老规矩,设置一个 meta 和 props 的Interface

设置列表的 meta IGridMeta

export interface IGridMeta {
  moduleId: number | string,
  idName: string,
  colOrder: Array<number|string>
}
  • moduleId:模块ID
  • idName:主键字段的名称
  • colOrder:列(字段)显示的顺序

非常简单,主要记录一下这是哪个模块的meta,默认一个模块只有一个列表,以及列表的主键字段名称,对应 el-table 的 row-key 属性。

最后就是字段的显示依据,需要显示哪些字段、以及字段的先后顺序。用数组的形式表示,可以方便调整和更改。

设置列表的 props IGridProps

export interface IGridProps<T> {
  gridMeta: IGridMeta,
  itemMeta: { [key:string | number]: IGridItem },
  selection: IGridSelection, 
  dataList: Array<T>,
  showHeader: boolean,
  height: number,
  stripe: boolean,
  border: boolean,
  highlightCurrentRow: boolean
}
  • gridMeta:列表的 meta
  • itemMeta:列的 mate
  • dataList:绑定的数据 Array, 对应 data
  • selection:记录选择了哪些数据
  • 原生属性
    • highlightCurrentRow:要高亮当前行
    • border:纵向边框
    • stripe:斑马纹
    • height:table的高度
    • showHeader:是否显示表头,监听用

设计这几个原生属性,是因为想设置默认值,因为我希望表格是带纵向边框、斑马纹、高亮的,但是默认不是。

showHeader,是想监听一下是否显示表头,因为我给表头设置了拖拽事件,隐藏后事件就没了,重新显示后需要再次挂载,所以要监听一下。

选择的数据 IGridSelection

用户选择记录之后,可能去修改、批量删除或者导出等操作,那么就需要一个容器来记录用户选择了哪些记录。

export interface IGridSelection<T> {
  dataId: number | string,
  row: T,
  dataIds: Array<number | string>,
  rows: Array<T>
}
  • dataId:单选,记录主键ID值
  • row:单选,记录选择的row
  • dataIds:多选,记录ID集合
  • rows:多选,记录 row 的集合

兼容单选和多选,可以只关心选择了哪些ID,也可以获得选择行的数据。

这里前后端的思维好像有点小差别:列表里的row是否记录了全部信息?

后端的习惯是,一般这个 row 不会记录全部信息,比如博客内容、产品介绍等显然不能放进去,所以关心的是ID,通过ID获取全部信息,这样可以完整和实时。

前端嘛,瞎猜一下,可能认为row就是全部内容。当然只是瞎猜。

json 文件

一份完整的json文件:

{
  "gridMeta": { 
    "moduleId": 142,
    "idName": "ID",
    "colOrder": [ 90,  101, 102, 105 ... ]
  },
  "gridPros": {
    "height": 400,
    "stripe": true,
    "border": true,
    "fit": true,
    "highlight-current-row": true
  },
  "itemMeta": {
    // 列的信息
  }
}  

在json文件里面,列表的 props 集中放置,这样便于维护json和扩展其他属性,不会分散开。好吧,这几个都设置默认值了,可以不填的。

实现基础功能的表格

基础功能都有哪些?

  • 行列
  • 锁定行列
  • 可以多选和单选
  • 记录选择的数据

其他的都作为扩展功能。

  <el-table
    ref="refControl"
    style="width: 100%"
    :height="height"
    v-bind="$attrs"
    :data="dataList"
    :row-key="gridMeta.idName"
    @selection-change="selectionChange"
    @current-change="currentChange"
  >
    <!--显示选择框-->
    <el-table-column
      type="selection"
      width="55"
      align="center"
      header-align="center"
    >
    </el-table-column>
    <!--显示字段列表-->
    <el-table-column
      v-for="(id, index) in gridMeta.colOrder"
      :key="'grid_list_' + index + '_' + id"
      :min-width="50"
      :column-key="'col_' + id"
      v-bind="itemMeta[id]"
      :prop="itemMeta[id].colName"
    >
    </el-table-column>
    <!--右面的操作列-->
  </el-table>

遍历集合,用 v-for 创建列表的列。是不是很简单。我们只需要准备好一份json文件,然后设置属性即可。

效果:

【手撸低代码工具】二次封装UI库(七)封装列表

行内操作按钮

我个人是喜欢把操作按钮放在列表的上面,添加、修改、删除、查看、导出等放上一排,需要哪个按哪个。

但是客户说,我想修改的时候,先选择一行,然后再去上面找按钮,太麻烦。

你猜怎么着,我会把修改按钮放在行里面吗?才不,我加了一个双击行弹窗修改的功能。

好吧,不开玩笑了,虽然那时候真的是这么干的,但是现在不行了,不可能每个客户都那么好说话。

不过,我觉得,封装最重要的是,要有限度,不能啥都往内部封装,早晚爆炸,你看el-table 都没有把按钮封装在里面,而是通过 slot 实现的,所以我们也可以借花献佛。

传递 el-table-column 的 slot

el-table-column 的插槽是匿名插槽,因为只需要应该是个字段即可,但是封装之后却需要应对多个字段,那么怎么办呢?给匿名插槽起个名字,然后转换一下即可。

插槽规则:

  • 字段名作为插槽名称,对应 el-table-column 的匿名插槽
  • header_字段名作为插槽名称,对应 el-table-column 的 header 插槽
  • option 作为插槽名称,对应操作列

外部设置插槽

  <nf-grid
    v-bind="gridMeta"
    :dataList="dataList"
  >
    <!--普通字段,用字段名作为插槽的名称-->
    <template #header_text>
      扩展表头
    </template>
    <template #text="scope">
      <span style="margin-left: 10px">扩展1:{{ scope.row.text }}</span>
    </template>
    <!--普通字段-->
    <template #week="scope">
      <span style="margin-left: 10px">{{ scope.row.week.replace('-w','年 第') + '周' }}</span>
    </template>
    <!--操作按钮-->
    <template #option="scope">
      <el-button size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
      <el-button
        type="danger"
        @click="handleDelete(scope.$index, scope.row)">删除</el-button>
    </template>
  </nf-grid>

这样就可以像 el-table 那样,灵活设置各种列了。

内部转换 插槽

那么内部如果做转换呢?

  <el-table-column 
    v-if="(slotsKey.includes(itemMeta[id].colName))"  // 如果插槽名称包含字段名称说明该字段需要插槽
    :column-key="'col_' + id"
    v-bind="itemMeta[id]"
  >
    <template #header
      v-if="slotsKey.includes('header_' + itemMeta[id].colName)" // 如果有header设置header插槽
    >
      <slot :name="'header_' + itemMeta[id].colName"></slot>
    </template>
    <template #default="scope"> // 设置匿名插槽
      <slot :name="itemMeta[id].colName" v-bind="scope"></slot> // 加载外部插槽内容,并且传出 scope
    </template>
  </el-table-column>

这样就可以做一个 slot 的中转,把外部设置的插槽转送给 el-table-column,把 scope 转送给外部。

转送组件的方法

UI库的组件一般都会提供一些方法,那么如果把方法都转送出去呢?有一个通用的方法:

export const myExpose = () => {
  const refControl = ref(null)
  const expose = {}
  onMounted(() => {
    Object.assign(expose, refControl.value)
  })

  return {
    refControl,
    expose
  }
}
  • refControl:接收组件传出来的信息
  • expose:设定一个转出的容器
  • onMounted:组件挂载后加载组件提供的方法到容器

defineExpose 不能放在外部,否则也可以放在这里。

封装组件里的使用方法给外部

首先引入 myExpose,然后设置两行即可:

  const { refControl, expose } = myExpose()
  defineExpose({ expose })

最后别忘了给el-table设置ref。这种方式支持各种组件,保证通用。

  <el-table
    ref="refControl">
  ...  

原文链接:https://juejin.cn/post/7244810003576471613 作者:金色海洋

(0)
上一篇 2023年6月16日 上午11:07
下一篇 2023年6月17日 上午10:00

相关推荐

发表回复

登录后才能评论