DataTable 数据表格

带类型的数据表格,查询、布局与编辑能力按需启用。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
'use client'

import { DataTable, Tag } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns, statusLabels } = tableDemo('zh-CN')
const entries = rows.slice(0, 4)

export default function Demo() {
  return (
    <DataTable
      rows={entries}
      columns={columns}
      rowKey="id"
      label="条目列表"
      renderCell={({ row, column }) =>
        column.key === 'status' ? (
          <Tag tone={row.status === 'active' ? 'success' : 'neutral'}>
            {statusLabels[row.status]}
          </Tag>
        ) : undefined
      }
    />
  )
}
tsx

使用

import { DataTable, type DataTableColumn } from '@hina-ui/react'
ts

传入 rows、columns 与稳定的 rowKey。默认只显示表格,其他控件按需开启。样式对齐 Table,支持密度、深色模式与 RTL。

列、渲染函数、回调与 ref API 保留原始行类型。数据变化时传入新数组,以便重新计算数据处理结果。

示例

单元格

renderCell 渲染数据单元格的内容,用 column.key 区分列;返回 undefined 时显示默认内容。它接收带类型的原始 row、column、value、稳定的 key、源数组中的 index、depth,以及选择、展开的方法。field 可指定其他字段,accessor 优先级更高。format 只影响显示。

align 同时作用于表头与单元格。rowClickable 支持点击、Enter 和 Space,并调用 onRowClick;单元格内的交互控件保留自己的行为。onRowContextmenu 提供原始行和事件,自定义菜单时由调用方执行 event.preventDefault()。

renderCell 根据 column.key 为名称列组合 Avatar 与两行 Text,为状态列渲染 Tag,为数量列组合 Progress 与数值。它只替换单元格内容,外围单元格、对齐与排序仍由 DataTable 管理。

状态
A

条目 A

ID 1

已发布

18

B

条目 B

ID 2

草稿

55

C

条目 C

ID 3

已发布

92

D

条目 D

ID 4

已发布

129

E

条目 E

ID 5

已归档

16

'use client'

import { Avatar, DataTable, Inline, Progress, Stack, Tag, Text } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns, statusLabels } = tableDemo('zh-CN')
const entries = rows.slice(0, 5)

export default function Demo() {
  return (
    <DataTable
      rows={entries}
      columns={columns}
      rowKey="id"
      label="条目列表"
      renderCell={({ row, column, value }) => {
        if (column.key === 'name')
          return (
            <Inline gap="sm" wrap={false} className="min-w-36 py-2">
              <Avatar name={row.name.slice(-1)} size="sm" aria-hidden="true" />
              <Stack gap="none">
                <Text size="sm" weight="medium" className="whitespace-nowrap">
                  {row.name}
                </Text>
                <Text size="xs" tone="muted">
                  ID {row.id}
                </Text>
              </Stack>
            </Inline>
          )
        if (column.key === 'status')
          return (
            <Tag tone={row.status === 'active' ? 'success' : 'neutral'}>
              {statusLabels[row.status]}
            </Tag>
          )
        if (column.key === 'count')
          return (
            <Inline gap="sm" wrap={false} className="min-w-32">
              <Progress
                value={Number(value)}
                max={150}
                aria-label={column.label}
                size="sm"
                className="flex-1"
              />
              <Text size="sm" className="w-8 shrink-0 text-end tabular-nums">
                {String(value)}
              </Text>
            </Inline>
          )
        return String(value)
      }}
    />
  )
}
tsx

排序

列设置 sortable: true 后,表头按升序、降序、取消排序循环。sorting / onSortingChange 存储 { key, desc }[],multiSort 允许 Shift 点击追加排序。数字与日期按值比较,字符串按当前语言自然排序,缺失值始终放在末尾。sort(a, b) 自定义升序比较。

renderHeader 接收 sorting、sortIndex 和 toggleSort(multi?),同样用 column.key 区分列,返回 undefined 时显示默认表头;自定义表头仍保留外围单元格与 aria-sort。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
条目 F
已发布
53

未排序

'use client'

import { useState } from 'react'
import { DataTable, Text, Stack, type DataTableSort } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 6)

export default function Demo() {
  const [sorting, setSorting] = useState<DataTableSort[]>([])

  return (
    <Stack className="w-full">
      <DataTable
        sorting={sorting}
        onSortingChange={setSorting}
        rows={entries}
        columns={columns}
        rowKey="id"
        multiSort
        label="条目列表"
      />
      <Text size="sm" tone="muted">
        {sorting.length
          ? sorting
              .map(
                sort =>
                  `${columns.find(column => column.key === sort.key)?.label ?? sort.key} ${sort.desc ? '降序' : '升序'}`,
              )
              .join(' · ')
          : '未排序'}
      </Text>
    </Stack>
  )
}
tsx

全局筛选

filter / onFilterChange 在参与筛选的列中进行不区分大小写的子串匹配。filterable: false 将列排除出全局筛选,filter(row, query) 自定义该列的匹配方式。隐藏列仍可参与,任一列匹配即可保留该行。

可在 renderToolbar 中放置 SearchInput。防抖、请求与过期响应处理由调用方管理。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { DataTable, SearchInput } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')

export default function Demo() {
  const [filter, setFilter] = useState('')

  return (
    <DataTable
      filter={filter}
      onFilterChange={setFilter}
      rows={rows}
      columns={columns}
      rowKey="id"
      pagination
      defaultPageSize={5}
      label="条目列表"
      renderToolbar={() => (
        <SearchInput
          size="sm"
          value={filter}
          onValueChange={setFilter}
          placeholder="筛选名称"
          aria-label="筛选名称"
          className="w-64 max-w-full"
        />
      )}
    />
  )
}
tsx

独立列筛选

columnFilters / onColumnFiltersChange 存储 { key, value }[],各列条件取交集,并与全局筛选同时生效。filterMode 支持 contains、equals、in(数组)和 range([min, max],任一边界可为空)。数字、布尔值和日期保留原类型,filterValue(row, value) 可自定义匹配。

renderHeader 还接收 filterValue 与 setFilter(value)。传入 null、undefined、空字符串或空数组清除该列条件。筛选控件按需添加,示例使用 Select。

条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { DataTable, Select, type DataTableFilter } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns, statusLabels } = tableDemo('zh-CN')
const options = Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
const filteredColumns = columns.map(column =>
  column.key === 'status' ? { ...column, filterMode: 'equals' as const } : column,
)

export default function Demo() {
  const [filters, setFilters] = useState<DataTableFilter[]>([])

  return (
    <DataTable
      columnFilters={filters}
      onColumnFiltersChange={setFilters}
      rows={rows}
      columns={filteredColumns}
      rowKey="id"
      pagination
      defaultPageSize={5}
      label="条目列表"
      renderHeader={({ column, filterValue, setFilter }) =>
        column.key === 'status' ? (
          <Select
            value={filterValue === undefined ? null : String(filterValue)}
            options={options}
            placeholder={column.label}
            aria-label={column.label}
            clearable
            size="sm"
            className="w-36 py-1"
            onValueChange={setFilter}
          />
        ) : undefined
      }
    />
  )
}
tsx

多选

selectable 添加 Checkbox,也可传入函数禁止选择部分行。selected / onSelectedChange 存储行键,数字与字符串区分处理。翻页、筛选和替换远程结果保留已选键,删除数据后是否清除对应键由调用方决定。

表头默认控制当前页可选行,selectAll="filtered" 则控制当前已加载数据中所有符合筛选条件的行。两者都不会选择尚未加载的远程记录。rowLabel 用于提供可读的控件名称。

已选:1

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { Button, DataTable, Inline, Text, type DataTableKey } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')

export default function Demo() {
  const [selected, setSelected] = useState<DataTableKey[]>([1])

  return (
    <DataTable
      selected={selected}
      onSelectedChange={setSelected}
      rows={rows}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      selectable={row => row.status !== 'archived'}
      pagination
      defaultPageSize={5}
      label="条目列表"
      renderToolbar={() => (
        <Inline align="center" justify="between">
          <Text size="sm" tone="muted">
            已选:{selected.length}
          </Text>
          <Button
            variant="ghost"
            tone="neutral"
            size="sm"
            disabled={!selected.length}
            onClick={() => setSelected([])}
          >
            清空选择
          </Button>
        </Inline>
      )}
    />
  )
}
tsx

单选

selectionMode="single" 使用单选控件,新选择替换旧选择。selected 仍是键数组,最多保留一个键;表头不显示全选控件。

选择此行状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
'use client'

import { useState } from 'react'
import { DataTable, type DataTableKey } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 4)

export default function Demo() {
  const [selected, setSelected] = useState<DataTableKey[]>([2])

  return (
    <DataTable
      selected={selected}
      onSelectedChange={setSelected}
      rows={entries}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      selectable
      selectionMode="single"
      label="条目列表"
    />
  )
}
tsx

分页

pagination 启用 Pagination,page 从 1 开始,pageSize 默认 10。排序、全局或列筛选、分组及每页数量改变后复位到第一页;autoResetPage={false} 可保留当前页。已知总数时,数据减少会将越界页码调回有效范围。

默认页脚只显示分页控件。如需总数、每页数量或跳页输入,可在 renderFooter 中组合 Pagination。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { DataTable, Pagination } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')

export default function Demo() {
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(5)

  return (
    <DataTable
      page={page}
      onPageChange={setPage}
      pageSize={pageSize}
      onPageSizeChange={setPageSize}
      rows={rows}
      columns={columns}
      rowKey="id"
      pagination
      label="条目列表"
      renderFooter={({ total, rows: visible }) => (
        <Pagination
          value={page}
          onValueChange={setPage}
          pageSize={pageSize}
          onPageSizeChange={setPageSize}
          total={total}
          itemCount={visible.length}
          pageSizeOptions={[5, 10, 20]}
          showInfo
          showJump
          align="between"
        />
      )}
    />
  )
}
tsx

远程数据

manual 同时绕过本地筛选、排序、分组与分页。rows 为当前请求结果,total 提供远程总数。受控状态可从 URL 或 store 初始化。

onChange 在同一更新周期稳定后调用一次,参数为完整的 { page, pageSize, sorting, filter, columnFilters, grouping }。挂载时不调用,首次请求、取消与过期响应处理由调用方负责。

loading 保留现有行,阻止其交互,并使用 LoadingOverlay。加载期间临时清空结果不会收缩页码。

