For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/components/table.md.

DataTable 表格

浏览、筛选和管理结构化数据。

安装与导入

DataTable 传入数据与列即可使用排序和分页;复杂场景传入自己的 TanStack Table 实例,获得筛选、选择、展开和列管理的统一界面。使用前安装依赖:

npm install @tanstack/react-table@^8.21.3
import { DataTable } from '@ifhover/leaf-ui/table';
import type { DataTableProps, DataTableColumnMeta } from '@ifhover/leaf-ui/table';

表格子路径自动加载所需样式。DataTable 及其数据类型从 @ifhover/leaf-ui/table 导入,DataTable 不从组件库根入口导出。

基础用法

Alex Morganalex@example.comDesign活跃
Sam Taylorsam@example.comEngineering活跃
Jordan Leejordan@example.comProduct已邀请
basic.tsx
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 MorganDesign
Sam TaylorEngineering
Jordan LeeProduct
appearance.tsx
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>
  );
}

排序、筛选、选择与分页

0 项已选择
展开行
Alex Morgan
member1@example.comDesign1活跃
Sam Taylor
member2@example.comEngineering8活跃
Jordan Lee
member3@example.comProduct15已邀请
Riley Chen
member4@example.comDesign4活跃
Casey Wilson
member5@example.comEngineering11活跃
data.tsx
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 标记第一排序列。

固定表头、固定列与列宽

拖动表头右侧边缘,或聚焦边缘后用方向键调整列宽。





Alex Morgan
member1@example.comDesign1活跃
Sam Taylor
member2@example.comEngineering8活跃
Jordan Lee
member3@example.comProduct15已邀请
Riley Chen
member4@example.comDesign4活跃
Casey Wilson
member5@example.comEngineering11活跃
Jamie Park
member6@example.comProduct18活跃
Alex Morgan
member7@example.comDesign7已邀请
Sam Taylor
member8@example.comEngineering14活跃
Jordan Lee
member9@example.comProduct3活跃
Riley Chen
member10@example.comDesign10活跃
Casey Wilson
member11@example.comEngineering17已邀请
Jamie Park
member12@example.comProduct6活跃
Alex Morgan
member13@example.comDesign13活跃
Sam Taylor
member14@example.comEngineering2活跃
Jordan Lee
member15@example.comProduct9已邀请
Riley Chen
member16@example.comDesign16活跃
Casey Wilson
member17@example.comEngineering5活跃
Jamie Park
member18@example.comProduct12活跃
Alex Morgan
member19@example.comDesign1已邀请
Sam Taylor
member20@example.comEngineering8活跃
Jordan Lee
member21@example.comProduct15活跃
Riley Chen
member22@example.comDesign4活跃
Casey Wilson
member23@example.comEngineering11已邀请
Jamie Park
member24@example.comProduct18活跃
Alex Morgan
member25@example.comDesign7活跃
Sam Taylor
member26@example.comEngineering14活跃
data.tsx
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
advanced.tsx
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.comDesign
member2@example.comEngineering
member3@example.comProduct
advanced.tsx
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 或其他控件;业务数据、提交和校验仍由应用管理,不建立另一套表格表单仓库。行详情收起后保留已打开内容中的输入状态;未打开的详情不挂载业务内容。

行和列排序

使用行首和表头的手柄调整顺序。

团队顺序
移动行
成员
团队
状态
Alex Morgan
Design活跃
Sam Taylor
Engineering活跃
Jordan Lee
Product已邀请
reordering.tsx
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 明确控制。拖拽不会修改行选择或编辑器状态。

大数据

Member 00001
member1@example.comDesign1活跃
Member 00002
member2@example.comEngineering8活跃
Member 00003
member3@example.comProduct15已邀请
Member 00004
member4@example.comDesign4活跃
Member 00005
member5@example.comEngineering11活跃
Member 00006
member6@example.comProduct18活跃
Member 00007
member7@example.comDesign7已邀请
Member 00008
member8@example.comEngineering14活跃
Member 00009
member9@example.comProduct3活跃
Member 00010
member10@example.comDesign10活跃
Member 00011
member11@example.comEngineering17已邀请
Member 00012
member12@example.comProduct6活跃
advanced.tsx
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:不提供方向键逐单元格导航、列虚拟化或自动移动端卡片化。普通表格维持原生语义和横向滚动;需要浏览全部数据的屏幕阅读器用户可关闭虚拟化,改用分页。

服务端数据

成员
邮箱
团队
项目数
状态
处理中
advanced.tsx
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,此时只渲染提供的子组件。

空数据、加载和错误

暂无数据
basic.tsx
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。

属性类型默认值说明
childrenReactNode—自定义布局;传入后不再渲染默认 Content 和 Pagination。
captionReactNode—表格说明。
size'sm' | 'md' | 'lg''md'展示尺寸。
borderedbooleanfalse展示外框和列分隔线;无边框时表头底部为 1px 分割线。
stripedbooleantrue展示交替行背景并隐藏行间分割线;关闭时显示行间细线。
stickyHeaderbooleantrue表头保持在滚动区域顶部。
maxHeightCSSProperties['maxHeight']—滚动区域最大高度;虚拟模式未指定时使用 400。
minWidthCSSProperties['minWidth']—表格最小宽度;默认根据可见列宽计算。
loadingbooleanfalse展示加载状态。
emptyContentReactNode—空数据内容。
errorReactNode—请求失败提示;null、undefined 或 false 不展示。
onRetry() => void—请求重试。
selectionfalse | 'single' | 'multiple'false不选择、单选或多选。
selectAllMode'page' | 'all''page'全选当前页,或实例内已加载的可选择行。
resizablebooleanfalse展示可调整列的宽度手柄。
columnReorderingbooleanfalse展示单层表头的拖拽手柄。
virtualboolean | DataTableVirtualOptionsfalse启用行虚拟化。
paginationbooleantrue默认布局是否展示分页器。
tableRefRef<HTMLTableElement>—原生表格 DOM ref。
tableTanStackTable<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 属性与事件。

