# Element-ui Tree 节点可拖拽更新节点数据

# 业务介绍:

如下图,树形菜单是可以进行拖拽的,我需要让其拖拽到指定位置的时候更新当前节点的 parent_cid (父节点) 和 cat_level (层级) 还有 sort (排序)

PixPin_2024-05-21_10-57-13

要做这个功能我们首先需要监听拖拽成功这个事件,当我们拖拽成功以后我们就可以将所有收集到的数据拿来发给数据库,这样的事件我们可以参考 Element-ui 的文档 https://element.eleme.cn/#/zh-CN/component/tree

在文档中的 Events 中有一个 node-drop 属性如下介绍:

说明回调参数
拖拽成功完成时触发的事件共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event

添加到代码中:

image-20240521110839368

在 method 函数中添加回调函数,具体文档中有如下:

image-20240521111052747

函数中有三个参数其中 draggingNode 是当前 拖拽的节点,dropNode 是被拖拽到的节点,dropType 进入到该节点的什么位置比如前后左右 (英文表示的)

PixPin_2024-05-21_11-13-25

此时第一个 Node 就是我们当前正在拖拽的节点 也就是 手机通讯

image-20240521111507576

此时第二个 Node 就是进入到哪个节点 最后面是位置 (英文的) after 意思也就是 港台图书 之后

image-20240521111630428

# 代码实现

通过如上的信息我们需要获取如下数据:

  1. 当前节点最新的父节点 id
  2. 当前拖拽节点的最新顺序
  3. 当前拖拽节点的最新层级

第一步:

handleDrop(draggingNode, dropNode, dropType, ev) {
      // 1、当前节点最新的父节点 id 的变量
      let pCid = 0;
      // 记录拖拽到的节点的节点集合 (为第二步做准备)
      let siblings = null;
      // 1.2、判断如果是上下关系就记录当前拖拽到的节点的父节点的 catId 和 childNodes 集合
      if (dropType == "before" || dropType == "after") {
      // 使用逻辑中断防止拖拽到的节点的父节点 catId 为 unidefed
        pCid = dropNode.parent.data.catId || 0;
        siblings = dropNode.parent.childNodes;
      // 如果是左右关系就记录当前拖拽到的节点的 catId 和 childNodes
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
		}
}

第二步:

handleDrop(draggingNode, dropNode, dropType, ev) {
//=========================== 第一步 ===========================
     // 1、当前节点最新的父节点 id 的变量
      let pCid = 0;
      // 记录拖拽到的节点的节点集合 (为第二步做准备)
      let siblings = null;
      // 1.2、判断如果是上下关系就记录当前拖拽到的节点的父节点的 catId 和 childNodes 集合
      if (dropType == "before" || dropType == "after") {
      // 使用逻辑中断防止拖拽到的节点的父节点 catId 为 unidefed
        pCid = dropNode.parent.data.catId || 0;
        siblings = dropNode.parent.childNodes;
      // 如果是左右关系就记录当前拖拽到的节点的 catId 和 childNodes
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
		}
//=============================== 第二步 ===============================
      // 2、当前拖拽节点的最新顺序
      for (let i = 0; i < siblings.length; i++) {
        // 2.1、判断遍历的是否为当前拖拽的节点
        if (siblings[i].data.catId == draggingNode.data.catId) {
          // 2.2、当前拖拽的节点需要改顺序和它的父节点的 id
          this.updateNodes.push({
             // 表示正在处理哪条数据 catId 是唯一性的
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          // 而其它节点需要更新顺序
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }
}

其中 updateNodes 是保存所有需要修改的节点

在 data () 中添加

image-20240521151613419

第三步:

创建修改当前节点的子节点以及子子节点层级方法

// 修改当前节点的子节点层级方法
updateChildNodeLevel(node) {
   if (node.childNodes.length > 0) {
      // 遍历当前节点的子节点
      for (let i = 0; i < node.childNodes.length; i++) {
         // 遍历子节点的数据
         let cNode = node.childNodes[i].data;
         // 更新子节点的层级
         this.updateNodes.push({
            catId: cNode.catId,
            catLevel: node.childNodes[i].level,
         });
         // 深入调用更新子子节点的层级
         this.updateChildNodeLevel(node.childNodes[i]);
      }
   }
},

继续回到第二步的循环中的判断遍历节点是否是当前拖拽节点的 if 里面进行 判断如果层级与当前节点的层级不同就调用函数进行修改

handleDrop(draggingNode, dropNode, dropType, ev) {
//=========================== 第一步 ===========================
     // 1、当前节点最新的父节点 id 的变量
      let pCid = 0;
      // 记录拖拽到的节点的节点集合 (为第二步做准备)
      let siblings = null;
      // 1.2、判断如果是上下关系就记录当前拖拽到的节点的父节点的 catId 和 childNodes 集合
      if (dropType == "before" || dropType == "after") {
      // 使用逻辑中断防止拖拽到的节点的父节点 catId 为 unidefed
        pCid = dropNode.parent.data.catId || 0;
        siblings = dropNode.parent.childNodes;
      // 如果是左右关系就记录当前拖拽到的节点的 catId 和 childNodes
      } else {
        pCid = dropNode.data.catId;
        siblings = dropNode.childNodes;
		}
//=============================== 第二步 ===============================
      // 2、当前拖拽节点的最新顺序
      for (let i = 0; i < siblings.length; i++) {
        // 2.1、判断遍历的是否为当前拖拽的节点
        if (siblings[i].data.catId == draggingNode.data.catId) {
//=============================== 第三步 ===============================
           // 定义当前节点的默认层级变量
			  let catLevel = draggingNode.level;
			  // 2.3、如果遍历的是当前节点而且层级发生了变化
			  // 2.4、 判断如果当前层级和默认层级不一样,说明当前层级发生了变化
			  if (siblings[i].level != draggingNode.level) {
			     // 更新最新层级
			     catLevel = siblings[i].level;
			     // 如果当前节点的层级发生变化了,那么子节点的层级肯定会发生变化
			     // 修改子节点的层级以及子节点的子节点层级 (递归)
			     this.updateChildNodeLevel(siblings[i]);
			  }
//=================================================================
          // 2.5、当前拖拽的节点需要改顺序和它的父节点的 id
          this.updateNodes.push({
             // 表示正在处理哪条数据 catId 是唯一性的
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          // 而其它节点只需要更新顺序
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }
}

最后我们需要对接接口来更新数据库中的数据来持久化

后端需要提供一个批量修改的接口

/**
 * 批量修改排序
 * @param category
 * @return
 */
@RequestMapping("/update/sort")
// @RequiresPermissions("product:category:update")
public R updateSort(@RequestBody CategoryEntity[] category){
   categoryService.updateBatchById(Arrays.asList(category));
   return R.ok();
}

前端调用接口

handleDrop(draggingNode, dropNode, dropType, ev) {
   // 1、当前节点最新的父节点 id 的变量
   let pCid = 0;
   // 记录拖拽到的节点的节点集合 (为第二步做准备)
   let siblings = null;
   // 1.2、判断如果是上下关系就记录当前拖拽到的节点的父节点的 catId 和 childNodes 集合
   if (dropType == "before" || dropType == "after") {
      // 使用逻辑中断防止拖拽到的节点的父节点 catId 为 unidefed
      pCid = dropNode.parent.data.catId || 0;
      siblings = dropNode.parent.childNodes;
      // 如果是左右关系就记录当前拖拽到的节点的 catId 和 childNodes
   } else {
      pCid = dropNode.data.catId;
      siblings = dropNode.childNodes;
   }
   // 2、当前拖拽节点的最新顺序
   for (let i = 0; i < siblings.length; i++) {
      // 2.1、判断遍历的是否为当前拖拽的节点
      if (siblings[i].data.catId == draggingNode.data.catId) {
         // 定义当前节点的默认层级变量
         let catLevel = draggingNode.level;
         // 2.3、如果遍历的是当前节点而且层级发生了变化
         // 2.4、 判断如果当前层级和默认层级不一样,说明当前层级发生了变化
         if (siblings[i].level != draggingNode.level) {
            // 更新最新层级
            catLevel = siblings[i].level;
            // 如果当前节点的层级发生变化了,那么子节点的层级肯定会发生变化
            // 修改子节点的层级以及子节点的子节点层级 (递归)
            this.updateChildNodeLevel(siblings[i]);
         }
         // 2.5、当前拖拽的节点需要改顺序和它的父节点的 id
         this.updateNodes.push({
            // 表示正在处理哪条数据 catId 是唯一性的
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
         });
      } else {
         // 而其它节点只需要更新顺序
         this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
      }
   }
//=========================== 调用批量修改接口 ===========================
   this.$http({
      url: this.$http.adornUrl("/product/category/update/sort"),
      method: "post",
      data: this.$http.adornData(this.updateNodes, false),
   }).then(({ data }) => {
      this.$message({
         message: "菜单顺序修改成功",
         type: "success",
      });
      // 刷新菜单数据
      this.getMenus();
      // 设置默认展开的菜单
      this.expandedKey = [pCid];
      this.updateNodes = [];
      this.maxLevel = 0;
   });
},

为防止误操作拖拽节点,我们可以加上一个开关进行控制

在 element-ui 文档中找到 switch 开关的模块复制源码

image-20240522090847878

<el-switch v-model="value1" active-text="按月付费" inactive-text="按年付费"></el-switch>

定义一个与开关关联的属性在 Tree 拖拽节点的代码中

image-20240522091112687

这个 draggable 是 true 则能拖拽,是 false 则不能拖拽,在 data () 中定义 draggable 的默认值

下面是效果展示:

PixPin_2024-05-22_09-13-29

# 批量保存拖拽的数据

在做这个功能时我们需要改动一些代码:

首先前端添加一个批量保存的按钮

使用 v-if 来判断如果开启了拖拽功能才显示批量保存的按钮否则不显示

image-20240522093124769

更改代码如下:

将批量更新数据库数据的操作放到点击按钮后触发的函数 batchSave 里

batchSave() {
   this.$http({
      url: this.$http.adornUrl("/product/category/update/sort"),
      method: "post",
      data: this.$http.adornData(this.updateNodes, false),
   }).then(({ data }) => {
      this.$message({
         message: "菜单顺序修改成功",
         type: "success",
      });
      // 刷新菜单数据
      this.getMenus();
      // 设置默认展开的菜单
      this.expandedKey = this.pCid;
      this.updateNodes = [];
      this.maxLevel = 0;
   });
},

我们发现批量保存函数中需要 pCid 而又没有提供,所以我们需要将 pCid 在 data () 中写一个并给默认值为 [] 数组 然后再 拖拽函数中 给 this.pCid 赋值

image-20240522093814787

handleDrop(draggingNode, dropNode, dropType, ev) {
   // 1、当前节点最新的父节点 id 的变量
   let pCid = 0;
   // 记录拖拽到的节点的节点集合 (为第二步做准备)
   let siblings = null;
   // 1.2、判断如果是上下关系就记录当前拖拽到的节点的父节点的 catId 和 childNodes 集合
   if (dropType == "before" || dropType == "after") {
      // 使用逻辑中断防止拖拽到的节点的父节点 catId 为 unidefed
      pCid = dropNode.parent.data.catId || 0;
      siblings = dropNode.parent.childNodes;
      // 如果是左右关系就记录当前拖拽到的节点的 catId 和 childNodes
   } else {
      pCid = dropNode.data.catId;
      siblings = dropNode.childNodes;
   }
   this.pCid.push(pCid);
   // 2、当前拖拽节点的最新顺序
   for (let i = 0; i < siblings.length; i++) {
      // 2.1、判断遍历的是否为当前拖拽的节点
      if (siblings[i].data.catId == draggingNode.data.catId) {
         // 定义当前节点的默认层级变量
         let catLevel = draggingNode.level;
         // 2.3、如果遍历的是当前节点而且层级发生了变化
         // 2.4、 判断如果当前层级和默认层级不一样,说明当前层级发生了变化
         if (siblings[i].level != draggingNode.level) {
            // 更新最新层级
            catLevel = siblings[i].level;
            // 如果当前节点的层级发生变化了,那么子节点的层级肯定会发生变化
            // 修改子节点的层级以及子节点的子节点层级 (递归)
            this.updateChildNodeLevel(siblings[i]);
         }
         // 2.5、当前拖拽的节点需要改顺序和它的父节点的 id
         this.updateNodes.push({
            // 表示正在处理哪条数据 catId 是唯一性的
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
         });
      } else {
         // 而其它节点只需要更新顺序
         this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
      }
   }
},

我们再进行批量保存的时候就会报错需要修改如下代码:

allowDrop(draggingNode, dropNode, type) {
   // 被拖动的当前节点,以及所在的父节点总层数不能大于 3
   // 被拖动的当前节点总层数
   console.log("allowDrop: ", draggingNode, dropNode, type);
   // 计算当前节点的总层数
   this.countNodeLevel(draggingNode);
   // 当前正在拖动的节点 + 父节点所在的深度不大于 3 即可
   // 最大深度 - 当前深度 + 1 = 当前节点的深度
   let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
   console.log("深度:", deep);
   if (type == "inner") {
      return deep + dropNode.level <= 3;
   } else {
      return deep + dropNode.parent.level <= 3;
   }
},
   countNodeLevel(node) {
      // 找到所有子节点,求出最大深度
      if (node.childNodes != null && node.childNodes.length > 0) {
         for (let i = 0; i < node.childNodes.length; i++) {
            if (node.childNodes[i].level > this.maxLevel) {
               this.maxLevel = node.childNodes[i].level;
            }
            this.countNodeLevel(node.childNodes[i]);
         }
      }
   },

PixPin_2024-05-22_09-40-50

# 批量删除数据

提供批量删除的接口

逻辑删除什么字段不重要主要看自己

/**
 * 删除
 */
@PostMapping("/delete")
// @RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds){
   categoryService.removeMenuByIds(Arrays.asList(catIds));
   return R.ok();
}

前端代码:

添加批量删除按钮

<el-button type="danger" @click="batchDel">批量删除</el-button>

逻辑代码:

batchDel() {
   let catIds = [];
   let name = [];
   let checkedNodes = this.$refs.menuTree.getCheckedNodes();
   console.log("被选中的元素: ", checkedNodes);
   for (let i = 0; i < checkedNodes.length; i++) {
      catIds.push(checkedNodes[i].catId);
      name.push(checkedNodes[i].name);
   }
   this.$confirm(
      `此操作将永久批量删除[ ${name} ]菜单, 是否继续?`,
      "提示",
      {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
         type: "warning",
      }
   )
      .then(({ data }) => {
      this.$http({
         url: this.$http.adornUrl("/product/category/delete"),
         method: "post",
         data: this.$http.adornData(catIds, false),
      }).then(({ data }) => {
         this.$message({
            message: "菜单删除成功",
            type: "success",
         });
         this.getMenus();
      });
   })
      .catch(({ data }) => {});
},