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' ;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 } }), avatar : faker.image .avatar (), description : faker.word .words ({ count : { min : 20 , max : 40 } }), content : faker.word .words ({ count : { min : 100 , max : 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" } ,
现在,依次执行以下命令:
pnpm mock:generate
(只需执行一次,用于生成 db.json
)pnpm mock:api
(启动 API 服务)您的 API http://localhost:3001/news
现在已经可以提供 50 条新闻数据了。
第二步:基础列表与数据绑定 List
组件的核心是 数据驱动 。我们需要从 API 获取数据,并将其传递给 List
,然后通过 renderItem
函数定义每一项的渲染方式。
核心属性 :
dataSource
: 列表的数据源数组 。renderItem
: 渲染列表中每一项的函数 。该函数接收 item
和 index
作为参数,返回一个 React 节点。loading
: 布尔值,用于控制列表的加载状态,true
时会显示骨架屏。List.Item
/ List.Item.Meta
: 用于快速构建标准列表项结构的辅助组件。文件路径 : src/components/demos/BasicListDemo.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 import React , { useEffect, useState } from "react" ;import { Avatar , List } from "antd" ;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
(新建文件)
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 AdvancedListDemo from './components/demos/AdvancedListDemo' ;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 > ); };
通过 pagination
和 grid
属性的简单配置,我们就能轻松地将一个长列表,转化为带分页的、响应式的卡片网格,这在构建产品展示墙、图片库等场景中极为高效。List
组件的灵活性和可扩展性,使其成为处理序列数据的首选方案。
7.14.3. Table (Part 1 - 基础篇): 构建你的第一个数据表格 Table
(表格) 是所有后台管理、数据分析、信息管理类应用的心脏。当您的数据拥有多个维度(列),并且需要进行结构化的行列展示时,Table
便无可替代。相比于 List
,Table
提供了列对齐、排序、筛选、分页、行选择等一系列专为复杂数据集设计的强大功能。
在本篇中,我们将专注于掌握 Table
的两大基石,并结合 json-server
构建一个功能完备的、带 异步加载 和 客户端分页 功能的基础表格。
第一步:两大核心概念 - dataSource
与 columns
要渲染一个 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
(新建文件)
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" ;interface User { id : string ; name : string ; email : string ; jobTitle : string ; country : string ; } 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
是一个独立的组件,但它最常以内置的形式,通过 Table
或 List
的 pagination
属性进行使用。
客户端分页 (我们当前使用的) : 当您的 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
最核心的 dataSource
和 columns
配置,并学会了如何处理异步加载和实现客户端分页。
在 Part 2 - 交互篇 中,我们将在此基础上,为表格添加 排序、筛选和行选择 等强大的交互功能。
7.14.4. Table (Part 2 - 交互篇): 排序、筛选与选择 在基础篇中,我们成功地将数据显示在了表格里。但一个真正的企业级表格,远不止于静态展示,它必须是 可交互的 。用户需要能够根据自己的需求,对数据进行排序、筛选和选择,以便快速定位和处理信息。
在本篇中,我们将为上一节的表格添加三种最核心的交互能力:
排序 (sorter
) : 允许用户点击表头,对该列数据进行升序或降序排列。筛选 (filters
) : 提供一个筛选菜单,让用户可以根据特定条件过滤数据。行选择 (rowSelection
) : 允许用户勾选一行或多行,以进行批量操作。第一步:升级 Mock API - 添加可排序与筛选的数据 为了更好地演示排序和筛选功能,我们需要对 db.json
的数据结构做一点小小的升级:
为用户添加一个 age
字段,用于演示 数字排序 。 将 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
(新建文件)
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" ;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 ) => 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 ; const onSelectChange = (newSelectdRowKeys : React .Key [] ) => { message.info (`当前选中的行: ${newSelectdRowKeys} ` ); setSelectedRowKeys (newSelectdRowKeys); }; 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 ;
第三步:重点解析 排序 (sorter
)
我们在需要排序的列定义中添加了 sorter
属性。 sorter: (a, b) => a.age - b.age
是一个 比较函数 ,Table
组件会用它在 客户端 对数据进行排序。它的工作方式与 JavaScript 的 Array.prototype.sort()
完全相同。对于字符串,我们使用 localeCompare
来进行正确的比较。筛选 (filters
& onFilter
)
filters
: 一个对象数组,定义了筛选菜单中可用的选项。text
是显示给用户的文本,value
是筛选时真正使用的值。onFilter
: 一个函数,用于执行筛选逻辑。Table
会遍历 dataSource
中的每一条 record
,并用 filters
中选中的 value
来调用此函数。如果函数返回 true
,该行数据则被保留。行选择 (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 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
服务。
第二步:实战演练 - 构建一个带固定列和自定义渲染的表格 我们将综合运用 render
、scroll
和 fixed
三个核心 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" ;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 []>([]); 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 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
与受控操作 与 Collapse
、Descriptions
等现代 antd 组件一样,构建 Tree
的最佳实践是使用 treeData
属性,以 数据驱动 的方式进行渲染。同时,Tree
的核心交互(如展开、选中)都是通过 受控模式 来管理的。
核心属性 :
treeData
: 一个对象数组,用于定义整个树的结构。每个对象的核心属性为:title
: 节点的标题。key
: 节点的唯一标识符,在整棵树中必须唯一 。children
: 子节点数组,其结构与父级相同。expandedKeys
: (受控) 一个包含 key
的数组,用于控制当前哪些节点是展开的。selectedKeys
: (受控) 一个包含 key
的数组,用于控制当前哪些节点是被选中的。onExpand
: 展开/收起节点时的回调函数,我们需要在此函数中更新 expandedKeys
state。onSelect
: 点击并选中节点时的回调函数,我们需要在此函数中更新 selectedKeys
state。文件路径 : src/components/demos/BasicTreeDemo.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 import React , from 'react' ;import { Tree } 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' , children : [{ title : '产品设计组' , key : '0-0-1-0' }], }, ], }, ]; const BasicTreeDemo : React .FC = () => { const [expandedKeys, setExpandedKeys] = useState<React .Key []>(['0-0' , '0-0-0' ]); const [selectedKeys, setSelectedKeys] = useState<React .Key []>([]); 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
(新建文件)
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 = () => { const [checkedKeys, setCheckedKeys] = useState<React .Key []>(["0-0-0-0" ]); 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} ` ); 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 CheckableTreeDemo from './components/demos/CheckableTreeDemo' ;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 > ); };