| 状态 | ||
|---|---|---|
条目 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
}
/>
)
}
使用
import { DataTable, type DataTableColumn } from '@hina-ui/react'
传入 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)
}}
/>
)
}
排序
列设置 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>
)
}
全局筛选
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"
/>
)}
/>
)
}
独立列筛选
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
}
/>
)
}
多选
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>
)}
/>
)
}
单选
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="条目列表"
/>
)
}
分页
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"
/>
)}
/>
)
}
远程数据
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
}
/>
)
}
未知总数
远程模式省略 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}
/>
)
}
列显隐
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>
)}
/>
)
}
列宽、固定与顺序
列支持 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>
)
}
多级表头与汇总
嵌套列的 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)}
/>
)
}
行展开
expandable 添加展开控件,也可传入函数限定可展开行。expanded / onExpandedChange 存储行键。renderExpansion 接收行上下文,内容显示在该行下方并跨越全部列。展开内容不额外占用分页名额。
| 展开 | 状态 | ||
|---|---|---|---|
条目 A | 已发布 | 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>
)}
/>
)
}
树形数据
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="条目列表"
/>
)
}
分组与聚合
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="条目列表"
/>
)
}
单元格与整行编辑
设置列的 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
}
/>
)
}
行重排
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="条目列表"
/>
)
}
滚动
横向溢出限制在 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="条目列表"
/>
)
}
撑满与固定汇总
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>
)
}
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>
)}
/>
)
}
空状态与加载
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>
)
}
虚拟滚动
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>
)}
/>
)
}
无障碍
组件保留原生表格语义、多级表头作用域与虚拟行位置。用 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
}