【vue】el-tree的新增/编辑/删除节点

1、概述

关于树形结构的新增同级节点新增子级节点修改节点名称删除节点等四种操作,各种参数配置完全继承el-tree,本篇使用vue2 + element-ui

2、效果图展示

3、调用方式

javascript 复制代码
<template>
    <Tree
        :data="treeData"
        :props="defaultProps"
        :default-expanded-keys="expandedKeys"
        node-key="id"
        highlight-current
        :highligh-color="highlighColor"
        :show-btn-group="showBtnGroup"
        :draggable="true"
        @node-click="nodeClick"
        @editNodeSubmit="addNode"
        @deleteNodeSubmit="deleteNode"
        @node-drag-end="handleDragEnd"
      />
</template>      

<script>
export default {
    name: 'Tree',
    data() {
        return {
          defaultProps: {
            children: 'children',
            label: 'label'
          },
          expandedKeys: [], // 展开节点
          highlighColor: {
            color: '#FFAE0D',
            bgColor: 'rgba(255, 174, 13, .1)'
          },
          showBtnGroup: true,
          treeData: []
        }
    }
}
</script>

4、源码解析

javascript 复制代码
<template>
  <el-tree
    ref="tree"
    :class="[
      'menu-el-tree',
      iconStyle.src && 'menu-expand-icon',
      highlighColor && 'menu-node-highligh'
    ]"
    :style="{
      '--bgUrl': 'url(' + iconStyle.src + ')',
      '--iconWidth': iconStyle.width,
      '--iconHeight': iconStyle.height,
      '--iconTransform': iconStyle.transform,
      '--hoverColor': hoverStyle.color,
      '--hoverBgColor': hoverStyle.bgColor,
      '--highlighColor': highlighColor.color,
      '--highlighBgColor': highlighColor.bgColor
    }"
    node-key="id"
    :draggable="draggable"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template v-slot="{ node, data }">
      <div v-if="!data.isEdit" class="custom-tree-node-root" @mouseenter="nodeMouseEnter(node)" @mouseleave="nodeMouseLeve(node)">
        <span class="custom-tree-node">{{ node.label }}</span>
        <section v-show="node.showBtn" class="tree-btn-group">
          <el-tooltip
            v-for="item in btnGroup"
            :key="item"
            effect="light"
            placement="top"
            popper-class="common-tooltip-primary"
            :content="{
              'addChild': '新增子级',
              'delete': '删除',
              'edit': '修改',
              'addSibling': '新增同级'
            }[item]"
          >
            <i
              :class="{
                'addChild': 'el-icon-circle-plus-outline',
                'delete': 'el-icon-circle-close',
                'edit': 'el-icon-edit',
                'addSibling': 'el-icon-plus'
              }[item]"
              @click.stop="() => handleOperaion(item, node, data)"
            />
          </el-tooltip>
        </section>
      </div>
      <div v-else class="custom-tree-node-root-edit">
        <el-input v-model="inputValue" :size="editInputStyle.size" :style="{ width: editInputStyle.width }" />
        <section :id="data.id">
          <el-tooltip
            content="确定"
            popper-class="common-tooltip-primary"
            effect="light"
            placement="top"
          >
            <i class="el-icon-success" @click.stop="() => editMenuSubmit(node, data)" />
          </el-tooltip>
          <el-tooltip
            content="取消"
            popper-class="common-tooltip-primary"
            effect="light"
            placement="top"
          >
            <i class="el-icon-error" @click.stop="() => addSiblingCancel(node, data)" />
          </el-tooltip>
        </section>
      </div>
    </template>
  </el-tree>