请求页 1 · 返回 0 / 0

状态
加载中
'use client'

import { useState } from 'react'
import { DataTable, SearchInput, Text, Tag, Inline } from '@hina-ui/react'
import { useRemoteTableDemo } from '../../data-table'

export default function Demo() {
  const { rows, columns, statusLabels, total, loading, lastQuery, load } =
    useRemoteTableDemo('zh-CN')
  const [filter, setFilter] = useState('')

  return (
    <DataTable
      filter={filter}
      onFilterChange={setFilter}
      rows={rows}
      columns={columns}
      total={total}
      loading={loading}
      rowKey="id"
      manual
      pagination
      defaultPageSize={5}
      label="条目列表"
      onChange={load}
      renderToolbar={() => (
        <Inline justify="between">
          <SearchInput
            size="sm"
            value={filter}
            onValueChange={setFilter}
            placeholder="搜索名称"
            aria-label="搜索名称"
            className="w-64 max-w-full"
          />
          <Text size="sm" tone="muted">
            请求页 {lastQuery.page} · 返回 {rows.length} / {total}
          </Text>
        </Inline>
      )}
      renderCell={({ row, column }) =>
        column.key === 'status' ? (
          <Tag tone={row.status === 'active' ? 'success' : 'neutral'}>
            {statusLabels[row.status]}
          </Tag>
        ) : undefined
      }
    />
  )
}
tsx

未知总数

远程模式省略 total 时显示上一页、下一页控件。hasNextPage 显式控制下一页是否可用;省略时根据返回行数是否达到 pageSize 判断。

状态
加载中
'use client'

import { DataTable } from '@hina-ui/react'
import { useRemoteTableDemo } from '../../data-table'

export default function Demo() {
  const { rows, columns, loading, total, lastQuery, load } = useRemoteTableDemo('zh-CN')
  const hasNextPage = lastQuery.page * lastQuery.pageSize < total

  return (
    <DataTable
      rows={rows}
      columns={columns}
      rowKey="id"
      manual
      pagination
      defaultPageSize={5}
      hasNextPage={hasNextPage}
      loading={loading}
      label="条目列表"
      onChange={load}
    />
  )
}
tsx

列显隐

hiddenColumns / onHiddenColumnsChange 存储隐藏列的键。显隐变化保留排序和筛选状态,组件默认不生成显隐控件。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
'use client'

import { useMemo, useState } from 'react'
import { Checkbox, DataTable } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 4)

export default function Demo() {
  const [showCount, setShowCount] = useState(true)
  const hiddenColumns = useMemo(() => (showCount ? [] : ['count']), [showCount])

  return (
    <DataTable
      rows={entries}
      columns={columns}
      hiddenColumns={hiddenColumns}
      rowKey="id"
      label="条目列表"
      renderToolbar={() => (
        <Checkbox checked={showCount} onCheckedChange={value => setShowCount(value === true)}>
          显示数量列
        </Checkbox>
      )}
    />
  )
}
tsx

列宽、固定与顺序

列支持 width、minWidth、maxWidth 以及逻辑方向的 pin: 'start' | 'end'。truncate 将默认文本限制为单行,仅溢出时显示 Tooltip。自定义单元格内容自行处理截断。

resizable 允许拖动表头边界调宽,拖动时显示贯穿表格的指示线。默认 resizeMode="fit" 与相邻列交换宽度,保持当前表格总宽不变。普通列从末端边界调整,固定在末端的列从内侧起始边界调整,RTL 下方向镜像。普通末列没有外侧手柄;相邻列禁止调宽或两列之间没有可调整空间时,不显示对应手柄。resizeMode="expand" 只调整当前列,其他列宽保持不变;放宽时表格可超出容器并横向滚动,收窄时最多消耗超出的宽度,到达容器宽度后停止,不会继续缩出空白。表格已经铺满时,要继续缩窄一列并将宽度交给相邻列,使用 fit。两种模式均遵守列的上下限。

调宽手柄位于所属表头的可见范围内,并与固定区分隔线分开。横向滚动或调宽后,部分被固定列遮挡的表头仍保留可操作的手柄;完全离开可见区的手柄不参与点击和键盘导航。悬停通过 Tooltip 显示受影响的列名,拖动时显示各列的当前像素宽度。fit 的共享手柄同时标明相邻两列,expand 只标明当前列。交互调整固定列宽度时,为中间非固定列保留至少 48px 的可视区域。

聚焦边界后,左右方向键每次调整 1px,Shift 调整 10px,Home/End 到达可调整范围的边界。Esc 撤销当前拖动。columnWidths / onColumnWidthsChange 存储手动指定的像素宽度。fit 只记录调整的两列,其他未指定宽度的列继续分配容器剩余空间;expand 同时记录其余列的显示宽度,以保证它们不会一起变化。仅按下再松开手柄不会写入宽度,Esc 恢复本次拖动前的设置。清空 columnWidths可恢复自动分配。交互调宽使用数字边界;静态列也支持 CSS 长度。

reorderColumns 允许直接拖动叶子表头,列预览与插入线显示松手后的落点。轻点仍执行排序,拖动不会触发排序,Esc 取消重排。聚焦表头后也可使用 Alt + 左右方向键。columnOrder / onColumnOrderChange 存储列键,重排限制在同一固定区域内。列的 resizable: false、reorderable: false 分别禁用调宽与重排。layout="fixed"、调宽、截断或虚拟化会约束表格布局。

