- 前端
- UI组件
【免费下载链接】ui
The Intuitive Vue UI Library powered by Reka UI & Tailwind CSS.
本篇技术指南围绕 Nuxt UI(即当前仓库gh_mirrors/ui4/ui对应的开源组件库)中的Tree组件展开。Tree 用于以树形结构展示和交互层级化数据,例如文件目录、组织架构、分类菜单等。读完本文,你将掌握items数据模型、选中/展开的受控用法、扁平化渲染、图标定制、复选框联动、拖拽排序与虚拟化等完整实战能力,并能借助源码与测试理解其底层实现。
组件定位与底层架构
Tree 是一个基于 Reka UI,其中:
TreeRoot负责整棵树的选中、展开状态管理与键盘交互;TreeItem负责单个节点的展开/选中行为,并通过as-child透传;TreeVirtualizer在启用虚拟化时按需渲染可见节点。
组件对外暴露的类型TreeItem与TreeProps均在同一文件中定义,TypeScript 泛型T extends TreeItem[]、M extends boolean让多选与非多选场景拥有精确的类型推导(见 test/components/Tree.spec.ts 底部的expectEmitPayloadType断言)。
基础用法:用 items 渲染一棵树
在 Nuxt 中直接使用<UTree>(Vue 场景对应vue.UTree),通过items属性传入层级数据:
<script setup lang="ts"> import type { TreeItem } from '#ui/components/Tree' const items: TreeItem[] = [ { label: 'app/', defaultExpanded: true, children: [ { label: 'composables/', children: [ { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' }, { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' } ] }, { label: 'components/', defaultExpanded: true, children: [ { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' }, { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' } ] } ] }, { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' }, { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' } ] </script> <template> <UTree :items="items" class="w-60" /> </template>上例中带children的节点自动渲染为可展开的父节点(文件夹形态),叶子节点渲染为普通条目;defaultExpanded: true让app/与components/默认展开。
items 数据结构与 TreeItem 属性
items是TreeItem[]数组,每个节点支持以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
icon | string | 节点前导图标(Iconify 名称),通常用于叶子文件图标 |
label | string | 节点显示文本,也是默认唯一标识来源 |
trailingIcon | string | 节点右侧图标,用于覆盖父节点的展开箭头 |
defaultExpanded | boolean | 节点是否默认展开 |
disabled | boolean | 禁用单个节点 |
slot | string | 为节点指定自定义插槽名称,实现逐节点定制 |
children | TreeItem[] | 子节点数组,存在即视为父节点 |
onToggle | (e: TreeItemToggleEvent) => void | 节点级展开/收起回调 |
onSelect | (e: TreeItemSelectEvent) => void | 节点级选中回调 |
class | any | 作用于该节点链接的 class |
ui | Partial<...> | 作用于该节点的插槽 class,含item、itemWithChildren、link、linkLeadingIcon、linkLabel、linkTrailing、linkTrailingIcon、listWithChildren |
其中class与ui允许对单个节点进行局部样式覆盖;[key: string]: any允许携带任意自定义字段(如id),供getKey或数据逻辑使用。
关于唯一标识的重要约束
每个节点都需要一个唯一标识符。默认情况下组件以label作为标识,若label不唯一(例如多个重名目录),必须通过get-key函数返回唯一标识,或使用label-key指定参与标识的字段。源码中getItemKey的逻辑为:优先使用props.getKey(item),否则回退到getItemLabel(item)(即labelKey字段,默认'label'):
// src/runtime/components/Tree.vue function getItemKey(item: TreeItem): string { return props.getKey ? props.getKey(item) || getItemLabel(item) : getItemLabel(item) }多选:multiple 与选中值
设置multiple后,v-model(或default-value)的值由单个节点变为节点数组:
<UTree :items="items" multiple default-expanded class="w-60" />defaultExpanded在这里也接受TreeItem[]以默认展开多个节点。多选与受控选中均可在 test/components/Tree.spec.ts 中找到对应渲染用例(with multiple、with multiple and modelValue、with multiple and defaultValue)。
渲染模式:nested 与扁平列表
nested属性控制树以嵌套 DOM 结构渲染还是以扁平列表渲染,默认true:
<UTree :items="items" :nested="false" class="w-60" />当nested为false时,所有节点渲染在同一层级,依靠缩进表现层级关系——这正是拖拽与虚拟化场景所依赖的形态。源码中nested的计算是props.virtualize ? false : props.nested,即开启虚拟化时强制扁平化。
扁平模式下的缩进通过内联padding-inline-start计算,由flattenedPaddingFormula按size与节点level推算(xs到xl各尺寸对应不同的基础值与每级递增量,见 src/runtime/components/Tree.vue 第 185–195 行)。因此即使传入自定义主题尺寸也不会抛错——测试does not throw when flattened with a custom theme size专门覆盖了该场景。
外观定制
Color 与 Size
color控制选中态高亮颜色,默认primary;size控制紧凑程度,支持xs/sm/md/lg/xl,默认md:
<UTree :items="items" color="neutral" size="xl" class="w-60" />主题定义(src/theme/tree.ts)中,color变体为选中态链接生成before:outline-{color}/25轮廓(neutral使用before:outline-inverted/25),size变体则调整内边距、字号与图标尺寸。
Trailing Icon(父节点展开箭头)
父节点右侧默认渲染i-lucide-chevron-down,可通过trailing-icon覆盖:
<UTree :items="items" trailing-icon="i-lucide-arrow-down" class="w-60" />若某个节点自身声明了trailingIcon,则节点级图标始终优先于组件级trailing-icon。该默认图标定义在appConfig.ui.icons.chevronDown(默认值见 src/theme/icons.ts 的i-lucide-chevron-down),可在 Nuxt 的app.config.ts或 Vue 的vite.config.ts中全局替换。
Expanded / Collapsed Icon(文件夹图标)
父节点未展开时默认显示i-lucide-folder,展开后显示i-lucide-folder-open,分别对应collapsed-icon与expanded-icon:
<UTree :items="items" expanded-icon="i-lucide-book-open" collapsed-icon="i-lucide-book" class="w-60" />全局默认值位于appConfig.ui.icons.folder/appConfig.ui.icons.folderOpen(src/theme/icons.ts)。渲染优先级在 src/runtime/components/Tree.vue 模板中体现:节点自带icon优先,否则有子节点时按展开状态渲染expandedIcon或collapsedIcon。
Disabled(禁用)
组件级disabled会禁用整棵树的全部交互;也可以只对单个节点设置item.disabled。禁用态的链接样式由主题disabled变体定义(cursor-not-allowed opacity-75):
<UTree :items="items" disabled class="w-60" />受控用法:选中与展开
控制选中项
通过v-model或default-value受控管理选中项;传入v-model时建议同时用get-key指明取值函数,确保值与数据一一对应:
<script setup lang="ts"> const selected = ref<TreeItem>() const getKey = (item: TreeItem) => item.label </script> <template> <UTree v-model="selected" :items="items" :get-key="getKey" class="w-60" /> </template>若希望点击父节点时只展开/收起而不选中,可通过节点属性item.onSelect或组件级select事件阻止:
const onSelect = (event: TreeItemSelectEvent<TreeItem>) => { if (event.originalEvent.type === 'click' && event.node.children) { // 阻止选中,仅切换展开状态 } }控制展开项
同理,展开状态可通过v-model(数组)或default-expanded受控;item.onToggle或组件级toggle事件可用于阻止展开/收起,例如"选中父节点但不展开其子项"的场景。
高级示例(4.1+)
节点内嵌复选框
在item-leading插槽中放置 Checkbox 组件,配合multiple、propagate-select(选中向父/子节点传播)与bubble-select属性即可实现带父子联动关系的勾选树,再通过select、toggle事件同步选中与展开状态。由于Checkbox本身渲染为button,需要通过as属性将树节点由button改为div以规避嵌套按钮的无障碍问题:
<UTree :items="items" multiple propagate-select bubble-select :as="{ root: 'ul', link: 'div' }" class="w-60" />拖拽排序
使用@vueuse/integrations提供的useSortable(内部封装 Sortable.js)可让树支持拖拽。前提是设置nested: false将树渲染为扁平列表,使每个节点成为可被 Sortable 识别/移动的同级元素:
import { useSortable } from '@vueuse/integrations/useSortable' const treeRef = ref<HTMLElement>() const { } = useSortable(treeRef, items, { animation: 150 })<UTree ref="treeRef" :items="items" :nested="false" class="w-60" />虚拟化:大数据量渲染
virtualize属性可为大列表启用虚拟化,传true或配置对象{ estimateSize, overscan }:
<UTree :items="items" :virtualize="{ estimateSize: 32, overscan: 12 }" class="w-60" />estimateSize:每项预估高度(像素),默认32,也可以是(index) => number函数;overscan:可视区外额外渲染的项数,默认12。
需要注意:虚拟化开启时树结构会被扁平化(效果等同nested: false),且滚动容器由主题virtualize: true变体提供(root: 'overflow-y-auto')。源码通过getEstimateSize(items, size)(见 src/utils/virtualizer.ts)依据当前size自动推算默认预估高度,实际渲染由 Reka UI 的TreeVirtualizer完成。
自定义插槽
节点级slot属性可指定个性化插槽,每个具名插槽又衍生出五个子插槽:
#{item.slot}-wrapper:包裹整个节点#{item.slot}:节点主体内容#{item.slot}-leading:前导区域(图标)#{item.slot}-label:文本标签区域#{item.slot}-trailing:尾部区域(展开箭头)
未指定slot时对应基础插槽item-wrapper/item/item-leading/item-label/item-trailing。所有插槽均暴露以下作用域属性:
| 属性 | 说明 |
|---|---|
item | 当前节点数据 |
index | 节点在同级中的索引 |
level | 节点所在层级(从 1 开始) |
expanded/selected/indeterminate | 展开、选中、半选状态 |
handleSelect/handleToggle | 手动触发选中/展开的行为函数 |
ui | 当前主题的ui对象(含插槽 class) |
示例:为slot: 'app'的节点定制内容:
<template #app-label="{ item }"> <span class="font-bold">{{ item.label }}</span> </template>主题定制
Tree 的主题结构定义在 src/theme/tree.ts,包含插槽样式与变体:
| 插槽 | 默认样式要点 |
|---|---|
root | relative isolate(虚拟化时追加overflow-y-auto) |
item | w-full |
listWithChildren | border-s border-default(左侧层级线) |
itemWithChildren | ps-1.5 -ms-px(对齐层级线) |
link | 弹性布局、text-sm、before伪元素圆角高亮底 |
linkLeadingIcon/linkTrailingIcon | shrink-0,展开箭头带group-data-expanded:rotate-180旋转动画 |
linkLabel | truncate(文本截断) |
linkTrailing | ms-auto inline-flex gap-1.5 |
变体方面支持virtualize、color、size、selected、disabled,默认变体为color: 'primary'、size: 'md';selected与color的组合通过compoundVariants生成选中态文字颜色(如text-primary),未选中且未禁用时提供hover:text-highlighted悬停反馈。通过app.config.ts(Nuxt)或vite.config.ts(Vue)中的ui.tree键即可覆盖上述任意插槽 class,也可利用ui.icons.chevronDown、ui.icons.folder、ui.icons.folderOpen全局更换默认图标。
API 汇总
Props(核心)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
as | any \| { root, link } | 'ul'/'button' | 根节点与链接的渲染元素 |
items | TreeItem[] | — | 树数据 |
color | 'primary' \| 'neutral' \| ... | 'primary' | 选中态颜色 |
size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | 紧凑程度 |
getKey | (item) => string | — | 唯一标识函数 |
labelKey | string | 'label' | 作为标识/文本的字段 |
multiple | boolean | false | 是否多选 |
modelValue/defaultValue | TreeItem \| TreeItem[] | — | 受控/非受控选中值 |
nested | boolean | true | 嵌套 DOM 还是扁平列表 |
virtualize | boolean \| { estimateSize?, overscan? } | false | 虚拟化开关与参数 |
trailingIcon | string | ui.icons.chevronDown | 父节点展开箭头 |
expandedIcon/collapsedIcon | string | ui.icons.folderOpen/ui.icons.folder | 展开/收起态文件夹图标 |
disabled | boolean | false | 禁用整棵树 |
expanded/defaultExpanded | TreeItem[] | — | 展开状态受控/默认值 |
selectionBehavior/propagateSelect/bubbleSelect/loop | 继承自 Reka UI | — | 选择与焦点行为 |
onSelect/onToggle | 回调 | — | 组件级选中/展开回调 |
Slots
item-wrapper、item、item-leading、item-label、item-trailing,以及由item.slot派生的动态插槽(#{slot}-wrapper、#{slot}、#{slot}-leading、#{slot}-label、#{slot}-trailing)。所有插槽共享上文列出的作用域属性。
Emits
组件发出继承自 Reka UITreeRoot的事件,主要包括update:modelValue(v-model选中值)、select(选中某节点)、toggle(展开/收起某节点)等,且携带对应节点数据,供item.onSelect/item.onToggle之外的全局拦截使用。
测试与可访问性保障
test/components/Tree.spec.ts 以快照渲染方式覆盖了本文提及的绝大部分能力:items、modelValue、defaultValue、expanded、defaultExpanded、labelKey、getKey、multiple、disabled、nested、virtualize、三类图标、全部size变体、neutral颜色、as、class、ui覆盖,以及item-wrapper/item/item-leading/item-trailing与动态插槽的渲染。此外还包含两项关键断言:
- 通过
axe无障碍审计(passes accessibility tests),验证了基于 Reka UI 的 WAI-ARIA 语义、键盘导航与焦点管理; - 通过
expectEmitPayloadType验证update:modelValue在普通与getKey场景下的载荷类型推导。
结合文档与测试,可以确认 Tree 组件在交互语义与视觉表现上均具备生产可用性。建议在实际项目中优先使用getKey保证数据唯一性,并在数据量较大时开启virtualize以获得稳定性能。
提示:本文对应的官方交互式示例(选中控制、展开控制、复选框树、拖拽、虚拟化、自定义插槽)均可通过仓库文档目录 docs/content/docs/2.components/tree.md 中嵌入的 component-example 宏查看源码与实时效果。
- 前端
- UI组件
【免费下载链接】ui
The Intuitive Vue UI Library powered by Reka UI & Tailwind CSS.
相关推荐
如何快速安装 ESP-IDF:用 EIM 一条命令装好工具链(完整覆盖 4 大场景)
如何快速安装 ESP IDF:用 EIM 一条命令装好工具链(完整覆盖 4 大场景) ESP IDF(乐鑫 IoT 开发框架)是 Espressif 芯片的官方
物联网嵌入式手柄操作B站客户端:wiliwili 在Switch上从安装到流畅播放的5步实战指南
手柄操作B站客户端:wiliwili 在Switch上从安装到流畅播放的5步实战指南 如果你想在电视或掌机上,仅靠手柄就能刷视频、发弹幕、追番剧,而不必忍受传统
音视频桌面应用rsuite 树形表格(Tree Table)完全指南:层级数据的展示、展开控制与虚拟化实战
rsuite 树形表格(Tree Table)完全指南:层级数据的展示、展开控制与虚拟化实战 树形表格是展示具有层级结构关系数据的表格形态,它通过在普通表格中引
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考