Tree 树
层级数据展示与管理:选中、勾选(含父子半选联动)、展开、异步加载、搜索高亮、自定义渲染、拖拽。
展开态用 defaultExpandedKeys / expandedKeys 或 defaultExpandAll 控制。
基本使用
不传 expandedKeys 时默认全部折叠。默认整行可点击触发展开 / 折叠 —— 设 expandAction="false" 改为仅 switcher 图标响应。
展开方式 expandAction
'click'(默认)整行点击切换展开;false 仅 switcher 图标响应,行点击退化为纯选中。
受控 / 非受控
v-model:selected-keys、v-model:expanded-keys、v-model:checked-keys 都支持双向绑定;只想初始化用 default-* 系列。
多选
<c-tree multiple v-model:selected-keys="selected" :data="data" />勾选 + 父子半选联动
checkable 开启复选框。默认勾选父级会勾选所有可勾选后代;后代部分勾选时父级显示半选;checkStrictly 关闭联动。
fieldNames 字段名映射
<c-tree :data="data" :field-names="{ key: 'id', title: 'name', children: 'items' }" default-expand-all />异步加载
loadData 在展开非叶子且尚未加载的节点时调用,返回 Promise。回调中给原始 node 写入 children 即可。
搜索 + 高亮
searchValue 自动按节点 title 模糊匹配并保留命中节点的祖先;命中部分会用 __highlight span 包裹便于自定义颜色。filterTreeNode 接受函数自定义匹配逻辑。
自定义渲染
通过 title / switcher / icon 插槽自定义节点。title 插槽接收 { node, data, expanded },switcher 接收 { expanded, node }。
键盘导航
将焦点设到 Tree 上后,可以用键盘漫游:
| 按键 | 行为 |
|---|---|
| ↑ / ↓ | 上一个 / 下一个可见节点 |
| → | 折叠节点 → 展开;已展开 → 移到第一个子节点 |
| ← | 已展开 → 折叠;已折叠 → 移到父节点 |
| Home / End | 首个 / 最后一个可见节点 |
| Enter | 选中聚焦节点(或勾选,若 checkable=true) |
| Space | 同 Enter |
focusedKey 支持受控(v-model:focused-key),事件 focus-change 同步外部状态。聚焦节点用 roving tabindex(仅它是 tabindex=0,其它都是 -1),保证 Tab 进入只到一个位置。
虚拟滚动
数据量大(数百 / 数千节点)时启用 virtualScroll,组件只渲染可视区 + 缓冲区:
<c-tree :data="hugeData" default-expand-all virtual-scroll :virtual-item-height="32" :virtual-max-height="400" />键盘导航触发的焦点变化会自动滚到可见区。
showLine 连接线
showLine 启用后,每个节点按祖先深度自动渲染垂直连接线。connector 插槽可以替换默认渲染:
<!-- 自定义 connector 内容 -->
<c-tree :data="data" show-line>
<template #connector="{ depth }">
<span style="color: var(--ccui-color-border);">·</span>
</template>
</c-tree>拖拽 hover 自动展开
启用 draggable 后,把节点拖到一个折叠节点上停留 dragHoverExpandDelay(默认 600ms)会自动展开它,方便把节点拖入深层目录。设为 0 关闭该行为。
<c-tree :data="data" draggable :drag-hover-expand-delay="800" />拖拽 auto-scroll
启用虚拟滚动或自定义滚动容器后,拖到顶/底边缘 dragAutoScrollEdge(默认 32px)以内时容器按 dragAutoScrollSpeed(默认 12px/帧)滚动。dragAutoScroll=false 关闭。
<c-tree
:data="big"
draggable
virtual-scroll
:virtual-max-height="240"
:drag-auto-scroll-edge="48"
:drag-auto-scroll-speed="20"
/>异步加载错误重试
loadData 抛错时,对应节点的 switcher 切换成红色感叹号按钮,点击触发重试;同时 emit load-error 携带 { error, node }。
<script setup lang="ts">
async function loadData(node: any) {
if (Math.random() < 0.5) throw new Error('mock fail')
node.children = [{ key: `${node.key}-1`, title: 'Loaded' }]
}
function onLoadError(info: { error: Error; node: any }) {
console.warn('failed to load', info.node.key, info.error)
}
</script>
<template>
<c-tree :data="data" :load-data="loadData" @load-error="onLoadError" />
</template>或编程式重试 / 查询状态:
<script setup lang="ts">
import { ref } from 'vue'
const treeRef = ref<any>(null)
function retry(key: string) {
treeRef.value?.retryLoad(key)
}
</script>
<template>
<c-tree ref="treeRef" :data="data" :load-data="loadData" />
<button @click="retry('lazy-1')">手动重试</button>
</template>拖拽排序
draggable 开启后,drop 事件回调里给出 { event, node, dragNode, dropPosition }。组件不会自动改写 data——业务侧根据 dropPosition 改造数据结构。
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([...])
function onDrop(info: { dragNode: any; node: any; dropPosition: 'before' | 'inside' | 'after' }) {
// 业务自行从 data 中移动 dragNode 到 node 的 dropPosition 位置
console.log(info)
}
</script>
<template>
<c-tree :data="data" draggable default-expand-all @drop="onDrop" />
</template>Props
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| data | TreeNodeData[] | [] | 树数据 |
| fieldNames | { key?, title?, children?, disabled?, isLeaf? } | -- | 字段名映射 |
| selectable | boolean | true | 是否允许选中 |
| multiple | boolean | false | 是否允许多选 |
| selectedKeys | (string | number)[] | -- | 选中 key,配 v-model:selected-keys 接管 |
| defaultSelectedKeys | (string | number)[] | [] | 初始选中 key |
| checkable | boolean | false | 是否显示勾选框 |
| checkedKeys | (string | number)[] | -- | 勾选 key,配 v-model:checked-keys 接管 |
| defaultCheckedKeys | (string | number)[] | [] | 初始勾选 key |
| checkStrictly | boolean | false | 关闭父子勾选联动 |
| expandedKeys | (string | number)[] | -- | 展开 key,配 v-model:expanded-keys 接管 |
| defaultExpandedKeys | (string | number)[] | [] | 初始展开 key |
| defaultExpandAll | boolean | false | 初始展开所有节点 |
| disabled | boolean | false | 整树禁用 |
| loadData | (node) => Promise<void> | -- | 异步加载子节点;展开未加载节点时调用 |
| draggable | boolean | false | 是否允许拖拽 |
| showLine | boolean | false | 是否显示连接线(保留接口,样式可由消费者扩展) |
| blockNode | boolean | false | 是否独占整行 |
| expandAction | 'click' | false | 'click' | 点击节点正文是否切换展开;false 仅 switcher 图标可展开 |
| searchValue | string | '' | 搜索关键字(默认按 title 子串匹配) |
| filterTreeNode | (node, parentKeys) => boolean | -- | 自定义过滤谓词,返回 true 命中 |
| indentSize | number | 24 | 每级缩进像素 |
| virtualScroll | boolean | false | 启用虚拟滚动 |
| virtualItemHeight | number | 32 | 虚拟滚动单项高度(px) |
| virtualMaxHeight | number | 320 | 虚拟滚动可视高度(px) |
| focusedKey | string | number | -- | 聚焦节点 key,配 v-model:focused-key 接管 |
| dragHoverExpandDelay | number | 600 | 拖到 inside 区停留多少 ms 后自动展开,0 关闭 |
| dragAutoScroll | boolean | true | 拖到滚动容器边缘自动滚动 |
| dragAutoScrollEdge | number | 32 | 触发 auto-scroll 的边缘范围(px) |
| dragAutoScrollSpeed | number | 12 | auto-scroll 每帧滚动距离(px) |
事件
| 事件 | 回调签名 | 说明 |
|---|---|---|
| update:selected-keys | (keys) | 选中变化(v-model) |
| update:checked-keys | (keys) | 勾选变化(v-model) |
| update:expanded-keys | (keys) | 展开变化(v-model) |
| update:focused-key | (key) | 聚焦变化(v-model) |
| focus-change | (key) | 聚焦节点变化 |
| load-error | ({ error, node }) | 异步加载失败 |
| select | (keys, { selectedKeys, selected, node, event }) | 选中变化 |
| check | (keys, { checkedKeys, halfCheckedKeys, checked, node, event }) | 勾选变化 |
| expand | (keys, { expanded, node }) | 展开变化 |
| load | (loadedKeys, { node }) | 异步加载完成 |
| drop | ({ event, node, dragNode, dropPosition }) dropPosition: 'before' / 'inside' / 'after' | 拖拽放下 |
| dragstart / dragenter / dragover / dragleave | ({ event, node }) | 标准拖拽生命周期事件 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
| title | { node, data, expanded } | 自定义节点标题 |
| switcher | { expanded, node, loading, loadFailed } | 自定义展开 / 折叠箭头(含 loading / 错误状态) |
| icon | { node, expanded } | 自定义节点前缀图标 |
| connector | { depth, node } | showLine 时每根连接线的内容 |
组件方法(通过 ref 调用)
| 方法 | 签名 | 说明 |
|---|---|---|
retryLoad(key) | (key) => Promise<void> | 重试 loadData,常用于错误后手动恢复 |
isNodeLoading(key) | (key) => boolean | 节点是否正在异步加载 |
hasLoadError(key) | (key) => boolean | 节点最近一次 loadData 是否失败 |
类型
type TreeNodeKey = string | number
interface TreeNodeData {
key?: TreeNodeKey
title?: VNodeChild | string
children?: TreeNodeData[]
disabled?: boolean
disableCheckbox?: boolean
selectable?: boolean
isLeaf?: boolean
icon?: VNodeChild
[key: string]: unknown
}
interface FlattenedTreeNode {
key: TreeNodeKey
raw: TreeNodeData
title: VNodeChild | string
level: number
parentKeys: TreeNodeKey[]
isLeaf: boolean
disabled: boolean
disableCheckbox: boolean
selectable: boolean
hasChildren: boolean
childKeys: TreeNodeKey[]
}
type TreeDropPosition = 'before' | 'inside' | 'after'协议要点
key必须唯一;不传则按渲染顺序自动生成__auto_n兜底。disableCheckbox与disabled区分:前者只锁勾选,后者锁选中 + 勾选 + 拖拽。- 父子勾选联动忽略
disabled/disableCheckbox后代——它们不计入"全选"判定。 loadData完成后由消费者直接修改原node.children,组件通过 reactive 检测重新平铺。drop事件不会自动改data——业务实现移动逻辑。