三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从零封装:基于el-tree与穿梭框的树形穿梭组件实践

从零封装:基于el-tree与穿梭框的树形穿梭组件实践

1. 为什么需要树形穿梭组件

在日常开发中,我们经常会遇到需要选择树形结构数据的场景。比如权限管理系统中的菜单分配、电商后台的商品分类选择、组织架构的人员调配等。传统的解决方案要么使用普通穿梭框+扁平数据,要么单独使用树形控件,但都存在明显缺陷:

普通穿梭框处理树形数据时,会丢失原有的层级关系,用户无法直观看到父子节点的从属关系。而单独使用树形控件又缺乏便捷的双向移动功能,需要手动实现拖拽或按钮操作。这时候,一个结合了树形展示和穿梭功能的组件就显得尤为重要。

我在最近的一个后台管理系统项目中就遇到了这个问题。客户需要在角色权限配置时,能够清晰地看到菜单的层级结构,同时要支持批量移动多级菜单。尝试了多种方案后,最终决定基于Element UI的el-tree和穿梭框封装一个el-tree-transfer组件。

2. 核心设计思路

2.1 数据结构的处理

树形穿梭组件的第一个难点是如何处理树形数据。Element UI的el-tree组件需要标准的树形结构数据,而穿梭功能则需要扁平化的数据便于操作。我们采用了"存储用树形,操作用扁平"的设计思路:

// 原始树形数据 const treeData = [ { id: 1, label: '一级菜单', children: [ { id: 2, label: '二级菜单', children: [...] } ] } ] // 扁平化处理后的数据 const flatData = { 1: { id: 1, pid: 0, label: '一级菜单' }, 2: { id: 2, pid: 1, label: '二级菜单' } }

这里有个关键点:第一层数据的pid必须设为0,后续层级的pid等于父节点的id。这个约定非常重要,是后续父子节点联动逻辑的基础。

2.2 父子节点联动逻辑

当用户操作父节点时,子节点该如何处理?我们实现了三种模式:

  1. 严格模式:移动父节点会强制移动所有子节点
  2. 宽松模式:移动父节点不影响子节点
  3. 半选模式:部分子节点被选中时的特殊处理

实现这一逻辑的关键在于递归遍历树形数据:

function traverse(node, callback) { callback(node) if (node.children && node.children.length) { node.children.forEach(child => traverse(child, callback)) } }

3. 关键API设计与实现

3.1 组件props设计

为了让组件更灵活,我们设计了以下核心props:

参数说明类型默认值
mode模式切换:transfer(穿梭)/addressList(通讯录)stringtransfer
fromData源数据array[]
toData目标数据array[]
defaultProps树形配置项object{label:'label'}
height组件高度string'100%'
filter是否启用搜索booleanfalse

3.2 事件回调设计

组件提供了丰富的事件回调:

// 添加事件 @addBtn="handleAdd" // 移除事件 @removeBtn="handleRemove" // 节点点击事件 @nodeClick="handleNodeClick" methods: { handleAdd(fromData, toData, obj) { // fromData: 移动后左侧数据 // toData: 移动后右侧数据 // obj: 移动的节点信息 } }

4. 实际应用场景

4.1 权限管理系统

在权限管理系统中,树形穿梭组件可以完美呈现菜单层级结构:

<tree-transfer :from-data="menuData" :to-data="selectedMenus" :default-props="{label: 'menuName'}" @add-btn="savePermissions" />

4.2 商品分类选择

电商后台中,商品经常需要关联多级分类:

<tree-transfer :from-data="categoryTree" :to-data="selectedCategories" mode="addressList" filter />

5. 常见问题与解决方案

5.1 性能优化

当树形数据量很大时(超过1000节点),可能会遇到性能问题。我们通过以下方式优化:

  1. 虚拟滚动:只渲染可视区域内的节点
  2. 懒加载:动态加载子节点
  3. 防抖处理:搜索功能添加防抖

5.2 数据同步问题

左右两侧数据的同步是个易错点。我们采用Vue的响应式特性,配合深拷贝来确保数据一致性:

function deepClone(data) { return JSON.parse(JSON.stringify(data)) }

6. 进阶技巧

6.1 自定义节点内容

通过scoped slot可以自定义节点显示:

<tree-transfer> <template #default="{node}"> <span :class="{'is-disabled': node.disabled}"> {{ node.label }} </span> </template> </tree-transfer>

6.2 动态禁用节点

某些场景下需要动态禁用节点:

:default-props="{ label: 'name', disabled: 'isDisabled' }"

在实际项目中封装这个组件时,最大的收获是理解了树形数据与扁平数据之间的转换技巧。特别是在处理半选状态时,需要特别注意父子节点间的联动关系。建议在复杂场景下,先画出一个完整的状态转换图,再开始编码会事半功倍。

← 返回列表