示例通过 Select 切换调宽模式,通过 Button 清空列宽与顺序,恢复初始布局。

调宽模式

ID
1
条目 A — 这是一段会随列宽变化而截断的名称
已发布
18
2
条目 B — 这是一段会随列宽变化而截断的名称
草稿
55
3
条目 C — 这是一段会随列宽变化而截断的名称
已发布
92
4
条目 D — 这是一段会随列宽变化而截断的名称
已发布
129
'use client'

import { useState } from 'react'
import {
  DataTable,
  Stack,
  Inline,
  Text,
  Select,
  Button,
  type DataTableColumn,
} from '@hina-ui/react'
import { tableDemo, type TableDemoRow } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const modes = [
  { value: 'fit', label: '保持总宽' },
  { value: 'expand', label: '仅调整当前列' },
]
const sizedColumns: DataTableColumn<TableDemoRow>[] = [
  { key: 'id', label: 'ID', width: 72, pin: 'start', reorderable: false },
  { ...columns[0]!, width: 220, minWidth: 120, maxWidth: 360, truncate: true },
  { ...columns[1]!, width: 180 },
  { ...columns[2]!, width: 140, pin: 'end', reorderable: false },
]
const longRows = rows
  .slice(0, 4)
  .map(row => ({ ...row, name: `${row.name} — 这是一段会随列宽变化而截断的名称` }))

export default function Demo() {
  const [mode, setMode] = useState<'fit' | 'expand'>('fit')
  const [order, setOrder] = useState<string[]>([])
  const [widths, setWidths] = useState<Record<string, number>>({})

  return (
    <Stack gap="sm" className="w-full">
      <Inline justify="between">
        <Inline gap="sm">
          <Text size="sm" tone="muted">
            调宽模式
          </Text>
          <Select
            value={mode}
            onValueChange={value => setMode(value as 'fit' | 'expand')}
            options={modes}
            size="sm"
            className="w-44"
            aria-label="调宽模式"
          />
        </Inline>
        <Button
          size="sm"
          variant="ghost"
          tone="neutral"
          disabled={!order.length && !Object.keys(widths).length}
          onClick={() => {
            setOrder([])
            setWidths({})
          }}
        >
          恢复列布局
        </Button>
      </Inline>
      <DataTable
        columnOrder={order}
        onColumnOrderChange={setOrder}
        columnWidths={widths}
        onColumnWidthsChange={setWidths}
        rows={longRows}
        columns={sizedColumns}
        rowKey="id"
        resizable
        resizeMode={mode}
        reorderColumns
        label="条目列表"
      />
    </Stack>
  )
}
tsx

多级表头与汇总

嵌套列的 children 生成多级表头。父列负责标签与表头内容(renderHeader),叶子列负责数据、排序与布局。隐藏或重排叶子列会自动更新跨行、跨列范围。

aggregate 支持 sum、min、max、mean、count、uniqueCount 或函数。footer: true 显示该聚合结果,也可用字符串或 (rows) => text 自定义。汇总使用当前已加载且符合筛选的所有行,包含折叠行。renderColumnFooter 替换单个汇总单元格,renderSummary 替换 <tfoot> 内部内容,应返回表格行。

基本信息
状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
合计310
'use client'

import { DataTable, type DataTableColumn } from '@hina-ui/react'
import { tableDemo, type TableDemoRow } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 5)
const groupedColumns: DataTableColumn<TableDemoRow>[] = [
  { key: 'details', label: '基本信息', children: [columns[0]!, columns[1]!] },
  { ...columns[2]!, aggregate: 'sum', footer: true },
]

export default function Demo() {
  return (
    <DataTable
      rows={entries}
      columns={groupedColumns}
      rowKey="id"
      label="条目列表"
      renderColumnFooter={({ column }) => (column.key === 'name' ? '合计' : undefined)}
    />
  )
}
tsx

行展开

expandable 添加展开控件,也可传入函数限定可展开行。expanded / onExpandedChange 存储行键。renderExpansion 接收行上下文,内容显示在该行下方并跨越全部列。展开内容不额外占用分页名额。

展开状态
条目 A
已发布
18
ID
1
状态
已发布
数量
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
'use client'

import { useState } from 'react'
import {
  DescriptionList,
  DescriptionTerm,
  DescriptionDetails,
  DataTable,
  type DataTableKey,
} from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns, statusLabels } = tableDemo('zh-CN')
const entries = rows.slice(0, 4)

export default function Demo() {
  const [expanded, setExpanded] = useState<DataTableKey[]>([1])

  return (
    <DataTable
      expanded={expanded}
      onExpandedChange={setExpanded}
      rows={entries}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      expandable
      label="条目列表"
      renderExpansion={({ row }) => (
        <DescriptionList className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-sm">
          <DescriptionTerm className="text-muted">ID</DescriptionTerm>
          <DescriptionDetails>{row.id}</DescriptionDetails>
          <DescriptionTerm className="text-muted">状态</DescriptionTerm>
          <DescriptionDetails>{statusLabels[row.status]}</DescriptionDetails>
          <DescriptionTerm className="text-muted">数量</DescriptionTerm>
          <DescriptionDetails>{row.count}</DescriptionDetails>
        </DescriptionList>
      )}
    />
  )
}
tsx

树形数据

getChildren(row) 提供子行,行键在整棵树中保持唯一。expanded 控制展开状态,缩进跟随层级。分页按根行计数,筛选保留匹配子行的祖先。

默认选择父行会同时选择可选后代,部分子行选中时父行显示半选态。selectChildren={false} 让各行独立选择,单选模式始终只选择一行。

展开状态
条目 A
已发布
18
条目 D
已发布
129
条目 E
已归档
16
条目 B
草稿
55
条目 C
已发布
92
'use client'

import { useState } from 'react'
import { DataTable, type DataTableKey } from '@hina-ui/react'
import { treeTableDemo } from '../../data-table'

const { rows, columns, getChildren } = treeTableDemo('zh-CN')

export default function Demo() {
  const [selected, setSelected] = useState<DataTableKey[]>([4])
  const [expanded, setExpanded] = useState<DataTableKey[]>([1])

  return (
    <DataTable
      selected={selected}
      onSelectedChange={setSelected}
      expanded={expanded}
      onExpandedChange={setExpanded}
      rows={rows}
      columns={columns}
      getChildren={getChildren}
      rowKey="id"
      rowLabel="name"
      selectable
      label="条目列表"
    />
  )
}
tsx

分组与聚合

grouping / onGroupingChange 按顺序存储分组列键,expandedGroups / onExpandedGroupsChange 控制生成的分组键,与数据行的 expanded 分开保存。多个列键产生嵌套分组,分页按最外层分组计数。

默认分组行显示分组值、数量及列聚合结果。renderGroup 接收分组的 key、column、value、rows、depth、expanded、toggleExpanded() 和 aggregate(columnKey)。分组行不可选择或编辑。远程模式由数据源负责分组,可用 getChildren 表达返回的层级。

状态
—382
条目 A
已发布
18
条目 C
已发布
92
条目 D
已发布
129
条目 F
已发布
53
条目 G
已发布
90
—182
—16
'use client'

import { useState } from 'react'
import { DataTable } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 8)
const groupedColumns = columns.map(column =>
  column.key === 'count' ? { ...column, aggregate: 'sum' as const } : column,
)

export default function Demo() {
  const [grouping, setGrouping] = useState(['status'])
  const [expandedGroups, setExpandedGroups] = useState(['status:active'])

  return (
    <DataTable
      grouping={grouping}
      onGroupingChange={setGrouping}
      expandedGroups={expandedGroups}
      onExpandedGroupsChange={setExpandedGroups}
      rows={entries}
      columns={groupedColumns}
      rowKey="id"
      label="条目列表"
    />
  )
}
tsx

单元格与整行编辑

设置列的 editable 与 editMode="cell" | "row"。双击单元格,或聚焦后按 Enter 进入编辑;整行模式提供编辑按钮。renderEditor 替换输入控件(用 column.key 区分列),接收 value、updateValue、pending、error、commit 和 cancel。保存、取消与错误反馈仍由组件负责。示例用 Select 替换一列编辑器。

草稿不会直接修改传入数据。parse 转换草稿值,validate 同步或异步返回错误字符串或 undefined。onSave(edit) 可返回 Promise;等待期间禁止重复提交。抛错会保留草稿、显示错误并调用 onEditError,不会留下未处理的拒绝。

成功后调用 onEdit,参数为 { key, row, column?, values },其中 values 按列键组织。调用方负责应用修改,包括将 accessor 列映射回原始字段。onSave 在 onEdit 之前执行一次,不要在两处重复发送请求。默认输入框支持 Enter 保存与 Escape 取消。

整行编辑的保存错误在行下方显示一次;列 validate 返回的错误只显示在对应字段,renderEditor 收到的 error 也只包含该字段的校验错误。编辑器通过 InputGroup 的 bare 变体统一尺寸和外观,自定义编辑器中的 Input、Select 等控件会继承嵌入样式。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
'use client'

import { Checkbox, DataTable, Select, Inline } from '@hina-ui/react'
import { useEditableTableDemo } from '../../data-table'

const modes = [
  { value: 'cell', label: '单元格' },
  { value: 'row', label: '整行' },
]

export default function Demo() {
  const { rows, columns, statusLabels, mode, setMode, fail, setFail, save } =
    useEditableTableDemo('zh-CN')
  const options = Object.entries(statusLabels).map(([value, label]) => ({ value, label }))

  return (
    <DataTable
      rows={rows}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      editMode={mode}
      onSave={save}
      layout="fixed"
      label="条目列表"
      renderToolbar={() => (
        <Inline align="center" wrap>
          <Select
            size="sm"
            value={mode}
            onValueChange={value => setMode(value as 'cell' | 'row')}
            options={modes}
            aria-label="编辑模式"
            className="w-32"
          />
          <Checkbox checked={fail} onCheckedChange={value => setFail(value === true)}>
            模拟保存失败
          </Checkbox>
        </Inline>
      )}
      renderEditor={({ column, value, updateValue, pending }) =>
        column.key === 'status' ? (
          <Select
            value={value as string}
            options={options}
            disabled={pending}
            aria-label="状态"
            size="sm"
            onValueChange={updateValue}
          />
        ) : undefined
      }
    />
  )
}
tsx

