当前位置: 首页 > 工具软件 > cnode-vue > 使用案例 >

M2(学习)-07-Vue使用ElementUI中树形控件el-tree实现多级菜单拖拽功能

魏泰
2023-12-01

一、前端拖拽实现-数据库的静态标签信息未更改

增加节点拖拽功能即实现通过拖拽节点从而改变父子关系和层级结构的功能。只需要给el-tree添加属性draggable就可以拖拽节点,但会出现超出设置的三层菜单的层级限制,需要额外添加属性**:allow-drap=“allowDrop”,并在method中定义方法allowDrop(draggingNode,dropNode,type),其中draggingNode表示当前节点,dropNode是目标节点,type有三种情况:‘prev’,'inner’和’next’分别表示放置在目标节点前,目标节点中和目标节点后,allowDrop返回的标记决定是否能够放置。另外额外提取了一个countNodeLevel(node)**方法用来递归调用计算当前节点的子节点最大层数。

具体方法编写如下:

    //判断节点是否可以被拖拽 其中draggingNode表示当前节点,dropNode是目标节点,type有三种情况:
    //'prev','inner'和'next'分别表示放置在目标节点前,目标节点中和目标节点后
    allowDrop(draggingNode, dropNode, type) {
      //判断依据 当前被拖动的节点的深度+目标节点层级不能大于3
      //当前节点的深度(待拖动节点加上子节点有几层) = 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1
      //console.log("allowDrop", draggingNode, dropNode, type);
      //1.计算当前节点的深度 即 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1
      //1.1 求出当前节点的子节点最大层级 值更新在this.maxlevel中
      // draggingNode.data中是Node节点中从后台数据库获取到的静态信息,这里不使用 因为没有更新层级改变此处数据有可能失真
      this.countNodeLevel(draggingNode);
      //console.log("当前节点的子节点最大层级",this.maxLevel)
      //1.2 计算深度
      let deep = this.maxLevel - draggingNode.level + 1;
      //console.log("当前拖拽节点深度",deep)
      //2 判断是否可以拖动 拖动到目标节点内或者前后两种情况
      if (type == "inner") {
        //拖动到目标节点内 只需要当前节点深度+目标节点层级<=3即可
        let isDrag = deep + dropNode.level <= 3;
        console.log(`拖拽类型${type}: 当前节点的子节点最大层级:${this.maxLevel}--当前节点层级:${draggingNode.level}
        --当前节点深度:${deep}--目标节点层级:${dropNode.level}--是否允许拖拽:${isDrag}`);
        //判断完毕给data中的maxLevel赋初值
        this.maxLevel = 0;
        //this.updateNodes = [];
        return isDrag;
      } else {
        //拖动到目标节点前或后 只需要判断当前节点深度+目标节点父节点层级<=3即可
        let isDrag = deep + dropNode.parent.level <= 3;
        console.log(`拖拽类型${type}: 当前节点的子节点最大层级:${this.maxLevel}--当前节点层级:${draggingNode.level}
        --当前节点深度:${deep}--目标节点父节点层级:${dropNode.parent.level}--是否允许拖拽:${isDrag}`);
        //判断完毕给data中的maxLevels赋初值
        this.maxLevel = 0;
        //this.updateNodes = [];
        return isDrag;
      }
    },

    //计算当前节点的子节点最大层数
    countNodeLevel(node) {
      //console.log("当前节点信息",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]);
        }
      } else {
        //没有子节点 将maxLevel设置为当前节点层级 为了正确计算当前节点深度
        //console.log("无子节点的maxlevel设置",node.level)
        this.maxLevel = node.level;
      }
    },

这个功能需要注意的是整个实现思想

①允许拖拽后放置的依据:当前被拖动的节点的深度+目标节点层级不能大于设置的菜单最大层级3

当前节点的深度通俗来说就是当前节点和子节点以及子节点的子节点全部加起来总共有几层菜单

即当前节点的深度(待拖动节点加上子节点有几层) = 当前节点的子节点的最大层级maxLevel - 当前节点所处层级catlevel + 1

②计算当前节点的子节点最大层级的方法countNodeLevel(node)使用了递归

二、后端拖拽实现-更改数据库中标签信息

以上方法实现后,在拖拽菜单后el-tree会自动计算出拖拽后应处于的层级level和子节点childNodes,但从数据库中获取到的静态数据data里的节点信息还没更新,先通过拖拽菜单成功后触发的事件函handleDrop(draggingNode, dropNode, dropType, ev)来处理拖拽后的新节点数据并将拖拽后的新节点数据封装为节点对象数组updateNodes: []传入后端从而更新数据库中的节点数据。

注意更新思想:当前拖拽节点最新的父节点id|当前拖拽节点最新的顺序 - 遍历兄弟节点数组|当前拖拽节点最新的层级

//拖拽菜单成功后触发的事件函数
    //draggingNode当前正拖拽的节点 dropNode目标节点|参考节点
    //dropType拖拽到参考节点的哪个位置 ev事件对象
    handleDrop(draggingNode, dropNode, dropType, ev) {
      //console.log("tree drop: ", draggingNode, dropNode, dropType);
      //1.当前拖拽节点最新的父节点id 根据方式判断
      let pCid = 0;
      let siblings = null;
      if (dropType == "before" || dropType == "after") {
        //父id应该是兄弟节点|目标节点的父id
        //pCid = dropNode.parent.data.catId;
        //这里避免一个小bug 如果移动到第一个一级菜单之前 由于之前一级菜单的父节点没有数据
        //所以移动后pCid会变成undefined 这里加个三元判断
        pCid =
          dropNode.parent.data.catId == undefined
            ? 0
            : dropNode.parent.data.catId;

        //当前拖拽节点的兄弟节点就是目标节点的父节点的子节点 - 注意childNodes是拖拽后自动改变后的新值
        //不同于data中后台获取到的children静态值
        siblings = dropNode.parent.childNodes;
      } else {
        //inner
        //父Id就是目标节点的id
        pCid = dropNode.data.catId;
        //当前拖拽节点的兄弟节点就是目标节点的子节点
        siblings = dropNode.childNodes;
      }
      //给全局pCid赋值
      this.pCid.push(pCid);

      //2.当前拖拽节点最新的顺序 - 遍历兄弟节点数组
      //3.当前拖拽节点最新的层级
      for (let i = 0; i < siblings.length; i++) {
        //遍历到当前拖拽节点
        if (siblings[i].data.catId == draggingNode.data.catId) {
          //将节点信息push到updateNodes中 除了排序改变还要将父id 以及层级(视情况而定)
          //判断层级是否发生变化 这里判断使用的siblings[i].level是会随拖拽后自动变化的 - 也就是目标值|正确值
          //而draggingNode.data.catLevel是数据库中存的静态数据 如果二者不相等则需要封装
          let catLevel = draggingNode.data.catLevel;
          if (siblings[i].level != catLevel) {
            //当前拖拽节点层级改变
            catLevel = siblings[i].level;
            //当前节点子节点层级改变 将当前遍历到的拖拽节点传入参数 其childNodes是子节点 抽成一个方法
            this.updateChildrenNodeLevel(siblings[i]);
          }
          this.updateNodes.push({
            catId: siblings[i].data.catId,
            sort: i,
            parentCid: pCid,
            catLevel: catLevel,
          });
        } else {
          //遍历到其它节点
          this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
        }
      }

      //打印最新整理好的updateNodes
      console.log("updateNodes:", this.updateNodes);
    },

    //拖拽后层级改变,当前拖拽节点的子节点层级改变
    updateChildrenNodeLevel(node) {
      //遍历
      for (let i = 0; i < node.childNodes.length; i++) {
        //let cNode = node.childNodes[i].data;//遍历到当前子节点存储的后端节点数据
        //cNode.catId = cNode.catId;//待更新的id
        //cNode.catLevel = node.childNodes[i].level//待更新的后端catLevel层级
        //console.log("待更新的子节点id",node.childNodes[i].data.catId)
        //console.log("待更新的子节点后端catLevel层级",node.childNodes[i].level)
        this.updateNodes.push({
          catId: node.childNodes[i].data.catId,
          catLevel: node.childNodes[i].level,
        });
        //递归调用
        this.updateChildrenNodeLevel(node.childNodes[i]);
      }
    },

