第七章 第六节 Ant Design 高级数据组件篇 —— 解析复杂数据集处理逻辑

7.14. 高级数据组件:驾驭复杂数据集

摘要:在本章中,我们将深入学习 Ant Design 中为处理结构化、大规模数据集而设计的核心组件。这些组件是企业级应用(尤其是后台管理系统)的绝对主力。我们将从最基础的序列数据展示(List)开始,逐步掌握层级数据(Tree)和二维表格数据(Table)的渲染与交互,最终构建出功能强大的数据驱动界面。

7.14.1. List: 灵活的序列数据展示

List (列表) 是用于展示 序列数据(即数组)最基础也最重要的组件。相比于在代码中手动 map 一个数组来渲染 JSX,List 组件提供了更丰富、更标准化的功能,如加载状态、分页、头部/尾部、多种布局以及栅格化展示,是 Table 组件在简单场景下的轻量级替代方案。

核心应用场景:

  • 新闻/文章列表:展示文章的标题、摘要、作者头像等。
  • 消息通知中心:展示一系列通知信息。
  • 产品或图片墙:以网格布局展示多个商品或图片。

第一步:准备数据 - Mock API 升级

为了真实地模拟从后端获取列表数据的过程,我们需要再次请出 json-server,并为它准备一份列表数据。这次,我们将通过脚本生成一份模拟的新闻文章列表。

1. 安装/确认依赖
请确保您的 devDependencies 中已包含 @faker-js/faker,用于生成逼真的假数据。

2. 创建数据生成脚本
我们将创建一个脚本,用于生成包含 50 条新闻的 db.json 文件。

文件路径: scripts/generate-mock-data.mjs (新建或修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fakerZH_CN as faker } from '@faker-js/faker';
import fs from 'fs';

// 使用中文语言环境的 Faker 实例

// 生成新闻列表的函数
const generateNewsList = (count) => {
const newsList = [];
for (let i = 0; i< count;i++){
newsList.push({
id: faker.string.uuid(),
title: faker.word.words({ count: { min: 8, max: 15 } }), // 8-15 个中文词作为标题
avatar: faker.image.avatar(),
description: faker.word.words({ count: { min: 20, max: 40 } }), // 20-40 个中文词作为描述
content: faker.word.words({ count: { min: 100, max: 200 } }), // 100-200 个中文词作为内容
})
}
return newsList;
}

const db = {
news : generateNewsList(50),
}


fs.writeFileSync('db.json', JSON.stringify(db, null, 2));
console.log('Mock data (news list) generated successfully!');

3. 生成数据并启动服务
首先,在 package.json 中添加或修改 mock:generate 脚本。

1
2
3
4
5
"scripts": {
// ... 其他脚本
"mock:generate": "node ./scripts/generate-mock-data.mjs",
"mock:api": "json-server --watch db.json --port 3001"
},

现在,依次执行以下命令:

  1. pnpm mock:generate (只需执行一次,用于生成 db.json)
  2. pnpm mock:api (启动 API 服务)

您的 API http://localhost:3001/news 现在已经可以提供 50 条新闻数据了。


第二步:基础列表与数据绑定

List 组件的核心是 数据驱动。我们需要从 API 获取数据,并将其传递给 List,然后通过 renderItem 函数定义每一项的渲染方式。

核心属性:

  • dataSource: 列表的数据源数组
  • renderItem: 渲染列表中每一项的函数。该函数接收 itemindex 作为参数,返回一个 React 节点。
  • loading: 布尔值,用于控制列表的加载状态,true 时会显示骨架屏。
  • List.Item / List.Item.Meta: 用于快速构建标准列表项结构的辅助组件。

文件路径: src/components/demos/BasicListDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useEffect, useState } from "react";
import { Avatar, List } from "antd";
// 定义数据项的 TypeScript 类型
interface NewsItem {
id: string;
title: string;
avatar: string;
description: string;
content: string;
}

const BasicListDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<NewsItem[]>([]);

