概述
树组件在企业有多常用我就不用说了,特别是做管理系统相关的,如果能够将组件库的树组件经过二次高度封装后,其他地方类似就不用重复自己去写一遍,其实一个公共树组件封装得很完善的话还是挺麻烦的,因此,以下组件主要封装项目常见的树组件公共操作,减少项目实际使用还要额外去花很多时间单独封装,见下面详解
实现效果

演示

代码仓库
github.com/vgnip/vue3-... 使用demon:


组件功能清单
二次高度封装的数包含如下核心功能:
- 节点获取
- 节点拖拽
- 懒加载和全量加载切换
- 空状态配置
- 异常状态配置
- 可层级节点分页
- 可定位任意等级节点(展开并高亮)
- 树刷新
- 树节点增删改操作
- 树节点菜单配置
- ...
技术栈
vue3+elementplus+ tiny-emitter
注意:多数api操作通过事件回调触发。
核心功能使用解释
上述树组件demon在源代码/component/test-tree/index.vue下,下面方法可以在对应文件中查询到
1、切换懒加载
默认情况使用全量加载数据,如果数据需要懒加载,需要配置
js
/** 树懒加载切换 */
function lazyChange(val: boolean | string | number) {
if (!val) {
tagId.value = '';
nodeNum.value = '5';
nodeLevel.value = '5';
} else {
tagId.value = '';
nodeNum.value = '';
nodeLevel.value = '';
}
refreshTree(true);
}
// 懒加载和全量加载函数
function treeLoad(data: any, node: any): Promise<any> {
if (node?.level === 0) {
treeId.value = -1;
}
return new Promise((resolve, inject) => {
setTimeout(() => {
if (isError.value) {
// eslint-disable-next-line prefer-promise-reject-errors
inject();
return;
}
let list = [];
if (lazy.value) {
list = isEmpty.value
? []
: Array.from({ length: nodeTagNum.value }).fill(1).map(() => {
treeId.value++;
return {
label: `节点点_${treeId.value}`,
id: `${treeId.value}`,
isLeaf: node?.level >= nodeTagLevel.value - 1,
};
});
} else {
list = isEmpty.value ? [] : initTreeData(nodeNum.value, nodeLevel.value);
}
console.log("tree-list", list)
resolve(list);
}, 300);
});
}
2、空状态和异常状态切换
异常状态
空状态
默认数据为空即可

3、节点分页
有些情况下,就算树是懒加载了,但是某个节点的数据依然很多,就需要在某个节点下进行二次分页(常见的就是机构下用户的情况)

props
ini
:node-page="isNodePage" // 是否开启分页
:node-page-size="nodePageSize" //分页每页条数大小
:node-page-local="isLocalPage" // 是否本地分页
4、显示checkbox
某些情况需要选择某些节点,包含懒加载

props
js
:show-checkbox="showCheckBox"
获取选中节点
js
eventBus.emit(
TREE_EVENT_BUS + treeName,
TreeEventTypes.getTree,
(treeRefs: any) => {
console.log("选中节点为", treeRefs.getCheckedNodes())
},
);
5、层级定位
事件名为toTagNodeByIdList和setCurrentKey,需要从父级开始到叶节点的id数组。

js
/** node 定位 */
function toTagNode() {
if (tagId.value) {
const ids = tagId.value.split(',');
const tagName
= ids.length > 1 ? TreeEventTypes.toTagNodeByIdList : TreeEventTypes.setCurrentKey;
const data = ids.length > 1 ? ids : ids[0];
eventBus.emit(TREE_EVENT_BUS + treeName, tagName, data);
}
}
6、刷新树
js
/** 刷新树 */
function refreshTree(val?: any) {
if (!val) {
eventBus.emit(TREE_EVENT_BUS + treeName, TreeEventTypes.reloadTreeData);
return;
}
treeId.value = -1;
treeKey.value++;
}
7、节点获取

js
/** 获取node */
function getNode() {
eventBus.emit(
TREE_EVENT_BUS + treeName,
TreeEventTypes.getNodeById,
nodeId.value,
(node: any) => {
console.log('获取结果:', node?.data);
nodeData.value = cloneDeep(node?.data);
delete nodeData.value?.children;
},
);
}
8、节点操作
可对树节点进行crud操作
增加
js
/** 添加子节点/编辑节点 */
function addChild(isEdit = false, data?: any, isBefore?: boolean) {
const obj = newNodeObj();
const list = newNodeStr.value.split(',');
try {
const strObj: any = {};
list.forEach((el) => {
const item = el.split('=');
strObj[item[0]] = item[1];
});
if ('id' in strObj) {
obj.data.id = strObj.id;
obj.data.label = `new_${strObj.id}`;
}
if ('label' in strObj) {
obj.data.label = strObj.label;
}
if ('toActive' in strObj) {
obj.toActive = strObj.toActive === 'true';
}
if ('tagId' in strObj) {
obj.tagId = strObj.tagId;
if (isEdit) {
obj.data.id = strObj.tagId;
}
}
if (data) {
obj.tagId = data.id;
}
} catch {
console.error('JSON数据格式错误');
}
let eventType = isEdit ? TreeEventTypes.editNode : TreeEventTypes.append;
if (isBefore === true || isBefore === false) {
eventType = isBefore ? TreeEventTypes.insertBefore : TreeEventTypes.insertAfter;
}
console.log('TREE_EVENT_BUS + treeName--', eventType, obj.data, obj.tagId, obj.toActive)
eventBus.emit(TREE_EVENT_BUS + treeName, eventType, obj.data, obj.tagId, obj.toActive);
}
9、节点拖拽
开启拖拽,可进行节点拖拽,并且对应事件处理
props
js
:draggable="true"
事件
拖拽完成通过dropEvent可以传递回调,具体返回参数可参考源码所示返回
js
:dropEvent="dropEvent"
/** 节点拖拽完成执行函数 */
function nodeDrop(draggingNode: any, dropNode: any, dropType: TREE_DRAG_TYPE) {
if (props.dropEvent && typeof props.dropEvent === "function") {
props.dropEvent(
instance.exposed,
draggingNode,
dropNode,
dropType,
nodeDropEvent
);
} else {
nodeDropEvent(instance.exposed, draggingNode, dropNode, dropType);
}
}
10、树节点下拉
树节点添加下拉应该是非常常见的需求,如果自己去写,还是挺麻烦的,因此上面树组件内置集成通过配置式方式添加树节点下拉相关配置处理