行重排

reorderable 添加支持鼠标与触摸的拖动手柄,也支持上下方向键。rows / onRowsChange 接收重排后的根数组,原数组不会被修改。onRowReorder 包含 { row, target, parent?, from, to, rows }。子行重排时,由调用方将新的同级数组应用到 parent,不改变父子归属。

排序、筛选或分组改变显示顺序时禁用行拖动。可传入函数禁止移动部分行。重排范围是已加载的同级行,持久化由调用方负责。

移动行名称状态数量
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { DataTable } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const source = tableDemo('zh-CN')
const columns = source.columns.map(column => ({ ...column, sortable: false }))

export default function Demo() {
  const [rows, setRows] = useState(() => source.rows.slice(0, 5))

  return (
    <DataTable
      rows={rows}
      onRowsChange={setRows}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      reorderable
      label="条目列表"
    />
  )
}
tsx

滚动

横向溢出限制在 ScrollArea 内。maxHeight 限制滚动区高度,stickyHeader 固定表头。className 作用于完整 DataTable,tableClass 作用于滚动区。ref 暴露 viewport 与原生表格 element。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
条目 F
已发布
53
条目 G
已发布
90
条目 H
草稿
127
条目 I
已发布
14
条目 J
已归档
51
条目 K
草稿
88
条目 L
已发布
125
条目 M
已发布
12
条目 N
草稿
49
条目 O
已归档
86
条目 P
已发布
123
条目 Q
草稿
10
条目 R
已发布
47
条目 S
已发布
84
条目 T
已归档
121
条目 U
已发布
8
条目 V
已发布
45
条目 W
草稿
82
条目 X
已发布
119
'use client'

import { DataTable } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const wideColumns = columns.map(column => ({
  ...column,
  width: column.key === 'name' ? 320 : 200,
}))

export default function Demo() {
  return (
    <DataTable
      rows={rows}
      columns={wideColumns}
      rowKey="id"
      stickyHeader
      maxHeight={240}
      label="条目列表"
    />
  )
}
tsx

撑满与固定汇总

height 设置包含工具栏、分页的组件总高度;fill 撑满具有确定高度的父容器。表格在剩余空间中滚动,stickyFooter 固定汇总单元格。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
条目 F
已发布
53
条目 G
已发布
90
条目 H
草稿
127
条目 I
已发布
14
条目 J
已归档
51
条目 K
草稿
88
条目 L
已发布
125
条目 M
已发布
12
条目 N
草稿
49
条目 O
已归档
86
合计1644
'use client'

import { Stack, DataTable } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const summaryColumns = columns.map(column =>
  column.key === 'count' ? { ...column, aggregate: 'sum' as const, footer: true } : column,
)

export default function Demo() {
  return (
    <Stack className="h-80 w-full">
      <DataTable
        rows={rows}
        columns={summaryColumns}
        rowKey="id"
        fill
        stickyHeader
        stickyFooter
        pagination
        defaultPageSize={15}
        label="条目列表"
        renderColumnFooter={({ column }) => (column.key === 'name' ? '合计' : undefined)}
      />
    </Stack>
  )
}
tsx

CSV 导出

api.toCsv(options) 返回 CSV 字符串,api.exportCsv(options) 下载文件。scope 支持 page、filtered(默认)、selected 和 all。远程模式同样只处理已加载记录;仅有已选键而没有行对象的记录无法导出。

导出跟随可见列顺序,排除 exportable: false 的列,columns 可进一步指定列键。exportValue 优先提供导出值,否则 formatted 决定是否使用显示格式。字符串会转义并加引号,类似公式的字符串受到保护,默认启用 UTF-8 BOM。可配置 delimiter、bom 与 filename。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
条目 D
已发布
129
条目 E
已归档
16
'use client'

import { useState } from 'react'
import { Button, DataTable, Inline, type DataTableKey } from '@hina-ui/react'
import { tableDemo } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 5)

export default function Demo() {
  const [selected, setSelected] = useState<DataTableKey[]>([1, 2])

  return (
    <DataTable
      selected={selected}
      onSelectedChange={setSelected}
      rows={entries}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      selectable
      label="条目列表"
      renderToolbar={({ api }) => (
        <Inline wrap>
          <Button
            size="sm"
            variant="soft"
            tone="neutral"
            onClick={() => api.exportCsv({ formatted: true, filename: 'entries.csv' })}
          >
            导出 CSV
          </Button>
          <Button
            size="sm"
            variant="ghost"
            tone="neutral"
            disabled={!selected.length}
            onClick={() =>
              api.exportCsv({ scope: 'selected', formatted: true, filename: 'selected.csv' })
            }
          >
            导出所选
          </Button>
        </Inline>
      )}
    />
  )
}
tsx

空状态与加载

emptyText 替换默认提示,empty 可放入 Empty 等自定义内容。loadingContent 替换 LoadingOverlay 的内容,应保留状态播报。

状态
条目 A
已发布
18
条目 B
草稿
55
条目 C
已发布
92
状态

暂无条目

'use client'

import { useState } from 'react'
import { Button, DataTable, Empty, Stack } from '@hina-ui/react'
import { tableDemo, type TableDemoRow } from '../../data-table'

const { rows, columns } = tableDemo('zh-CN')
const entries = rows.slice(0, 3)
const noRows: TableDemoRow[] = []

export default function Demo() {
  const [loading, setLoading] = useState(false)

  return (
    <Stack className="w-full">
      <Button
        size="sm"
        variant="soft"
        tone="neutral"
        className="self-start"
        onClick={() => setLoading(!loading)}
      >
        切换加载状态
      </Button>
      <DataTable rows={entries} columns={columns} rowKey="id" loading={loading} label="条目列表" />
      <DataTable
        rows={noRows}
        columns={columns}
        rowKey="id"
        label="条目列表"
        empty={<Empty size="sm" title="暂无条目" icon={false} />}
      />
    </Stack>
  )
}
tsx

虚拟滚动

virtualize 只渲染可见行与缓冲区,可用 { estimateSize, overscan } 指定初始行高估计和缓冲数量。实际行高与展开内容会自动测量,可组合固定列、选择、展开和分组。提供 height、maxHeight 或 fill,否则滚动区默认上限为 400px。

数据处理仍针对传入的完整行集。虚拟化减少挂载的 DOM,不负责请求数据。api.scrollToRow(key) 可滚动到当前显示行集中的任意行,包括渲染窗口之外的行。折叠或其他页内的行需先展开祖先或切换页码。

展开状态
条目 1
已发布
0
条目 2
已发布
1
条目 3
已发布
2
条目 4
已发布
3
条目 5
已发布
4
条目 6
已发布
5
条目 7
已发布
6
条目 8
已发布
7
条目 9
已发布
8
条目 10
已发布
9
条目 11
已发布
10
条目 12
已发布
11
条目 13
已发布
12
条目 14
已发布
13
条目 15
已发布
14
条目 16
已发布
15
'use client'

import { useRef } from 'react'
import { Stack, Button, DataTable, type DataTableHandle } from '@hina-ui/react'
import { tableDemo, type TableDemoRow } from '../../data-table'

const { columns } = tableDemo('zh-CN')
const rows: TableDemoRow[] = Array.from({ length: 10000 }, (_, index) => ({
  id: index + 1,
  name: '条目 ' + (index + 1),
  status: 'active',
  count: index,
}))

export default function Demo() {
  const table = useRef<DataTableHandle<TableDemoRow>>(null)

  return (
    <DataTable
      ref={table}
      rows={rows}
      columns={columns}
      rowKey="id"
      rowLabel="name"
      virtualize={{ estimateSize: 44, overscan: 6 }}
      height={320}
      stickyHeader
      expandable
      label="条目列表"
      renderToolbar={() => (
        <Button
          size="sm"
          variant="soft"
          tone="neutral"
          className="self-start"
          onClick={() => table.current?.api.scrollToRow(5000)}
        >
          跳到第 5,000 行
        </Button>
      )}
      renderExpansion={({ row }) => (
        <Stack className="flex h-32 items-center text-muted">{row.name} · 展开内容</Stack>
      )}
    />
  )
}
tsx

无障碍

组件保留原生表格语义、多级表头作用域与虚拟行位置。用 caption 或 label 提供无障碍名称。排序、选择、调宽、重排和编辑均提供键盘入口,保存或取消后恢复到编辑入口。自定义编辑器与汇总行应保留对应语义。

API

Props

Prop
类型
默认值
说明
rows
T[]
Required
传入的行
columns
DataTableColumn<T>[]
Required
列定义
rowKey
DataTableKeyField<T> | ((row: T) => DataTableKey)
Required
全局唯一且稳定的行键
rowLabel
keyof T | ((row: T) => string)
Row key
可访问的行标签
selectable
boolean | ((row: T) => boolean)
false
是否可选
selectionMode
'single' | 'multiple'
multiple
选择模式
selectAll
'page' | 'filtered'
page
表头选择范围
selectChildren
boolean
true
级联选择后代
expandable
boolean | ((row: T) => boolean)
false
详情展开条件
getChildren
(row: T) => T[] | undefined
—
树形子行
pagination
boolean
false
启用分页
manual
boolean
false
外部处理查询
total
number
—
已知远程总数
hasNextPage
boolean
—
未知总数时能否翻到下一页
autoResetPage
boolean
true
查询条件变化后复位页码
multiSort
boolean
false
Shift 追加排序
resizable
boolean
false
列宽手柄
resizeMode
'fit' | 'expand'
fit
调整相邻列保持总宽,或仅调整当前列
reorderColumns
boolean
false
拖动表头重排
reorderable
boolean | ((row: T) => boolean)
false
行重排手柄
virtualize
boolean | { estimateSize?: number; overscan?: number }
false
虚拟渲染;估计行高 44,缓冲 6
editMode
'cell' | 'row'
—
编辑模式
onSave
(edit: DataTableEdit<T>) => void | Promise<void>
—
等待完成的保存回调
loading
boolean
false
加载时阻止旧数据交互
disabled
boolean
false
禁用内置交互
rowClickable
boolean
false
启用行激活
rowClass
(row: T) => string | undefined
—
行样式
variant
'primary' | 'secondary'
primary
外观
hover
boolean
true
行悬停反馈
stickyHeader
boolean
false
固定表头
stickyFooter
boolean
false
固定汇总
height
number | string
—
组件总高度
maxHeight
number | string
—
滚动区最大高度
fill
boolean
false
撑满具有确定高度的父容器
layout
'auto' | 'fixed'
auto
表格布局;约束列宽的能力会使用 fixed
caption
string
—
可见标题
label
string
—
无障碍名称
emptyText
string
Locale
空状态文本
className
string
—
根样式
tableClass
string
—
滚动区样式

列定义

属性
类型
说明
key
string
唯一列键
label
string
表头标签
children
DataTableColumn<T>[]
嵌套表头
field
keyof T
数据字段,默认使用 key
accessor
(row: T) => unknown
自定义取值
sortable
boolean
启用排序
sort
(a: T, b: T) => number
升序比较器
filterable
boolean
参与全局筛选,默认 true
filter
(row: T, query: string) => boolean
全局筛选匹配器
filterMode
'contains' | 'equals' | 'in' | 'range'
列匹配模式,默认 contains
filterValue
(row: T, value: unknown) => boolean
自定义列匹配器
format
(value: unknown, row: T) => string | number
显示格式
align
'start' | 'center' | 'end'
逻辑方向对齐
width / minWidth / maxWidth
number | string
列宽及上下限
pin
'start' | 'end'
固定区域
truncate
boolean
单行截断与溢出提示
resizable / reorderable
boolean
false 禁用相应手柄
aggregate
DataTableAggregate | ((rows: T[]) => unknown)
分组与汇总聚合
footer
boolean | string | ((rows: T[]) => string | number)
汇总内容
editable
boolean | ((row: T) => boolean)
可编辑条件
parse
(value: unknown, row: T) => unknown
转换草稿值
validate
(value: unknown, row: T) => string | undefined | Promise<string | undefined>
返回校验错误
exportable
boolean
false 排除出 CSV
exportValue
(row: T) => unknown
覆盖导出值
headerClass
string
表头样式
cellClass
string | ((row: T) => string | undefined)
单元格样式

受控状态

绑定
类型
默认值
page / onPageChange
number
1
pageSize / onPageSizeChange
number
10
sorting / onSortingChange
DataTableSort[]
[]
filter / onFilterChange
string
''
columnFilters / onColumnFiltersChange
DataTableFilter[]
[]
grouping / onGroupingChange
string[]
[]
selected / onSelectedChange
DataTableKey[]
[]
expanded / onExpandedChange
DataTableKey[]
[]
expandedGroups / onExpandedGroupsChange
string[]
[]
hiddenColumns / onHiddenColumnsChange
string[]
[]
columnOrder / onColumnOrderChange
string[]
[]
columnWidths / onColumnWidthsChange
Record<string, number>
{}

根行重排可使用 rows / onRowsChange,其他情况下照常传入 rows。

内容属性

属性
Props
说明
renderCell
DataTableCellContext<T>
数据单元格
renderHeader
DataTableHeaderContext<T>
表头内容
renderEditor
DataTableEditorContext<T>
编辑控件
renderExpansion
DataTableRowContext<T>
展开详情
renderGroup
DataTableGroupContext<T>
分组行
renderColumnFooter
{ column, rows: T[] }
汇总单元格
renderSummary
DataTableState<T>
tfoot 内容
renderToolbar / renderFooter
DataTableState<T>
工具栏与分页页脚
empty / loadingContent
—
状态内容

DataTableState<T> 包含查询参数、total、当前显示的原始 rows、selected、expanded、visibleColumns 与 api。

回调

回调
参数
说明
onChange
DataTableQuery
查询变化
onRowClick
(row: T, event: MouseEvent | KeyboardEvent)
行激活
onRowContextmenu
(row: T, event: MouseEvent)
右键菜单事件
onRowsChange
T[]
重排后的根数组
onRowReorder
DataTableReorder<T>
根或同级行重排
onEdit
DataTableEdit<T>
编辑成功
onEditError
(error: unknown, edit: DataTableEdit<T>)
保存或校验异常

Ref

ref 暴露 element: HTMLTableElement | undefined、viewport: HTMLElement | undefined、state: DataTableState<T> 与 api: DataTableApi<T>,renderToolbar 与 renderFooter 也收到同一份 API。

方法返回值
说明
getRows(scope?)T[]
page / filtered / selected / all,只含已加载数据
toggleSelected(key, value?)void
选择已加载行
toggleExpanded(key, value?)void
控制行展开
setFilter(column, value)void
设置列筛选
setColumnHidden(column, hidden)void
改变列显隐
setColumnWidth(column, width)void
直接设置指定列的像素宽度并遵守上下限,不与相邻列交换
moveColumn(column, target)void
在同一固定区域内移动列
moveRow(key, target)void
重排可移动同级行
startEdit(key, column?)void
开始单元格或行编辑
cancelEdit()void
取消草稿
commitEdit()Promise<void>
校验并保存
scrollToRow(key)void
滚动到显示行集中的行
toCsv(options?)string
返回 CSV
exportCsv(options?)void
下载 CSV
interface DataTableExportOptions {
  scope?: 'page' | 'filtered' | 'selected' | 'all'
  columns?: string[]
  formatted?: boolean
  delimiter?: ',' | ';' | '\t'
  bom?: boolean
  filename?: string
}
ts