企业级tree组件封装实现,即拿即用(附git地址)

0 阅读9分钟

Vue 3 Tree 组件

一个功能完整、高度可定制的 Vue 3 树形组件,适用于企业级应用开发。

image.png

📋 目录

🌟 功能特性

核心功能

  • 树形结构展示:支持无限层级的树形数据展示
  • 展开/折叠:可控制节点展开状态,支持手风琴模式
  • 节点选择:单选和多选支持,带有选中状态高亮
  • 复选框选择:可选的复选框模式,支持父子节点联动
  • 搜索过滤:内置搜索功能,支持自定义过滤方法
  • 拖拽排序:支持节点拖拽重新排序和移动
  • 懒加载:支持大数据量的懒加载机制
  • 虚拟滚动:处理大数据集时的性能优化
  • 自定义渲染:支持自定义节点内容和图标
  • 右键菜单:可定制的上下文菜单
  • 键盘导航:完整的键盘操作支持
  • 响应式设计:完美适配桌面端和移动端

设计特色

  • 🎨 现代化 UI:简洁美观的界面设计,支持主题定制
  • 📱 移动端优化:针对触摸设备优化的交互体验
  • 高性能:虚拟滚动和懒加载确保大数据量下的流畅体验
  • 🔧 高可扩展性:丰富的插槽和事件系统,便于功能扩展
  • 🛡️ 类型安全:完整的 TypeScript 类型定义(可选)
  • 无障碍:遵循 WCAG 标准的无障碍设计

🚀 快速开始

安装

npm install vue3-tree-component
# 或
yarn add vue3-tree-component

基本使用

<template>
  <Tree
    :data="treeData"
    :show-checkbox="true"
    :filterable="true"
    @node-click="handleNodeClick"
  />
</template>

<script setup>
import { ref } from 'vue'
import Tree from 'vue3-tree-component'

const treeData = ref([
  {
    id: 1,
    label: '节点1',
    children: [
      {
        id: 2,
        label: '子节点1-1'
      },
      {
        id: 3,
        label: '子节点1-2'
      }
    ]
  }
])

const handleNodeClick = (node) => {
  console.log('点击了节点:', node)
}
</script>

📖 基础用法

基本树形结构

<template>
  <Tree :data="basicData" />
</template>

<script setup>
const basicData = [
  {
    id: '1',
    label: '一级节点',
    children: [
      {
        id: '1-1',
        label: '二级节点1',
        children: [
          { id: '1-1-1', label: '三级节点1' },
          { id: '1-1-2', label: '三级节点2' }
        ]
      },
      {
        id: '1-2', 
        label: '二级节点2'
      }
    ]
  }
]
</script>

带复选框的树

<template>
  <Tree
    :data="checkboxData"
    :show-checkbox="true"
    :default-checked-keys="['1-1', '1-2-1']"
    @check-change="handleCheckChange"
  />
</template>

<script setup>
const handleCheckChange = (node, checked, checkedKeys) => {
  console.log('选中状态改变:', { node, checked, checkedKeys })
}
</script>

可搜索的树

<template>
  <Tree
    :data="searchableData"
    :filterable="true"
    :filter-node-method="filterNode"
    filter-placeholder="请输入关键字搜索"
  />
</template>

<script setup>
const filterNode = (value, data) => {
  if (!value) return true
  return data.label.includes(value)
}
</script>

可拖拽的树

<template>
  <Tree
    :data="draggableData"
    :draggable="true"
    @node-drop="handleNodeDrop"
  />
</template>

<script setup>
const handleNodeDrop = (dragNode, dropNode, position) => {
  console.log('节点拖拽:', { dragNode, dropNode, position })
  // 在这里处理数据更新逻辑
}
</script>

📚 API 文档

Tree Props

