For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/v/0.6.0/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/v/0.6.0/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/v/0.6.0/components/table.md.
  • 简体中文
  • Table 表格

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

    选择用法

    • Table:用表头、行和单元格组合静态表格,或自行控制内容。无需安装表格引擎。
    • DataTable:传入数据与列即可使用排序和分页;复杂场景传入自己的 TanStack Table 实例,获得筛选、选择、展开和列管理的统一界面。
    import { Table } from '@ifhover/leaf-ui/table';

    使用 DataTable 时安装可选依赖:

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

    组件子路径自动加载所需样式,在应用入口按快速开始导入一次基础样式即可。DataTable 及其类型只从适配器子路径导入,不从组件库根入口导出。

    基础用法

    成员邮箱团队状态
    Alex Morganalex@example.comDesign活跃
    Sam Taylorsam@example.comEngineering活跃
    Jordan Leejordan@example.comProduct已邀请
    basic.tsx
    import { Table, Tag } from '@ifhover/leaf-ui';
    
    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 (
        <Table aria-label={'团队成员'} minWidth={560}>
          <Table.Header>
            <Table.Row>
              <Table.ColumnHeader>{'成员'}</Table.ColumnHeader>
              <Table.ColumnHeader>{'邮箱'}</Table.ColumnHeader>
              <Table.ColumnHeader>{'团队'}</Table.ColumnHeader>
              <Table.ColumnHeader>{'状态'}</Table.ColumnHeader>
            </Table.Row>
          </Table.Header>
          <Table.Body>
            {people.map((person) => (
              <Table.Row key={person.id}>
                <Table.Cell>
                  <strong>{person.name}</strong>
                </Table.Cell>
                <Table.Cell>{person.email}</Table.Cell>
                <Table.Cell>{person.role}</Table.Cell>
                <Table.Cell>
                  <Tag color={person.active ? 'success' : 'default'}>
                    {person.active ? ('活跃') : '已邀请'}
                  </Tag>
                </Table.Cell>
              </Table.Row>
            ))}
          </Table.Body>
        </Table>
      );
    }
    

    Table 与 Table.Root 等价。支持原生属性、DOM ref、colSpan、rowSpan、自定义表头和单元格;可用 Table.Caption 或 aria-label 为表格命名。

    快捷数据表格

    Alex Morganalex@example.comDesign
    Sam Taylorsam@example.comEngineering
    Jordan Leejordan@example.comProduct
    basic.tsx
    import { DataTable } from '@ifhover/leaf-ui/table/tanstack';
    
    const simpleColumns = [
      { accessorKey: 'name', header: 'Name' },
      { accessorKey: 'email', header: 'Email' },
      { accessorKey: 'role', header: 'Team' },
    ];
    
    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 TableSimple() {
      return (
        <DataTable
          data={people}
          columns={simpleColumns}
          rowKey="id"
          aria-label={'快捷数据表格'}
          pagination={false}
        />
      );
    }
    

    data + columns + rowKey 模式内部创建一个 TanStack 实例,复用与高级模式相同的界面。rowKey 必须是稳定且唯一的字符串或数字字段,或返回字符串的函数。默认启用本地排序、筛选、分页和展开;使用 options 定制初始状态、受控回调或服务端配置。pagination={false} 时展示全部行,不在内部截断数据。

    两种模式互斥:提供 table 后不要再传 data、columns、rowKey 或 options。高级分组、请求缓存等业务状态建议直接使用自己的 useReactTable 实例。

    排序、筛选、选择与分页

    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/tanstack';
    
    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/tanstack';
    
    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="ghost"
                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 Morgan1
    Riley Chen4
    Alex Morgan7
    Engineering (3)合计: 33
    Sam Taylor8
    Casey Wilson11
    Sam Taylor14
    Product (2)合计: 33
    Jordan Lee15
    Jamie Park18
    advanced.tsx
    import { Tag } from '@ifhover/leaf-ui';
    
    import { DataTable } from '@ifhover/leaf-ui/table/tanstack';
    
    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/tanstack';
    
    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/tanstack';
    
    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/tanstack';
    
    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/tanstack';
    
    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="ghost" 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, Table } from '@ifhover/leaf-ui';
    
    import { useState } from 'react';
    
    export function TableStates() {
      const [state, setState] = useState<'empty' | 'loading' | 'error'>('empty');
      return (
        <Space direction="vertical" align="stretch">
          <Space wrap>
            <Button variant={state === 'empty' ? 'outline' : 'ghost'} onClick={() => setState('empty')}>
              {'空数据'}
            </Button>
            <Button
              variant={state === 'loading' ? 'outline' : 'ghost'}
              onClick={() => setState('loading')}
            >
              {'加载中'}
            </Button>
            <Button variant={state === 'error' ? 'outline' : 'ghost'} onClick={() => setState('error')}>
              {'加载失败'}
            </Button>
          </Space>
          <Table aria-label={'数据状态'}>
            <Table.Header>
              <Table.Row>
                <Table.ColumnHeader>{'成员'}</Table.ColumnHeader>
                <Table.ColumnHeader>{'团队'}</Table.ColumnHeader>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {state === 'empty' ? (
                <Table.Empty colSpan={2} />
              ) : state === 'loading' ? (
                <Table.Loading colSpan={2} />
              ) : (
                <Table.Error colSpan={2} onRetry={() => setState('empty')} />
              )}
            </Table.Body>
          </Table>
        </Space>
      );
    }
    

    Table API

    属性类型默认值说明
    size'sm' | 'md' | 'lg''md'行的展示尺寸。
    borderedbooleanfalse展示列之间的分隔线。
    stripedbooleanfalse展示交替行背景。
    stickyHeaderbooleantrue表头保持在滚动区域顶部。
    maxHeightCSSProperties['maxHeight']—滚动区域的最大高度。
    minWidthCSSProperties['minWidth']—表格最小宽度。
    tableLayoutCSSProperties['tableLayout']—原生表格布局方式。
    tableStyleCSSProperties—原生 table 的样式;style 与 className 用于外层。
    scrollRefRef<HTMLDivElement>—滚动区域的 DOM ref;组件 ref 指向 table。
    loadingbooleanfalse保留内容并展示加载遮罩。

    TableRow API

    属性类型默认值说明
    selectedboolean—展示行的选中背景;交互状态由选择控件表达。

    TableCell API

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

    TableColumnHeader API

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

    继承原生 th 属性,固定和对齐参数与 TableCell 相同。scope 默认是 'col';分组表头可设置 'colgroup'。

    TableState API

    用于 Table.Empty 和 Table.Loading;放在 Table.Body 中。

    属性类型默认值说明
    colSpannumber1状态行跨越的列数。
    childrenReactNode—自定义状态内容。

    TableError API

    属性类型默认值说明
    colSpannumber1状态行跨越的列数。
    childrenReactNode—自定义失败提示。
    onRetry() => void—传入后展示重试按钮。

    DataTable API

    此表用于传入 table 实例的模式;直接传入数据时使用 DataTableData API。

    属性类型默认值说明
    childrenReactNode—自定义布局;传入后不再渲染默认 Content 和 Pagination。
    captionReactNode—表格说明。
    size'sm' | 'md' | 'lg''md'展示尺寸。
    borderedbooleanfalse展示列之间的分隔线。
    stripedbooleanfalse展示交替行背景。
    stickyHeaderbooleantrue表头保持在滚动区域顶部。
    maxHeightTableProps['maxHeight']—滚动区域最大高度;虚拟模式未指定时使用 400。
    minWidthTableProps['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>) => TableRowProps—自定义行属性与事件。
    getCellProps(cell: Cell<TData, unknown>) => TableCellProps—自定义单元格属性、样式或合并;colSpan / rowSpan 为 0 时省略该单元格。
    onRowReorder(info: DataTableRowReorderInfo<TData>) => void—展示行拖拽手柄,并返回重排后的当前页行;应用负责更新数据。

    尺寸、边框、交替行、固定表头、滚动区域和 loading 与 Table 相同。aria-label / aria-labelledby 应用于原生表格,其他原生 div 属性用于外层。

    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 { TableProps, TableRowProps, TableCellProps, TableColumnHeaderProps, TableStateProps, TableErrorProps } from '@ifhover/leaf-ui/table';
    import type { DataTableProps, DataTableDataProps, DataTableSearchProps, DataTableColumnVisibilityProps, DataTablePaginationProps, DataTableContentProps, DataTableColumnMeta, DataTableVirtualOptions, DataTableRowReorderInfo } from '@ifhover/leaf-ui/table/tanstack';
    import type { ColumnDef, Table as TanStackTable, Row, Cell } from '@tanstack/react-table';

    Header / Body / Footer、Caption、Row 和 Cell 支持对应元素的原生属性与 DOM ref。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。
    maxHeightTableProps['maxHeight']—设置 maxHeight。
    minWidthTableProps['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。