配置下拉数据
js
//下拉数据
export const ConfigData = {
options: [
{ label: '新增分类', type: 'add' },
{ label: '新增其他类', type: 'other' },
{ label: '弹窗高亮node', type: 'dialog', isDialog: true },
{ label: '置顶', type: 'toTop' },
{ label: '置低', type: 'toBot', class: 'disabled' },
{ label: '删除', type: 'del' },
],
};
控制节点显示逻辑
js
/** 节点操作过滤 */
const optionsFilter: any = (data: TreeNodeBaseInfoBO, node: any, options: TreeItemOptions[]) => {
return options.filter((el) => {
return Number(data.id) % 2 === 0
? !['toTop', 'toBot', 'dialog', 'add'].includes(el.type)
: true;
});
};
props
js
:options="ConfigData.options"//下拉数据
:options-filter="optionsFilter"//控制每个节点显示逻辑
附组件prop
js
treeName?: string // 组件引用名称 用于事件区分
nodeKey?: string // 唯一索引
nodeLabel?: string // 显示字段
showOption?: boolean // 展示操作
closePopOnClick?: boolean // 点击下拉项时关闭pop
class?: string // 树组件样式
eventBus?: any // 事件监听
fetch?: (data?: TreeFetchBaseData, node?: any) => Promise<any[]> // 数据加载 异步请求
lazy?: boolean // 是否懒加载
nodePage?: boolean // 是否分页加载 默认false
nodePageLocal?: boolean // 是否本地分页加载(所在层级是否本地) 默认true
nodePageSize?: number // 分页大小 默认50
options?: TreeItemOptions[] // 下拉项集
optionWidth?: number // 下拉浮框宽度
dynamicOptionWidth?: (node: any, data: TreeNodeBaseInfoBO) => number // 动态下拉框宽度
optionIconWidth?: number // 下拉悬浮图标宽度
optionClass?: string // 下拉框浮框样式类
bagColor?: string // 高亮背景颜色
dropEvent?: any // 拖拽自定义事件
addChild?: any // 新增子点自定义
props?: any // 树组件props
allowDrop?: (draggingNode: any, dropNode: any, type: TREE_DRAG_TYPE) => boolean // 允许放置
allowDrag?: (node: any) => boolean // 允许拖拽
optionsFilter?: (
data: TreeNodeBaseInfoBO,
node: any,
options?: TreeItemOptions[]
) => TreeItemOptions[] // 下拉项过滤
附组件支持的事件
以下事件都通过eventBus.emit()触发,可参考源码查看对应参数示例
js
export enum TreeEventTypes {
refreshOperationPo = 'refreshOperationPo', // 更新右侧操作图标的定位 (会进行3次300mm延迟的定位,可频繁重复调用)
closePop = 'closePop', // 关闭节点下拉
closeDialog = 'closeDialog', // 弹窗关闭
getTree = 'getTree', // 获取树组件
getNodeById = 'getNodeById', // 根据id获取 node
getCurrentKey = 'getCurrentKey', // 获取当前选中节点key
setCurrentKey = 'setCurrentKey', // 设置当前选中节点key 默认会进行定位
navigateCurrentNode = 'navigateCurrentNode', // 定位到当前选中节点
toTagNodeByIdList = 'toTagNodeByIdList', // 根据id集 进行链路懒加载定位 根据id集 进行链路懒加载定位 父 -> 子 最后一个为目标节点
reloadTreeData = 'reloadTreeData', // 重新加载树数据
reBackDragNode = 'reBackDragNode', // 恢复上一次拖拽
append = 'append', // 添加子节点
insertBefore = 'insertBefore', // 指定节点之前插入
insertAfter = 'insertAfter', // 指定节点之后插入
remove = 'remove', // 删除节点
editNode = 'editNode', // 修改节点
setChecked = 'setChecked', // 设置节点选中状态
}
注意事项
- 节点分页加载按钮占用 node 节点
- 未展示出来的节点不可以定位
- 未展示出来的节点无法通过 id 进行获取
- 本地分页时加载按钮会存储本地未展示的 list 数据
- 非本地分页时当加载的节点数据>=nodePageSize 时加载按钮会展示出来
- 新增节点默认排在最后的不建议开启分页功能(节点默认追加在更多操作节点前,需要自行处理其余情况)
- 更多详细的用法见 src/components/tree/bo 下的 event 和 treeBO 文件