const renderItem = (item: NewsItem) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={item.avatar} />}
title={item.title}
description={item.description}
/>
</List.Item>
);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch("http://localhost:3001/news");
const data = await response.json();
setData(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};

fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础新闻列表</h3>
<List
loading={loading}
itemLayout="horizontal"
dataSource={data}
renderItem={renderItem}
/>
</div>
);
};

export default BasicListDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicListDemo from './components/demos/BasicListDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicListDemo />
</div>
</ConfigProvider>
);
};

export default App;

第三步:增强功能 - 分页与栅格布局

当数据量巨大时,一次性加载所有数据是不现实的。List 内置了强大的 分页栅格 功能,以应对不同场景。

核心属性:

  • pagination: 配置分页器。传入一个对象,可以精细化控制分页行为,如 pageSize (每页条数)。List 会自动处理客户端的数据分割。
  • grid: 开启栅格布局。传入一个对象,可以配置每行显示的列数 (column)、间距 (gutter) 以及响应式断点。

文件路径: src/components/demos/AdvancedListDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useEffect, useState } from 'react';
import { List, Card } from 'antd';

interface NewsItem {
id: string;
title: string;
avatar: string;
}

const AdvancedListDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<NewsItem[]>([]);

useEffect(() => {
fetch('http://localhost:3001/news')
.then((res) => res.json())
.then((res) => {
setData(res);
setLoading(false);
});
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[1000px]">
<h3 className="text-xl font-bold mb-4 text-center">分页与栅格列表</h3>
<List
loading={loading}
dataSource={data}
// 1. 开启栅格布局每行 4
grid={{
gutter: 16,
xs: 1,
sm: 2,
md: 4,
lg: 4,
xl: 4,
xxl: 4,
}}
// 2. 开启分页每页 8
pagination={{
onChange: (page) => {
console.log(page);
},
pageSize: 8,
align: 'center',
}}
renderItem={(item) => (
<List.Item>
<Card title={item.title.substring(0, 15) + '...'}>Card content</Card>
</List.Item>
)}
/>
</div>
);
};

export default AdvancedListDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicListDemo from './components/demos/BasicListDemo';
import AdvancedListDemo from './components/demos/AdvancedListDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicListDemo /> */}
<AdvancedListDemo />
</div>
</ConfigProvider>
);
};

通过 paginationgrid 属性的简单配置,我们就能轻松地将一个长列表,转化为带分页的、响应式的卡片网格,这在构建产品展示墙、图片库等场景中极为高效。List 组件的灵活性和可扩展性,使其成为处理序列数据的首选方案。


7.14.3. Table (Part 1 - 基础篇): 构建你的第一个数据表格

Table (表格) 是所有后台管理、数据分析、信息管理类应用的心脏。当您的数据拥有多个维度(列),并且需要进行结构化的行列展示时,Table 便无可替代。相比于 ListTable 提供了列对齐、排序、筛选、分页、行选择等一系列专为复杂数据集设计的强大功能。

在本篇中,我们将专注于掌握 Table 的两大基石,并结合 json-server 构建一个功能完备的、带 异步加载客户端分页 功能的基础表格。


第一步:两大核心概念 - dataSourcecolumns

要渲染一个 Table,您只需要向它提供两样东西:数据在哪里(dataSource),以及数据如何展示(columns)。
dataSource (数据源)

  • 它是一个 对象数组,数组中的每个对象都代表表格中的 一行 数据。
  • 关键要求:数组中的每个对象都 必须 有一个 唯一key 属性,React 依赖它来进行高效的渲染和 diff。如果您的数据中没有 key 字段,您可以使用 rowKey 属性来指定另一个唯一标识符(例如 id)。

columns (列定义)

  • 它也是一个 对象数组,数组中的每个对象都定义了表格中的 一列
  • 核心属性:
    • title: 列头显示的标题文本。
    • dataIndex: 核心关联字段。它告诉 Table,这一列要显示 dataSource 中每个对象的哪个属性值。例如,dataIndex: 'name' 就会去取 dataSource 中每个对象的 name 属性。
    • key: 列的唯一标识符。如果 dataIndex 已经唯一,通常可以将其设为与 dataIndex 相同的值。

