深度封装tree公共组件

概述

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

实现效果

演示

代码仓库

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', // 设置节点选中状态
}

注意事项

  1. 节点分页加载按钮占用 node 节点
  2. 未展示出来的节点不可以定位
  3. 未展示出来的节点无法通过 id 进行获取
  4. 本地分页时加载按钮会存储本地未展示的 list 数据
  5. 非本地分页时当加载的节点数据>=nodePageSize 时加载按钮会展示出来
  6. 新增节点默认排在最后的不建议开启分页功能(节点默认追加在更多操作节点前,需要自行处理其余情况)
  • 更多详细的用法见 src/components/tree/bo 下的 event 和 treeBO 文件
相关推荐
mon_star°5 小时前
《浪浪山小妖怪》知识竞赛来袭!测测你是几级影迷?
前端·css·html
Jolyne_5 小时前
H5的Form表单项不够灵活怎么办?来看看这篇通用组件封装思路分享
前端
Ares-Wang5 小时前
JavaScript》》JS》》ES6》》 foreach 、for in 、for of
前端·javascript·es6
coding随想5 小时前
浏览器如何检测用户环境光变化:揭秘Ambient Light Events(环境光事件)
前端
ZSQA5 小时前
Hbuilder X cli项目使用本地的node执行编译。
前端
龙在天5 小时前
介绍一个🔥火热的React 应用状态管理库
前端
字节逆旅5 小时前
CodeBuddy+Figma+MCP,我指挥AI写代码,老板夸我鱼摸得好
前端·人工智能·mcp
满分观察网友z5 小时前
JavaScript 趣味编程:从基础循环到函数式,解锁打印三角形的 N 种姿势
前端
Null1555 小时前
前端ZIP处理:JSZip vs fflate 全方位对比,让你的文件操作效率翻倍!
前端