如此封装一个不一样的Table组件,让你的列表开发速度更快更优雅

3,281 阅读7分钟

也许你已经看过了 99 个 Table 组件的封装,但今天我们依然要为你介绍 第100 个 Table 组件的封装,快来看看和你之前看到的封装方式有什么不一样吧~

一、先看使用方式

image.png

如上图,我们封装了一个不太一样的表格:

  • 没有直接的传入表格列
  • 表格和分页组件是分开的
  • 表格内置了大量的事件支持
  • 还有很多...

今天我们就来简单描述下,我们是如何一步步封装这个表格的。

二、表格基础泛型支持

因为每个表格几乎都会对应到一个实体类,所以我们在封装表格的时候,也支持了泛型支持,如下所示:

<script generic="E extends AirEntity" lang="ts" setup>

其中 AirEntity 的定义,表示该数据具有 ID 属性,恰好对应了表格可能面对的一些有关单条数据的操作事件所需要的 主键ID

当然,你如果真的没有 ID ,你可以使用前端的 UUID 来实现。

三、定义表格基础属性

一个后台业务的表格,至少应该包含以下的属性:

  • 表格列
  • 表格数据
  • 加载状态
  • ...

因为我们使用的是 TypeScript + 装饰器,所以我们在支持传入表格列的前提下,默认支持了直接从类配置的装饰器中获取标记了 @Table() 的属性作为表格列:

const props = defineProps({
  /**
   * # 表格显示的数据数组
   */
  dataList: {
    type: Array<E>,
    required: true,
  },
  /**
   * # 显示字段列表
   * 如传入 则优先使用
   */
  fieldList: {
    type: Array<AirTableFieldConfig>,
    default: () => [],
  },
  /**
   * # 表格绑定的数据实体
   */
  entity: {
    type: Function as unknown as PropType<ClassConstructor<E>>,
    required: true,
  },
})

于是我们为表格添加了上面三个属性,其中:

3.1 dataList

dataList 作为表格数据的属性,对应为泛型数组的类型,且是必须传入的属性。

3.2 fieldList

fieldList 作为表格列的属性,对应为 AirTableFieldConfig 类型的数组,且是可选传入的属性,如果传入了该属性,则优先使用该属性作为表格列,否则将从下面的 entity 实体类中自动读取 @Table() 标记的属性作为表格列;

3.3 entity

entity 作为实体类的属性,对应为实体类的构造函数,且是必须传入的属性,用于从实体类中自动读取表格列。例如 :entity="User",则从 User 类中读取 @Table() 标记的属性作为表格列。

如上实现了这三个属性之后,表格就可以这么使用了:

<ATable
  :data-list="list"
  :entity="PermissionEntity"
/>
<script lang="ts" setup>
const list = ref<PermissionEntity[]>([])
</script>

当然,你可以自己传入 fieldList:

<ATable
  :data-list="list"
  :entity="PermissionEntity"
/>
<script lang="ts" setup>
const list = ref<PermissionEntity[]>([])
const fieldList = computed(()=>{
  return PermissionEntity.getTableFieldList().filter(item=>{
    // 隐藏其中某些列
    return item.key !== 'isDisabled'
  })
  // 当然,你也可以手动增加某些列
})
</script>

PermissionEntity 则是表格列对应实体类的声明,包含了这个表格每行数据是什么类型:

@Model({
  label: '权限',
})
export class PermissionEntity extends BaseEntity implements ITree {
  @Table({
    forceShow: true,
  })
  @Search()
  @Form({
    requiredString: true,
  })
  @Field({
    label: '权限名称',
  }) name!: string

  @Table({
    forceShow: true,
    copyField: true,
  })
  @Form({
    requiredString: true,
  })
  @Field({
    label: '权限标识',
  }) identity!: string

  // 更多字段
}

这个类不仅承载了关于 表格 的部分,也有关于 表单 搜索 等其他的公共内容,放到一个类中集中管理比较方便。

本文就不再就 面向对象 声明数据结构展开了,这里争论点很多,欢迎阅读专栏内其他这部分的文章。

四、表格的更多属性

表格除了上述三个属性外,我们针对于业务还增加了非常多的属性:

const props = defineProps({
  /**
   * # 是否隐藏编辑按钮
   */
  hideEdit: {
    type: Boolean,
    default: false,
  },
  /**
   * # 控制是否禁用行内编辑按钮的回调方法
   */
  disableEdit: {
    // eslint-disable-next-line no-unused-vars
    type: Function as PropType<(row: E) => boolean>,
    default: null,
  },
  /**
   * # 是否隐藏序号
   */
  hideIndex: {
    type: Boolean,
    default: false,
  },
  /**
   * # 是否隐藏字段选择
   * 如 `EntityConfig` 的 `hideFieldSelector` 设置为 `true`, 则此项失效
   */
  hideFieldSelector: {
    type: Boolean,
    default: false,
  },
  ... //太多了
})

上面只是举了些例子,如果你有兴趣,可以看看文章最后面的开源项目里的源代码。这里我们简单说明一下:

4.1 hideEdit 之类的隐藏或显示按钮配置

我们可以使用 hideEdit hideDelete showEnableAndDisable 等属性的配置来告诉 Table 组件需要支持显示一些按钮,如果按钮被点击的话,就会抛出相应的 emits 事件,后续会讲解这部分事件的处理。

4.2 hideFieldSelector 隐藏列选择器

可以通过这个属性来配置是否为表格显示 列选择器,选择列后,表格将只展示选择保存的部分列。

4.3 行回调事件属性

有时候,某些行我们是不允许操作的,所以我们可以使用 disableEdit disableDelete 等属性来传入回调函数去控制这些操作,如下:

<ATable
  :disable-delete="row => row.isSystem"
  :disable-edit="row => row.isSystem"
/>

例如上面的,行数据如果满足指定的条件,功能将被禁用。

其中,row 的类型即为泛型表格组件的实体 E 类型。

五、其他配置

还支持了非常多的配置,比如 按钮权限标识显示或隐藏部分功能列宽度配置更多按钮配置懒加载树表格 等等。

六、表格事件

我们为表格支持了各种事件,用于表格某些功能触发后的回调处理:

const emits = defineEmits<{
  onDetail: [row: E],
  onDelete: [row: E]
  onEdit: [row: E]
  onSelect: [list: E[]]
  onAdd: [row: E]
  onSort: [sort?: AirSort]
  onDisable: [row: E]
  onEnable: [row: E]
}>()

事件处理的话这里就没什么好多说的了,只是我们基于 泛型组件 而提供的标准的事件(一定会回传一个带 ID 属性的事件参数而已)。

但也恰好因为我们提供的是泛型组件,于是我们基于这个组件提供了更方便操作的一些 hooks:

七、表格Hooks

首先,我们需要提供一个基础的表格hook:

7.1 airTableHook 表格基础Hook

123.png

如上图,我们提供了一个基础的表格 Hook airTableHook,包含了表格的各种属性和事件,但不包含 增删改查 的部分。

提供了Hooks之后,我们也对一些关键事件节点提供了一些前后置方法,比如:

beforeSearch?: (requestData: AirRequestPage<E>) => AirRequestPage<E> | void

你可以基于这些前后置方法,轻松的在 增删改查 的过程中,做一些自定义的操作。

这个 Hook 是表格的基础,会被一般的 增删改查 表格组件 以及 选择类表格 所继承,于是有了下面俩子 Hook:

7.2 useAirTable 普通表格Hook

2222.png

这个 Hook 基于表格基础Hook扩展了增删改查的功能,同时提供了一些常用的方法。

所以使用的地方也将更简单:

<template>
  <ATable
    v-loading="isLoading"
    :data-list="response.list"
    :entity="UserEntity"
    @on-edit="onEdit"
    @on-delete="onDelete"
  />
</template>

<script lang="ts" setup>
const {
  isLoading, response,
  onEdit, onDelete,
} = useAirTable(UserEntity, UserService)

恰好因为我们使用了 面向对象 数据结构,所以 response 等都是对应 UserEntity 相应的数据请求响应类,取出的 .list 也是泛型 UserEntity[] 的标准数据数组结构。

7.3 useAirSelector 选择器表格Hook

33333.png

如上图,我们提供了选择器表格的 Hook useAirSelector,基于表格基础 Hook 扩展了选择器表格的功能,同时提供了一些选择器所使用的属性或方法。

当然,我们后续还提供了另外一个选择器专用的组件,和 Table 类似,封装最后使用起来更舒服了:

<template>
  <ASelector
    :editor="UserEditor"
    :entity="UserEntity"
    :props="props"
    :service="UserService"
  />
</template>

<script lang="ts" setup>
const props = defineProps(airPropsSelector<UserEntity>(new UserEntity()))
</script>

这样就直接完成了一个用户的表格选择器,需要使用的时候,直接使用下面的方式就可以弹出这个选择器并获取选择之后的返回结果:

const userList = await AirDialog.selectList(UserSelector)

美滋滋。

八、其他关联组件

因业务需要,与表格也会存在很多的关联组件,比如:

8.1 ToolBar 工具栏组件

工具栏组件中可能存在 新增刷新批量操作 等按钮,所以依然可以将 useAirTableuseAirSelector 等 Hook 返回的值作为参数传入,这样工具栏就可以直接联动这个 Hook 来与表格交互了。

工具栏还包含了 搜索 框,所以如果表格需要搜索功能,那么工具栏也会直接与表格联动。

8.2 Page 分页组件

因为表格数据可能存在分页和不分页、树表格 等情况,所以 分页组件 被我们单独了出来,可以自行排列组合。

而且,我们的响应对象都被 response 做了封装,都可以方便的取出所需要的数据。

九、总结

基于本文的内容,我们提供了非常丰富的表格组件、工具栏、分页 等组件,并提供了相应的 Hooks 支持,使得列表页面的开发更加的简单。

如果你对本文提到的内容感兴趣,可以关注我们的开源项目来阅读所有的源码:

Github: AirPower4T

Bye.