第二步:实战演练 - 异步加载与客户端分页

现在,我们将结合 json-server,从零开始构建一个“用户管理”表格。

1. 准备用户数据 API

我们需要一个能提供大量用户数据的 API。请修改您的 scripts/generate-mock-data.mjs 脚本,为 db.json 添加 users 数据。

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { fakerZH_CN as faker } from '@faker-js/faker';
import fs from 'fs';

// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
for (let i=0;i<count;i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.location.country(),
});
}
return users;
};


const db = {
users: generateUsers(100),
};

fs.writeFileSync("db.json", JSON.stringify(db, null, 2));
console.log('Mock data (users list) generated successfully!');

修改完成后,请务必重新执行 pnpm mock:generate 来生成新的 db.json 文件。然后,启动 pnpm mock:api,您的 http://localhost:3001/users 接口现在就可以使用了。

2. 编写表格组件

我们将创建一个组件,在加载时从 API 获取用户数据,并用带分页的 Table 将其展示出来。

文件路径: src/components/demos/BasicTableDemo.tsx (新建文件)

image-20250930111655744

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { useEffect, useState } from "react";
import { message, Table } from "antd";
import type { TableColumnsType } from "antd";

// 1. 定义 TypeScript 类型,增强代码健壮性
interface User {
id: string;
name: string;
email: string;
jobTitle: string;
country: string;
}

// 2. 定义列 (columns)
const columns: TableColumnsType<User> = [
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "邮箱", dataIndex: "email", key: "email" },
{ title: "职位", dataIndex: "jobTitle", key: "jobTitle" },
{ title: "国家", dataIndex: "country", key: "country" },
];

const BasicTableDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);

useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
message.error("Error fetching data");
} finally {
setLoading(false);
}
};
fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-4xl">
<h3 className="text-xl font-bold mb-4 text-center">
用户数据表格 (基础篇)
</h3>
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
}}
bordered
/>
</div>
);
};

export default BasicTableDemo;

3. 重点解析:Pagination (分页器)

虽然 Pagination 是一个独立的组件,但它最常以内置的形式,通过 TableListpagination 属性进行使用。

  • 客户端分页 (我们当前使用的): 当您的 dataSource 包含了 所有 数据时,Table 组件会非常智能地在 前端 自动为您完成分页逻辑。您只需在 pagination 对象中配置好每页的条数(pageSize)等显示选项即可。
  • 服务端分页 (后续会讲): 当数据量极大时(成千上万条),一次性加载所有数据是不现实的。这时,我们需要在每次切换页面时,重新向后端发起请求,获取当前页的数据。这被称为服务端分页,需要配合 onChange 事件来处理。

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTableDemo from './components/demos/BasicTableDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

至此,我们已经成功构建了一个功能完备的基础数据表格。您已经掌握了 Table 最核心的 dataSourcecolumns 配置,并学会了如何处理异步加载和实现客户端分页。

Part 2 - 交互篇 中,我们将在此基础上,为表格添加 排序、筛选和行选择 等强大的交互功能。


7.14.4. Table (Part 2 - 交互篇): 排序、筛选与选择

在基础篇中,我们成功地将数据显示在了表格里。但一个真正的企业级表格,远不止于静态展示,它必须是 可交互的。用户需要能够根据自己的需求,对数据进行排序、筛选和选择,以便快速定位和处理信息。

在本篇中,我们将为上一节的表格添加三种最核心的交互能力:

  1. 排序 (sorter): 允许用户点击表头,对该列数据进行升序或降序排列。
  2. 筛选 (filters): 提供一个筛选菜单,让用户可以根据特定条件过滤数据。
  3. 行选择 (rowSelection): 允许用户勾选一行或多行,以进行批量操作。

第一步:升级 Mock API - 添加可排序与筛选的数据

为了更好地演示排序和筛选功能,我们需要对 db.json 的数据结构做一点小小的升级:

  1. 为用户添加一个 age 字段,用于演示 数字排序
  2. country 字段的随机值范围缩小,以便于我们进行 分类筛选

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
// 预设的国家列表,方便筛选
const countries = ["中国", "美国", "日本", "英国", "澳大利亚"];
for (let i = 0; i < count; i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
// 新增:年龄字段,用于排序
age: faker.number.int({
min: 20,
max: 60,
}),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.helpers.arrayElement(countries),
});
}
return users;
};

修改后,请务必再次运行 pnpm mock:generate 更新您的 db.json 文件,然后重启 pnpm mock:api 服务。


第二步:实现交互功能

现在,我们将创建一个新的表格组件,并在 columns 定义和 Table 属性中添加交互配置。

文件路径: src/components/demos/InteractiveTableDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useEffect, useState } from "react";
import { Table, Button, Space, message } from "antd";
import type { TableColumnsType, TableProps } from "antd";
// TypeScript 接口定义
interface User {
id: string;
name: string;
email: string;
age: number;
jobTitle: string;
country: string;
}

const InteractiveTableDemo: React.FC = () => {
// 状态管理
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);

// 表格列定义
const columns: TableColumnsType<User> = [
{
title: "姓名",
dataIndex: "name",
key: "name",
},
{
title: "邮箱",
dataIndex: "email",
key: "email",
},
{
title: "年龄",
dataIndex: "age",
key: "age",
sorter: (record1, record2) => record1.age - record2.age,
},
{
title: "职位",
dataIndex: "jobTitle",
key: "jobTitle",
},
{
title: "国家",
dataIndex: "country",
key: "country",
filters: [
{ text: "中国", value: "中国" },
{ text: "美国", value: "美国" },
{ text: "英国", value: "英国" },
],
// onFilter会接受两个属性:
// value: 当前筛选的值
// record: 当前行数据
onFilter: (value, record) => record.country.includes(value as string),
},
{
title: "操作",
key: "action",
render: (text, record) => (
<Space>
<Button type="link">编辑</Button>
<Button type="link">删除</Button>
</Space>
),
},
];

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
message.error("Error fetching data");
} finally {
setLoading(false);
}
};
fetchData();
}, []);

const handleBulkAction = () => {
alert(`即将对 ${selectedRowKeys.length} 个用户进行批量操作!`);
};

const hasSelected: boolean = selectedRowKeys.length > 0;

// 3. 定义行选择相关的处理函数
const onSelectChange = (newSelectdRowKeys: React.Key[]) => {
message.info(`当前选中的行: ${newSelectdRowKeys}`);
setSelectedRowKeys(newSelectdRowKeys);
};

// 4. 定义 rowSelection 对象
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-4xl">
<h3 className="text-xl font-bold mb-4 text-center">
交互式表格 (排序、筛选、选择)
</h3>

<Space className="mb-4">
<Button
type="primary"
onClick={handleBulkAction}
disabled={!hasSelected}
>
批量操作
</Button>
<span className="ml-2">
{hasSelected ? `已选择 ${selectedRowKeys.length} 项` : ""}
</span>
</Space>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
bordered
pagination={{
pageSize: 5,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
}}
size="small"
/>
</div>
);
};

export default InteractiveTableDemo;