为了多次拖拽后统和数据改动一次传入后端从而减少与数据库交互次数,定义一个保存批量拖拽的按钮点击后向后端发送数据更新请求,另外也定义一个清除节点更新数组的重置按钮

//批量拖拽后向后台提交最新节点信息
    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();

        //设置默认展开的菜单s
        this.expandedKey = this.pCid;
        //置为初值
        this.updateNodes = [];
        //this.pCid = [];
      });
    },

    //取消批量拖拽
    cancelBatchDrag() {
      //刷新菜单
      this.getMenus();

      //设置默认展开的菜单s
      this.expandedKey = this.pCid;
      //置为初值
      this.updateNodes = [];
      this.pCid = [];
    },
  },

也别忘了给后端增加批量更新节点的方法com.atguigu.gulimall.product.controller.CategoryController#updateSort

/**
 * 自定义批量修改方法 用于拖拽时的更新需求
 * category是前端收集到的待更新节点数组由SpringMVC自动映射为List<CategoryEntity>
 */
@RequestMapping("/update/sort")
//@RequiresPermissions("product:category:update")
public R updateSort(@RequestBody List<CategoryEntity> category){
    categoryService.updateBatchById(category);
    return R.ok();
}

其中template中加入组件如下

<div>
    <el-switch
      v-model="draggable"
      active-color="#13ce66"
      inactive-color="#ff4949"
      active-text="启动菜单拖拽"
      inactive-text="关闭菜单拖拽"
    ></el-switch>

    <el-button
      type="primary"
      size="mini"
      round
      @click="batchSave"
      v-if="draggable"
      >保存批量拖拽</el-button
    >
    <el-button
      type="primary"
      size="mini"
      round
      @click="cancelBatchDrag"
      v-if="draggable"
      >取消批量拖拽</el-button
    >

    <el-tree
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
      :draggable="draggable"
      :allow-drop="allowDrop"
      @node-drop="handleDrop"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="() => append(data)"
          >
            Append
          </el-button>
          <el-button type="text" size="mini" @click="() => edit(data)">
            Edit
          </el-button>
          <el-button
            v-if="node.childNodes.length == 0"
            type="text"
            size="mini"
            @click="() => remove(node, data)"
          >
            Delete
          </el-button>
        </span>
      </span>
    </el-tree>

    <el-dialog
      :title="dialogType"
      :visible.sync="dialogVisible"
      width="30%"
      :close-on-click-modal="false"
    >
      <el-form :model="category">
        <el-form-item label="分类名称">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="图标">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="计量单位">
          <el-input
            v-model="category.productUnit"
            autocomplete="off"
          ></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitData">确 定</el-button>
      </div>
    </el-dialog>
  </div>

定义的全局data如下:

data() {
    return {
      menus: [],
      //expandedKey 菜单默认展开的结构状态 传入父节点id
      expandedKey: [],
      //dialogVisible控制对话框/模态框是否展示 默认不展示
      dialogVisible: false,
      //修改新增复用对话框的依据 edit|append
      dialogType: "",
      //菜单拖拽功能判断当前节点的子节点最大深度
      maxLevel: 0,
      //菜单拖拽后封装新的节点信息
      updateNodes: [],
      //菜单拖拽开启标记 默认不开启
      draggable: false,
      //pCid用于批量拖拽后向后台传递最新节点信息后保持之前结构用 由于可能需要展开多个菜单所以用数组接收
      pCid: [],
      //对话框内表单绑定的数据对象 其中菜单ID-catId是对话框修改新增复用的依据
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        showStatus: 1,
        sort: 0,
        icon: "",
        productUnit: "",
        catId: null,
      },
      defaultProps: {
        //label:哪个属性是作为标签的值需要展示出来,children:哪个属性需要作为标签的子树
        children: "children",
        label: "name",
      },
    };
  },
 类似资料: