> Leaf UI 0.8.1 · Package: @ifhover/leaf-ui · Source: 435bab23cad7d343653d9b63b049c0b0601eeefb
> Browser documentation: https://ifhover.github.io/leaf-ui/components/table.html

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

# DataTable 表格

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

## 安装与导入

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

```bash
npm install @tanstack/react-table@^8.21.3
```

```tsx
import { 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 | 已邀请 |

basic.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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 Morgan | Design |
| Sam Taylor | Engineering |
| Jordan Lee | Product |

appearance.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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 项已选择显示列

|  | 展开行 | 成员 | 邮箱 | 团队 | 项目数 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
|  |  | A**Alex Morgan** | member1@example.com | Design | 1 | 活跃 |
|  |
|  |  | S**Sam Taylor** | member2@example.com | Engineering | 8 | 活跃 |
|  |
|  |  | J**Jordan Lee** | member3@example.com | Product | 15 | 已邀请 |
|  |
|  |  | R**Riley Chen** | member4@example.com | Design | 4 | 活跃 |
|  |
|  |  | C**Casey Wilson** | member5@example.com | Engineering | 11 | 活跃 |
|  |

共 26 位成员123456

data.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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` 标记第一排序列。

## 固定表头、固定列与列宽


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

| 成员--- | 邮箱--- | 团队--- | 项目数--- | 状态--- |
| --- | --- | --- | --- | --- |
| A**Alex Morgan** | member1@example.com | Design | 1 | 活跃 |
| S**Sam Taylor** | member2@example.com | Engineering | 8 | 活跃 |
| J**Jordan Lee** | member3@example.com | Product | 15 | 已邀请 |
| R**Riley Chen** | member4@example.com | Design | 4 | 活跃 |
| C**Casey Wilson** | member5@example.com | Engineering | 11 | 活跃 |
| J**Jamie Park** | member6@example.com | Product | 18 | 活跃 |
| A**Alex Morgan** | member7@example.com | Design | 7 | 已邀请 |
| S**Sam Taylor** | member8@example.com | Engineering | 14 | 活跃 |
| J**Jordan Lee** | member9@example.com | Product | 3 | 活跃 |
| R**Riley Chen** | member10@example.com | Design | 10 | 活跃 |
| C**Casey Wilson** | member11@example.com | Engineering | 17 | 已邀请 |
| J**Jamie Park** | member12@example.com | Product | 6 | 活跃 |
| A**Alex Morgan** | member13@example.com | Design | 13 | 活跃 |
| S**Sam Taylor** | member14@example.com | Engineering | 2 | 活跃 |
| J**Jordan Lee** | member15@example.com | Product | 9 | 已邀请 |
| R**Riley Chen** | member16@example.com | Design | 16 | 活跃 |
| C**Casey Wilson** | member17@example.com | Engineering | 5 | 活跃 |
| J**Jamie Park** | member18@example.com | Product | 12 | 活跃 |
| A**Alex Morgan** | member19@example.com | Design | 1 | 已邀请 |
| S**Sam Taylor** | member20@example.com | Engineering | 8 | 活跃 |
| J**Jordan Lee** | member21@example.com | Product | 15 | 活跃 |
| R**Riley Chen** | member22@example.com | Design | 4 | 活跃 |
| C**Casey Wilson** | member23@example.com | Engineering | 11 | 已邀请 |
| J**Jamie Park** | member24@example.com | Product | 18 | 活跃 |
| A**Alex Morgan** | member25@example.com | Design | 7 | 活跃 |
| S**Sam Taylor** | member26@example.com | Engineering | 14 | 活跃 |

data.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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](https://ifhover.github.io/leaf-ui/components/scrollbar.md)，纵向条从表头下方开始；未溢出或到达滚动边界时，滚轮继续滚动页面。

列宽手柄可拖动，也可聚焦后用左右方向键调整；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

复制代码

```tsx lineNumbers file="../../src/examples/table/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.com | Design |
|  | member2@example.com | Engineering |
|  | member3@example.com | Product |

advanced.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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 或其他控件；业务数据、提交和校验仍由应用管理，不建立另一套表格表单仓库。行详情收起后保留已打开内容中的输入状态；未打开的详情不挂载业务内容。

## 行和列排序


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

团队顺序| 移动行 | 成员 | 团队 | 状态 |
| --- | --- | --- | --- |
|  | A**Alex Morgan** | Design | 活跃 |
|  | S**Sam Taylor** | Engineering | 活跃 |
|  | J**Jordan Lee** | Product | 已邀请 |

reordering.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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` 明确控制。拖拽不会修改行选择或编辑器状态。

## 大数据


| 成员 | 邮箱 | 团队 | 项目数 | 状态 |
| --- | --- | --- | --- | --- |
| M**Member 00001** | member1@example.com | Design | 1 | 活跃 |
| M**Member 00002** | member2@example.com | Engineering | 8 | 活跃 |
| M**Member 00003** | member3@example.com | Product | 15 | 已邀请 |
| M**Member 00004** | member4@example.com | Design | 4 | 活跃 |
| M**Member 00005** | member5@example.com | Engineering | 11 | 活跃 |
| M**Member 00006** | member6@example.com | Product | 18 | 活跃 |
| M**Member 00007** | member7@example.com | Design | 7 | 已邀请 |
| M**Member 00008** | member8@example.com | Engineering | 14 | 活跃 |
| M**Member 00009** | member9@example.com | Product | 3 | 活跃 |
| M**Member 00010** | member10@example.com | Design | 10 | 活跃 |
| M**Member 00011** | member11@example.com | Engineering | 17 | 已邀请 |
| M**Member 00012** | member12@example.com | Product | 6 | 活跃 |
|  |

advanced.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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：不提供方向键逐单元格导航、列虚拟化或自动移动端卡片化。普通表格维持原生语义和横向滚动；需要浏览全部数据的屏幕阅读器用户可关闭虚拟化，改用分页。

## 服务端数据


刷新模拟失败

| 成员 | 邮箱 | 团队 | 项目数 | 状态 |
| --- | --- | --- | --- | --- |
| 处理中 |

123456

advanced.tsx

复制代码

```tsx lineNumbers file="../../src/examples/table/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

复制代码

```tsx lineNumbers file="../../src/examples/table/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](#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`](#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`](#datatablerow-api)                  | —        | 自定义行属性与事件。                                    |
| `getCellProps`      | `(cell: Cell<TData, unknown>) => `[`DataTableCellProps`](#datatablecell-api)     | —        | 自定义单元格属性、样式或合并；colSpan / rowSpan 为 0 时省略该单元格。 |
| `onRowReorder`      | `(info: `[`DataTableRowReorderInfo`](#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](#datatable-api) 上配置，表格 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`  | 在可视区前后额外渲染的行数。 |

## 类型与组合

```ts
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 的 [列定义](https://tanstack.com/table/v8/docs/guide/column-defs)、[受控状态](https://tanstack.com/table/v8/docs/framework/react/guide/table-state)、[行选择](https://tanstack.com/table/v8/docs/guide/row-selection) 和 [列固定](https://tanstack.com/table/v8/docs/guide/column-pinning) 使用其原生类型与 API，不另造一套完整列配置。

## DataTableData API

此表展示快捷模式的额外属性；其余展示、选择、展开和滚动参数与 [DataTable 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`](#datatablevirtualoptions) | —   | 启用行虚拟化，默认关闭。         |
| `pagination`       | `boolean`                                                          | —   | 设置 pagination。       |
| `tableRef`         | `Ref<HTMLTableElement>`                                            | —   | 设置 tableRef。         |