第三步:重点解析

  1. 排序 (sorter)

    • 我们在需要排序的列定义中添加了 sorter 属性。
    • sorter: (a, b) => a.age - b.age 是一个 比较函数Table 组件会用它在 客户端 对数据进行排序。它的工作方式与 JavaScript 的 Array.prototype.sort() 完全相同。对于字符串,我们使用 localeCompare 来进行正确的比较。
  2. 筛选 (filters & onFilter)

    • filters: 一个对象数组,定义了筛选菜单中可用的选项。text 是显示给用户的文本,value 是筛选时真正使用的值。
    • onFilter: 一个函数,用于执行筛选逻辑。Table 会遍历 dataSource 中的每一条 record,并用 filters 中选中的 value 来调用此函数。如果函数返回 true,该行数据则被保留。
  3. 行选择 (rowSelection)

    • 行选择是一个典型的 受控 功能。
    • 我们必须提供一个 selectedRowKeys 数组(来自我们的 state)来告诉 Table 当前哪些行被选中。
    • 我们必须提供一个 onChange 回调函数 (onSelectChange)。当用户勾选或取消勾选时,Table 会调用这个函数,并传入 最新的 selectedRowKeys 数组。我们的职责就是在这个回调里,用 setSelectedRowKeys 来更新我们的 state,从而完成数据流的闭环。

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
// import BasicTableDemo from './components/demos/BasicTableDemo';
import InteractiveTableDemo from './components/demos/InteractiveTableDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicTableDemo /> */}
<InteractiveTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

现在,您的表格已经从一个静态的“展示板”变成了一个动态的“工作台”。用户可以自由地对数据进行排序、筛选和选择。

Part 3 - 高级篇 中,我们将探索 Table 更深层次的自定义能力,包括:自定义单元格渲染、固定头与列、可展开行


7.14.5. Table (Part 3 - 高级篇): 固定列、自定义渲染与服务端数据

在前两篇中,我们已经掌握了表格的数据绑定和核心交互。现在,我们将进入企业级应用中最为常见的复杂场景:处理列数超多的宽表格,以及在单元格内渲染自定义组件(如操作按钮、状态标签)。


第一步:升级 Mock API - 构造一个宽表格数据源

为了模拟真实的后台管理场景,一个表格往往有十几个甚至更多的列。我们需要为 users 数据添加更多字段,以便创建一个需要横向滚动的“宽表格”。

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
// 预设的国家列表,方便筛选
const countries = ["中国", "美国", "日本", "英国", "澳大利亚"];
for (let i = 0; i < count; i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
// 新增:年龄字段,用于排序
age: faker.number.int({
min: 20,
max: 60,
}),
// 新增:更多字段
phone: faker.phone.number(),
status: faker.helpers.arrayElement(["active", "pending", "inactive"]),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.helpers.arrayElement(countries),
createdAt: faker.date.past(2).toISOString(), // 创建日期
});
}
return users;
};

同样,修改后请再次运行 pnpm mock:generate 更新 db.json,并重启 pnpm mock:api 服务。


第二步:实战演练 - 构建一个带固定列和自定义渲染的表格

我们将综合运用 renderscrollfixed 三个核心 API,来构建一个功能强大的高级表格。

核心 API:

  • render: 列定义中的自定义渲染函数。它允许我们返回任意 React 节点,而不仅仅是文本。render 函数接收三个参数 (text, record, index),其中 record 代表 当前行的完整数据对象,这对于构建“操作”列至关重要。
  • scroll: Table 的顶层属性,用于设置表格的滚动行为。当 scroll.x 的值超过表格容器的宽度时,将出现横向滚动条。
  • fixed: 列定义中的属性,用于固定列。可设为 'left''right',使该列在水平滚动时保持固定。注意:要使 fixed 生效,必须先设置 scroll.x

文件路径: src/components/demos/AdvancedTableDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import React, { useEffect, useState } from "react";
import { Table, Tag, Space, Button } from "antd";
import type { TableColumnsType } from "antd";
import dayjs from "dayjs";

// 更新 TypeScript 类型以匹配新数据
interface User {
id: string;
name: string;
email: string;
age: number;
jobTitle: string;
country: string;
status: "active" | "pending" | "inactive";
createdAt: string;
}

const AdvancedTableDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);
// 1. 定义列,并添加 render, fixed, width 等高级属性
const columns: TableColumnsType<User> = [
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "邮箱", dataIndex: "email", key: "email" },
{
title: "年龄",
dataIndex: "age",
key: "age",
sorter: (a, b) => a.age - b.age,
},
{ title: "职位", dataIndex: "jobTitle", key: "jobTitle" },
{
title: "国家",
dataIndex: "country",
key: "country",
filters: [
{ text: "中国", value: "中国" },
{ text: "美国", value: "美国" },
{ text: "英国", value: "英国" },
],
onFilter: (value, record) => record.country.includes(value as string),
},

