Skip to content

Tree 树

层级数据展示与管理:选中、勾选(含父子半选联动)、展开、异步加载、搜索高亮、自定义渲染、拖拽。

展开态用 defaultExpandedKeys / expandedKeysdefaultExpandAll 控制。

基本使用

不传 expandedKeys 时默认全部折叠。默认整行可点击触发展开 / 折叠 —— 设 expandAction="false" 改为仅 switcher 图标响应。

展开方式 expandAction

'click'(默认)整行点击切换展开;false 仅 switcher 图标响应,行点击退化为纯选中。

受控 / 非受控

v-model:selected-keysv-model:expanded-keysv-model:checked-keys 都支持双向绑定;只想初始化用 default-* 系列。

多选

vue
<c-tree multiple v-model:selected-keys="selected" :data="data" />

勾选 + 父子半选联动

checkable 开启复选框。默认勾选父级会勾选所有可勾选后代;后代部分勾选时父级显示半选;checkStrictly 关闭联动。

fieldNames 字段名映射

vue
<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,组件只渲染可视区 + 缓冲区:

vue
<c-tree :data="hugeData" default-expand-all virtual-scroll :virtual-item-height="32" :virtual-max-height="400" />

键盘导航触发的焦点变化会自动滚到可见区。

showLine 连接线

showLine 启用后,每个节点按祖先深度自动渲染垂直连接线。connector 插槽可以替换默认渲染:

vue
<!-- 自定义 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 关闭该行为。

vue
<c-tree :data="data" draggable :drag-hover-expand-delay="800" />

拖拽 auto-scroll

启用虚拟滚动或自定义滚动容器后,拖到顶/底边缘 dragAutoScrollEdge(默认 32px)以内时容器按 dragAutoScrollSpeed(默认 12px/帧)滚动。dragAutoScroll=false 关闭。

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

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

或编程式重试 / 查询状态:

vue
<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 改造数据结构。

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

参数类型默认值说明
dataTreeNodeData[][]树数据
fieldNames{ key?, title?, children?, disabled?, isLeaf? }--字段名映射
selectablebooleantrue是否允许选中
multiplebooleanfalse是否允许多选
selectedKeys(string | number)[]--选中 key,配 v-model:selected-keys 接管
defaultSelectedKeys(string | number)[][]初始选中 key
checkablebooleanfalse是否显示勾选框
checkedKeys(string | number)[]--勾选 key,配 v-model:checked-keys 接管
defaultCheckedKeys(string | number)[][]初始勾选 key
checkStrictlybooleanfalse关闭父子勾选联动
expandedKeys(string | number)[]--展开 key,配 v-model:expanded-keys 接管
defaultExpandedKeys(string | number)[][]初始展开 key
defaultExpandAllbooleanfalse初始展开所有节点
disabledbooleanfalse整树禁用
loadData(node) => Promise<void>--异步加载子节点;展开未加载节点时调用
draggablebooleanfalse是否允许拖拽
showLinebooleanfalse是否显示连接线(保留接口,样式可由消费者扩展)
blockNodebooleanfalse是否独占整行
expandAction'click' | false'click'点击节点正文是否切换展开;false 仅 switcher 图标可展开
searchValuestring''搜索关键字(默认按 title 子串匹配)
filterTreeNode(node, parentKeys) => boolean--自定义过滤谓词,返回 true 命中
indentSizenumber24每级缩进像素
virtualScrollbooleanfalse启用虚拟滚动
virtualItemHeightnumber32虚拟滚动单项高度(px)
virtualMaxHeightnumber320虚拟滚动可视高度(px)
focusedKeystring | number--聚焦节点 key,配 v-model:focused-key 接管
dragHoverExpandDelaynumber600拖到 inside 区停留多少 ms 后自动展开,0 关闭
dragAutoScrollbooleantrue拖到滚动容器边缘自动滚动
dragAutoScrollEdgenumber32触发 auto-scroll 的边缘范围(px)
dragAutoScrollSpeednumber12auto-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 是否失败

类型

ts
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 兜底。
  • disableCheckboxdisabled 区分:前者只锁勾选,后者锁选中 + 勾选 + 拖拽。
  • 父子勾选联动忽略 disabled / disableCheckbox 后代——它们不计入"全选"判定。
  • loadData 完成后由消费者直接修改原 node.children,组件通过 reactive 检测重新平铺。
  • drop 事件不会自动改 data——业务实现移动逻辑。

Released under the MIT License.