参数说明类型默认值
data展示数据Array[]
nodeKey每个树节点用来作为唯一标识的属性String'id'
showCheckbox是否显示复选框Booleanfalse
showLine是否显示连接线Booleanfalse
draggable是否开启拖拽功能Booleanfalse
filterable是否可过滤Booleanfalse
filterPlaceholder过滤输入框的占位符String'请输入关键字进行过滤'
emptyText内容为空的时候展示的文本String'暂无数据'
defaultExpandedKeys默认展开的节点的 key 的数组Array[]
defaultSelectedKeys默认选中的节点的 key 的数组Array[]
defaultCheckedKeys默认勾选的节点的 key 的数组Array[]
defaultExpandAll是否默认展开所有节点Booleanfalse
highlightCurrent是否高亮当前选中节点Booleanfalse
accordion是否每次只打开一个同级树节点展开Booleanfalse
checkStrictly在显示复选框的情况下,是否严格的遵循父子不互相关联的做法Booleanfalse
lazy是否懒加载子节点Booleanfalse
load加载子树数据的方法Function-
filterNodeMethod对树节点进行筛选时执行的方法Function-

Tree Events

事件名称说明回调参数
node-click节点被点击时的回调(node, event)
node-expand节点被展开时的回调(node)
node-collapse节点被收起时的回调(node)
check-change节点选中状态发生变化时的回调(node, checked, checkedKeys)
current-change当前选中节点变化时的回调(current, previous)
node-drag-start节点开始拖拽时的回调(node, event)
node-drag-end节点拖拽结束时的回调(node, event)
node-drop拖拽成功完成时的回调(dragNode, dropNode, position, event)
node-context-menu当某一节点被鼠标右键点击时的回调(node, event)

Tree Methods

方法名说明参数
getNode根据 key 获取树节点(key)
getCheckedNodes获取目前被选中的节点(leafOnly)
setCheckedKeys通过 keys 设置目前勾选的节点(keys)
setCurrentNode设置当前选中的节点(node)
expandNode展开指定节点(node)
collapseNode收起指定节点(node)
expandAllNodes展开所有节点-
collapseAllNodes收起所有节点-

Tree Slots

插槽名说明参数
default自定义树节点的内容{ node }
icon自定义树节点的图标{ node, expanded }
empty无数据时的内容-
context-menu自定义右键菜单内容{ node }

🔧 高级功能

懒加载

懒加载适用于数据量较大或需要动态加载子节点的场景:

<template>
  <Tree
    :data="lazyData"
    :lazy="true"
    :load="loadNode"
    node-key="id"
  />
</template>

<script setup>
import { ref } from 'vue'

const lazyData = ref([
  {
    id: 1,
    label: '懒加载节点',
    isLeaf: false
  }
])

const loadNode = async (node) => {
  // 模拟异步加载
  return new Promise((resolve) => {
    setTimeout(() => {
      const children = []
      for (let i = 1; i <= 3; i++) {
        children.push({
          id: `${node.id}-${i}`,
          label: `子节点 ${node.id}-${i}`,
          isLeaf: i === 3
        })
      }
      resolve(children)
    }, 1000)
  })
}
</script>

虚拟滚动

对于大数据量的树,启用虚拟滚动可以提升性能:

<template>
  <Tree
    :data="bigData"
    :virtual-scroll="true"
    :item-size="32"
    :buffer-size="5"
  />
</template>

<script setup>
// 生成大量数据
const bigData = ref(generateBigData(10000))

function generateBigData(count) {
  const data = []
  for (let i = 0; i < count; i++) {
    data.push({
      id: i,
      label: `节点 ${i}`,
      children: i % 10 === 0 ? [
        { id: `${i}-1`, label: `子节点 ${i}-1` },
        { id: `${i}-2`, label: `子节点 ${i}-2` }
      ] : undefined
    })
  }
  return data
}
</script>

自定义节点渲染

使用插槽完全自定义节点的外观:

<template>
  <Tree :data="customData">
    <template #default="{ node }">
      <div class="custom-node">
        <span class="node-label">{{ node.label }}</span>
        <span class="node-badge" v-if="node.badge">{{ node.badge }}</span>
        <button 
          class="node-action"
          @click.stop="editNode(node)"
        >
          编辑
        </button>
      </div>
    </template>
    
    <template #icon="{ node, expanded }">
      <i :class="getIconClass(node, expanded)"></i>
    </template>
  </Tree>
</template>

<script setup>
const customData = ref([
  {
    id: 1,
    label: '项目文件',
    type: 'folder',
    badge: 'New',
    children: [
      {
        id: 2,
        label: 'src',
        type: 'folder',
        children: [
          { id: 3, label: 'App.vue', type: 'vue' },
          { id: 4, label: 'main.js', type: 'js' }
        ]
      }
    ]
  }
])