属性类型默认值说明
selectedboolean—设置行的选中样式。

DataTableCell API

DataTableCellProps 通过 getCellProps 配置原生 td 属性、合并与事件。

属性类型默认值说明
align'start' | 'center' | 'end''start'单元格内容对齐。
pinned'start' | 'end'—固定在逻辑起始或结束侧。
pinOffsetnumber0距离该侧的像素数。
pinEdgeboolean—展示固定列边缘的淡阴影。

DataTableSearch API

属性类型默认值说明
size'sm' | 'md' | 'lg'—默认跟随 DataTable 的尺寸。
status'error' | 'warning'—输入框的校验状态。
prefixReactNode—前置内容;默认是搜索图标。
suffixReactNode—后置内容。
allowClearbooleantrue展示清除操作。
onClear() => void—清除搜索内容时调用。
showCountboolean | ((value: string, maxLength?: number) => ReactNode)—显示或定制输入字数。
onPressEnterKeyboardEventHandler<HTMLInputElement>—按 Enter 时调用。
columnIdstring—指定筛选列;未指定时进行全局搜索。

其余属性沿用 Input;value 与 onChange 由实例管理。

DataTableColumnVisibility API

属性类型默认值说明
childrenReactNode—自定义按钮文字。
labelstring—面板的可访问名称。

DataTablePagination API

属性类型默认值说明
showSizeChangerbooleantrue展示每页条数选择器。
pageSizeOptionsreadonly number[]—可选择的每页条数。
showQuickJumperbooleanfalse回车或离开分页区域时跳转。
showTotalboolean | ((total: number, range: readonly [number, number]) => ReactNode)falsetrue 展示默认统计,也可传入函数定制总数及当前范围。
simpleboolean—使用简洁分页器。
disabledboolean—禁用交互。
hideOnSinglePageboolean—只有一页时隐藏分页器。
size'sm' | 'md' | 'lg'—默认跟随 DataTable 的尺寸。
variant'outline' | 'ghost''outline'分页按钮样式:描边 / 无边框。
hasNextPageboolean—未知总数时控制下一页是否可用。

沿用 Pagination 的展示参数,total、current、pageSize 和 onChange 由实例管理;showSizeChanger 默认开启。未知总数模式只提供上一页和下一页。

DataTableContent API

属性类型默认值说明
classNamestring—内容区域的自定义类名。
styleCSSProperties—内容区域的内联样式。

DataTable.Content 支持原生 div 属性,包括 className、style 和事件。表格的布局、选择、展开与加载参数在 DataTable 上配置,表格 DOM ref 使用 tableRef。公开类型为 DataTableContentProps。

DataTableColumnMeta

可以在 TanStack 列定义的 meta 中使用以下属性。此类型可用于自己组装配置;函数式 header 建议同时设置 label。

属性类型默认值说明
labelstring—列管理与行操作使用的纯文字列名。
align'start' | 'center' | 'end''start'表头和单元格对齐。
cellClassNamestring—单元格类名。
headerClassNamestring—表头类名。

DataTableVirtualOptions

属性类型默认值说明
estimateRowHeightnumber48测量前的估算行高。
overscannumber5在可视区前后额外渲染的行数。

类型与组合

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>。

属性类型默认值说明
datareadonly TData[]—必需。展示的数据,更新时使用新数组。
columnsColumnDef<TData>[]—必需。TanStack 的原生列定义。
rowKeykeyof TData | ((row: TData, index: number) => string)—必需。稳定唯一的标识字段,或返回字符串标识的函数。
optionsOmit<TableOptions<TData>, 'data' | 'columns' | 'getCoreRowModel' | 'getRowId'>—定制 TanStack 的状态、回调和服务端配置。

DataTableRowReorderInfo

属性类型默认值说明
rowsreadonly Row<TData>[]—必需。当前页重排后的行;通过 row.original 取得业务数据。
fromIndexnumber—必需。原始当前页索引,从 0 开始。
toIndexnumber—必需。目标当前页索引,从 0 开始。
activeRowRow<TData>—必需。正在移动的行,包含稳定 row.id。
overRowRow<TData>—必需。放置目标行,包含稳定 row.id。

DataTablePresentation API

ConfigProvider 的 components.DataTable 使用这些与行类型无关的参数。可从 @ifhover/leaf-ui 导入 DataTablePresentationProps 组装配置;只配置默认参数不需要安装 TanStack Table,使用 DataTable 时仍需要安装。

属性类型默认值说明
childrenReactNode—子内容;嵌套数据类型表示子节点。
captionReactNode—设置 caption。
size'sm' | 'md' | 'lg'—展示尺寸。
borderedboolean—设置 bordered。
stripedboolean—设置 striped。
stickyHeaderboolean—设置 stickyHeader。
maxHeightCSSProperties['maxHeight']—设置 maxHeight。
minWidthCSSProperties['minWidth']—设置 minWidth。
loadingboolean—展示加载状态。
emptyContentReactNode—没有内容时的展示。
errorReactNode—设置 error。
onRetry() => void—设置 onRetry。
selectionfalse | 'single' | 'multiple'—设置 selection。
selectAllMode'page' | 'all'—设置 selectAllMode。
resizableboolean—允许拖动和使用方向键调整大小。
columnReorderingboolean—设置 columnReordering。
virtualboolean | DataTableVirtualOptions—启用行虚拟化,默认关闭。
paginationboolean—设置 pagination。
tableRefRef<HTMLTableElement>—设置 tableRef。