树
概述
用于展示树形层级数据、并支持选择与展开/折叠的组件。STree 将 Aria 层 TreeRoot 一族基础原语(TreeRoot/TreeItem/TreeVirtualizerRoot/TreeVirtualizerItem,零样式)与 TreeRoot 上下文(受控/非受控选中与展开、单选/多选、级联选择、Roving Focus 键盘导航)组合;STreeVirtualizer 在其上叠加虚拟滚动,可平滑承载 1000+ 节点。节点内容完全由 item 插槽自定义,节点图标、勾选框、缩进与连接线样式均由使用方注入。
用法
特性
- 🌳 层级数据模型 —
items: TreeItemData<T>[](value+ 可选children递归结构),TreeItemData<T>泛型保留自定义字段类型 - 🎛️ 受控/非受控双通道 —
modelValue/defaultValue控制选中,expanded/defaultExpanded控制展开;均支持v-model与update:*事件回写 - ☑️ 单选与多选 —
multiple(与modelValue类型联动:单值字符串 ↔ 数组)切换;selectionBehavior: 'toggle' | 'replace'决定多选点击行为;Shift + 方向键连续范围选择 - 🧩 级联选择 —
propagateSelect(选择父节点时级联选中子孙)、bubbleSelect(子节点全部选中后自动选中父节点)、allowParentSelect(是否允许直接选中含子节点的父项) - 📂 展开策略 —
toggleBehavior: 'multiple' | 'single'(默认多节点展开;single下展开新节点自动折叠旧节点,行为类似手风琴) - ⌨️ 键盘导航 — Roving Focus:
↑/↓移动焦点、→/←展开/折叠或进入/返回父层级、Enter/Space选中、字符输入 typeahead 快速定位;loop(默认true)首尾循环;完整支持 RTL - ⚡ 虚拟滚动 —
STreeVirtualizer+height仅渲染可视节点,1000+ 节点仍保持流畅 - ♿ 无障碍 —
role="tree"/treeitem+aria-expanded/aria-selected/aria-level/aria-setsize/aria-posinset/aria-multiselectable/aria-disabled,配合data-vean-tree-*数据属性
组件家族
STree(styled)— 入口包装;TreeRootProps<T, U, M>泛型透传 +useForwardListeners事件合并 +top/item/bottom插槽;withDefaults镜像 Arialoop: true默认STreeVirtualizer(styled)— 虚拟滚动包装;contentProps/dynamicContentProps转发给VirtualizerContent,item插槽额外暴露virtualItemTreeRoot(Aria)— 根组件;useControllableState管理选中/展开,useSelectionBehavior处理单选/多选/范围选择,useRovingFocusGroup+useTypeahead实现键盘导航;provideTreeRootContext桥接子项TreeItem(Aria)— 单节点;useRovingFocusGroupItem管理焦点,渲染aria-*与data-*属性,select/toggle事件经handleAndDispatchCustomEvent派发TreeVirtualizerRoot(Aria)— 虚拟化根;VirtualizerRoot+ TanStack Virtual,转发扁平化flattenItemsTreeVirtualizerItem(Aria)— 虚拟化节点;TreeItem+VirtualizerItem组合(data-vean-tree-virtualizer-item)
演示
基础
- composables
- components
- home
- app.vue
- nuxt.config.ts
Directory Structure
虚拟列表
- 01 Basic — 基础目录树(
items+default-expanded+top插槽) - 02 Virtualizer —
STreeVirtualizer虚拟滚动渲染 1000+ 节点
API
Aria 层
6Tree<T>
插槽
| 插槽名 | 参数 | 描述 |
|---|---|---|
top | — | 树节点渲染前的内容。 |
bottom | — | 树节点之后渲染的内容。 |
item | { item: FlattenedItem<T>; modelValue: MaybeArray<string> | undefined; expanded: string[]; } | 单个树节点的内容。 |
TreeItem
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
value | string | — | 与当前项关联的值。 |
level | number | — | 难度等级。 |
disabled | boolean | — | 当值为 `true` 时,禁止用户选择或切换该项。 |
disabledSelect | boolean | — | 当值为 `true` 时,禁止用户选择该项。 |
disabledToggle | boolean | — | 当值为 `true` 时,禁止用户切换该项。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
select | TreeSelectEvent<string> | 选择项目时调用的事件处理程序。 |
toggle | TreeToggleEvent<string> | 切换项目时调用的事件处理程序。 |
TreeRoot<T, U, M>
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
modelValue | U | — | 树的受控值。可通过 `v-model` 进行绑定。 |
defaultValue | U | — | 树在初始渲染时的值。当不需要控制树的状态时使用 |
multiple | IsMultiple<U, M> | — | 用于确定一次可选择“单个”还是“多个”项目。 |
items | T[] | — | 项目列表 |
expanded | string[] | — | 展开项的受控值。可通过 `v-model` 进行绑定。 |
defaultExpanded | string[] | — | 初始渲染时展开树的值。 |
selectionBehavior | TreeSelectBehavior | — | 集合中多选功能应如何工作。 |
toggleBehavior | TreeToggleBehavior | 'multiple' | 用于确定一次可切换的项是“单个”还是“多个”。 |
dir | Direction | — | 阅读方向。 |
loop | boolean | true | 当值为 `true` 时,键盘导航将在最后一项和第一项之间循环切换。 |
disabled | boolean | — | 当设置为 `true` 时,禁止用户与树结构进行交互 |
propagateSelect | boolean | — | 当值为 `true` 时,选中父节点将同时选中其子节点。 |
bubbleSelect | boolean | — | 当值为 `true` 时,选择子节点将更新父节点的状态。 |
allowParentSelect | boolean | — | 当值为 `true` 时,可选中父元素。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | M extends true ? string[] : string | 当模型值发生变化时触发。 |
update:expanded | string[] | 在展开状态发生变化时触发。 |
TreeVirtualizer<T, U, M>
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
contentProps | VirtualizerContentProps | — | 转发给内容元素的属性。 |
dynamicContentProps | VirtualizerDynamicContentProps | — | 转发给动态内容元素的属性。 |
animated | boolean | false | 当设置为 `true` 时,将可见项目渲染在正常文档流中(动态模式),因此
在展开/折叠操作中,项目位置将保持正确,无需绝对偏移量。 |
modelValue | U | — | 树的受控值。可通过 `v-model` 进行绑定。 |
defaultValue | U | — | 树在初始渲染时的值。当不需要控制树的状态时使用 |
multiple | IsMultiple<U, M> | — | 用于确定一次可选择“单个”还是“多个”项目。 |
items | T[] | — | 项目列表 |
expanded | string[] | — | 展开项的受控值。可通过 `v-model` 进行绑定。 |
defaultExpanded | string[] | — | 初始渲染时展开树的值。 |
selectionBehavior | TreeSelectBehavior | — | 集合中多选功能应如何工作。 |
toggleBehavior | TreeToggleBehavior | 'multiple' | 用于确定一次可切换的项是“单个”还是“多个”。 |
dir | Direction | — | 阅读方向。 |
loop | boolean | true | 当值为 `true` 时,键盘导航将在最后一项和第一项之间循环切换。 |
disabled | boolean | — | 当设置为 `true` 时,禁止用户与树结构进行交互 |
propagateSelect | boolean | — | 当值为 `true` 时,选中父节点将同时选中其子节点。 |
bubbleSelect | boolean | — | 当值为 `true` 时,选择子节点将更新父节点的状态。 |
allowParentSelect | boolean | — | 当值为 `true` 时,可选中父元素。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
height | string | number | — | 虚拟化器根目录的高度 |
options | VirtualizerOptions | — | 选项。 |
dynamic | boolean | — | 是否为动态内容。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | M extends true ? string[] : string | 当模型值发生变化时触发。 |
update:expanded | string[] | 在展开状态发生变化时触发。 |
TreeVirtualizerItem
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
value | string | — | 与当前项关联的值。 |
level | number | — | 难度等级。 |
disabled | boolean | — | 当值为 `true` 时,禁止用户选择或切换该项。 |
disabledSelect | boolean | — | 当值为 `true` 时,禁止用户选择该项。 |
disabledToggle | boolean | — | 当值为 `true` 时,禁止用户切换该项。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
data | VirtualItem | — | 数据。 |
customStyle | boolean | — | 是否采用自定义样式。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
select | TreeSelectEvent<string> | 选择项目时调用的事件处理程序。 |
toggle | TreeToggleEvent<string> | 切换项目时调用的事件处理程序。 |
TreeVirtualizerRoot<T, U, M>
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
modelValue | U | — | 树的受控值。可通过 `v-model` 进行绑定。 |
defaultValue | U | — | 树在初始渲染时的值。当不需要控制树的状态时使用 |
multiple | IsMultiple<U, M> | — | 用于确定一次可选择“单个”还是“多个”项目。 |
items | T[] | — | 项目列表 |
expanded | string[] | — | 展开项的受控值。可通过 `v-model` 进行绑定。 |
defaultExpanded | string[] | — | 初始渲染时展开树的值。 |
selectionBehavior | TreeSelectBehavior | — | 集合中多选功能应如何工作。 |
toggleBehavior | TreeToggleBehavior | 'multiple' | 用于确定一次可切换的项是“单个”还是“多个”。 |
dir | Direction | — | 阅读方向。 |
loop | boolean | true | 当值为 `true` 时,键盘导航将在最后一项和第一项之间循环切换。 |
disabled | boolean | — | 当设置为 `true` 时,禁止用户与树结构进行交互 |
propagateSelect | boolean | — | 当值为 `true` 时,选中父节点将同时选中其子节点。 |
bubbleSelect | boolean | — | 当值为 `true` 时,选择子节点将更新父节点的状态。 |
allowParentSelect | boolean | — | 当值为 `true` 时,可选中父元素。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
height | string | number | — | 虚拟化器根目录的高度 |
options | VirtualizerOptions | — | 选项。 |
dynamic | boolean | — | 是否为动态内容。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | M extends true ? string[] : string | 当模型值发生变化时触发。 |
update:expanded | string[] | 在展开状态发生变化时触发。 |
说明
架构与对标差异
TreeRoot 拥有全部状态(选中/展开经 useControllableState 受控/非受控双通道)与选择策略(useSelectionBehavior 的 toggle/replace/范围选择),所有基础原语保持零样式;STree/STreeVirtualizer 仅负责 loop 默认值镜像与插槽透传,节点视觉完全由 item 插槽内的 STreeItem/STreeVirtualizerItem 组合实现(缩进、图标、勾选框、焦点态均经类名注入)。键盘导航基于 Roving Focus(↑/↓ 移动、→/← 展开/折叠、typeahead 定位),并额外支持 Shift 范围选择——该交互超出多数主流库的树组件默认契约。虚拟滚动基于 @vean/aria 内置 virtualizer,仅渲染可视节点。
| 能力 | VeanUI | Ant Design | Element Plus | Naive UI |
|---|---|---|---|---|
| Aria/样式分离 | ✅ | — | — | — |
| 单选/多选(multiple + toggle/replace) | ✅ | ✅ | ✅ | ✅ |
| 展开策略(single/multiple toggle) | ✅ | ✅ | ✅ | ✅ |
| 级联选择(propagate/bubble/parent) | ✅ | ✅ | ✅ | ⚠️ |
| 键盘导航(方向键 + 循环 + typeahead) | ✅ | ✅ | ✅ | ⚠️ |
| 虚拟滚动(1k 节点) | ✅ | ✅ | ⚠️ | ✅ |
| 可勾选模式(checkable 复选框) | ⚠️ | ✅ | ✅ | ✅ |
| 拖拽排序(draggable) | — | ✅ | ✅ | ✅ |
| 异步加载子节点(loadData) | — | ✅ | ✅ | ✅ |
| 搜索过滤(searchValue) | — | ✅ | ✅ | — |
⚠️ = 部分支持(VeanUI 的 checkable 可经 multiple + 自定义 item 插槽勾选图标 + data-selected/data-contains-selected 状态实现;Naive UI 无内置范围选择与字符定位)。
注意事项
modelValue类型与multiple联动:modelValue为单值字符串时强制单选;为数组且multiple为真时启用多选(IsMultiple<U, M>条件类型约束)。item插槽是自由组合模式:必须由使用方渲染STreeItem(或STreeVirtualizerItem)并传入item.value/item.level;插槽同时暴露item.data(原始节点数据)、isExpanded/isSelected/isIndeterminate/hasChildren等状态。- 含子节点的父项默认不可选中(
allowParentSelect默认false);需要父项可选中时显式开启。 propagateSelect/bubbleSelect仅在多选(multiple+ 数组值)下生效;两者同时开启时以data-contains-selected/isIndeterminate表达半选态。toggleBehavior: 'single'下展开行为类似手风琴——每次仅保留一个展开分支(findParentPath重建路径)。loop默认true(键盘焦点首尾循环),STree与STreeVirtualizer均已在包装层镜像该默认,直接使用 AriaTreeRoot时无需额外处理。- 虚拟滚动需要设置
height;STreeVirtualizer的item插槽额外提供virtualItem(用于:data透传与绝对定位样式)。 - 展开/折叠时对移动子树做高度塌缩过渡动画。
STreeVirtualizer提供可选的animated属性,让可见项按普通文档流渲染(dynamic 模式),展开/折叠时无需绝对定位偏移即可保持项位置正确——默认关闭,因为动态测量对超大数据集开销更重;动画运行期间过渡列表会临时以完整文档流渲染,结束后恢复窗口化。 - 节点文本完全由插槽提供,组件内无硬编码文案,因此无需本地化处理。
常见问题
如何实现可勾选(checkable)的树?
使用 multiple 多选 + 自定义 item 插槽渲染勾选图标,并消费 isSelected/isIndeterminate 状态:
<STree :items="items" multiple>
<template #item="{ item, isSelected, isIndeterminate }">
<STreeItem :value="item.value" :level="item.level">
<SIcon :icon="isIndeterminate ? 'lucide:minus' : isSelected ? 'lucide:check-square' : 'lucide:square'" />
{{ item.data.label }}
</STreeItem>
</template>
</STree>需要「选中父级联动子孙」时叠加 propagateSelect,需要「子孙全选联动父级」时叠加 bubbleSelect。
如何控制树的展开状态?
受控:v-model:expanded="expandedKeys"(字符串数组);或非受控初始值 :default-expanded="['node-1']"。展开状态事件为 update:expanded。
大数据量下如何优化?
使用 STreeVirtualizer 并设置 height:仅渲染可视节点(配合自定义 options.estimateSize 控制行高),1000+ 节点仍保持流畅。
<STreeVirtualizer height="360px" :items="items">
<template #item="{ item, virtualItem }">
<STreeVirtualizerItem :value="item.value" :level="item.level" :data="virtualItem">
{{ item.data.label }}
</STreeVirtualizerItem>
</template>
</STreeVirtualizer>如何阻止选择父节点?
allowParentSelect 默认 false——点击含子节点的父项仅触发展开/折叠而不选中;需要选中父项时显式设置 :allow-parent-select="true"。
如何实现异步加载子节点?
组件未内置 loadData 契约,可在 toggle 事件中懒加载后更新 items:
<STree :items="items" @toggle="event => loadChildren(event.value)">
<!-- item 插槽 -->
</STree>加载完成后向 items 对应节点写入 children 即可(展开状态由 expanded 维护)。
如何自定义节点图标与缩进?
item 插槽自由组合:缩进用 item.level 计算 padding-left,图标按 item.hasChildren/isExpanded 切换,焦点态用 focus:ring-* 类名——参见演示 01。
键盘如何操作?
↑/↓ 在可见节点间移动焦点;→ 展开折叠节点(或进入子级)、← 折叠(或返回父级);Enter/Space 选中当前节点;连续输入字符触发 typeahead 定位;多选下 Shift + ↑/↓ 进行范围选择;loop 开启时首尾循环(默认开启)。