DataTable 数据表格

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

状态
条目 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>
vue

使用

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

传入 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>
vue

排序

列设置 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>
vue

全局筛选

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

独立列筛选

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

多选

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

单选

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

分页

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

远程数据

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

未知总数

远程模式省略 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>
vue

列显隐

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

列宽、固定与顺序

列支持 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>
vue

多级表头与汇总

嵌套列的 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>
vue

行展开

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

展开状态
条目 A
已发布
18
ID
1
状态
已发布
数量
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>
vue

树形数据

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

分组与聚合

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

单元格与整行编辑

设置列的 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>
vue

行重排

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

滚动

横向溢出限制在 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>
vue

撑满与固定汇总

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

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

空状态与加载

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

虚拟滚动

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

无障碍

组件保留原生表格语义、多级表头作用域与虚拟行位置。用 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
}
ts