</template>
<script>
export default {
  name: 'MenuTree',
  inheritAttrs: true,
  props: {
    hoverStyle: {
      type: Object,
      default: () => ({
        color: '#606266',
        bgColor: '#F5F7FA'
      })
    },
    /**
     * @description 当前行选中高亮颜色配置
     */
    highlighColor: {
      type: Object,
      default: () => ({
        color: '#606266',
        bgColor: '#fff9ec'
      })
    },
    /**
     * @description 动态左侧图标配置(图片格式)
     */
    iconStyle: {
      type: Object,
      default: () => ({
        src: '',
        width: '20px',
        height: '17px',
        transform: 'rotate(270deg)'
      })
    },
    /**
     * @description 操作按钮组
     * addChild => 新增子级,delete => 删除, edit => 修改, addSibling => 新增同级
     */
    btnGroup: {
      type: Array,
      default: () => ['edit', 'addSibling', 'addChild', 'delete']
    },
    /**
     * @description 是否展示操作按钮
     */
    showBtnGroup: {
      type: Boolean,
      default: false
    },
    /**
     * @description 编辑模式输入框样式
     */
    editInputStyle: {
      type: Object,
      default: () => ({
        size: 'mini',
        width: '200px'
      })
    },
    /**
     * 是否可拖拽
     */
    draggable: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      inputValue: ''
    }
  },
  methods: {
    /**
     * @description 鼠标移入目录层级
     */
    nodeMouseEnter(data) {
      if (!this.showBtnGroup) return;
      this.$set(data, 'showBtn', true)
    },
    /**
     * @description 鼠标移除目录层级
     */
    nodeMouseLeve(data) {
      if (!this.showBtnGroup) return;
      this.$set(data, 'showBtn', false)
    },
    /**
     * 当前目录操作分发
     * @param {string} eventKey 事件名称
     * @param {object} currentNode 当前Node节点内容
     * @param {object} data 当前节点数据
     */
    handleOperaion(eventKey, currentNode, data) {
      console.log(currentNode, data, 'menuRoot');
      switch (eventKey) {
        case 'addSibling':
          this.addSibling(currentNode, data);
          break;
        case 'edit':
          this.editTreeItem(data);
          break;
        case 'addChild':
          this.addChild(currentNode, data);
          break;
        case 'delete':
          this.deleteTreeItem(currentNode, data);
          break;
      }
    },
    /**
     * @description 编辑当前节点
     */
    editTreeItem(data) {
      this.$set(data, 'isEdit', true);
      this.inputValue = data.label; // 当前正在编辑内容赋值
      this.$nextTick(() => {
        document.getElementById(data.id).previousElementSibling.firstElementChild.focus();
      })
    },
    /**
     * @description 添加同级节点
     */
    addSibling(currentNode, data) {
      const treeDOM = this.$refs.tree;
      const id = Math.ceil(Math.random() * 100)
      const newData = { id: id, pId: data.pId, label: '', isEdit: true, isNew: true, children: [] };
      treeDOM.insertAfter(newData, currentNode);
      // 聚焦当前新增目录
      this.$nextTick(() => {
        document.getElementById(newData.id).previousElementSibling.firstElementChild.focus();
      })
    },
    /**
     * @description 添加子级节点
     */
    addChild(currentNode, data) {
      const treeDOM = this.$refs.tree;
      const id = Math.ceil(Math.random() * 100)
      const newData = { id: id, pId: data.id, label: '', isEdit: true, isNew: true, children: [] };
      treeDOM.append(newData, currentNode);
      // 展开子节点后才能获取DOM聚焦
      treeDOM.store.nodesMap[data.id].expanded = true;
      setTimeout(() => {
        document.getElementById(newData.id).previousElementSibling.firstElementChild.focus();
      }, 500)
    },
    /**
     * @description 编辑模式修改确认
     */
    editMenuSubmit(node, data) {
      this.$emit('editNodeSubmit', {
        node,
        data,
        currentLabel: this.inputValue,
        // 新增同级/子级节点接口调用成功的话,即新增同级/子级节点
        callback: (status) => {
          if (status) {
            this.resetNode()
          }
        }
      });
    },
    /**
     * @description 删除当前节点
     */
    deleteTreeItem(node, data) {
      this.$emit('deleteNodeSubmit', {
        node,
        data,
        callback: (status) => {
          // 删除接口调用成功的话,即删除节点
          if (status) {
            const treeDOM = this.$refs.tree;
            treeDOM.remove(node);
          }
        }
      });
    },
    /**
     * @description 取消同级节点添加
     */
    addSiblingCancel(node, data) {
      // 如果是新增的节点,取消即是删除
      if (data.isNew) {
        const treeDOM = this.$refs.tree;
        treeDOM.remove(node);
      } else {
        // 重置修改内容
        this.inputValue = '';
        data.isEdit = false;
      }
    },
    /**
     * 寻找第一个叶子节点及叶子节点的父节点
     * @param {*} tree 平铺数组
     */
    findFirstChildAndParent(tree) {
      let firstChild = null;
      let parentOfFirstChild = null;
      const dfs = (node, parent) => {
        if (firstChild !== null) {
          return; // 如果已经找到了第一个子节点,则不再继续搜索
        }
        if (node.children && node.children.length > 0) {
          // eslint-disable-next-line
          for (const child of node.children) {
            dfs(child, node);
          }
        } else {
          firstChild = node;
          parentOfFirstChild = parent;
        }
      }
      // eslint-disable-next-line
      for (const node of tree) {
        dfs(node, null);
      }
      return {
        firstChild,
        parentOfFirstChild
      };
    },
    /**
     * 获取自身树结构实例
     */
    getTree() {
      return this.$refs.tree;
    },
    /**
     * 重置节点数据
     */
    resetNode() {
      this.inputValue = '';
    },
    /**
     * 寻找节点对应的父级节点
     * @param {*} tree
     * @param {*} nodeId
     */
    findParentByChildId(tree, nodeId) {
      let parentOfFirstChild = null;
      const dfs = (node, parent) => {
        if (parentOfFirstChild !== null) {
          return;
        }
        if (node.children && node.children.length > 0) {
          // eslint-disable-next-line
          for (const child of node.children) {
            dfs(child, node);
          }
        } else {
          // 找到对应节点后,返回其父节点
          if (node.id === nodeId) {
            parentOfFirstChild = parent;
          }
        }
      }
      // eslint-disable-next-line
      for (const node of tree) {
        dfs(node, null);
      }
      return parentOfFirstChild
    }
  }
}
</script>
<style scoped lang="scss">
// 动态配置右侧图标
.menu-expand-icon {
  ::v-deep .el-tree-node__expand-icon:not(.is-leaf) {
    &::before {
      background: var(--bgUrl);
      background-size: contain;
      background-repeat: no-repeat;
      background-position: center;
      content: '';
      width: var(--iconWidth);
      height: var(--iconHeight);
      display: inline-block;
      transform: var(--iconTransform);
    }
  }
}
// 动态配置hover样式
.menu-el-tree {
  ::v-deep .el-tree-node__content {
    &:hover {
      background: var(--hoverBgColor);
      color: var(--hoverColor);
    }
  }
  .custom-tree-node-root {
    display: flex;
    align-items: center;
    flex: 1;
    .tree-btn-group {
      margin-left: 15px;
      display: flex;
      align-items: center;
      column-gap: 8px;
      i {
        font-size: 15px;
        &:hover {
          color: $primary
        }
      }
    }
  }
}
.menu-node-highligh.el-tree--highlight-current {
  ::v-deep .el-tree-node.is-current>.el-tree-node__content {
    background: var(--highlighBgColor);
    color: var(--highlighColor);
  }
}
.custom-tree-node-root-edit {
  display: flex;
  align-items: center;
  .el-input, ::v-deep .el-input .el-input__inner {
    height: 26px;
  }
  section {
    i {
      margin-left: 10px;
      font-size: 15px;
      color: $primary;
    }
  }
}
</style>