{
title: "状态",
dataIndex: "status",
key: "status",
filters: [
{ text: "活跃", value: "active" },
{ text: "待处理", value: "pending" },
{ text: "禁用", value: "inactive" },
],
onFilter: (value, record) => record.status === value,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
render: (text) => dayjs(text).format("YYYY-MM-DD HH:mm:ss"),
},
{
title: "操作",
dataIndex: "action",
key: "action",
fixed: "right", // 固定在右侧
width: 100, // 设置宽度
render: (text, record) => (
<Space>
<Button type="link">编辑</Button>
<Button type="link">删除</Button>
</Space>
),
},
];

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-6xl">
<h3 className="text-xl font-bold mb-4 text-center">
高级表格 (固定列与自定义渲染)
</h3>
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
bordered
size="small"
pagination={{
pageSize: 5,
}}
// 2. 设置 scroll.x 使表格可横向滚动
scroll={{ x: 1500 }}
/>
</div>
);
};

export default AdvancedTableDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
// import InteractiveTableDemo from './components/demos/InteractiveTableDemo';
import AdvancedTableDemo from './components/demos/AdvancedTableDemo';


const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <InteractiveTableDemo /> */}
<AdvancedTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

7.14.3. Tree: 层级数据的展示与交互

Tree (树形控件) 是专门用于 展示和操作层级(或嵌套)数据 的组件。当您的数据本身具有父子关系时,例如文件目录、组织架构、商品分类等,使用 Tree 组件可以直观地反映这种结构,并提供展开、收起、选择等丰富的交互功能。

核心应用场景:

  • 文件/目录浏览器:以树状结构展示文件夹和文件。
  • 组织架构:展示公司的部门层级关系。
  • 权限管理:在树状的权限列表中,为角色勾选其拥有的权限。
  • 多级分类选择:在电商后台,为商品选择其所属的多级分类。

第一步:基础用法 - treeData 与受控操作

CollapseDescriptions 等现代 antd 组件一样,构建 Tree 的最佳实践是使用 treeData 属性,以 数据驱动 的方式进行渲染。同时,Tree 的核心交互(如展开、选中)都是通过 受控模式 来管理的。

核心属性:

  • treeData: 一个对象数组,用于定义整个树的结构。每个对象的核心属性为:
    • title: 节点的标题。
    • key: 节点的唯一标识符,在整棵树中必须唯一
    • children: 子节点数组,其结构与父级相同。
  • expandedKeys: (受控) 一个包含 key 的数组,用于控制当前哪些节点是展开的。
  • selectedKeys: (受控) 一个包含 key 的数组,用于控制当前哪些节点是被选中的。
  • onExpand: 展开/收起节点时的回调函数,我们需要在此函数中更新 expandedKeys state。
  • onSelect: 点击并选中节点时的回调函数,我们需要在此函数中更新 selectedKeys state。

文件路径: src/components/demos/BasicTreeDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, from 'react';
import { Tree } from 'antd';
import type { DataNode, TreeProps } from 'antd/es/tree';

// 1. 定义 treeData 的数据结构
const treeData: DataNode[] = [
{
title: '总部',
key: '0-0',
children: [
{
title: '研发部',
key: '0-0-0',
children: [
{ title: '前端组', key: '0-0-0-0' },
{ title: '后端组', key: '0-0-0-1' },
],
},
{
title: '产品部',
key: '0-0-1',
children: [{ title: '产品设计组', key: '0-0-1-0' }],
},
],
},
];

const BasicTreeDemo: React.FC = () => {
// 2. 使用 state 管理 expandedKeys 和 selectedKeys
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>(['0-0', '0-0-0']);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);

// 3. 定义 onExpand 和 onSelect 回调来更新 state
const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
console.log('onExpand', expandedKeysValue);
setExpandedKeys(expandedKeysValue);
};

