#DataTable 表格
浏览、筛选和管理结构化数据。
#安装与导入
DataTable 传入数据与列即可使用排序和分页;复杂场景传入自己的 TanStack Table 实例,获得筛选、选择、展开和列管理的统一界面。使用前安装依赖:
npm install @tanstack/react-table@^8.21.3import { DataTable } from '@ifhover/leaf-ui/table';
import type { DataTableProps, DataTableColumnMeta } from '@ifhover/leaf-ui/table';表格子路径自动加载所需样式。DataTable 及其数据类型从 @ifhover/leaf-ui/table 导入,DataTable 不从组件库根入口导出。
#基础用法
| Alex Morgan | alex@example.com | Design | 活跃 |
| Sam Taylor | sam@example.com | Engineering | 活跃 |
| Jordan Lee | jordan@example.com | Product | 已邀请 |
import { Tag } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
const people = [
{ id: '1', name: 'Alex Morgan', email: 'alex@example.com', role: 'Design', active: true },
{ id: '2', name: 'Sam Taylor', email: 'sam@example.com', role: 'Engineering', active: true },
{ id: '3', name: 'Jordan Lee', email: 'jordan@example.com', role: 'Product', active: false },
];
export function TableBasic() {
return (
<DataTable
data={people}
columns={[
{ accessorKey: 'name', header: '成员' },
{ accessorKey: 'email', header: '邮箱' },
{ accessorKey: 'role', header: '团队' },
{
accessorKey: 'active',
header: '状态',
cell: ({ row }) => (
<Tag color={row.original.active ? 'success' : 'default'}>
{row.original.active ? ('活跃') : '已邀请'}
</Tag>
),
},
]}
rowKey="id"
aria-label={'团队成员'}
minWidth={560}
pagination={false}
/>
);
}
data + columns + rowKey 模式内部创建一个 TanStack 实例,复用与高级模式相同的界面。rowKey 必须是稳定且唯一的字符串或数字字段,或返回字符串的函数。默认启用本地排序、筛选、分页和展开;使用 options 定制初始状态、受控回调或服务端配置。pagination={false} 时展示全部行,不在内部截断数据。
两种模式互斥:提供 table 后不要再传 data、columns、rowKey 或 options。高级分组、请求缓存等业务状态建议直接使用自己的 useReactTable 实例。
#边框与斑马纹
DataTable 默认无外框、无列分隔线,并开启 striped 斑马纹。无边框模式下没有圆角和表头灰底,表头底部保留 1px 分割线;设置 bordered 可展示带圆角的外框、白色表头和列分隔线(深色主题跟随深色表面)。两种模式的表头文字均加粗并略淡于正文,排序图标紧跟文字。斑马纹从第一行开始使用淡色背景,开启时不显示行间分割线,设置 striped={false} 后显示行间细线。普通行与条纹行悬停时均显示轻微的主色反馈。
| Alex Morgan | Design |
| Sam Taylor | Engineering |
| Jordan Lee | Product |
import { Checkbox, Space } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { useState } from 'react';
const people = [
{ name: 'Alex Morgan', team: 'Design' },
{ name: 'Sam Taylor', team: 'Engineering' },
{ name: 'Jordan Lee', team: 'Product' },
];
export function TableAppearance() {
const [bordered, setBordered] = useState(false);
const [striped, setStriped] = useState(true);
return (
<Space direction="vertical" align="stretch">
<Space wrap>
<Checkbox checked={bordered} onChange={(event) => setBordered(event.target.checked)}>
{'边框'}
</Checkbox>
<Checkbox checked={striped} onChange={(event) => setStriped(event.target.checked)}>
{'斑马纹'}
</Checkbox>
</Space>
<DataTable
data={people}
columns={[
{ accessorKey: 'name', header: '成员' },
{ accessorKey: 'team', header: '团队' },
]}
rowKey="name"
pagination={false}
aria-label={'表格外观'}
bordered={bordered}
striped={striped}
/>
</Space>
);
}
#排序、筛选、选择与分页
| 展开行 | ||||||
|---|---|---|---|---|---|---|
AAlex Morgan | member1@example.com | Design | 1 | 活跃 | ||
SSam Taylor | member2@example.com | Engineering | 8 | 活跃 | ||
JJordan Lee | member3@example.com | Product | 15 | 已邀请 | ||
RRiley Chen | member4@example.com | Design | 4 | 活跃 | ||
CCasey Wilson | member5@example.com | Engineering | 11 | 活跃 | ||
import { Avatar, Tag } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { type ColumnDef, type ColumnFiltersState, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type PaginationState, type RowSelectionState, type SortingState, useReactTable } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
export interface Member {
id: string;
name: string;
email: string;
team: string;
projects: number;
active: boolean;
}
export const members: Member[] = Array.from({ length: 26 }, (_, index) => ({
id: `member-${index + 1}`,
name:
['Alex Morgan', 'Sam Taylor', 'Jordan Lee', 'Riley Chen', 'Casey Wilson', 'Jamie Park'][
index % 6
] ?? '',
email: `member${index + 1}@example.com`,
team: ['Design', 'Engineering', 'Product'][index % 3] ?? '',
projects: ((index * 7) % 18) + 1,
active: index % 4 !== 2,
}));
export function useMemberColumns(english = false) {
return useMemo<ColumnDef<Member>[]>(
() => [
{
accessorKey: 'name',
header: english ? 'Member' : '成员',
size: 230,
minSize: 160,
maxSize: 400,
enableHiding: false,
cell: ({ row }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Avatar size={30}>{row.original.name.slice(0, 1)}</Avatar>
<strong>{row.original.name}</strong>
</div>
),
},
{
accessorKey: 'email',
header: english ? 'Email' : '邮箱',
size: 250,
minSize: 180,
maxSize: 400,
},
{
accessorKey: 'team',
header: english ? 'Team' : '团队',
size: 150,
minSize: 100,
maxSize: 240,
},
{
accessorKey: 'projects',
header: english ? 'Projects' : '项目数',
size: 110,
minSize: 90,
maxSize: 160,
meta: { align: 'end' },
},
{
accessorKey: 'active',
header: english ? 'Status' : '状态',
size: 110,
minSize: 100,
maxSize: 160,
cell: ({ getValue }) => (
<Tag color={getValue() ? 'success' : 'default'}>
{getValue() ? (english ? 'Active' : '活跃') : english ? 'Invited' : '已邀请'}
</Tag>
),
},
],
[english],
);
}
export function TableData() {
const columns = useMemberColumns(false);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5 });
const table = useReactTable({
data: members,
columns,
getRowId: (member) => member.id,
state: { sorting, rowSelection, columnFilters, pagination },
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand: () => true,
enableRowSelection: (row) => row.original.active,
});
return (
<DataTable
table={table}
aria-label={'团队成员列表'}
selection="multiple"
getRowLabel={(row) => `${row.original.name} (${row.original.id})`}
renderExpandedRow={(row) => (
<p style={{ margin: 0 }}>
{'联系邮箱'}: {row.original.email} · {'所属团队'}
: {row.original.team}
</p>
)}
>
<DataTable.Toolbar>
<DataTable.Search columnId="name" />
<span style={{ marginInlineStart: 'auto', color: 'var(--leaf-color-text-muted)' }}>
{Object.values(rowSelection).filter(Boolean).length} {'项已选择'}
</span>
<DataTable.ColumnVisibility />
</DataTable.Toolbar>
<DataTable.Content />
<DataTable.Footer>
<DataTable.Pagination
pageSizeOptions={[5, 10, 20]}
showTotal={(total) => (`共 ${total} 位成员`)}
/>
</DataTable.Footer>
</DataTable>
);
}
table 是唯一的数据状态来源。Leaf UI 不重新排序或分页,不另建 selectedRowKeys、筛选和展开状态。需要哪种能力,就在 useReactTable 中配置对应的行模型:
| 能力 | TanStack 配置 |
|---|---|
| 基础数据 | getCoreRowModel() |
| 本地排序 | getSortedRowModel() |
| 本地搜索与筛选 | getFilteredRowModel() |
| 本地分页 | getPaginationRowModel() |
| 树形或分组展开 | getExpandedRowModel(),树形数据同时配置 getSubRows |
| 行详情 | getRowCanExpand,以及 DataTable 的 renderExpandedRow |
| 分组与合计 | getGroupedRowModel()、列的 aggregationFn 与 aggregatedCell |
使用稳定的 getRowId,不要用排序或分页后的数组下标。默认全选只影响当前页可选择的行;selectAllMode="all" 作用于实例已加载并参与筛选的数据,不代表全选服务端尚未加载的记录。跨页选择由 TanStack 的 rowSelection 保存;服务端场景通过这些 key 处理业务,不要只依赖当前页的 getSelectedRowModel()。
DataTable.Search 默认更新全局搜索,指定 columnId 后只筛选该列。DataTable.ColumnVisibility 保留 enableHiding: false 的列。Shift 点击表头可多列排序;aria-sort 标记第一排序列。
#固定表头、固定列与列宽
AAlex Morgan | member1@example.com | Design | 1 | 活跃 |
SSam Taylor | member2@example.com | Engineering | 8 | 活跃 |
JJordan Lee | member3@example.com | Product | 15 | 已邀请 |
RRiley Chen | member4@example.com | Design | 4 | 活跃 |
CCasey Wilson | member5@example.com | Engineering | 11 | 活跃 |
JJamie Park | member6@example.com | Product | 18 | 活跃 |
AAlex Morgan | member7@example.com | Design | 7 | 已邀请 |
SSam Taylor | member8@example.com | Engineering | 14 | 活跃 |
JJordan Lee | member9@example.com | Product | 3 | 活跃 |
RRiley Chen | member10@example.com | Design | 10 | 活跃 |
CCasey Wilson | member11@example.com | Engineering | 17 | 已邀请 |
JJamie Park | member12@example.com | Product | 6 | 活跃 |
AAlex Morgan | member13@example.com | Design | 13 | 活跃 |
SSam Taylor | member14@example.com | Engineering | 2 | 活跃 |
JJordan Lee | member15@example.com | Product | 9 | 已邀请 |
RRiley Chen | member16@example.com | Design | 16 | 活跃 |
CCasey Wilson | member17@example.com | Engineering | 5 | 活跃 |
JJamie Park | member18@example.com | Product | 12 | 活跃 |
AAlex Morgan | member19@example.com | Design | 1 | 已邀请 |
SSam Taylor | member20@example.com | Engineering | 8 | 活跃 |
JJordan Lee | member21@example.com | Product | 15 | 活跃 |
RRiley Chen | member22@example.com | Design | 4 | 活跃 |
CCasey Wilson | member23@example.com | Engineering | 11 | 已邀请 |
JJamie Park | member24@example.com | Product | 18 | 活跃 |
AAlex Morgan | member25@example.com | Design | 7 | 活跃 |
SSam Taylor | member26@example.com | Engineering | 14 | 活跃 |
import { Avatar, Button, ConfigProvider, Tag } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { type ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo } from 'react';
export interface Member {
id: string;
name: string;
email: string;
team: string;
projects: number;
active: boolean;
}
export const members: Member[] = Array.from({ length: 26 }, (_, index) => ({
id: `member-${index + 1}`,
name:
['Alex Morgan', 'Sam Taylor', 'Jordan Lee', 'Riley Chen', 'Casey Wilson', 'Jamie Park'][
index % 6
] ?? '',
email: `member${index + 1}@example.com`,
team: ['Design', 'Engineering', 'Product'][index % 3] ?? '',
projects: ((index * 7) % 18) + 1,
active: index % 4 !== 2,
}));
export function useMemberColumns(english = false) {
return useMemo<ColumnDef<Member>[]>(
() => [
{
accessorKey: 'name',
header: english ? 'Member' : '成员',
size: 230,
minSize: 160,
maxSize: 400,
enableHiding: false,
cell: ({ row }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Avatar size={30}>{row.original.name.slice(0, 1)}</Avatar>
<strong>{row.original.name}</strong>
</div>
),
},
{
accessorKey: 'email',
header: english ? 'Email' : '邮箱',
size: 250,
minSize: 180,
maxSize: 400,
},
{
accessorKey: 'team',
header: english ? 'Team' : '团队',
size: 150,
minSize: 100,
maxSize: 240,
},
{
accessorKey: 'projects',
header: english ? 'Projects' : '项目数',
size: 110,
minSize: 90,
maxSize: 160,
meta: { align: 'end' },
},
{
accessorKey: 'active',
header: english ? 'Status' : '状态',
size: 110,
minSize: 100,
maxSize: 160,
cell: ({ getValue }) => (
<Tag color={getValue() ? 'success' : 'default'}>
{getValue() ? (english ? 'Active' : '活跃') : english ? 'Invited' : '已邀请'}
</Tag>
),
},
],
[english],
);
}
export function TableColumns() {
const columns = useMemberColumns(false);
const table = useReactTable({
data: members,
columns,
getRowId: (member) => member.id,
getCoreRowModel: getCoreRowModel(),
columnResizeMode: 'onChange',
initialState: { columnPinning: { left: ['name'], right: ['active'] } },
});
return (
<ConfigProvider density="compact">
<DataTable
table={table}
aria-label={'固定列与列宽调整'}
stickyHeader
maxHeight={300}
resizable
pagination={false}
>
<DataTable.Toolbar>
<span style={{ color: 'var(--leaf-color-text-muted)' }}>
{'拖动表头右侧边缘,或聚焦边缘后用方向键调整列宽。'}
</span>
<Button
style={{ marginInlineStart: 'auto' }}
size="sm"
variant="soft"
onClick={() => table.resetColumnSizing()}
>
{'重置列宽'}
</Button>
</DataTable.Toolbar>
<DataTable.Content />
</DataTable>
</ConfigProvider>
);
}
通过 columnPinning 控制固定列,columnOrder 控制列顺序,列的 size、minSize 和 maxSize 控制宽度。left / right 在 Leaf UI 中对应逻辑起始 / 结束侧,跟随 RTL 方向。RTL 场景同时给 TanStack 配置 columnResizeDirection: 'rtl'。
表格默认填满所在容器,列所需宽度超过容器时才在内部横向滚动。表头默认固定;设置 maxHeight 后,超出的行在表体内滚动。悬浮滚动条使用 Scrollbar,纵向条从表头下方开始;未溢出或到达滚动边界时,滚轮继续滚动页面。
列宽手柄可拖动,也可聚焦后用左右方向键调整;Shift 增大步长,Home / End 跳到边界,双击恢复初始宽度。建议给可调整列设置合理的最大宽度。ConfigProvider 的 density 和 theme.components.Table 控制整个区域的密度、圆角、内边距、字号、颜色、背景和阴影。
#分组与聚合
| 展开行 | |||
|---|---|---|---|
| Design (3) | 合计: 12 | ||
Alex Morgan | 1 | ||
Riley Chen | 4 | ||
Alex Morgan | 7 | ||
| Engineering (3) | 合计: 33 | ||
Sam Taylor | 8 | ||
Casey Wilson | 11 | ||
Sam Taylor | 14 | ||
| Product (2) | 合计: 33 | ||
Jordan Lee | 15 | ||
Jamie Park | 18 |
import { Tag } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { type ColumnDef, getCoreRowModel, getExpandedRowModel, getGroupedRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo } from 'react';
import { type Member, members } from './data';
const groupedMembers = members.slice(0, 8);
export function TableGrouped() {
const columns = useMemo<ColumnDef<Member>[]>(
() => [
{ accessorKey: 'team', header: '团队', size: 150 },
{ accessorKey: 'name', header: '成员', size: 240 },
{
accessorKey: 'projects',
header: '项目数',
aggregationFn: 'sum',
meta: { align: 'end' },
aggregatedCell: ({ getValue }) => (
<Tag>
{'合计'}: {String(getValue())}
</Tag>
),
},
],
[false],
);
const table = useReactTable({
data: groupedMembers,
columns,
getRowId: (member) => member.id,
getCoreRowModel: getCoreRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
initialState: { grouping: ['team'], expanded: true },
});
return (
<DataTable
table={table}
aria-label={'按团队分组的成员'}
pagination={false}
/>
);
}
普通分组表格展开收起时使用高度过渡,保留子行内容和条纹位置。关闭区域动效或启用系统减少动态效果时直接切换;大型或虚拟表格也直接切换。
#编辑单元格
| member1@example.com | Design | |
| member2@example.com | Engineering | |
| member3@example.com | Product |
import { Input } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { type ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import { type Member, members } from './data';
export function TableEditable() {
const [data, setData] = useState(members.slice(0, 3));
const columns = useMemo<ColumnDef<Member>[]>(
() => [
{
accessorKey: 'name',
header: '成员',
size: 240,
cell: ({ row }) => (
<Input
aria-label={`${'姓名'}: ${row.original.id}`}
value={row.original.name}
onChange={(event) => {
const name = event.currentTarget.value;
setData((previous) =>
previous.map((member) =>
member.id === row.original.id ? { ...member, name } : member,
),
);
}}
/>
),
},
{ accessorKey: 'email', header: '邮箱', size: 260 },
{ accessorKey: 'team', header: '团队', size: 140 },
],
[false],
);
const table = useReactTable({
data,
columns,
getRowId: (member) => member.id,
getCoreRowModel: getCoreRowModel(),
});
return (
<DataTable
table={table}
aria-label={'编辑成员姓名'}
pagination={false}
/>
);
}
通过列的 cell 渲染 Input、Select 或其他控件;业务数据、提交和校验仍由应用管理,不建立另一套表格表单仓库。行详情收起后保留已打开内容中的输入状态;未打开的详情不挂载业务内容。
#行和列排序
使用行首和表头的手柄调整顺序。
| 移动行 | 成员 | 团队 | 状态 |
|---|---|---|---|
AAlex Morgan | Design | 活跃 | |
SSam Taylor | Engineering | 活跃 | |
JJordan Lee | Product | 已邀请 |
import { Avatar, Space, Tag } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import type { ColumnDef } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
const members = [
{ id: 'alex', name: 'Alex Morgan', team: 'Design', status: 'active' },
{ id: 'sam', name: 'Sam Taylor', team: 'Engineering', status: 'active' },
{ id: 'jordan', name: 'Jordan Lee', team: 'Product', status: 'invited' },
];
type Member = (typeof members)[number];
export function TableReordering() {
const [data, setData] = useState(members);
const columns = useMemo<ColumnDef<Member>[]>(
() => [
{
accessorKey: 'name',
header: '成员',
size: 220,
cell: ({ row }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Avatar size="sm">{row.original.name[0]}</Avatar>
<strong>{row.original.name}</strong>
</div>
),
},
{ accessorKey: 'team', header: '团队' },
{
accessorKey: 'status',
header: '状态',
cell: ({ getValue }) => (
<Tag color={getValue() === 'active' ? 'success' : 'default'}>
{getValue() === 'active'
? '活跃'
: '已邀请'}
</Tag>
),
},
],
[false],
);
return (
<Space direction="vertical" align="stretch">
<p>
{'使用行首和表头的手柄调整顺序。'}
</p>
<DataTable
data={data}
columns={columns}
rowKey="id"
caption={'团队顺序'}
aria-label={'可排序团队'}
getRowLabel={(row) => row.original.name}
options={{ enableSorting: false }}
pagination={false}
columnReordering
onRowReorder={({ rows }) => setData(rows.map((row) => row.original))}
/>
</Space>
);
}
行首和表头的手柄支持鼠标、触摸和键盘。聚焦后按空格开始,方向键移动,空格确认,Escape 取消。onRowReorder 返回当前页重排后的行与稳定标识,应用负责更新业务数组或提交服务端;列拖拽直接更新 TanStack 的 columnOrder。
行拖拽仅用于未排序、未筛选、未分组、未展开的平面非虚拟表格,其他模式不展示行手柄,避免视觉顺序和业务顺序冲突。列拖拽仅支持单层表头且不跨越固定列分区;多层表头仍可通过 columnOrder 明确控制。拖拽不会修改行选择或编辑器状态。
#大数据
MMember 00001 | member1@example.com | Design | 1 | 活跃 |
MMember 00002 | member2@example.com | Engineering | 8 | 活跃 |
MMember 00003 | member3@example.com | Product | 15 | 已邀请 |
MMember 00004 | member4@example.com | Design | 4 | 活跃 |
MMember 00005 | member5@example.com | Engineering | 11 | 活跃 |
MMember 00006 | member6@example.com | Product | 18 | 活跃 |
MMember 00007 | member7@example.com | Design | 7 | 已邀请 |
MMember 00008 | member8@example.com | Engineering | 14 | 活跃 |
MMember 00009 | member9@example.com | Product | 3 | 活跃 |
MMember 00010 | member10@example.com | Design | 10 | 活跃 |
MMember 00011 | member11@example.com | Engineering | 17 | 已邀请 |
MMember 00012 | member12@example.com | Product | 6 | 活跃 |
import { DataTable } from '@ifhover/leaf-ui/table';
import { getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { type Member, members, useMemberColumns } from './data';
const manyMembers: Member[] = Array.from({ length: 10000 }, (_, index) => ({
...(members[index % members.length] as Member),
id: String(index),
name: `Member ${String(index + 1).padStart(5, '0')}`,
}));
export function TableVirtual() {
const columns = useMemberColumns(false);
const table = useReactTable({
data: manyMembers,
columns,
getRowId: (member) => member.id,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<DataTable
table={table}
aria-label={'10000 位成员'}
virtual={{ estimateRowHeight: 55, overscan: 6 }}
stickyHeader
maxHeight={320}
pagination={false}
/>
);
}
virtual 是显式的行虚拟化,默认不开启。为滚动区域设置 maxHeight,并按实际布局设置估算行高;动态高度会被重新测量,焦点所在的行不会因滚动而立即卸载。SSR 首屏输出估算窗口,客户端接管后测量实际高度。
这不是电子表格式 DataGrid:不提供方向键逐单元格导航、列虚拟化或自动移动端卡片化。普通表格维持原生语义和横向滚动;需要浏览全部数据的屏幕阅读器用户可关闭虚拟化,改用分页。
#服务端数据
成员 | 邮箱 | 团队 | 项目数 | 状态 |
|---|---|---|---|---|
处理中 | ||||
import { Button } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { getCoreRowModel, type PaginationState, useReactTable } from '@tanstack/react-table';
import { useEffect, useState } from 'react';
import { type Member, members, useMemberColumns } from './data';
export function TableServer() {
const columns = useMemberColumns(false);
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5 });
const [data, setData] = useState<Member[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [request, setRequest] = useState(0);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(false);
const timer = setTimeout(
() => {
if (controller.signal.aborted) return;
setData(
members.slice(
pagination.pageIndex * pagination.pageSize,
(pagination.pageIndex + 1) * pagination.pageSize,
),
);
setLoading(false);
},
request > 0 ? 350 : 500,
);
return () => {
controller.abort();
clearTimeout(timer);
};
}, [pagination, request]);
const table = useReactTable({
data,
columns,
state: { pagination },
onPaginationChange: setPagination,
manualPagination: true,
manualSorting: true,
enableSorting: false,
rowCount: members.length,
getRowId: (member) => member.id,
getCoreRowModel: getCoreRowModel(),
});
return (
<DataTable
table={table}
aria-label={'远程成员'}
loading={loading}
error={error ? ('成员加载失败') : undefined}
onRetry={() => setRequest((value) => value + 1)}
>
<DataTable.Toolbar>
<Button
variant="outline"
disabled={loading}
onClick={() => setRequest((value) => value + 1)}
>
{'刷新'}
</Button>
<Button variant="soft" disabled={loading} onClick={() => setError(true)}>
{'模拟失败'}
</Button>
</DataTable.Toolbar>
<DataTable.Content />
<DataTable.Footer>
<DataTable.Pagination pageSizeOptions={[5, 10, 20]} />
</DataTable.Footer>
</DataTable>
);
}
远程分页使用 manualPagination: true 与 rowCount。远程排序 / 筛选分别使用 manualSorting / manualFiltering;把 TanStack 的受控状态用于请求参数,并取消过期请求。没有总数时可配置 pageCount: -1,分页器只展示上一页与下一页,通过 <DataTable.Pagination hasNextPage={hasMore} /> 控制能否继续翻页。
刷新时保留已有行,展示 loading 遮罩并禁止重复交互。首次加载、空数据、请求失败使用独立状态行。DataTable 自动展示内容和分页器;若不分页,请传 pagination={false};自定义布局时通过 children 显式排列 Toolbar、Content 和 Footer,此时只渲染提供的子组件。
#空数据、加载和错误
暂无数据 | |
import { Button, Space } from '@ifhover/leaf-ui';
import { DataTable } from '@ifhover/leaf-ui/table';
import { useState } from 'react';
const people = [
{ id: '1', name: 'Alex Morgan', email: 'alex@example.com', role: 'Design', active: true },
{ id: '2', name: 'Sam Taylor', email: 'sam@example.com', role: 'Engineering', active: true },
{ id: '3', name: 'Jordan Lee', email: 'jordan@example.com', role: 'Product', active: false },
];
const emptyPeople: typeof people = [];
export function TableStates() {
const [state, setState] = useState<'empty' | 'loading' | 'error'>('empty');
return (
<Space direction="vertical" align="stretch">
<Space wrap>
<Button variant={state === 'empty' ? 'outline' : 'soft'} onClick={() => setState('empty')}>
{'空数据'}
</Button>
<Button
variant={state === 'loading' ? 'outline' : 'soft'}
onClick={() => setState('loading')}
>
{'加载中'}
</Button>
<Button variant={state === 'error' ? 'outline' : 'soft'} onClick={() => setState('error')}>
{'加载失败'}
</Button>
</Space>
<DataTable
data={emptyPeople}
columns={[
{ accessorKey: 'name', header: '成员' },
{ accessorKey: 'role', header: '团队' },
]}
rowKey="id"
aria-label={'数据状态'}
pagination={false}
loading={state === 'loading'}
error={state === 'error' ? ('成员加载失败') : null}
onRetry={() => setState('empty')}
/>
</Space>
);
}
#DataTable API
此表用于传入 table 实例的模式;直接传入数据时使用 DataTableData API。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children | ReactNode | — | 自定义布局;传入后不再渲染默认 Content 和 Pagination。 |
caption | ReactNode | — | 表格说明。 |
size | 'sm' | 'md' | 'lg' | 'md' | 展示尺寸。 |
bordered | boolean | false | 展示外框和列分隔线;无边框时表头底部为 1px 分割线。 |
striped | boolean | true | 展示交替行背景并隐藏行间分割线;关闭时显示行间细线。 |
stickyHeader | boolean | true | 表头保持在滚动区域顶部。 |
maxHeight | CSSProperties['maxHeight'] | — | 滚动区域最大高度;虚拟模式未指定时使用 400。 |
minWidth | CSSProperties['minWidth'] | — | 表格最小宽度;默认根据可见列宽计算。 |
loading | boolean | false | 展示加载状态。 |
emptyContent | ReactNode | — | 空数据内容。 |
error | ReactNode | — | 请求失败提示;null、undefined 或 false 不展示。 |
onRetry | () => void | — | 请求重试。 |
selection | false | 'single' | 'multiple' | false | 不选择、单选或多选。 |
selectAllMode | 'page' | 'all' | 'page' | 全选当前页,或实例内已加载的可选择行。 |
resizable | boolean | false | 展示可调整列的宽度手柄。 |
columnReordering | boolean | false | 展示单层表头的拖拽手柄。 |
virtual | boolean | DataTableVirtualOptions | false | 启用行虚拟化。 |
pagination | boolean | true | 默认布局是否展示分页器。 |
tableRef | Ref<HTMLTableElement> | — | 原生表格 DOM ref。 |
table | TanStackTable<TData> | — | 必需。由 useReactTable 创建的实例,唯一的数据与状态来源。 |
getRowLabel | (row: Row<TData>) => string | — | 行操作的可访问名称;默认使用稳定 row.id。 |
renderExpandedRow | (row: Row<TData>) => ReactNode | — | 渲染叶子行详情,同时配置 getRowCanExpand。 |
getRowProps | (row: Row<TData>) => DataTableRowProps | — | 自定义行属性与事件。 |
getCellProps | (cell: Cell<TData, unknown>) => DataTableCellProps | — | 自定义单元格属性、样式或合并;colSpan / rowSpan 为 0 时省略该单元格。 |
onRowReorder | (info: DataTableRowReorderInfo<TData>) => void | — | 展示行拖拽手柄,并返回重排后的当前页行;应用负责更新数据。 |
aria-label / aria-labelledby 应用于原生表格,其他原生 div 属性用于外层。
#DataTableRow API
DataTableRowProps 通过 getRowProps 配置原生 tr 属性与事件。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
selected | boolean | — | 设置行的选中样式。 |
#DataTableCell API
DataTableCellProps 通过 getCellProps 配置原生 td 属性、合并与事件。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | 单元格内容对齐。 |
pinned | 'start' | 'end' | — | 固定在逻辑起始或结束侧。 |
pinOffset | number | 0 | 距离该侧的像素数。 |
pinEdge | boolean | — | 展示固定列边缘的淡阴影。 |
#DataTableSearch API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | — | 默认跟随 DataTable 的尺寸。 |
status | 'error' | 'warning' | — | 输入框的校验状态。 |
prefix | ReactNode | — | 前置内容;默认是搜索图标。 |
suffix | ReactNode | — | 后置内容。 |
allowClear | boolean | true | 展示清除操作。 |
onClear | () => void | — | 清除搜索内容时调用。 |
showCount | boolean | ((value: string, maxLength?: number) => ReactNode) | — | 显示或定制输入字数。 |
onPressEnter | KeyboardEventHandler<HTMLInputElement> | — | 按 Enter 时调用。 |
columnId | string | — | 指定筛选列;未指定时进行全局搜索。 |
其余属性沿用 Input;value 与 onChange 由实例管理。
#DataTableColumnVisibility API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children | ReactNode | — | 自定义按钮文字。 |
label | string | — | 面板的可访问名称。 |
#DataTablePagination API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showSizeChanger | boolean | true | 展示每页条数选择器。 |
pageSizeOptions | readonly number[] | — | 可选择的每页条数。 |
showQuickJumper | boolean | false | 回车或离开分页区域时跳转。 |
showTotal | boolean | ((total: number, range: readonly [number, number]) => ReactNode) | false | true 展示默认统计,也可传入函数定制总数及当前范围。 |
simple | boolean | — | 使用简洁分页器。 |
disabled | boolean | — | 禁用交互。 |
hideOnSinglePage | boolean | — | 只有一页时隐藏分页器。 |
size | 'sm' | 'md' | 'lg' | — | 默认跟随 DataTable 的尺寸。 |
variant | 'outline' | 'ghost' | 'outline' | 分页按钮样式:描边 / 无边框。 |
hasNextPage | boolean | — | 未知总数时控制下一页是否可用。 |
沿用 Pagination 的展示参数,total、current、pageSize 和 onChange 由实例管理;showSizeChanger 默认开启。未知总数模式只提供上一页和下一页。
#DataTableContent API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | — | 内容区域的自定义类名。 |
style | CSSProperties | — | 内容区域的内联样式。 |
DataTable.Content 支持原生 div 属性,包括 className、style 和事件。表格的布局、选择、展开与加载参数在 DataTable 上配置,表格 DOM ref 使用 tableRef。公开类型为 DataTableContentProps。
#DataTableColumnMeta
可以在 TanStack 列定义的 meta 中使用以下属性。此类型可用于自己组装配置;函数式 header 建议同时设置 label。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | — | 列管理与行操作使用的纯文字列名。 |
align | 'start' | 'center' | 'end' | 'start' | 表头和单元格对齐。 |
cellClassName | string | — | 单元格类名。 |
headerClassName | string | — | 表头类名。 |
#DataTableVirtualOptions
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
estimateRowHeight | number | 48 | 测量前的估算行高。 |
overscan | number | 5 | 在可视区前后额外渲染的行数。 |
#类型与组合
import type { DataTableProps, DataTableDataProps, DataTableRowProps, DataTableCellProps, DataTableSearchProps, DataTableColumnVisibilityProps, DataTablePaginationProps, DataTableContentProps, DataTableColumnMeta, DataTableVirtualOptions, DataTableRowReorderInfo } from '@ifhover/leaf-ui/table';
import type { ColumnDef, Table as TanStackTable, Row, Cell } from '@tanstack/react-table';DataTable.Toolbar / Footer / Content 支持原生 div 属性。TanStack 的 列定义、受控状态、行选择 和 列固定 使用其原生类型与 API,不另造一套完整列配置。
#DataTableData API
此表展示快捷模式的额外属性;其余展示、选择、展开和滚动参数与 DataTable API 相同,不接受 table。公开类型为 DataTableDataProps<TData>。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data | readonly TData[] | — | 必需。展示的数据,更新时使用新数组。 |
columns | ColumnDef<TData>[] | — | 必需。TanStack 的原生列定义。 |
rowKey | keyof TData | ((row: TData, index: number) => string) | — | 必需。稳定唯一的标识字段,或返回字符串标识的函数。 |
options | Omit<TableOptions<TData>, 'data' | 'columns' | 'getCoreRowModel' | 'getRowId'> | — | 定制 TanStack 的状态、回调和服务端配置。 |
#DataTableRowReorderInfo
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rows | readonly Row<TData>[] | — | 必需。当前页重排后的行;通过 row.original 取得业务数据。 |
fromIndex | number | — | 必需。原始当前页索引,从 0 开始。 |
toIndex | number | — | 必需。目标当前页索引,从 0 开始。 |
activeRow | Row<TData> | — | 必需。正在移动的行,包含稳定 row.id。 |
overRow | Row<TData> | — | 必需。放置目标行,包含稳定 row.id。 |
#DataTablePresentation API
ConfigProvider 的 components.DataTable 使用这些与行类型无关的参数。可从 @ifhover/leaf-ui 导入 DataTablePresentationProps 组装配置;只配置默认参数不需要安装 TanStack Table,使用 DataTable 时仍需要安装。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children | ReactNode | — | 子内容;嵌套数据类型表示子节点。 |
caption | ReactNode | — | 设置 caption。 |
size | 'sm' | 'md' | 'lg' | — | 展示尺寸。 |
bordered | boolean | — | 设置 bordered。 |
striped | boolean | — | 设置 striped。 |
stickyHeader | boolean | — | 设置 stickyHeader。 |
maxHeight | CSSProperties['maxHeight'] | — | 设置 maxHeight。 |
minWidth | CSSProperties['minWidth'] | — | 设置 minWidth。 |
loading | boolean | — | 展示加载状态。 |
emptyContent | ReactNode | — | 没有内容时的展示。 |
error | ReactNode | — | 设置 error。 |
onRetry | () => void | — | 设置 onRetry。 |
selection | false | 'single' | 'multiple' | — | 设置 selection。 |
selectAllMode | 'page' | 'all' | — | 设置 selectAllMode。 |
resizable | boolean | — | 允许拖动和使用方向键调整大小。 |
columnReordering | boolean | — | 设置 columnReordering。 |
virtual | boolean | DataTableVirtualOptions | — | 启用行虚拟化,默认关闭。 |
pagination | boolean | — | 设置 pagination。 |
tableRef | Ref<HTMLTableElement> | — | 设置 tableRef。 |