const getIconClass = (node, expanded) => {
  if (node.type === 'folder') {
    return expanded ? 'icon-folder-open' : 'icon-folder'
  }
  return `icon-${node.type}`
}

const editNode = (node) => {
  console.log('编辑节点:', node)
}
</script>

<style scoped>
.custom-node {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

.node-label {
  flex: 1;
}

.node-badge {
  background: #ff4757;
  color: white;
  padding: 2px 6px;
  border-radius: 10px;
  font-size: 10px;
}

.node-action {
  background: #3742fa;
  color: white;
  border: none;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
}
</style>

🔨 技术实现

架构设计

本组件采用组合式 API 和响应式设计,主要包含以下核心模块:

  1. Tree.vue - 主组件,负责整体逻辑控制
  2. TreeNode.vue - 节点组件,处理单个节点的展示和交互
  3. useTree - 组合式函数,封装树形数据处理逻辑
  4. useVirtualScroll - 虚拟滚动实现
  5. useDragDrop - 拖拽功能实现

核心算法

1. 树形数据扁平化
// 将树形结构转换为扁平化映射,便于快速查找和操作
const buildFlattenNodes = (nodes, map = new Map(), parent = null) => {
  nodes.forEach(node => {
    const processedNode = { ...node, parent }
    map.set(node[nodeKey], processedNode)
    
    if (node.children) {
      buildFlattenNodes(node.children, map, processedNode)
    }
  })
  
  return map
}
2. 复选框状态联动
// 计算父子节点复选框状态联动
const updateCheckedState = (node, checked) => {
  // 更新子节点
  const updateChildren = (children, checked) => {
    if (!children) return
    children.forEach(child => {
      child.checked = checked
      updateChildren(child.children, checked)
    })
  }
  
  // 更新父节点
  const updateParent = (parent) => {
    if (!parent) return
    
    const siblings = parent.children
    const checkedCount = siblings.filter(s => s.checked).length
    const indeterminateCount = siblings.filter(s => s.indeterminate).length
    
    if (checkedCount === siblings.length) {
      parent.checked = true
      parent.indeterminate = false
    } else if (checkedCount > 0 || indeterminateCount > 0) {
      parent.checked = false
      parent.indeterminate = true
    } else {
      parent.checked = false
      parent.indeterminate = false
    }
    
    updateParent(parent.parent)
  }
  
  updateChildren(node.children, checked)
  updateParent(node.parent)
}
3. 虚拟滚动实现
// 虚拟滚动核心逻辑
const useVirtualScroll = (items, itemSize, containerHeight) => {
  const scrollTop = ref(0)
  const visibleCount = computed(() => Math.ceil(containerHeight / itemSize))
  const startIndex = computed(() => Math.floor(scrollTop.value / itemSize))
  const endIndex = computed(() => Math.min(startIndex.value + visibleCount.value, items.length))
  
  const visibleItems = computed(() => {
    return items.slice(startIndex.value, endIndex.value)
  })
  
  const offsetY = computed(() => startIndex.value * itemSize)
  const totalHeight = computed(() => items.length * itemSize)
  
  return {
    visibleItems,
    offsetY,
    totalHeight,
    scrollTop
  }
}

性能优化策略

  1. 虚拟滚动:大数据量时只渲染可见区域的节点
  2. 懒加载:按需加载子节点数据
  3. 事件委托:减少事件监听器数量
  4. 防抖优化:搜索和过滤功能使用防抖
  5. 响应式优化:合理使用 computedwatch

移动端适配

  1. 触摸优化:增大触摸区域,优化触摸反馈
  2. 响应式布局:使用 CSS Grid 和 Flexbox
  3. 手势支持:支持滑动展开/收起
  4. 性能优化:移动端特殊的性能考虑

💡 最佳实践

1. 数据结构设计

// 推荐的数据结构
const treeDataStructure = {
  id: 'unique-id',           // 唯一标识,必须
  label: '显示文本',          // 显示文本,必须
  children: [],              // 子节点数组,可选
  disabled: false,           // 是否禁用,可选
  expanded: false,           // 是否展开,可选
  checked: false,            // 是否选中,可选
  icon: 'icon-name',         // 图标名称,可选
  class: 'custom-class',     // 自定义CSS类,可选
  // 其他自定义属性...
}

2. 性能优化建议

// 1. 使用 nodeKey 确保唯一性
<Tree :data="data" node-key="id" />

// 2. 大数据量时启用虚拟滚动
<Tree :data="bigData" :virtual-scroll="true" />

// 3. 懒加载大数据集
<Tree :data="data" :lazy="true" :load="loadFunction" />

// 4. 搜索防抖
const debouncedFilter = debounce((value) => {
  // 过滤逻辑
}, 300)

3. 事件处理模式

// 统一的事件处理模式
const treeEventHandlers = {
  onNodeClick: (node, event) => {
    console.log('Node clicked:', node)
  },
  
  onCheckChange: (node, checked, checkedKeys) => {
    // 处理选中状态变化
    updateCheckedStatus(checkedKeys)
  },
  
  onNodeDrop: (dragNode, dropNode, position) => {
    // 处理拖拽结果
    updateTreeStructure(dragNode, dropNode, position)
  }
}

4. 状态管理

// 使用 Pinia 或 Vuex 管理树状态
import { defineStore } from 'pinia'

export const useTreeStore = defineStore('tree', {
  state: () => ({
    treeData: [],
    selectedNode: null,
    checkedNodes: [],
    expandedNodes: []
  }),
  
  actions: {
    updateTreeData(data) {
      this.treeData = data
    },
    
    selectNode(node) {
      this.selectedNode = node
    },
    
    toggleNodeExpansion(nodeId) {
      const index = this.expandedNodes.indexOf(nodeId)
      if (index > -1) {
        this.expandedNodes.splice(index, 1)
      } else {
        this.expandedNodes.push(nodeId)
      }
    }
  }
})

❓ 常见问题

Q1: 如何处理大数据量的性能问题?

A: 对于大数据量(>1000个节点),建议:

  1. 启用虚拟滚动:<Tree :virtual-scroll="true" />
  2. 使用懒加载:<Tree :lazy="true" :load="loadFunction" />
  3. 避免默认展开所有节点
  4. 使用搜索功能限制显示数量

Q2: 如何自定义节点样式?

A: 可以通过以下方式自定义:

  1. 使用 default 插槽自定义节点内容
  2. 使用 icon 插槽自定义图标
  3. 通过 CSS 变量修改主题色彩
  4. 添加自定义 CSS 类

Q3: 拖拽功能不工作怎么办?

A: 检查以下几点:

  1. 确保设置了 draggable="true"
  2. 检查节点是否被设置为 disabled
  3. 确保浏览器支持拖拽 API
  4. 检查是否有其他事件阻止了拖拽

Q4: 复选框状态不正确怎么办?

A: 可能的原因:

  1. 检查 check-strictly 属性设置
  2. 确保 nodeKey 属性正确设置
  3. 检查数据结构是否正确
  4. 使用 setCheckedKeys 方法手动设置

Q5: 如何实现右键菜单?

A: 使用 context-menu 插槽:

<Tree @node-context-menu="handleContextMenu">
  <template #context-menu="{ node }">
    <div class="context-menu">
      <div @click="editNode(node)">编辑</div>
      <div @click="deleteNode(node)">删除</div>
    </div>
  </template>
</Tree>

Q6: 如何实现节点搜索高亮?

A: 自定义过滤方法和节点渲染:

<Tree 
  :filter-node-method="filterNode"
  :filterable="true"
>
  <template #default="{ node }">
    <span v-html="highlightText(node.label, searchText)"></span>
  </template>
</Tree>

🤝 贡献指南

欢迎贡献代码!请按照以下步骤:

  1. Fork 项目
  2. 创建特性分支:git checkout -b feature/new-feature
  3. 提交更改:git commit -am 'Add new feature'
  4. 推送分支:git push origin feature/new-feature
  5. 提交 Pull Request

📄 许可证

MIT License

🙏 致谢

感谢所有贡献者和使用者的支持!


gitee.com/xie-yaozu/t… 如果你觉得这个组件有用,请给个 ⭐ Star 支持一下!