const onSelect: TreeProps['onSelect'] = (selectedKeysValue, info) => {
console.log('onSelect', info);
setSelectedKeys(selectedKeysValue);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[400px]">
<h3 className="text-xl font-bold mb-4 text-center">基础树与受控操作</h3>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={treeData}
defaultExpandAll // 方便演示默认展开所有
/>
</div>
);
};

export default BasicTreeDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTreeDemo from './components/demos/BasicTreeDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTreeDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:复选框与父子联动 (checkable & checkStrictly)

Tree 组件最强大的应用场景之一就是权限分配,这需要为每个节点提供复选框。checkable 属性可以开启此功能,而 checkStrictly 属性则用于控制父子节点之间的联动逻辑。

核心属性:

  • checkable: 布尔值,设置为 true 可为每个节点添加复选框。
  • checkedKeys: (受控) 选中复选框的 key 数组。
  • onCheck: 点击复选框时的回调函数,用于更新 checkedKeys state。
  • checkStrictly: 布尔值,用于控制父子节点的勾选行为是否关联。

checkStrictly 的两种模式

  • false (默认): 父子联动模式。勾选父节点,会自动勾选其所有子孙节点;勾选一个父节点下的所有子节点,父节点会自动变为勾选状态。这是最常见的“权限包”逻辑。
  • true: 父子解绑模式。每个节点的勾选状态都是完全独立的,互不影响。在这种模式下,checkedKeys 属性需要传入一个对象 { checked: string[], halfChecked: string[] },以区分全选和半选状态。

文件路径: src/components/demos/CheckableTreeDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import React, { useState } from "react";
import { Tree, Divider, message } from "antd";
import type { DataNode, TreeProps } from "antd/es/tree";

const treeData: DataNode[] = [
{
title: "系统权限",
key: "0-0",
children: [
{
title: "用户管理",
key: "0-0-0",
children: [
{ title: "查看用户", key: "0-0-0-0" },
{ title: "编辑用户", key: "0-0-0-1" },
],
},
{ title: "文章管理", key: "0-0-1" },
],
},
];

const CheckableTreeDemo: React.FC = () => {
// 默认联动模式的 state
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>(["0-0-0-0"]);
// 严格模式的 state
const [strictlyCheckedKeys, setStrictlyCheckedKeys] = useState<React.Key[]>([
"0-0-1",
]);

const onCheck: TreeProps["onCheck"] = (checkedKeysValue) => {
message.info(`checked: ${checkedKeysValue}`);
setCheckedKeys(checkedKeysValue as React.Key[]);
};

const onStrictlyCheck: TreeProps["onCheck"] = (checkedKeysValue) => {
message.info(`checked: ${checkedKeysValue}`);
// 在 checkStrictly 模式下,checkedKeysValue 是一个对象或数组,我们只关心它的 checked 部分
const keys = Array.isArray(checkedKeysValue)
? checkedKeysValue
: checkedKeysValue.checked;
setStrictlyCheckedKeys(keys);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg flex gap-8">
<div className="w-[300px]">
<h3 className="text-lg font-bold mb-2">父子联动 (默认)</h3>
<Tree
checkable
onCheck={onCheck}
checkedKeys={checkedKeys}
treeData={treeData}
defaultExpandAll
/>
</div>
<Divider type="vertical" style={{ height: "auto" }} />
<div className="w-[300px]">
<h3 className="text-lg font-bold mb-2">父子解绑 (checkStrictly)</h3>
<Tree
checkable
checkStrictly
onCheck={onStrictlyCheck}
checkedKeys={strictlyCheckedKeys}
treeData={treeData}
defaultExpandAll
/>
</div>
</div>
);
};

export default CheckableTreeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicTreeDemo from './components/demos/BasicTreeDemo';
import CheckableTreeDemo from './components/demos/CheckableTreeDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicTreeDemo /> */}
<CheckableTreeDemo />
</div>
</ConfigProvider>
);
};