- 分组 A
- 节点 A.1
- 分组 A.2
- 节点 A.2.1
- 节点 A.2.2
- 分组 B
- 节点 B.1
- 节点 B.2
<script setup lang="ts">
import { ref } from 'vue'
import { Tree, type TreeValue } from '@hina-ui/vue'
import { nodes } from './data'
const checked = ref<TreeValue[]>(['a-2-1'])
</script>
<template>
<Tree
v-model="checked"
multiple
:items="nodes"
:default-expanded="['a', 'a-2', 'b']"
aria-label="树形勾选"
class="w-80 max-w-full"
/>
</template>
用法
import { Tree, type TreeNode, type TreeValue } from '@hina-ui/vue'
Tree 直接渲染树形列表;需要带触发器的单选浮层时,使用 TreeSelect。通过 items 提供节点,children 定义下一级。点击展开箭头只改变展开状态,点击节点文字或按空格、Enter 改变选择。
默认单选,v-model 为节点的 value,再次点击当前节点会取消选择并写入 null。
- 分组 A
- 节点 A.1
- 分组 A.2
- 分组 B
选中值:—
<script setup lang="ts">
import { ref } from 'vue'
import { Stack, Text, Tree, type TreeValue } from '@hina-ui/vue'
import { nodes } from './data'
const selected = ref<TreeValue | null>(null)
</script>
<template>
<Stack class="w-80 max-w-full">
<Tree v-model="selected" :items="nodes" :default-expanded="['a']" aria-label="单选树" />
<Text size="sm" tone="muted">选中值:{{ selected ?? '—' }}</Text>
</Stack>
</template>
示例
多选勾选与半选
设置 multiple 显示勾选框,v-model 改为 TreeValue[]。勾选父节点会选中所有可用后代,取消父节点会清除这些选择;点击半选父节点会补全勾选。部分后代被选中时,父节点显示横线;全部可用子节点选中后,父节点显示勾选。
状态按完整树计算,折叠不会清除选择或改变半选结果。更新事件返回所有完整勾选节点的值,包含自动勾选的父节点;半选节点不写入数组。已知节点按树的前序排列,未出现在当前 items 中的绑定值会保留。
外部回填可以只传叶子值,父节点状态会自动推导;传入父节点值会勾选它的可用后代。组件不会仅因初始化、展开或数据刷新而主动改写绑定值。勾选框沿用 Checkbox 的视觉与动效。
- 分组 A
- 节点 A.1
- 分组 A.2
- 节点 A.2.1
- 节点 A.2.2
- 分组 B
- 节点 B.1
- 节点 B.2
绑定值:a-2-1
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Inline, Stack, Text, Tree, type TreeValue } from '@hina-ui/vue'
import { nodes } from './data'
const checked = ref<TreeValue[]>(['a-2-1'])
</script>
<template>
<Stack class="w-96 max-w-full">
<Tree
v-model="checked"
multiple
:items="nodes"
:default-expanded="['a', 'a-2', 'b']"
aria-label="父子联动勾选"
/>
<Inline>
<Button
size="sm"
variant="soft"
tone="neutral"
@click="checked = nodes.map(node => node.value)"
>
全选
</Button>
<Button size="sm" variant="ghost" tone="neutral" @click="checked = []">清空</Button>
</Inline>
<Text size="sm" tone="muted" class="break-all">绑定值:{{ checked.join(', ') || '—' }}</Text>
</Stack>
</template>
控制展开
defaultExpanded 设置初始展开节点,v-model:expanded 可在外部控制展开状态。它们都使用节点的原始 value;传入 expanded 时优先使用受控值,包含空数组。没有子节点或 children: [] 的节点不会显示展开箭头。
- 分组 A
- 节点 A.1
- 分组 A.2
- 分组 B
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Inline, Stack, Tree, type TreeValue } from '@hina-ui/vue'
import { nodes } from './data'
const expanded = ref<TreeValue[]>(['a'])
const checked = ref<TreeValue[]>(['a-2-1'])
</script>
<template>
<Stack class="w-80 max-w-full">
<Inline>
<Button size="sm" variant="soft" tone="neutral" @click="expanded = ['a', 'a-2', 'b']">
展开全部
</Button>
<Button size="sm" variant="ghost" tone="neutral" @click="expanded = []">收起全部</Button>
</Inline>
<Tree
v-model="checked"
v-model:expanded="expanded"
multiple
:items="nodes"
aria-label="受控展开"
/>
</Stack>
</template>
自定义节点
node 插槽替换节点文字区域,trailing 插槽位于行尾,两者都接收原节点及 selected、indeterminate、expanded、disabled 状态。selected 仅表示完整选中;半选时为 false,indeterminate 为 true。展开箭头和勾选框由组件保留。
示例通过 Tag 显示完整选中和部分选中状态。
- 分组 A部分选中
- 节点 A.1
- 分组 A.2部分选中
- 节点 A.2.1已选中
- 节点 A.2.2
- 分组 B
- 节点 B.1
- 节点 B.2
<script setup lang="ts">
import { ref } from 'vue'
import { File, Folder, FolderOpen } from '@lucide/vue'
import { Inline, Tag, Text, Tree, type TreeValue } from '@hina-ui/vue'
import { nodes } from './data'
const checked = ref<TreeValue[]>(['a-2-1'])
</script>
<template>
<Tree
v-model="checked"
multiple
:items="nodes"
:default-expanded="['a', 'a-2', 'b']"
aria-label="自定义节点"
class="w-96 max-w-full"
>
<template #node="{ node, expanded }">
<Inline as="span" :wrap="false" gap="sm">
<FolderOpen
v-if="node.children?.length && expanded"
class="text-muted size-4 shrink-0"
aria-hidden="true"
/>
<Folder
v-else-if="node.children?.length"
class="text-muted size-4 shrink-0"
aria-hidden="true"
/>
<File v-else class="text-muted size-4 shrink-0" aria-hidden="true" />
<Text as="span" size="sm">{{ node.label }}</Text>
</Inline>
</template>
<template #trailing="{ selected, indeterminate }">
<Tag v-if="indeterminate" size="sm" tone="neutral">部分选中</Tag>
<Tag v-else-if="selected" size="sm">已选中</Tag>
</template>
</Tree>
</template>
禁用
节点的 disabled 会禁用该节点及其子树,阻止点击、键盘选择和展开。父子勾选联动跳过禁用子树,父节点的全选、半选判断也不计入它们;原有绑定值保留。只有禁用子节点的可用父节点,可以单独勾选。
组件的 disabled 禁用整棵树,已有选择仍可见。FormField 的禁用、校验状态、标题和描述关联会自动传递到树上。
节点禁用
- 根节点
- 可用节点
- 禁用节点
- 禁用子树中的节点
整体禁用
- 根节点
- 可用节点
- 禁用节点
<script setup lang="ts">
import { Stack, Text, Tree, type TreeNode } from '@hina-ui/vue'
const items: TreeNode[] = [
{
value: 'root',
label: '根节点',
children: [
{ value: 'available', label: '可用节点' },
{
value: 'disabled',
label: '禁用节点',
disabled: true,
children: [{ value: 'child', label: '禁用子树中的节点' }],
},
],
},
]
</script>
<template>
<Stack class="w-80 max-w-full">
<Text size="sm" tone="muted">节点禁用</Text>
<Tree multiple :items="items" :default-expanded="['root', 'disabled']" aria-label="节点禁用" />
<Text size="sm" tone="muted">整体禁用</Text>
<Tree
multiple
disabled
:items="items"
:model-value="['available']"
:default-expanded="['root']"
aria-label="整体禁用"
/>
</Stack>
</template>
滚动
组件自身不限制高度,也不添加面板背景或边框。需要限制可见高度时,用 ScrollArea 包裹树。
- 分组
- 节点 1
- 节点 2
- 节点 3
- 节点 4
- 节点 5
- 节点 6
- 节点 7
- 节点 8
- 节点 9
- 节点 10
- 节点 11
- 节点 12
- 节点 13
- 节点 14
- 节点 15
- 节点 16
- 节点 17
- 节点 18
- 节点 19
- 节点 20
- 节点 21
- 节点 22
- 节点 23
- 节点 24
- 节点 25
- 节点 26
- 节点 27
- 节点 28
- 节点 29
- 节点 30
- 节点 31
- 节点 32
- 节点 33
- 节点 34
- 节点 35
- 节点 36
- 节点 37
- 节点 38
- 节点 39
- 节点 40
<script setup lang="ts">
import { ref } from 'vue'
import { ScrollArea, Tree, type TreeNode, type TreeValue } from '@hina-ui/vue'
const checked = ref<TreeValue[]>([])
const items: TreeNode[] = [
{
value: 'root',
label: '分组',
children: Array.from({ length: 40 }, (_, index) => ({
value: index,
label: `节点 ${index + 1}`,
})),
},
]
</script>
<template>
<ScrollArea class="h-64 w-80 max-w-full">
<Tree
v-model="checked"
multiple
:items="items"
:default-expanded="['root']"
aria-label="可滚动树"
/>
</ScrollArea>
</template>
空状态
没有节点时显示语言包中的空状态文字,可通过 empty 插槽替换。空状态使用 role="status",不会产生一个没有节点的 role="tree"。
还没有节点
<script setup lang="ts">
import { Stack, Text, Tree } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-80 max-w-full">
<Tree :items="[]" aria-label="默认空状态" />
<Tree :items="[]" aria-label="自定义空状态">
<template #empty><Text size="sm" tone="muted">还没有节点</Text></template>
</Tree>
</Stack>
</template>
虚拟滚动
virtualize 按需渲染可见范围附近的条目,与 VirtualList 共用测量与滚动底层。默认关闭;可传 { estimateSize, overscan } 调整预估行高和两侧预渲染数量,行高会按实际内容测量。键盘导航覆盖完整数据,禁用项会跳过。 只对展开后的可见节点进行虚拟化,父子导航与勾选状态不依赖节点是否挂载。 maxHeight 默认 320px,仅开启虚拟化时生效,可传数字或 CSS 长度。 条目离开渲染范围后会卸载;插槽内需要持久保留的状态应按唯一 value 存在外部。
已选: 1
<script setup lang="ts">
import { ref } from 'vue'
import { Tree, Stack, Text } from '@hina-ui/vue'
const selected = ref<Array<string | number>>([7890])
const options = Array.from({ length: 10000 }, (_, index) => ({
value: index,
label: `条目 ${String(index + 1).padStart(5, '0')}`,
disabled: index % 97 === 0,
}))
const items = [{ value: 'root', label: '全部节点', children: options }]
</script>
<template>
<Stack gap="sm" class="w-80 max-w-full">
<Tree
v-model="selected"
:items="items"
:default-expanded="['root']"
:virtualize="{ estimateSize: 36, overscan: 6 }"
multiple
:max-height="320"
aria-label="一万项"
/>
<Text size="sm" tone="muted">已选: {{ selected.length }}</Text>
</Stack>
</template>
键盘与无障碍
- 树为
role="tree",节点为role="treeitem",保留层级、同级位置和展开状态。使用aria-label、aria-labelledby或 FormField 提供名称。 - 单选用
aria-selected表达选择;多选用aria-checked,半选为mixed。勾选框是节点状态的视觉表现,不会增加额外的 Tab 停靠点。 - 上下方向键移动焦点,Home / End 到达首尾可用节点;空格和 Enter 改变选择。
- 向行尾方向的箭头展开节点或进入子级,向行首方向的箭头收起节点或返回父级;RTL 下左右键互换。禁用节点不进入键盘焦点序列。
API
Props 与双向绑定
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | TreeNode[] | — | 必填。树节点 |
virtualize | VirtualizeOptions | false | 虚拟滚动;预估行高按内容,overscan 6 |
maxHeight | number | string | 320 | 虚拟滚动视口的最大高度 |
multiple | boolean | false | 启用父子联动的多选勾选 |
modelValue | TreeValue | TreeValue[] | null | — | 单选值或多选值数组,支持 v-model |
defaultExpanded | TreeValue[] | [] | 初始展开节点 |
expanded | TreeValue[] | — | 展开节点,支持 v-model:expanded |
disabled | boolean | false | 禁用整棵树 |
invalid | boolean | false | 标记校验失败 |
class | string | — | 追加到树根节点的类名 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
node | TreeNodeSlot | 节点内容 |
trailing | TreeNodeSlot | 行尾内容,不提供时不占位 |
empty | — | 空状态内容 |
节点类型
export type TreeValue = string | number
export interface TreeNode {
value: TreeValue
label: string
description?: string
disabled?: boolean
children?: TreeNode[]
}
export interface TreeNodeSlot {
node: TreeNode
selected: boolean
indeterminate: boolean
expanded: boolean
disabled: boolean
}
value 必须在整棵树中唯一,数字 1 与字符串 '1' 是不同的值。description 默认显示在节点文字下方;disabled 插槽参数包含从祖先和组件继承的禁用状态。
type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }