| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
<script setup lang="ts">
import { DataTable, Tag } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns, statusLabels } = tableDemo('zh-CN')
</script>
<template>
<DataTable :rows="rows.slice(0, 4)" :columns="columns" row-key="id" label="条目列表">
<template #cell-status="{ row }">
<Tag :tone="row.status === 'active' ? 'success' : 'neutral'">
{{ statusLabels[row.status] }}
</Tag>
</template>
</DataTable>
</template>
使用
import { DataTable, type DataTableColumn } from '@hina-ui/vue'
传入 rows、columns 与稳定的 rowKey。默认只显示表格,其他控件按需开启。样式对齐 Table,支持密度、深色模式与 RTL。
列、插槽、回调与实例 API 保留原始行类型。数据变化时传入新数组,以便重新计算数据处理结果。
示例
单元格
cell-key 替换指定列的内容,cell 作为公共后备。插槽提供带类型的原始 row、column、value、稳定的 key、源数组中的 index、depth,以及选择、展开的方法。field 可指定其他字段,accessor 优先级更高。format 只影响显示。专属 cell-key 的优先级高于通用 cell。
align 同时作用于表头与单元格。rowClickable 支持点击、Enter 和 Space,并触发 rowClick;单元格内的交互控件保留自己的行为。rowContextmenu 提供原始行和事件,自定义菜单时由调用方执行 event.preventDefault()。
名称列通过 #cell-name 组合 Avatar 与两行 Text,状态列通过 #cell-status 渲染 Tag。通用 #cell 根据 column.key 为数量列组合 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 |
<script setup lang="ts">
import { Avatar, DataTable, Inline, Progress, Stack, Tag, Text } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns, statusLabels } = tableDemo('zh-CN')
</script>
<template>
<DataTable :rows="rows.slice(0, 5)" :columns="columns" row-key="id" label="条目列表">
<template #cell-name="{ row }">
<Inline gap="sm" :wrap="false" class="min-w-36 py-2">
<Avatar :name="row.name.slice(-1)" size="sm" aria-hidden="true" />
<Stack gap="none">
<Text size="sm" weight="medium" class="whitespace-nowrap">{{ row.name }}</Text>
<Text size="xs" tone="muted">ID {{ row.id }}</Text>
</Stack>
</Inline>
</template>
<template #cell-status="{ row }">
<Tag :tone="row.status === 'active' ? 'success' : 'neutral'">
{{ statusLabels[row.status] }}
</Tag>
</template>
<template #cell="{ column, value }">
<Inline v-if="column.key === 'count'" gap="sm" :wrap="false" class="min-w-32">
<Progress
:value="Number(value)"
:max="150"
:aria-label="column.label"
size="sm"
class="flex-1"
/>
<Text size="sm" class="w-8 shrink-0 text-end tabular-nums">{{ value }}</Text>
</Inline>
<template v-else>{{ value }}</template>
</template>
</DataTable>
</template>
排序
列设置 sortable: true 后,表头按升序、降序、取消排序循环。v-model:sorting 存储 { key, desc }[],multi-sort 允许 Shift 点击追加排序。数字与日期按值比较,字符串按当前语言自然排序,缺失值始终放在末尾。sort(a, b) 自定义升序比较。
header-key / header 提供 sorting、sortIndex 和 toggleSort(multi?);自定义表头仍保留外围单元格与 aria-sort。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 E | 已归档 | 16 |
条目 F | 已发布 | 53 |
未排序
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, Text, Stack, type DataTableSort } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const sorting = ref<DataTableSort[]>([])
</script>
<template>
<Stack class="w-full">
<DataTable
v-model:sorting="sorting"
:rows="rows.slice(0, 6)"
:columns="columns"
row-key="id"
multi-sort
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>
</template>
全局筛选
v-model:filter 在参与筛选的列中进行不区分大小写的子串匹配。filterable: false 将列排除出全局筛选,filter(row, query) 自定义该列的匹配方式。隐藏列仍可参与,任一列匹配即可保留该行。
可在 toolbar 中放置 SearchInput。防抖、请求与过期响应处理由调用方管理。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 E | 已归档 | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, SearchInput } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const filter = ref('')
</script>
<template>
<DataTable
v-model:filter="filter"
:rows="rows"
:columns="columns"
row-key="id"
pagination
:page-size="5"
label="条目列表"
>
<template #toolbar>
<SearchInput
size="sm"
v-model="filter"
placeholder="筛选名称"
aria-label="筛选名称"
class="w-64 max-w-full"
/>
</template>
</DataTable>
</template>
独立列筛选
v-model:columnFilters 存储 { key, value }[],各列条件取交集,并与全局筛选同时生效。filterMode 支持 contains、equals、in(数组)和 range([min, max],任一边界可为空)。数字、布尔值和日期保留原类型,filterValue(row, value) 可自定义匹配。
表头插槽提供 filterValue 与 setFilter(value)。传入 null、undefined、空字符串或空数组清除该列条件。筛选控件按需添加,示例使用 Select。
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 E | 已归档 | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, Select, type DataTableFilter } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns, statusLabels } = tableDemo('zh-CN')
const filters = ref<DataTableFilter[]>([])
const options = Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
const filteredColumns = columns.map(column =>
column.key === 'status' ? { ...column, filterMode: 'equals' as const } : column,
)
</script>
<template>
<DataTable
v-model:column-filters="filters"
:rows="rows"
:columns="filteredColumns"
row-key="id"
pagination
:page-size="5"
label="条目列表"
>
<template #header-status="{ column, filterValue, setFilter }">
<Select
:model-value="filterValue === undefined ? undefined : String(filterValue)"
:options="options"
:placeholder="column.label"
:aria-label="column.label"
clearable
size="sm"
class="w-36 py-1"
@update:model-value="setFilter"
/>
</template>
</DataTable>
</template>
多选
selectable 添加 Checkbox,也可传入函数禁止选择部分行。v-model:selected 存储行键,数字与字符串区分处理。翻页、筛选和替换远程结果保留已选键,删除数据后是否清除对应键由调用方决定。
表头默认控制当前页可选行,selectAll="filtered" 则控制当前已加载数据中所有符合筛选条件的行。两者都不会选择尚未加载的远程记录。rowLabel 用于提供可读的控件名称。
已选:1
| 状态 | |||
|---|---|---|---|
条目 A | 已发布 | 18 | |
条目 B | 草稿 | 55 | |
条目 C | 已发布 | 92 | |
条目 D | 已发布 | 129 | |
条目 E | 已归档 | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { Button, DataTable, Inline, Text, type DataTableKey } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const selected = ref<DataTableKey[]>([1])
</script>
<template>
<DataTable
v-model:selected="selected"
:rows="rows"
:columns="columns"
row-key="id"
row-label="name"
:selectable="row => row.status !== 'archived'"
pagination
:page-size="5"
label="条目列表"
>
<template #toolbar>
<Inline align="center" justify="between">
<Text size="sm" tone="muted">已选:{{ selected.length }}</Text>
<Button
variant="ghost"
tone="neutral"
size="sm"
:disabled="!selected.length"
@click="selected = []"
>
清空选择
</Button>
</Inline>
</template>
</DataTable>
</template>
单选
selectionMode="single" 使用单选控件,新选择替换旧选择。selected 仍是键数组,最多保留一个键;表头不显示全选控件。
| 选择此行 | 状态 | ||
|---|---|---|---|
条目 A | 已发布 | 18 | |
条目 B | 草稿 | 55 | |
条目 C | 已发布 | 92 | |
条目 D | 已发布 | 129 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, type DataTableKey } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const selected = ref<DataTableKey[]>([2])
</script>
<template>
<DataTable
v-model:selected="selected"
:rows="rows.slice(0, 4)"
:columns="columns"
row-key="id"
row-label="name"
selectable
selection-mode="single"
label="条目列表"
/>
</template>
分页
pagination 启用 Pagination,page 从 1 开始,pageSize 默认 10。排序、全局或列筛选、分组及每页数量改变后复位到第一页;:auto-reset-page="false" 可保留当前页。已知总数时,数据减少会将越界页码调回有效范围。
默认页脚只显示分页控件。如需总数、每页数量或跳页输入,可在 footer 中组合 Pagination。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 E | 已归档 | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, Pagination } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const page = ref(1)
const pageSize = ref(5)
</script>
<template>
<DataTable
v-model:page="page"
v-model:page-size="pageSize"
:rows="rows"
:columns="columns"
row-key="id"
pagination
label="条目列表"
>
<template #footer="{ total, rows: visible }">
<Pagination
v-model="page"
v-model:page-size="pageSize"
:total="total"
:item-count="visible.length"
:page-size-options="[5, 10, 20]"
show-info
show-jump
align="between"
/>
</template>
</DataTable>
</template>
远程数据
manual 同时绕过本地筛选、排序、分组与分页。rows 为当前请求结果,total 提供远程总数。受控模型可从 URL 或 store 初始化。
change 在同一更新周期稳定后发出一次完整的 { page, pageSize, sorting, filter, columnFilters, grouping }。挂载时不触发,首次请求、取消与过期响应处理由调用方负责。
loading 保留现有行,阻止其交互,并使用 LoadingOverlay。加载期间临时清空结果不会收缩页码。
请求页 1 · 返回 0 / 0
| 状态 | ||
|---|---|---|
加载中 | ||
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, SearchInput, Text, Tag, Inline } from '@hina-ui/vue'
import { useRemoteTableDemo } from '../../data-table'
const { rows, columns, statusLabels, total, loading, lastQuery, load } =
useRemoteTableDemo('zh-CN')
const filter = ref('')
</script>
<template>
<DataTable
v-model:filter="filter"
:rows="rows"
:columns="columns"
:total="total"
:loading="loading"
row-key="id"
manual
pagination
:page-size="5"
label="条目列表"
@change="load"
>
<template #toolbar>
<Inline justify="between">
<SearchInput
size="sm"
v-model="filter"
placeholder="搜索名称"
aria-label="搜索名称"
class="w-64 max-w-full"
/>
<Text size="sm" tone="muted">
请求页 {{ lastQuery.page }} · 返回 {{ rows.length }} / {{ total }}
</Text>
</Inline>
</template>
<template #cell-status="{ row }">
<Tag :tone="row.status === 'active' ? 'success' : 'neutral'">
{{ statusLabels[row.status] }}
</Tag>
</template>
</DataTable>
</template>
未知总数
远程模式省略 total 时显示上一页、下一页控件。hasNextPage 显式控制下一页是否可用;省略时根据返回行数是否达到 pageSize 判断。
| 状态 | ||
|---|---|---|
加载中 | ||
<script setup lang="ts">
import { computed } from 'vue'
import { DataTable } from '@hina-ui/vue'
import { useRemoteTableDemo } from '../../data-table'
const { rows, columns, loading, total, lastQuery, load } = useRemoteTableDemo('zh-CN')
const hasNextPage = computed(() => lastQuery.value.page * lastQuery.value.pageSize < total.value)
</script>
<template>
<DataTable
:rows="rows"
:columns="columns"
row-key="id"
manual
pagination
:page-size="5"
:has-next-page="hasNextPage"
:loading="loading"
label="条目列表"
@change="load"
/>
</template>
列显隐
v-model:hiddenColumns 存储隐藏列的键。显隐变化保留排序和筛选状态,组件默认不生成显隐控件。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Checkbox, DataTable } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const showCount = ref(true)
const hiddenColumns = computed(() => (showCount.value ? [] : ['count']))
</script>
<template>
<DataTable
:rows="rows.slice(0, 4)"
:columns="columns"
:hidden-columns="hiddenColumns"
row-key="id"
label="条目列表"
>
<template #toolbar><Checkbox v-model="showCount">显示数量列</Checkbox></template>
</DataTable>
</template>
列宽、固定与顺序
列支持 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 撤销当前拖动。v-model:columnWidths 存储手动指定的像素宽度。fit 只记录调整的两列,其他未指定宽度的列继续分配容器剩余空间;expand 同时记录其余列的显示宽度,以保证它们不会一起变化。仅按下再松开手柄不会写入宽度,Esc 恢复本次拖动前的设置。清空该模型可恢复自动分配。交互调宽使用数字边界;静态列也支持 CSS 长度。
reorderColumns 允许直接拖动叶子表头,列预览与插入线显示松手后的落点。轻点仍执行排序,拖动不会触发排序,Esc 取消重排。聚焦表头后也可使用 Alt + 左右方向键。v-model:columnOrder 存储列键,重排限制在同一固定区域内。列的 resizable: false、reorderable: false 分别禁用调宽与重排。layout="fixed"、调宽、截断或虚拟化会约束表格布局。
示例通过 Select 切换调宽模式,通过 Button 清空列宽与顺序,恢复初始布局。
调宽模式
| ID | |||
|---|---|---|---|
1 | 条目 A — 这是一段会随列宽变化而截断的名称 | 已发布 | 18 |
2 | 条目 B — 这是一段会随列宽变化而截断的名称 | 草稿 | 55 |
3 | 条目 C — 这是一段会随列宽变化而截断的名称 | 已发布 | 92 |
4 | 条目 D — 这是一段会随列宽变化而截断的名称 | 已发布 | 129 |
<script setup lang="ts">
import { ref } from 'vue'
import {
DataTable,
Stack,
Inline,
Text,
Select,
Button,
type DataTableColumn,
} from '@hina-ui/vue'
import { tableDemo, type TableDemoRow } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const mode = ref<'fit' | 'expand'>('fit')
const modes = [
{ value: 'fit', label: '保持总宽' },
{ value: 'expand', label: '仅调整当前列' },
]
const order = ref<string[]>([])
const widths = ref<Record<string, number>>({})
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} — 这是一段会随列宽变化而截断的名称` }))
</script>
<template>
<Stack gap="sm" class="w-full">
<Inline justify="between">
<Inline gap="sm">
<Text size="sm" tone="muted">调宽模式</Text>
<Select v-model="mode" :options="modes" size="sm" class="w-44" aria-label="调宽模式" />
</Inline>
<Button
size="sm"
variant="ghost"
tone="neutral"
:disabled="!order.length && !Object.keys(widths).length"
@click="
() => {
order = []
widths = {}
}
"
>
恢复列布局
</Button>
</Inline>
<DataTable
v-model:column-order="order"
v-model:column-widths="widths"
:rows="longRows"
:columns="sizedColumns"
row-key="id"
resizable
:resize-mode="mode"
reorder-columns
label="条目列表"
/>
</Stack>
</template>
多级表头与汇总
嵌套列的 children 生成多级表头。父列负责标签与表头插槽,叶子列负责数据、排序与布局。隐藏或重排叶子列会自动更新跨行、跨列范围。
aggregate 支持 sum、min、max、mean、count、uniqueCount 或函数。footer: true 显示该聚合结果,也可用字符串或 (rows) => text 自定义。汇总使用当前已加载且符合筛选的所有行,包含折叠行。footer-key 替换单个汇总单元格,summary 替换 <tfoot> 内部内容,应返回表格行。
| 基本信息 | ||
|---|---|---|
| 状态 | ||
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 E | 已归档 | 16 |
| 合计 | 310 | |
<script setup lang="ts">
import { DataTable, type DataTableColumn } from '@hina-ui/vue'
import { tableDemo, type TableDemoRow } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const groupedColumns: DataTableColumn<TableDemoRow>[] = [
{ key: 'details', label: '基本信息', children: [columns[0]!, columns[1]!] },
{ ...columns[2]!, aggregate: 'sum', footer: true },
]
</script>
<template>
<DataTable :rows="rows.slice(0, 5)" :columns="groupedColumns" row-key="id" label="条目列表">
<template #footer-name>合计</template>
</DataTable>
</template>
行展开
expandable 添加展开控件,也可传入函数限定可展开行。v-model:expanded 存储行键。expansion 接收行上下文,内容显示在该行下方并跨越全部列。展开内容不额外占用分页名额。
| 展开 | 状态 | ||
|---|---|---|---|
条目 A | 已发布 | 18 | |
| |||
条目 B | 草稿 | 55 | |
条目 C | 已发布 | 92 | |
条目 D | 已发布 | 129 | |
<script setup lang="ts">
import { ref } from 'vue'
import {
DescriptionList,
DescriptionTerm,
DescriptionDetails,
DataTable,
type DataTableKey,
} from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns, statusLabels } = tableDemo('zh-CN')
const expanded = ref<DataTableKey[]>([1])
</script>
<template>
<DataTable
v-model:expanded="expanded"
:rows="rows.slice(0, 4)"
:columns="columns"
row-key="id"
row-label="name"
expandable
label="条目列表"
>
<template #expansion="{ row }">
<DescriptionList class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-sm">
<DescriptionTerm class="text-muted">ID</DescriptionTerm>
<DescriptionDetails>{{ row.id }}</DescriptionDetails>
<DescriptionTerm class="text-muted">状态</DescriptionTerm>
<DescriptionDetails>{{ statusLabels[row.status] }}</DescriptionDetails>
<DescriptionTerm class="text-muted">数量</DescriptionTerm>
<DescriptionDetails>{{ row.count }}</DescriptionDetails>
</DescriptionList>
</template>
</DataTable>
</template>
树形数据
getChildren(row) 提供子行,行键在整棵树中保持唯一。expanded 控制展开状态,缩进跟随层级。分页按根行计数,筛选保留匹配子行的祖先。
默认选择父行会同时选择可选后代,部分子行选中时父行显示半选态。:select-children="false" 让各行独立选择,单选模式始终只选择一行。
| 展开 | 状态 | |||
|---|---|---|---|---|
条目 A | 已发布 | 18 | ||
条目 D | 已发布 | 129 | ||
条目 E | 已归档 | 16 | ||
条目 B | 草稿 | 55 | ||
条目 C | 已发布 | 92 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable, type DataTableKey } from '@hina-ui/vue'
import { treeTableDemo } from '../../data-table'
const { rows, columns, getChildren } = treeTableDemo('zh-CN')
const selected = ref<DataTableKey[]>([4])
const expanded = ref<DataTableKey[]>([1])
</script>
<template>
<DataTable
v-model:selected="selected"
v-model:expanded="expanded"
:rows="rows"
:columns="columns"
:get-children="getChildren"
row-key="id"
row-label="name"
selectable
label="条目列表"
/>
</template>
分组与聚合
v-model:grouping 按顺序存储分组列键,v-model:expandedGroups 控制生成的分组键,与数据行的 expanded 分开保存。多个列键产生嵌套分组,分页按最外层分组计数。
默认分组行显示分组值、数量及列聚合结果。group 提供分组的 key、column、value、rows、depth、expanded、toggleExpanded() 和 aggregate(columnKey)。分组行不可选择或编辑。远程模式由数据源负责分组,可用 getChildren 表达返回的层级。
| 状态 | ||
|---|---|---|
| — | 382 | |
条目 A | 已发布 | 18 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
条目 F | 已发布 | 53 |
条目 G | 已发布 | 90 |
| — | 182 | |
| — | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const grouping = ref(['status'])
const expandedGroups = ref(['status:active'])
const groupedColumns = columns.map(column =>
column.key === 'count' ? { ...column, aggregate: 'sum' as const } : column,
)
</script>
<template>
<DataTable
v-model:grouping="grouping"
v-model:expanded-groups="expandedGroups"
:rows="rows.slice(0, 8)"
:columns="groupedColumns"
row-key="id"
label="条目列表"
></DataTable>
</template>
单元格与整行编辑
设置列的 editable 与 editMode="cell" | "row"。双击单元格,或聚焦后按 Enter 进入编辑;整行模式提供编辑按钮。editor-key / editor 替换输入控件,接收 value、updateValue、pending、error、commit 和 cancel。保存、取消与错误反馈仍由组件负责。示例用 Select 替换一列编辑器。
草稿不会直接修改传入数据。parse 转换草稿值,validate 同步或异步返回错误字符串或 undefined。onSave(edit) 可返回 Promise;等待期间禁止重复提交。抛错会保留草稿、显示错误并触发 editError,不会留下未处理的拒绝。
成功后触发 edit,参数为 { key, row, column?, values },其中 values 按列键组织。调用方负责应用修改,包括将 accessor 列映射回原始字段。onSave 在 edit 之前执行一次,不要在两处重复发送请求。默认输入框支持 Enter 保存与 Escape 取消。
整行编辑的保存错误在行下方显示一次;列 validate 返回的错误只显示在对应字段,error 插槽参数也只包含该字段的校验错误。编辑器通过 InputGroup 的 bare 变体统一尺寸和外观,自定义插槽中的 Input、Select 等控件会继承嵌入样式。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
条目 D | 已发布 | 129 |
<script setup lang="ts">
import { Checkbox, DataTable, Select, Inline } from '@hina-ui/vue'
import { useEditableTableDemo } from '../../data-table'
const { rows, columns, statusLabels, mode, fail, save } = useEditableTableDemo('zh-CN')
const modes = [
{ value: 'cell', label: '单元格' },
{ value: 'row', label: '整行' },
]
const options = Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
</script>
<template>
<DataTable
:rows="rows"
:columns="columns"
row-key="id"
row-label="name"
:edit-mode="mode"
:on-save="save"
layout="fixed"
label="条目列表"
>
<template #toolbar>
<Inline align="center" wrap>
<Select size="sm" v-model="mode" :options="modes" aria-label="编辑模式" class="w-32" />
<Checkbox v-model="fail">模拟保存失败</Checkbox>
</Inline>
</template>
<template #editor-status="{ value, updateValue, pending }">
<Select
:model-value="value as string"
:options="options"
:disabled="pending"
aria-label="状态"
size="sm"
@update:model-value="updateValue"
/>
</template>
</DataTable>
</template>
行重排
reorderable 添加支持鼠标与触摸的拖动手柄,也支持上下方向键。v-model:rows 接收重排后的根数组,原数组不会被修改。rowReorder 包含 { row, target, parent?, from, to, rows }。子行重排时,由调用方将新的同级数组应用到 parent,不改变父子归属。
排序、筛选或分组改变显示顺序时禁用行拖动。可传入函数禁止移动部分行。重排范围是已加载的同级行,持久化由调用方负责。
| 移动行 | 名称 | 状态 | 数量 |
|---|---|---|---|
条目 A | 已发布 | 18 | |
条目 B | 草稿 | 55 | |
条目 C | 已发布 | 92 | |
条目 D | 已发布 | 129 | |
条目 E | 已归档 | 16 |
<script setup lang="ts">
import { ref } from 'vue'
import { DataTable } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const source = tableDemo('zh-CN')
const rows = ref(source.rows.slice(0, 5))
const columns = source.columns.map(column => ({ ...column, sortable: false }))
</script>
<template>
<DataTable
v-model:rows="rows"
:columns="columns"
row-key="id"
row-label="name"
reorderable
label="条目列表"
/>
</template>
滚动
横向溢出限制在 ScrollArea 内。maxHeight 限制滚动区高度,stickyHeader 固定表头。class 作用于完整 DataTable,tableClass 作用于滚动区。实例暴露 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 |
<script setup lang="ts">
import { DataTable } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const wideColumns = columns.map(column => ({
...column,
width: column.key === 'name' ? 320 : 200,
}))
</script>
<template>
<DataTable
:rows="rows"
:columns="wideColumns"
row-key="id"
sticky-header
:max-height="240"
label="条目列表"
/>
</template>
撑满与固定汇总
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 |
<script setup lang="ts">
import { Stack, DataTable } from '@hina-ui/vue'
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,
)
</script>
<template>
<Stack class="h-80 w-full">
<DataTable
:rows="rows"
:columns="summaryColumns"
row-key="id"
fill
sticky-header
sticky-footer
pagination
:page-size="15"
label="条目列表"
>
<template #footer-name>合计</template>
</DataTable>
</Stack>
</template>
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 |
<script setup lang="ts">
import { ref } from 'vue'
import { Button, DataTable, Inline, type DataTableKey } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const selected = ref<DataTableKey[]>([1, 2])
</script>
<template>
<DataTable
v-model:selected="selected"
:rows="rows.slice(0, 5)"
:columns="columns"
row-key="id"
row-label="name"
selectable
label="条目列表"
>
<template #toolbar="{ api }">
<Inline wrap>
<Button
size="sm"
variant="soft"
tone="neutral"
@click="api.exportCsv({ formatted: true, filename: 'entries.csv' })"
>
导出 CSV
</Button>
<Button
size="sm"
variant="ghost"
tone="neutral"
:disabled="!selected.length"
@click="api.exportCsv({ scope: 'selected', formatted: true, filename: 'selected.csv' })"
>
导出所选
</Button>
</Inline>
</template>
</DataTable>
</template>
空状态与加载
emptyText 替换默认提示,empty 可放入 Empty 等自定义内容。loading 插槽替换 LoadingOverlay 的内容,应保留状态播报。
| 状态 | ||
|---|---|---|
条目 A | 已发布 | 18 |
条目 B | 草稿 | 55 |
条目 C | 已发布 | 92 |
| 状态 | ||
|---|---|---|
暂无条目 | ||
<script setup lang="ts">
import { ref } from 'vue'
import { Button, DataTable, Empty, Stack } from '@hina-ui/vue'
import { tableDemo } from '../../data-table'
const { rows, columns } = tableDemo('zh-CN')
const loading = ref(false)
</script>
<template>
<Stack class="w-full">
<Button size="sm" variant="soft" tone="neutral" class="self-start" @click="loading = !loading">
切换加载状态
</Button>
<DataTable
:rows="rows.slice(0, 3)"
:columns="columns"
row-key="id"
:loading="loading"
label="条目列表"
/>
<DataTable :rows="[]" :columns="columns" row-key="id" label="条目列表">
<template #empty><Empty size="sm" title="暂无条目" :icon="false" /></template>
</DataTable>
</Stack>
</template>
虚拟滚动
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 |
<script setup lang="ts">
import { ref } from 'vue'
import { Stack, Button, DataTable, type DataTableApi } from '@hina-ui/vue'
import { tableDemo, type TableDemoRow } from '../../data-table'
const { columns } = tableDemo('zh-CN')
const table = ref<{ api: DataTableApi<TableDemoRow> }>()
const rows: TableDemoRow[] = Array.from({ length: 10000 }, (_, index) => ({
id: index + 1,
name: '条目 ' + (index + 1),
status: 'active',
count: index,
}))
</script>
<template>
<DataTable
ref="table"
:rows="rows"
:columns="columns"
row-key="id"
row-label="name"
:virtualize="{ estimateSize: 44, overscan: 6 }"
:height="320"
sticky-header
expandable
label="条目列表"
>
<template #toolbar>
<Button
size="sm"
variant="soft"
tone="neutral"
class="self-start"
@click="table?.api.scrollToRow(5000)"
>
跳到第 5,000 行
</Button>
</template>
<template #expansion="{ row }">
<Stack class="flex h-32 items-center text-muted">{{ row.name }} · 展开内容</Stack>
</template>
</DataTable>
</template>
无障碍
组件保留原生表格语义、多级表头作用域与虚拟行位置。用 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 | 空状态文本 |
class | 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) | 单元格样式 |
模型
| 绑定 | 类型 | 默认值 |
|---|---|---|
v-model:page | number | 1 |
v-model:pageSize | number | 10 |
v-model:sorting | DataTableSort[] | [] |
v-model:filter | string | '' |
v-model:columnFilters | DataTableFilter[] | [] |
v-model:grouping | string[] | [] |
v-model:selected | DataTableKey[] | [] |
v-model:expanded | DataTableKey[] | [] |
v-model:expandedGroups | string[] | [] |
v-model:hiddenColumns | string[] | [] |
v-model:columnOrder | string[] | [] |
v-model:columnWidths | Record<string, number> | {} |
根行重排可使用 v-model:rows,其他情况下照常传入 rows。
插槽
| 插槽 | Props | 说明 |
|---|---|---|
cell-key / cell | DataTableCellContext<T> | 数据单元格 |
header-key / header | DataTableHeaderContext<T> | 表头内容 |
editor-key / editor | DataTableEditorContext<T> | 编辑控件 |
expansion | DataTableRowContext<T> | 展开详情 |
group | DataTableGroupContext<T> | 分组行 |
footer-key | { column, rows: T[] } | 汇总单元格 |
summary | DataTableState<T> | tfoot 内容 |
toolbar / footer | DataTableState<T> | 工具栏与分页页脚 |
empty / loading | — | 状态内容 |
DataTableState<T> 包含查询参数、total、当前显示的原始 rows、selected、expanded、visibleColumns 与 api。
事件
| 事件 | 参数 | 说明 |
|---|---|---|
change | DataTableQuery | 查询变化 |
rowClick | (row: T, event: MouseEvent | KeyboardEvent) | 行激活 |
rowContextmenu | (row: T, event: MouseEvent) | 右键菜单事件 |
update:rows | T[] | 重排后的根数组 |
rowReorder | DataTableReorder<T> | 根或同级行重排 |
edit | DataTableEdit<T> | 编辑成功 |
editError | (error: unknown, edit: DataTableEdit<T>) | 保存或校验异常 |
实例
实例暴露 element: HTMLTableElement | undefined、viewport: HTMLElement | undefined、state: DataTableState<T> 与 api: DataTableApi<T>,工具栏与页脚插槽也提供同一份 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
}