5、疑难解答

1、拖拽节点时,如何监听和区分拖拽事件?

使用的是el-tree内置的node-drag-end事件,当拖拽完成时触发,抛出四个参数

* @param {*} draggingNode 被拖拽的节点

* @param {*} dropNode 最后进入的节点

* @param {*} dropType 相对比的位置

* @param {*} ev 事件

2、新增和删除节点,如何再触发后端接口后再执行节点的新增和删除?

节点新增和删除执行下列两个事件

@editNodeSubmit="addNode"

@deleteNodeSubmit="deleteNode"

他们回调的最后一个参数都是callback,当callback(true)传入为true时,则代表接口成功,则进行后续的树节点操作。

------有不懂的或建议欢迎直接评论噢~------

相关推荐
ObjectX前端实验室20 分钟前
交互式md文档渲染实现
前端·github·markdown
小怪瘦7938 分钟前
JS实现Table表格数据跑马灯效果
开发语言·javascript·信息可视化
励志成为大佬的小杨1 小时前
c语言中的枚举类型
java·c语言·前端
罗_三金1 小时前
微信小程序打印生产环境日志
javascript·微信小程序·小程序·bug
shuishen492 小时前
Web Bluetooth API 开发记录
javascript·web·js
前端熊猫2 小时前
Element Plus 日期时间选择器大于当天时间置灰
前端·javascript·vue.js
傻小胖2 小时前
React 组件通信完整指南 以及 自定义事件发布订阅系统
前端·javascript·react.js
JaxNext2 小时前
开发 AI 应用的无敌配方,半小时手搓学英语利器
前端·javascript·aigc
万亿少女的梦1682 小时前
高校网络安全存在的问题与对策研究
java·开发语言·前端·网络·数据库·python
Python私教2 小时前
Vue3中的`ref`与`reactive`:定义、区别、适用场景及总结
前端·javascript·vue.js