一个功能强大、高度可定制的 Vue 3 树形组件 npm 包,完全复刻 PrimeVue Tree 的功能特性,支持拖拽、多选、键盘导航等高级功能。
本项目采用 MIT 协议 开源,您可以自由使用、修改和分发本项目。
- 新增
nodeActions属性 - 参考 Element Plus Tree 的节点操作模式,可在每个节点右侧添加操作按钮(如"添加"/"删除"),悬停时显示 - 新增
#actions插槽 - 完全自定义节点操作区域,点击不会触发选中/展开/拖拽 - 支持图标/文本按钮、危险操作样式、
visible/disabled按节点动态计算
- 数据保护 - 修复拖拽目标失效时节点被静默删除、数据变化导致选择/展开状态被清空的问题
- 跨树拖拽 - 修复根级别/空树放置失败、
crossTreeAutoUpdate手动模式失效、crossTreeDataProvider未生效、重复/矛盾的结束事件、多选拖拽只移动一项等问题 - checkbox 选择 - 修复 3 层以上级联状态错误、
selectAll不更新父节点、clearSelection事件为空、v-model 不同步等问题 - v-model:expandedKeys - 修复双向绑定完全失效(
update:expandedKeys从未发射) - 键盘导航 - 修复焦点管理完全不可用的问题,支持文件夹聚焦与 ←/→ 展开折叠
- 过滤功能 - 修复
filterMode/filterBy未接线、搜索结果丢失层级结构的问题
- 单元测试与 CI - 新增 Vitest 单元测试(40+ 断言,覆盖数据操作、checkbox 级联、过滤、节点操作按钮)与 GitHub Actions CI
- 依赖瘦身 - 移除
clsx/tailwind-merge/vue-router运行时依赖(产物 gzip 由 32.5KB 降至约 25KB) - SSR 修复 - 主题管理器不再在模块加载时访问 DOM,无 DOM 环境可直接导入
- props 补齐 -
propagateSelectionUp/propagateSelectionDown/metaKeySelection从"声明未实现"变为真实生效 - 内部收敛 - 选择状态统一由
useSelection管理,移除 85 处调试日志与死代码
- 新增拖拽测试页面 - 添加了专门的 DragTestPage.vue,提供完整的拖拽功能测试和调试环境
- 增强事件日志记录 - 改进了拖拽事件的详细日志记录,包含完整的事件数据和时间戳
- 实时调试信息 - 提供实时的拖拽状态显示,包括 isDragDropEnabled 状态和拖拽配置信息
- 操作指导优化 - 添加了详细的拖拽操作指导和最佳实践说明
- 集成指南文档 - 新增 INTEGRATION_GUIDE.md,提供详细的外部项目集成指南
- 故障排除文档 - 新增 TROUBLESHOOTING.md,包含常见问题和解决方案
- 外部项目示例 - 新增 EXTERNAL_PROJECT_EXAMPLE.md,提供完整的外部项目使用示例
- 发布说明文档 - 新增 RELEASE_NOTES_1.4.3.md,详细记录版本更新内容
- 调试工具增强 - 提供更好的调试工具和测试按钮,帮助开发者快速验证拖拽配置
- 错误诊断改进 - 增强了拖拽功能的错误诊断能力,提供更准确的问题定位
- 配置验证 - 添加了配置参数的实时验证和反馈机制
- 修复跨树拖拽核心问题 - 解决了
performCrossTreeDrop返回值检查逻辑错误,确保跨树拖拽操作正确执行 - 完善拖拽属性配置 - 为 Tree 组件添加了缺失的
dragdrop-scope属性支持,修复了isDragDropEnabled计算属性逻辑 - 简化演示代码架构 - 重构 TreeDemo.vue 中的跨树拖拽演示,移除复杂的手动数据处理逻辑
- 组件内部自动化 - 现在跨树拖拽完全由组件内部自动处理,用户只需简单配置即可使用
- 提升开发体验 - 大幅简化了跨树拖拽的使用方式,减少了样板代码和配置复杂度
- 修复 TreeDemo.vue 拖拽排序功能 - 为拖拽排序的 Tree 组件添加了缺少的
:dragdrop="true"属性 - 修复 TreeDemo.vue 跨树拖拽功能 - 为跨树拖拽的两个 Tree 组件添加了
:dragdrop="true"和:crossTreeAutoUpdate="false"属性 - 确保演示功能正常工作 - 修复了演示页面中拖拽功能的配置问题,用户现在可以正常体验所有拖拽特性
- 配置一致性 - 统一了拖拽相关属性的配置,确保文档示例与实际代码保持一致
- 44个构建错误全部修复 - 完成了大规模的 TypeScript 类型系统重构,确保构建过程零错误
- 生产就绪状态 - 库文件构建稳定,支持 ES 模块和 UMD 两种格式,完整的类型定义文件
- TypeScript 类型安全 - 完善的类型定义,确保开发时的类型安全和 IDE 智能提示
- 跨树拖拽自动更新功能完整集成 - 将 TreeDemo 中的高级跨树拖拽功能完全集成到插件核心,提供开箱即用的跨树拖拽体验
- CrossTreeDataProvider 接口 - 新增跨树数据提供者接口,支持灵活的数据源管理和自定义数据更新逻辑
- 增强的拖拽状态管理 - 新增 useCrossTreeDragState 全局状态管理,确保跨树拖拽状态的一致性和可靠性
- 完善的错误处理 - 修复所有关键运行时错误,提升组件稳定性和可靠性
- 插件化架构优化 - 所有跨树拖拽功能现已完全集成到插件核心,符合插件规范,无需额外配置
- 性能优化 - 生产构建自动移除调试日志,优化包体积和运行性能
- 开发体验提升 - 完整的开发工具支持,热重载,类型检查等
- 完整的树形结构支持 - 支持无限层级的树形数据展示,经过大规模测试验证
- 多种选择模式 - 支持单选、多选、复选框选择,状态管理稳定可靠
- 拖拽功能 - 支持节点拖拽排序和跨树拖拽,包含完整的错误处理和状态恢复
- 键盘导航 - 完整的键盘快捷键支持,符合无障碍访问标准
- 懒加载 - 支持大数据量的懒加载展示,性能优化到位
- 过滤搜索 - 内置过滤器,支持自定义过滤逻辑,搜索性能优异
- 节点操作按钮 - 参考 Element Plus Tree,支持在每个节点右侧添加操作按钮(悬停显示),声明式
nodeActions或#actions插槽完全自定义 - 跨树拖拽自动更新 - 全新的自动更新模式,支持多个树组件之间的智能数据同步
- CrossTreeDataProvider 接口 - 灵活的数据提供者模式,支持自定义数据源和更新逻辑
- 全局状态管理 - useCrossTreeDragState 提供一致的跨组件状态管理
- 调试和诊断工具 - 内置的拖拽测试页面和实时调试信息,帮助快速定位问题
- 自定义模板 - 支持节点内容、图标、展开/折叠图标的自定义
- 事件系统 - 丰富的事件回调,包含完整的错误处理和状态追踪
- 主题定制 - 支持自定义样式和主题,CSS 变量支持
- Vue 3 Composition API - 基于最新的 Vue 3 技术栈,充分利用响应式系统
- TypeScript 完全支持 - 100% TypeScript 编写,完整的类型安全支持,零类型错误
- 生产就绪 - 经过严格测试,44个构建错误全部修复,稳定可靠
- Tree Shaking - 支持按需引入,优化包体积,生产构建自动优化
- SSR 友好 - 支持服务端渲染,无 DOM 环境下导入不报错,同构应用兼容
- 轻量依赖 - 运行时仅依赖 Vue 3 与 Lucide 图标库,包体积 gzip 约 25KB
当前最新版本:v1.4.3
本版本是一个功能增强版本,主要改进内容:
- 新增了专门的拖拽测试页面,提供完整的调试和诊断环境
- 增强了拖拽事件的日志记录和实时状态显示功能
- 完善了集成指南、故障排除和外部项目示例文档
- 提升了开发体验,添加了更好的调试工具和配置验证
- 改进了错误诊断能力,帮助开发者快速定位和解决问题
v1.4.2 版本主要修复:
- 修复了跨树拖拽功能的核心问题,解决了
performCrossTreeDrop返回值检查逻辑错误 - 完善了 Tree 组件的拖拽属性配置,添加了缺失的
dragdrop-scope属性支持 - 修复了
isDragDropEnabled计算属性的逻辑,确保拖拽功能正确启用 - 重构并简化了 TreeDemo.vue 中的跨树拖拽演示代码
- 移除了复杂的手动数据处理逻辑,现在完全由组件内部自动处理
- 大幅提升了跨树拖拽功能的易用性和稳定性
v1.4.1 版本主要改进:
- 修复了 TreeDemo.vue 中拖拽排序功能的配置问题
- 修复了 TreeDemo.vue 中跨树拖拽功能的配置问题
- 为 Tree 组件添加了缺少的
:dragdrop="true"属性 - 为跨树拖拽添加了
:crossTreeAutoUpdate="false"属性 - 确保演示页面中的拖拽功能正常工作
v1.4.0 版本主要改进:
- 完整集成跨树拖拽自动更新功能到插件核心
- 新增 CrossTreeDataProvider 接口,支持灵活的数据源管理
- 增强拖拽状态管理,新增全局状态管理机制
- 修复关键错误,提升组件稳定性和可靠性
- 优化插件化架构,所有功能完全集成到核心
- 构建状态: ✅ 通过 (44个错误已修复)
- 类型检查: ✅ 完全通过
- 生产就绪: ✅ 是
- 包格式: ES 模块 + UMD
- 类型定义: 完整的 .d.ts 文件
- 包体积: 优化后的生产构建
- Tree Shaking: ✅ 支持
- 调试日志: 生产环境自动移除
- 依赖: 仅 Vue 3 + Lucide 图标库
- TypeScript: 100% 类型安全
- 错误处理: 完整的错误边界
- 状态管理: 稳定的响应式状态
- 测试覆盖: 核心功能全覆盖
# 使用 npm
npm install vue3-super-tree
# 使用 yarn
yarn add vue3-super-tree
# 使用 pnpm
pnpm add vue3-super-tree<template>
<div>
<Tree
:value="nodes"
:selectionMode="'single'"
v-model:selectionKeys="selectedKeys"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const nodes = ref([
{
key: '0',
label: '根节点',
children: [
{
key: '0-0',
label: '子节点 1'
},
{
key: '0-1',
label: '子节点 2'
}
]
}
])
const selectedKeys = ref({})
</script><template>
<div>
<Tree
:value="nodes"
:selectionMode="'multiple'"
:filter="true"
:dragdrop="true"
:loading="loading"
v-model:selectionKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
@node-select="onNodeSelect"
@node-unselect="onNodeUnselect"
@node-expand="onNodeExpand"
@node-collapse="onNodeCollapse"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const nodes = ref([...]) // 你的树形数据
const selectedKeys = ref({})
const expandedKeys = ref({})
const loading = ref(false)
const onNodeSelect = (node) => {
console.log('节点选中:', node)
}
const onNodeUnselect = (node) => {
console.log('节点取消选中:', node)
}
const onNodeExpand = (node) => {
console.log('节点展开:', node)
}
const onNodeCollapse = (node) => {
console.log('节点折叠:', node)
}
</script>import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import type { TreeNode, TreeSelectionKeys, TreeExpandedKeys } from 'vue3-super-tree/types'
import 'vue3-super-tree/style.css'
const nodes = ref<TreeNode[]>([
{
key: '0',
label: '根节点',
children: [...]
}
])
const selectedKeys = ref<TreeSelectionKeys>({})
const expandedKeys = ref<TreeExpandedKeys>({})| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
value |
TreeNode[] |
[] |
树形数据,支持无限层级嵌套 |
modelValue |
TreeSelectionKeys |
null |
选中的节点(v-model),支持双向绑定 |
selectionMode |
'single' | 'multiple' | 'checkbox' |
null |
选择模式:单选、多选或复选框模式 |
selectionKeys |
TreeSelectionKeys |
{} |
选中状态对象,键为节点 key,值为选中状态 |
expandedKeys |
TreeExpandedKeys |
{} |
展开状态对象,键为节点 key,值为展开状态 |
dragdrop |
boolean |
false |
是否启用拖拽功能 |
dragdropScope |
string |
undefined |
拖拽作用域,相同作用域的树可以互相拖拽 |
autoUpdate |
boolean |
false |
同树拖拽时是否自动更新数据源 |
crossTreeAutoUpdate |
boolean |
false |
🆕 v1.4.0 跨树拖拽时是否自动更新数据源 |
crossTreeDataProvider |
CrossTreeDataProvider |
undefined |
🆕 v1.4.0 跨树数据提供者,用于获取和更新不同树的数据 |
nodeActions |
TreeNodeAction[] |
undefined |
🆕 节点操作按钮数组,显示在每个节点右侧(悬停可见),也可用 #actions 插槽自定义 |
filter |
boolean |
false |
是否启用过滤功能 |
filterMode |
'lenient' | 'strict' |
'lenient' |
过滤模式:宽松模式显示匹配节点的父节点,严格模式只显示匹配节点 |
filterBy |
string |
'label' |
过滤字段,指定按哪个字段进行过滤 |
lazy |
boolean |
false |
是否启用懒加载,适用于大数据量场景 |
loading |
boolean |
false |
加载状态,显示加载指示器 |
indent |
number |
1 |
缩进级别,控制子节点的缩进距离 |
metaKeySelection |
boolean |
true |
是否支持 Ctrl/Cmd 键多选 |
| 事件 | 参数 | 描述 |
|---|---|---|
node-select |
TreeNodeSelectEvent |
节点选中时触发 |
node-unselect |
TreeNodeUnselectEvent |
节点取消选中时触发 |
node-expand |
TreeNodeExpandEvent |
节点展开时触发 |
node-collapse |
TreeNodeCollapseEvent |
节点折叠时触发 |
node-focus |
TreeNodeFocusEvent |
节点获得焦点时触发 |
node-blur |
TreeNodeBlurEvent |
节点失去焦点时触发 |
node-click |
TreeNodeClickEvent |
节点点击时触发 |
node-drop |
TreeNodeDropEvent |
节点拖拽放置时触发 |
node-drag-start |
{ originalEvent, node } |
开始拖拽时触发 |
node-drag-end |
{ originalEvent, node } |
拖拽结束时触发 |
cross-tree-drag-start |
CrossTreeDragStartEvent |
跨树拖拽开始时触发 |
cross-tree-drag-enter |
CrossTreeDragEnterEvent |
拖拽节点进入目标树时触发 |
cross-tree-drag-over |
CrossTreeDragOverEvent |
拖拽节点在目标树上方移动时触发 |
cross-tree-drag-leave |
CrossTreeDragLeaveEvent |
拖拽节点离开目标树时触发 |
cross-tree-drop |
CrossTreeDropEvent |
跨树拖拽放置时触发 |
cross-tree-drag-end |
CrossTreeDragEndEvent |
跨树拖拽结束时触发(无论成功或失败) |
cross-tree-drag-cancel |
CrossTreeDragCancelEvent |
跨树拖拽取消时触发(如按 ESC 键) |
interface TreeNode {
key: string | number // 节点唯一标识
label: string // 节点显示文本
data?: any // 节点关联数据
icon?: string | Component // 节点图标
children?: TreeNode[] // 子节点
leaf?: boolean // 是否为叶子节点
expanded?: boolean // 是否展开
selectable?: boolean // 是否可选择
draggable?: boolean // 是否可拖拽
droppable?: boolean // 是否可作为拖拽目标
styleClass?: string // 自定义样式类
style?: Record<string, any> // 自定义样式
}🆕 节点操作按钮的定义(参考 Element Plus Tree 的节点操作模式):
interface TreeNodeAction {
key: string // 操作唯一标识
label?: string // 按钮文本(未提供 icon 时显示)
icon?: string | Component // 按钮图标(组件或图标类名)
title?: string // 悬停提示文本
danger?: boolean // 危险操作(红色样式)
disabled?: boolean | ((node: TreeNode) => boolean) // 是否禁用
visible?: boolean | ((node: TreeNode) => boolean) // 是否可见(默认可见)
onClick: (node: TreeNode, event: MouseEvent) => void // 点击回调
}注意事项:
- 按钮默认隐藏,悬停或键盘聚焦到节点时显示
- 点击操作按钮不会触发节点选中、展开/折叠或拖拽
visible和disabled支持传入函数,按节点动态计算(例如只对文件夹显示"添加"按钮)- 需要更复杂的操作区域时,使用
#actions插槽完全自定义(见下方插槽文档)
| 插槽 | 作用域参数 | 描述 |
|---|---|---|
#node |
{ node, level } |
自定义节点内容(替换默认标签) |
#actions |
{ node, level } |
🆕 自定义节点操作区域(节点右侧,悬停可见) |
#empty |
无 | 自定义空树状态 |
🆕 v1.4.0 新增功能
CrossTreeDataProvider 是跨树拖拽自动更新功能的核心接口,它提供了统一的数据访问和更新机制。
interface CrossTreeDataProvider {
/**
* 获取指定树的数据
* @param treeId 树的唯一标识符(通常是组件的 id 属性)
* @returns 树的数据数组,如果树不存在则返回 null 或 undefined
*/
getTreeData: (treeId: string) => TreeNode[] | null | undefined
/**
* 更新指定树的数据
* @param treeId 树的唯一标识符
* @param newData 新的树数据
*/
updateTreeData: (treeId: string, newData: TreeNode[]) => void
}- 必须为每个树组件设置唯一的
id属性 - 实现
getTreeData方法:根据treeId返回对应树的数据 - 实现
updateTreeData方法:根据treeId更新对应树的数据 - 将同一个
CrossTreeDataProvider实例传递给所有参与跨树拖拽的树组件
- 如果
getTreeData返回null或undefined,跨树拖拽将被阻止 - 确保
updateTreeData方法能正确处理数据更新,避免响应式丢失 - 建议在方法中添加适当的错误处理和日志记录
<template>
<Tree :value="basicData" />
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const basicData = [
{
key: '1',
label: '文档',
children: [
{ key: '1-1', label: '安装指南.md' },
{ key: '1-2', label: '快速开始.md' }
]
}
]
</script><template>
<Tree
:value="data"
v-model:selectionKeys="selectedKeys"
selectionMode="multiple"
:metaKeySelection="false"
/>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const selectedKeys = ref({})
</script><template>
<Tree
:value="data"
v-model:selectionKeys="selectedKeys"
selectionMode="checkbox"
/>
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
</script><template>
<Tree
:value="data"
:autoUpdate="true"
dragdropScope="demo"
@node-drop="onNodeDrop"
/>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const data = ref([...]) // 你的树形数据
const onNodeDrop = (event) => {
// 自动更新模式下,数据会自动更新,只需要接受拖拽
event.accept()
console.log('拖拽完成,数据已自动更新')
}
</script><template>
<Tree
:value="data"
:autoUpdate="false"
dragdropScope="demo"
@node-drop="onNodeDrop"
/>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const data = ref([...]) // 你的树形数据
const onNodeDrop = (event) => {
// 手动处理拖拽逻辑
const { dragNode, dropNode, dropIndex } = event
// 自定义拖拽处理逻辑
if (dropNode) {
// 拖拽到节点上
if (!dropNode.children) dropNode.children = []
dropNode.children.push(dragNode)
} else {
// 拖拽到根级别
data.value.splice(dropIndex, 0, dragNode)
}
event.accept()
}
</script><template>
<div class="flex gap-4">
<!-- 源树 -->
<div class="w-1/2">
<h3>源树</h3>
<Tree
id="source-tree"
:value="sourceData"
:dragdrop="true"
dragdropScope="cross-tree"
:crossTreeAutoUpdate="true"
:crossTreeDataProvider="crossTreeDataProvider"
@cross-tree-drop="onCrossTreeDrop"
/>
</div>
<!-- 目标树 -->
<div class="w-1/2">
<h3>目标树</h3>
<Tree
id="target-tree"
:value="targetData"
:dragdrop="true"
dragdropScope="cross-tree"
:crossTreeAutoUpdate="true"
:crossTreeDataProvider="crossTreeDataProvider"
@cross-tree-drop="onCrossTreeDrop"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const sourceData = ref([
{
key: 'source-1',
label: '源节点 1',
children: [
{ key: 'source-1-1', label: '源子节点 1-1' },
{ key: 'source-1-2', label: '源子节点 1-2' }
]
}
])
const targetData = ref([
{
key: 'target-1',
label: '目标节点 1',
children: []
}
])
// 跨树数据提供者
const crossTreeDataProvider = {
getTreeData: (treeId) => {
if (treeId === 'source-tree') return sourceData.value
if (treeId === 'target-tree') return targetData.value
return null
},
updateTreeData: (treeId, newData) => {
if (treeId === 'source-tree') {
sourceData.value = newData
} else if (treeId === 'target-tree') {
targetData.value = newData
}
}
}
// 可选:监听跨树拖拽事件
const onCrossTreeDrop = (event) => {
console.log('跨树拖拽完成:', {
sourceTreeId: event.sourceTreeId,
targetTreeId: event.targetTreeId,
dragNode: event.dragNode,
dropNode: event.dropNode,
dropIndex: event.dropIndex
})
}
</script><template>
<div class="flex gap-4">
<Tree
ref="sourceTree"
:value="sourceData"
dragdropScope="cross-tree"
@node-drop="onCrossTreeDrop"
/>
<Tree
ref="targetTree"
:value="targetData"
dragdropScope="cross-tree"
@node-drop="onCrossTreeDrop"
/>
</div>
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const onCrossTreeDrop = (event) => {
// 处理跨树拖拽
if (event.isCrossTree) {
// 跨树拖拽逻辑
event.accept()
}
}
</script><template>
<div class="flex gap-4">
<Tree
id="tree1"
:value="sourceData"
dragdropScope="cross-tree"
@node-drop="onCrossTreeDrop"
@cross-tree-drag-start="onCrossTreeDragStart"
@cross-tree-drag-enter="onCrossTreeDragEnter"
@cross-tree-drag-over="onCrossTreeDragOver"
@cross-tree-drag-leave="onCrossTreeDragLeave"
@cross-tree-drop="onCrossTreeDropEvent"
@cross-tree-drag-end="onCrossTreeDragEnd"
@cross-tree-drag-cancel="onCrossTreeDragCancel"
/>
<Tree
id="tree2"
:value="targetData"
dragdropScope="cross-tree"
@node-drop="onCrossTreeDrop"
@cross-tree-drag-start="onCrossTreeDragStart"
@cross-tree-drag-enter="onCrossTreeDragEnter"
@cross-tree-drag-over="onCrossTreeDragOver"
@cross-tree-drag-leave="onCrossTreeDragLeave"
@cross-tree-drop="onCrossTreeDropEvent"
@cross-tree-drag-end="onCrossTreeDragEnd"
@cross-tree-drag-cancel="onCrossTreeDragCancel"
/>
</div>
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
// 跨树拖拽开始
const onCrossTreeDragStart = (event) => {
console.log('跨树拖拽开始:', {
sourceTreeId: event.sourceTreeId,
dragNode: event.dragNode,
isCrossTree: event.isCrossTree,
timestamp: event.timestamp
})
}
// 拖拽节点进入目标树
const onCrossTreeDragEnter = (event) => {
console.log('进入目标树:', {
sourceTreeId: event.sourceTreeId,
targetTreeId: event.targetTreeId,
dragNode: event.dragNode
})
}
// 拖拽节点在目标树上方移动(频繁触发)
const onCrossTreeDragOver = (event) => {
// 由于频繁触发,通常只做必要的处理
console.log('在目标树上方移动')
}
// 拖拽节点离开目标树
const onCrossTreeDragLeave = (event) => {
console.log('离开目标树:', {
targetTreeId: event.targetTreeId,
dragNode: event.dragNode
})
}
// 跨树拖拽放置
const onCrossTreeDropEvent = (event) => {
console.log('跨树拖拽放置:', {
sourceTreeId: event.sourceTreeId,
targetTreeId: event.targetTreeId,
dragNode: event.dragNode,
dropNode: event.dropNode,
dropPosition: event.dropPosition
})
}
// 跨树拖拽结束
const onCrossTreeDragEnd = (event) => {
console.log('跨树拖拽结束:', {
success: event.success,
sourceTreeId: event.sourceTreeId,
targetTreeId: event.targetTreeId
})
}
// 跨树拖拽取消(如按 ESC 键)
const onCrossTreeDragCancel = (event) => {
console.log('跨树拖拽取消:', {
sourceTreeId: event.sourceTreeId,
dragNode: event.dragNode
})
}
// 传统的 node-drop 事件处理
const onCrossTreeDrop = (event) => {
if (event.isCrossTree) {
event.accept()
}
}
</script>crossTreeAutoUpdate 参数控制跨树拖拽时是否自动更新数据源。v1.3.0 版本完善了跨树拖拽的数据同步机制,确保在两种模式下都能正确更新数据:
- 自动更新模式 (
crossTreeAutoUpdate: true):拖拽操作会自动调用accept()方法并同步更新源树和目标树的数据 - 手动确认模式 (
crossTreeAutoUpdate: false):需要手动调用accept()或reject()方法,确认后才会更新数据
<template>
<div class="flex gap-4">
<!-- 自动更新模式 -->
<Tree
id="auto-tree1"
:value="autoData1"
:crossTreeAutoUpdate="true"
:dragdrop="true"
dragdropScope="auto-cross-tree"
@cross-tree-drop="onAutoUpdateDrop"
/>
<Tree
id="auto-tree2"
:value="autoData2"
:crossTreeAutoUpdate="true"
:dragdrop="true"
dragdropScope="auto-cross-tree"
@cross-tree-drop="onAutoUpdateDrop"
/>
<!-- 手动控制模式 -->
<Tree
id="manual-tree1"
:value="manualData1"
:crossTreeAutoUpdate="false"
:dragdrop="true"
dragdropScope="manual-cross-tree"
@cross-tree-drop="onManualControlDrop"
/>
<Tree
id="manual-tree2"
:value="manualData2"
:crossTreeAutoUpdate="false"
:dragdrop="true"
dragdropScope="manual-cross-tree"
@cross-tree-drop="onManualControlDrop"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const autoData1 = ref([...]) // 自动更新模式的源树数据
const autoData2 = ref([...]) // 自动更新模式的目标树数据
const manualData1 = ref([...]) // 手动控制模式的源树数据
const manualData2 = ref([...]) // 手动控制模式的目标树数据
// 自动更新模式:拖拽会自动完成数据同步
const onAutoUpdateDrop = (event) => {
console.log('自动更新拖拽:', event)
// v1.3.0 版本已完善数据同步机制,无需手动处理数据更新
// 组件会自动调用 accept() 并更新源树和目标树的数据
}
// 手动控制模式:需要手动决定是否接受拖拽
const onManualControlDrop = (event) => {
console.log('手动控制拖拽:', event)
// 根据业务逻辑决定是否接受拖拽
const shouldAccept = validateDragOperation(event)
if (shouldAccept) {
event.accept() // 接受拖拽,自动更新源树和目标树数据
} else {
event.reject() // 拒绝拖拽,不更新数据
}
}
const validateDragOperation = (event) => {
// 自定义验证逻辑
return event.dragNode.label !== '禁止拖拽的节点'
}
</script>使用场景:
- 自动更新模式 (
crossTreeAutoUpdate: true):适用于简单的拖拽操作,无需复杂验证 - 手动控制模式 (
crossTreeAutoUpdate: false):适用于需要验证、确认或异步处理的拖拽操作
注意事项:
crossTreeAutoUpdate只影响跨树拖拽,不影响同树内的拖拽操作- 手动控制模式下,必须调用
accept()或reject()方法,否则拖拽状态不会清理 - 可以与
autoUpdate参数配合使用,分别控制同树和跨树的拖拽行为
<template>
<Tree
:value="data"
:filter="true"
filterBy="label"
filterPlaceholder="搜索节点..."
/>
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
</script><template>
<Tree
:value="data"
:lazy="true"
@node-load="onNodeLoad"
/>
</template>
<script setup>
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const onNodeLoad = async (event) => {
// 异步加载子节点
const children = await fetchChildren(event.node.key)
event.node.children = children
}
</script>参考 Element Plus Tree 的节点操作模式,在每个节点右侧添加按钮(悬停显示,点击不影响选中/展开/拖拽)。
<template>
<Tree
:value="data"
:nodeActions="nodeActions"
selectionMode="single"
/>
</template>
<script setup>
import { ref } from 'vue'
import { Tree } from 'vue3-super-tree'
import 'vue3-super-tree/style.css'
const data = ref([
{
key: '1',
label: '一级节点',
children: [{ key: '1-1', label: '二级节点' }]
}
])
let idSeed = 100
const nodeActions = [
{
key: 'append',
label: '添加',
title: '添加子节点',
// 只对文件夹显示
visible: (node) => !!node.children,
onClick: (node) => {
if (!node.children) node.children = []
node.children.push({ key: `new-${idSeed++}`, label: '新节点' })
data.value = [...data.value] // 触发响应式更新
}
},
{
key: 'delete',
label: '删除',
title: '删除节点',
danger: true, // 红色危险样式
onClick: (node) => {
removeNode(data.value, node.key)
data.value = [...data.value]
}
}
]
const removeNode = (nodes, key) => {
const index = nodes.findIndex(n => n.key === key)
if (index !== -1) {
nodes.splice(index, 1)
return
}
nodes.forEach(n => n.children && removeNode(n.children, key))
}
</script><template>
<Tree :value="data">
<template #actions="{ node }">
<button @click="appendNode(node)">添加</button>
<button @click="removeNode(node)">删除</button>
</template>
</Tree>
</template>插槽内的点击已自动阻止冒泡,无需再写 @click.stop。
| 按键 | 功能 |
|---|---|
↑/↓ |
上下导航 |
←/→ |
展开/折叠节点 |
Enter |
选择节点 |
Space |
切换复选框状态 |
Home |
跳转到第一个节点 |
End |
跳转到最后一个节点 |
Ctrl/Cmd + A |
全选(多选模式) |
// 导入默认样式
import 'vue3-super-tree/style.css'组件使用浏览器原生的 :focus 伪类来管理焦点样式,提供更好的性能和无障碍访问体验:
/* 组件内置的焦点样式 */
.tree-node:focus {
outline: 2px solid #3b82f6;
outline-offset: -2px;
background-color: #eff6ff;
}
/* 自定义焦点样式 */
.tree-node:focus {
outline: 2px solid #10b981;
background-color: #ecfdf5;
border-radius: 0.375rem;
}组件支持通过 CSS 变量进行主题定制:
:root {
--tree-node-padding: 0.5rem;
--tree-node-border-radius: 0.375rem;
--tree-selected-bg: #3b82f6;
--tree-selected-color: white;
--tree-focus-bg: #eff6ff;
--tree-focus-color: #1e40af;
--tree-focus-outline: #3b82f6;
}<Tree
:value="data"
class="custom-tree"
:selectedBackgroundColor="#10b981"
:selectedTextColor="white"
:focusBackgroundColor="#ecfdf5"
:focusTextColor="#059669"
/>- npm 包: https://www.npmjs.com/package/vue3-super-tree
- GitHub 仓库: https://github.com/zuoanCo/vue3-super-tree
- 在线演示: https://zuoanCo.github.io/vue3-super-tree
- API 文档: https://github.com/zuoanCo/vue3-super-tree#api-文档
- 问题反馈: https://github.com/zuoanCo/vue3-super-tree/issues
- Vue 3.4+ - 渐进式 JavaScript 框架
- TypeScript - 类型安全的 JavaScript 超集
- Vite - 快速的前端构建工具
- Tailwind CSS - 实用优先的 CSS 框架
- Lucide Icons - 美观的图标库
组件在生产环境下自动进行以下优化:
- 调试日志移除 - 自动移除所有
console.log调试信息 - 代码压缩 - 使用 Terser 进行代码压缩和混淆
- Tree Shaking - 移除未使用的代码,减小包体积
- 原生焦点管理 - 使用浏览器原生
:focus伪类,减少 JavaScript 开销
- 智能更新 - 只更新变化的节点,避免全量重渲染
- 内存优化 - 及时清理事件监听器和引用
说明:虚拟滚动尚未实现(
virtualScrollerOptions属性已预留),大数据量场景建议先使用懒加载与分页。
vue3-super-tree/
├── src/
│ ├── components/ # 组件目录
│ │ ├── Tree.vue # 主树组件
│ │ ├── TreeNode.vue # 树节点组件
│ │ └── index.ts # 组件导出
│ ├── lib/ # 核心库
│ │ ├── types.ts # TypeScript 类型定义
│ │ ├── utils.ts # 工具函数
│ │ ├── themes.ts # 主题配置
│ │ └── index.ts # 库导出
│ ├── composables/ # 组合式函数
│ │ ├── useDragDrop.ts # 拖拽功能
│ │ ├── useSelection.ts # 选择功能
│ │ ├── useFilter.ts # 过滤功能
│ │ └── index.ts # 组合函数导出
│ └── style.css # 样式文件
├── dist/ # 构建输出
├── package.json # 包配置
├── vite.lib.config.ts # Vite 库构建配置
└── README.md # 项目文档
问题: TypeScript 构建失败,出现类型错误
error TS2742: The inferred type of 'xxx' cannot be named without a reference to '@vue/shared'
解决方案:
- 确保使用的是 v1.4.0 或更高版本,该版本已修复所有构建错误
- 检查 TypeScript 版本是否兼容(推荐 4.9+)
- 清理缓存:
rm -rf node_modules/.cache && pnpm install
问题: 跨树拖拽功能无法正常工作
解决方案:
- 确保所有参与跨树拖拽的树组件都设置了相同的
dragdropScope - 检查是否为每个树组件设置了唯一的
id属性 - 确保
CrossTreeDataProvider正确实现了getTreeData和updateTreeData方法 - 检查控制台是否有错误信息
问题: 设置了 autoUpdate 或 crossTreeAutoUpdate 但数据没有自动更新
解决方案:
- 确保在拖拽事件处理器中调用了
event.accept() - 检查数据是否为响应式对象(使用
ref或reactive) - 对于跨树拖拽,确保
CrossTreeDataProvider的updateTreeData方法正确更新了响应式数据
问题: 大数据量时性能较差
解决方案:
- 启用懒加载:设置
lazy="true" - 避免在节点模板中使用复杂的计算
- 考虑分页或分批加载数据(虚拟滚动规划中)
问题: 样式显示不正确
解决方案:
- 确保导入了样式文件:
import 'vue3-super-tree/style.css' - 检查 CSS 优先级,避免样式被覆盖
- 使用浏览器开发者工具检查样式应用情况
在开发环境中,组件会输出详细的调试信息:
// 在浏览器控制台中查看调试信息
console.log('Vue3 Super Tree Debug Mode: ON')监听所有相关事件来调试问题:
<template>
<Tree
@node-drop="onNodeDrop"
@cross-tree-drop="onCrossTreeDrop"
@cross-tree-drag-start="onDragStart"
@cross-tree-drag-end="onDragEnd"
/>
</template>
<script setup>
const onNodeDrop = (event) => {
console.log('Node drop:', event)
}
const onCrossTreeDrop = (event) => {
console.log('Cross tree drop:', event)
}
const onDragStart = (event) => {
console.log('Drag start:', event)
}
const onDragEnd = (event) => {
console.log('Drag end:', event)
}
</script>确保树形数据结构正确:
// 正确的数据结构
const treeData = [
{
key: 'unique-key', // 必须:唯一标识
label: 'Node Label', // 必须:显示文本
children: [ // 可选:子节点数组
{
key: 'child-key',
label: 'Child Node'
}
]
}
]<template>
<Tree
:value="data"
:lazy="true"
:loading="loading"
@node-expand="loadChildren"
/>
</template>
<script setup>
const loadChildren = async (node) => {
loading.value = true
try {
const children = await fetchChildren(node.key)
node.children = children
} finally {
loading.value = false
}
}
</script><script setup>
import { computed, shallowRef } from 'vue'
// 使用 shallowRef 减少深度响应式开销
const treeData = shallowRef([])
// 使用 computed 缓存计算结果
const filteredData = computed(() => {
return filterTreeData(treeData.value, searchTerm.value)
})
</script><script setup>
// 使用防抖避免频繁触发
import { debounce } from 'lodash-es'
const handleSearch = debounce((searchTerm) => {
// 搜索逻辑
}, 300)
</script>-
新增功能:
- 跨树拖拽自动更新功能
CrossTreeDataProvider接口- 全局状态管理
-
破坏性变更:
- 无破坏性变更,完全向后兼容
-
推荐更新:
- 使用新的跨树拖拽自动更新功能替代手动处理
- 更新 TypeScript 类型定义
欢迎贡献代码!vue3-super-tree 是一个开源 npm 包,我们欢迎社区的贡献。
- 克隆项目
git clone https://github.com/zuoanCo/vue3-super-tree.git
cd vue3-super-tree- 安装依赖
pnpm install- 启动开发服务器
pnpm run dev- 构建库文件
pnpm run build:lib- Fork 项目
- 创建特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 开启 Pull Request
- 使用 TypeScript 编写代码
- 遵循 Vue 3 Composition API 最佳实践
- 添加适当的类型注解
- 编写清晰的注释
- 确保代码通过 ESLint 检查
- 添加相应的测试用例
项目使用 ESLint 进行代码检查:
# 检查代码
pnpm run lint
# 自动修复
pnpm run lint:fix本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情。
- Vue.js - 渐进式 JavaScript 框架
- PrimeVue - 功能参考来源
- Tailwind CSS - 样式框架
- Lucide - 图标库
- Vite - 构建工具
- 当前版本: v1.4.0 🆕
- 发布状态: 生产就绪 ✅
- 构建状态: 通过 (44个错误已修复) ✅
- 包大小: ~220KB (生产优化后)
- 支持的 Vue 版本: 3.4+
- Node.js 版本: 16.0+
- TypeScript 支持: 100% 类型安全 ✅
- Tree Shaking: 完全支持 ✅
- SSR 支持: 服务端渲染友好 ✅
- 原生焦点管理: 基于浏览器原生 :focus 伪类 ✅
- 自动拖拽更新: 同树拖拽自动更新 ✅
- 跨树拖拽自动更新: v1.4.0 新增功能 🆕
- 全局状态管理: useCrossTreeDragState ✅
- 错误处理: 完整的错误边界和恢复机制 ✅
- 性能优化: 生产构建自动优化 ✅
- ES 模块:
dist/index.es.js - UMD 版本:
dist/index.umd.js - 类型定义:
dist/index.d.ts - 样式文件:
dist/style.css
如果这个项目对你有帮助,请给它一个 ⭐️!