table 组件
由于这段时间在使用 antd 组件库, 使用 **table 组件 **始终一直有种 不够拿捏,不得心应手,一直以来 对 table 这个组件就使用起来就不够丝滑。这种感觉很难受啊。所以,干脆一次性向 table 这个组件结构 发起猛攻 !!今天之后,彻底 拿捏 table 。
原生的 table
先熟悉一下:
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
<th>职业</th>
</tr>
</thead>
<tbody>
<tr>
<th>周杰伦</th>
<th>18</th>
<th>男</th>
<th>歌手</th>
</tr>
<tr>
<th>邓紫棋</th>
<th>18</th>
<th>女</th>
<th>歌手</th>
</tr>
</tbody>
</table>
// table : 表格区
// thead : 表头区
// tbody : 表格内容区
// tr : 表行数据
// th : 表列数据(单元格)
弄清楚概念
需要注意的就是:
-
什么叫表头:表头就是一个表格的最上面一排,是每一列的 Title
-
什么叫单元格:单元格就是表格的每一个,最小的单位,存放一个数据。
-
什么是一行:一行就是一行,没什么好解释的。(一行就是包括了一组完整字段的数据,按一般的表格方向来看)
-
表格方向:根据 表头的方向看,一般是行的方向。
只需要清楚一般就是 数据横向展示的,即 一行就是 包含一条完整的实例的数据。(比如学生表,一行就包含了某个学生的 姓名,年龄,性别,班级 ...)这也是 符合常规认知,第一直觉的。
详细来看的方向,指的是:
-
数据展示方向
默认情况下,表格采用横向展示(行记录、列表头),即:
- 每一行代表一条完整的数据记录(
dataSource中的一个对象) - 每一列对应数据对象的一个属性(
columns中定义的字段)
如果需要纵向展示(列记录、行表头),即把行列颠倒(适合数据量少但字段多的场景),可以使用
tableLayout="fixed"配合自定义渲染,或通过List组件模拟。 - 每一行代表一条完整的数据记录(
-
排列方向 (布局方向)
主要指表格内容的文本排列方向和整体布局方向,通过
direction属性控制(针对国际化场景):direction="ltr"(默认):左到右排列(适合中文、英文等)direction="rtl":右到左排列(适合阿拉伯语、希伯来语等从右向左阅读的语言)
-
组件库中使用 table
Ant-design-vue 组件库(react 也有对应的 antd 组件库)
官网:Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js
最重要的 参数
-
columns:表头配置对象
columns 是一个数组,每一项就是一个 column配置对象,用于配置一个表头。常见的配置:
插槽:作用就是,很多情况我们想 将表头 或 单元格的 内容设置的更加丰富,而不是简单的一个 字符串,一个数字;而是 自己定义里面的组件结构。这时就将表头或单元格的设置成一个作用域插槽,有名字,可以传参。
插槽:
- 可以设置在columns 的slots 配置中,title 表示 表头的插槽,customRender 表示单元格的插槽。(ts 不支持,js 可以)
- column 中的 customRender 直接就可以接受一个 render函数。这样直接给 单元格设置组件结构。同理,column的 title 是否也可以接受一个 render函数呢?我想可以。(jsx/tsx的形式)
- 总之:title 表示表头,customRender 表示单元格。
插槽参数:固定的插槽参数有:
- text:当前单元格的数据(对象或文本)(即 record[column.dataIndex])
- record:当前行的完整的数据对象。
- index:当前行的索引。
- column:当前列的配置对象(即当前列的表头的配置对象),说白了就是,对于这一列的字段的配置。
columns = [ { title:''// 标头的标题(眼睛看到的) key:''// 该表头的唯一标识 dataIndex:''// 该列的索引,和 key 一样就行。 // ts 不能用这种方式,无法进行 插槽自定义 ①.slots:{title:'表头的插槽',customRender:'在该表头下的单元格的插槽'} ②.customRender:()=>{}// render 函数 } ] -
dataSource:数据源
注意:record 是一整行的数据对象,他不一定里面所有的数据都要展示在 table 的字段里,**table字段只会展示 配置对象里面配置的字段。**所以说只是很灵活的,任何数据对象都可以展示在 table中,只要它里面包含了table 配置对象里面的字段。即 鸭子类型(Duck Typing)。
动态类型语言中多态性的精髓 就是 “鸭子类型”。
table 组件内部渲染构建机制
我的问题:使用 插槽 bodycell时,table 的表头不止一个,为什么能直接结构出 column呢,难道是bodycell插槽内部有玄坤,比如循环每一个字段?
解答:实际上,Table组件内部在处理表格渲染时,会遍历每一列的配置,然后为每一列生成对应的单元格。对于自定义单元格内容,Table组件会将当前列的配置、当前行的数据、当前行的索引等信息通过作用域插槽传递给插槽内容。
具体来说,Table组件内部大致会做这样的处理:
- 根据columns配置生成表头。
- 遍历数据源dataSource的每一项(即每一行)。
- 对于每一行,遍历columns配置的每一列。
- 对于每一列,检查是否有自定义渲染(比如通过作用域插槽),如果有,则调用自定义渲染函数或使用作用域插槽,并传入当前列、当前行、当前行的索引等信息。
因此,我们在使用#bodyCell插槽时,实际上是在为每一个单元格提供自定义渲染。Table组件在遍历每一列的时候,会为每个单元格执行这个插槽,并将当前列的column信息传递进来。
所以,bodyCell插槽并不是只执行一次,而是对每行每列都会执行。在插槽内部,我们可以根据不同的column来做不同的渲染。
使用示例
config 配置对象:
const configTable = [
{
title: 'ID',
dataIndex: 'id',
width: 60,
customRender: ({ record }: { record: BusItem }) => (
<div class='text-sm text-secondary-200 code'>{record.id}</div>
),
},
{
title: '班车时间',
dataIndex: 'ctime',
width: 150,
customCell: () => ({
class: 'text-sm',
}),
},
{
title: 'Tag',
dataIndex: 'stage',
width: 140,
customRender: ({ record }: { record: BusItem }) => (
<div class='text-sm'>
{record.subTagId && (
<Tag>
<span class='text-secondary-200'>基准: </span>
<span class='font-semibold'>{record.subTagId}</span>
</Tag>
)}
{record.tagId && (
<Tag>
<span class='text-secondary-200'>发布: </span>
<span class='font-semibold'>{record.tagId}</span>
</Tag>
)}
</div>
),
},
{
title: '更新时间',
dataIndex: 'mtime',
width: 150,
customCell: () => ({
class: 'text-sm',
}),
},
{
title: '操作',
dataIndex: 'operatot',
width: 140,
fixed: 'right',
},
] as any[];
使用 table 组件:
<Table
rowKey="id"
sticky
size="small"
:scroll="{ x: configTableWidth }"
:loading="state.loading"
:columns="configTable"
:dataSource="state.data"
:pagination="pagination"
>
<template #bodyCell="{ column, record }">
<template v-if="column.title === '操作'">
<a class="text-blue-700 dib mr-2" @click="handleInfo(record.id)"
>查看</a
>
<a
class="text-blue-700 dib"
v-if="
[BUS_TYPE_AB, BUS_TYPE_NOAB].includes(record.busType) &&
((record.stage === 'ab' || record.status === 3) &&
dayjs(record.ctime).isAfter(
dayjs(dayjs().format('YYYY-MM-DD 00:00:00')),
))
"
@click="handleRerun(record.id)"
>重发班车</a
>
</template>
</template>
</Table>
这里的 bodyCell 插槽 完全可以写在 config 中 titlie = '操作'的 所在列的 customRender字段中。
就像这样 :
{
title: '操作',
dataIndex: 'operator',
width: 140,
fixed: 'right',
customRender: ({ record }) => {
const actions = [];
actions.push(
<a
key="view"
className="text-blue-700 dib mr-2"
onClick={() => handleInfo(record.id)}
>
查看
</a>
);
if (
[BUS_TYPE_AB, BUS_TYPE_NOAB].includes(record.busType) &&
((record.stage === 'ab' || record.status === 3) &&
dayjs(record.ctime).isAfter(dayjs(dayjs().format('YYYY-MM-DD 00:00:00'))))
) {
actions.push(
<a
key="rerun"
className="text-blue-700 dib"
onClick={() => handleRerun(record.id)}
>
重发班车
</a>
);
}
return <div>{actions}</div>;
}
}
在 Ant Design Table 中,完全可以通过 customRender 属性来替代 bodyCell 插槽的功能,尤其是对于操作列这种需要自定义内容的场景,使用 customRender 会让代码结构更清晰,也更符合组件化的思想。
这种写法 ( 写在 config 里面 ) 的优势:
- 逻辑更内聚 - 列的配置和渲染逻辑放在一起,便于维护。
- 减少模板代码 - 不需要额外的
<template #bodyCell>插槽。 - 更好的类型支持 - 在 TypeScript 中能获得更明确的类型提示。
- 更灵活的渲染控制 - 可以在 customRender 中进行更复杂的条件判断和元素组合。
好了,这下确实通透了很多了,真正能 达到 拿捏的 水平还需要多多在实战中打磨。平时容易忘没关系,小问题。用多了,踩了坑多了,印象深刻自然就深刻了。
正所谓,古人长云:“ 无他,唯手熟尔 ”,盖此章也 。