一、前言

iview-admin中提供了 v-org-tree 这么一个vue组件可以实现树形菜单,下面小编来提供一下在element-ui中的使用教程

小编集成了el-dropdown下拉菜单(鼠标左击显示菜单),和右击自定义菜单,两种方式,效果图如下:
element组织树形菜单

二、使用教程

(1)安装依赖
npm install clipboard
npm install v-click-outside-x
npm install v-org-tree
(2)引入组件

在main.js文件中引入

温馨小提示:也可在单个页面中进行局部引入哦

// 导入组织树形菜单组件 v-org-tree
import OrgTree from 'v-org-tree'
import 'v-org-tree/dist/v-org-tree.css'
Vue.use(OrgTree)

import importDirective from '@/directive'
import { directive as clickOutside } from 'v-click-outside-x'
// 注册指令
importDirective(Vue)
Vue.directive('clickOutside', clickOutside)
(3)引入部分js工具方法

在项目目录下 -> src -> directive文件夹中引入如下5个js文件
在这里插入图片描述
clipboard.js

import Clipboard from 'clipboard'
export default {
  bind: (el, binding) => {
    const clipboard = new Clipboard(el, {
      text: () => binding.value.value
    })
    el.__success_callback__ = binding.value.success
    el.__error_callback__ = binding.value.error
    clipboard.on('success', e => {
      const callback = el.__success_callback__
      callback && callback(e)
    })
    clipboard.on('error', e => {
      const callback = el.__error_callback__
      callback && callback(e)
    })
    el.__clipboard__ = clipboard
  },
  update: (el, binding) => {
    el.__clipboard__.text = () => binding.value.value
    el.__success_callback__ = binding.value.success
    el.__error_callback__ = binding.value.error
  },
  unbind: (el, binding) => {
    delete el.__success_callback__
    delete el.__error_callback__
    el.__clipboard__.destroy()
    delete el.__clipboard__
  }
}

draggable.js

import { on } from './tools'
export default {
  inserted: (el, binding, vnode) => {
    let triggerDom = document.querySelector(binding.value.trigger)
    triggerDom.style.cursor = 'move'
    let bodyDom = document.querySelector(binding.value.body)
    let pageX = 0
    let pageY = 0
    let transformX = 0
    let transformY = 0
    let canMove = false
    const handleMousedown = e => {
      let transform = /\(.*\)/.exec(bodyDom.style.transform)
      if (transform) {
        transform = transform[0].slice(1, transform[0].length - 1)
        let splitxy = transform.split('px, ')
        transformX = parseFloat(splitxy[0])
        transformY = parseFloat(splitxy[1].split('px')[0])
      }
      pageX = e.pageX
      pageY = e.pageY
      canMove = true
    }
    const handleMousemove = e => {
      let xOffset = e.pageX - pageX + transformX
      let yOffset = e.pageY - pageY + transformY
      if (canMove) bodyDom.style.transform = `translate(${xOffset}px, ${yOffset}px)`
    }
    const handleMouseup = e => {
      canMove = false
    }
    on(triggerDom, 'mousedown', handleMousedown)
    on(document, 'mousemove', handleMousemove)
    on(document, 'mouseup', handleMouseup)
  },
  update: (el, binding, vnode) => {
    if (!binding.value.recover) return
    let bodyDom = document.querySelector(binding.value.body)
    bodyDom.style.transform = ''
  }
}

tools.js

/**
 * @description 绑定事件 on(element, event, handler)
 */
export const on = (function () {
  if (document.addEventListener) {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on' + event, handler)
      }
    }
  }
})()

/**
 * @description 解绑事件 off(element, event, handler)
 */
export const off = (function () {
  if (document.removeEventListener) {
    return function (element, event, handler) {
      if (element && event) {
        element.removeEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event) {
        element.detachEvent('on' + event, handler)
      }
    }
  }
})()

directives.js

import draggable from './module/draggable'
import clipboard from './module/clipboard'

const directives = {
  draggable,
  clipboard
}

export default directives

index.js

import directive from './directives'

const importDirective = Vue => {
  /**
   * 拖拽指令 v-draggable="options"
   * options = {
   *  trigger: /这里传入作为拖拽触发器的CSS选择器/,
   *  body:    /这里传入需要移动容器的CSS选择器/,
   *  recover: /拖动结束之后是否恢复到原来的位置/
   * }
   */
  Vue.directive('draggable', directive.draggable)
  /**
   * clipboard指令 v-draggable="options"
   * options = {
   *  value:    /在输入框中使用v-model绑定的值/,
   *  success:  /复制成功后的回调/,
   *  error:    /复制失败后的回调/
   * }
   */
  Vue.directive('clipboard', directive.clipboard)
}

export default importDirective
(4)正式使用v-org-tree组件

在所要使用的地方新增如下4个文件,比如我要写在user-group文件夹中

温馨小提示:注意在router.js路由中配置index访问页面哦~

在这里插入图片描述
org-view.vue

<template>
  <div
    ref="dragWrapper"
    class="org-tree-drag-wrapper"
    @mousedown="mousedownView"
    @contextmenu="handleDocumentContextmenu"
  >
    <div class="org-tree-wrapper" :style="orgTreeStyle">
      <v-org-tree
        v-if="data"
        :data="data"
        :node-render="nodeRender"
        :expand-all="true"
        @on-node-click="handleNodeClick"
        collapsable
      ></v-org-tree>
    </div>

    <!-- 右击显示的自定义菜单 -->
    <div v-show="menuVisible">
      <ul id="menu" class="menu">
        <li class="menu__item" @click="addGroup">添加同级组织</li>
        <li class="menu__item" @click="addGroup">添加下级组织</li>
        <li class="menu__item" @click="updateGroup">修改信息</li>
        <li class="menu__item" @click="deleteGroup" style="color: red;">删除信息</li>
      </ul>
    </div>

  </div>
</template>

<script>

import { on, off } from '@/directive/module/tools'

const menuList = [
  {
    key: 'edit',
    label: '编辑'
  },
  {
    key: 'detail',
    label: '查看'
  },
  {
    key: 'new',
    label: '新增'
  },
  {
    key: 'delete',
    label: '删除'
  }
]

export default {
  name: 'OrgView',
  props: {
    zoomHandled: {
      type: Number,
      default: 1
    },
    data: Object
  },
  data () {
    return {
      currentContextMenuId: '',
      orgTreeOffsetLeft: 0,
      orgTreeOffsetTop: 0,
      initPageX: 0,
      initPageY: 0,
      oldMarginLeft: 0,
      oldMarginTop: 0,
      canMove: false,
      menuVisible: false,// 默认菜单为隐藏状态
    }
  },
  computed: {
    orgTreeStyle () {
      return {
        transform: `translate(-50%, -50%) scale(${this.zoomHandled}, ${
          this.zoomHandled
        })`,
        marginLeft: `${this.orgTreeOffsetLeft}px`,
        marginTop: `${this.orgTreeOffsetTop}px`
      }
    }
  },
  methods: {
    // 处理右击菜单方法
    addGroup(){
      alert("add")
    },
    deleteGroup(){
      alert("delete")
    },
    updateGroup(){},

    handleNodeClick (e, data, expand) {
      expand()
    },
    // 监听鼠标点击自定义菜单以外的事件 -> 即点击其他地方隐藏菜单
    closeMenu () {
      this.menuVisible = false;// 隐藏菜单
      this.currentContextMenuId = '';
    },
    getBgColor (data) {
      return this.currentContextMenuId === data.id
        ? data.isRoot
          ? '#0d7fe8'
          : '#5d6c7b'
        : ''
    },
    // 组织数据及下拉菜单 【注:事件方法写法  ex:@command变为on-command才会生效】
    nodeRender (h, data) {
      return (
        <div on-mousedown={event => event.stopPropagation()} on-contextmenu={this.contextmenu.bind(this, data)} >
          <el-dropdown
            trigger="click"
            on-command={this.handleContextMenuClick.bind(this, data)}
            class="context-menu"
            nativeOn-click={this.handleDropdownClick}
            style={{ transform: `scale(${1 / this.zoomHandled}, ${1 / this.zoomHandled})` }}
            v-click-outside={this.closeMenu}
          >
              <span class={['custom-org-node', data.children && data.children.length ? 'has-children-label' : '']}>
                {data.label}
              </span>
              <el-dropdown-menu slot="dropdown">
                {menuList.map(item => {
                  return (
                    <el-dropdown-item command={item.key}>{item.label}</el-dropdown-item>
                  )
                })}
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      )
    },
    // 处理右击事件 -> 右击覆盖浏览器原生右击事件
    contextmenu (data, $event) {
      console.log("右击事件:"+data.label)
      this.menuVisible = true;// 显示隐藏菜单

      let event = $event || window.event
      // console.log(event)
      event.preventDefault
        ? event.preventDefault()
        : (event.returnValue = false)
      this.currentContextMenuId = data.id
    },
    setDepartmentData (data) {
      data.isRoot = true
      this.departmentData = data
    },
    mousedownView (event) {
      this.canMove = true
      this.initPageX = event.pageX
      this.initPageY = event.pageY
      this.oldMarginLeft = this.orgTreeOffsetLeft
      this.oldMarginTop = this.orgTreeOffsetTop
      on(document, 'mousemove', this.mousemoveView)
      on(document, 'mouseup', this.mouseupView)
    },
    mousemoveView (event) {
      if (!this.canMove) return
      const { pageX, pageY } = event
      this.orgTreeOffsetLeft = this.oldMarginLeft + pageX - this.initPageX
      this.orgTreeOffsetTop = this.oldMarginTop + pageY - this.initPageY
    },
    mouseupView () {
      this.canMove = false
      off(document, 'mousemove', this.mousemoveView)
      off(document, 'mouseup', this.mouseupView)
    },
    handleDropdownClick (event) {
      event.stopPropagation()
    },
    handleDocumentContextmenu () {
      this.canMove = false
    },
    handleContextMenuClick (data, key) {
      this.$emit('on-menu-click', { data, key })
    }
  },
  mounted () {
    on(document, 'contextmenu', this.handleDocumentContextmenu)
  },
  beforeDestroy () {
    off(document, 'contextmenu', this.handleDocumentContextmenu)
  }
}
</script>

<style>
  .menu__item {
    display: block;
    line-height: 20px;
    text-align: center;
    margin-top: 10px;
  }
  .menu {
    height: 120px;
    width: 100px;
    position: absolute;
    border-radius: 10px;
    border: 1px solid #999999;
    background-color: #f4f4f4;
  }
  li:hover {
    background-color: #1790ff;
    color: white;
  }
</style>

zoom-controller.vue

<template>
  <div class="zoom-wrapper">
    <button class="zoom-button" @click="scale('down')">
      <Icon type="md-remove" :size="14" color="#fff"/>
    </button>
    <span class="zoom-number">{{ value }}%</span>
    <button class="zoom-button" @click="scale('up')">
      <Icon type="md-add" :size="14" color="#fff"/>
    </button>
  </div>
</template>

<script>
export default {
  name: 'ZoomController',
  props: {
    value: {
      type: Number,
      default: 100
    },
    step: {
      type: Number,
      default: 20
    },
    min: {
      type: Number,
      default: 10
    },
    max: {
      type: Number,
      default: 200
    }
  },
  methods: {
    scale (type) {
      const zoom = this.value + (type === 'down' ? -this.step : this.step)
      if (
        (zoom < this.min && type === 'down') ||
        (zoom > this.max && type === 'up')
      ) {
        return
      }
      this.$emit('input', zoom)
    }
  }
}
</script>

<style lang="less">
.trans(@duration) {
  transition: ~"all @{duration} ease-in";
}
.zoom-wrapper {
  .zoom-button {
    width: 20px;
    height: 20px;
    line-height: 10px;
    border-radius: 50%;
    background: rgba(157, 162, 172, 1);
    box-shadow: 0px 2px 8px 0px rgba(218, 220, 223, 0.7);
    border: none;
    cursor: pointer;
    outline: none;
    &:active {
      box-shadow: 0px 0px 2px 2px rgba(218, 220, 223, 0.2) inset;
    }
    .trans(0.1s);
    &:hover {
      background: #1890ff;
      .trans(0.1s);
    }
  }
  .zoom-number {
    color: #657180;
    padding: 0 8px;
    display: inline-block;
    width: 46px;
    text-align: center;
  }
}
</style>

index.less

@wrapper: ~'department';
.percent-100 {
  width: 100%;
  height: 100%;
}
.@{wrapper}-outer {
  .percent-100;
  overflow: hidden;
  .tip-box{
    position: absolute;
    left: 20px;
    top: 20px;
    z-index: 12;
  }
  .zoom-box {
    position: absolute;
    right: 30px;
    bottom: 30px;
    z-index: 2;
  }
  .view-box {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1;
    cursor: move;
    .org-tree-drag-wrapper {
      width: 100%;
      height: 100%;
    }
    .org-tree-wrapper {
      display: inline-block;
      position: absolute;
      left: 50%;
      top: 50%;
      transition: transform 0.2s ease-out;
      .org-tree-node-label {
        box-shadow: 0px 2px 12px 0px rgba(143, 154, 165, 0.4);
        border-radius: 4px;
        .org-tree-node-label-inner {
          padding: 0;
          .custom-org-node {
            padding: 14px 41px;
            background: #738699;
            user-select: none;
            word-wrap: none;
            white-space: nowrap;
            border-radius: 4px;
            color: #ffffff;
            font-size: 14px;
            font-weight: 500;
            line-height: 20px;
            transition: background 0.1s ease-in;
            cursor: default;
            &:hover {
              background: #5d6c7b;
              transition: background 0.1s ease-in;
            }
            &.has-children-label {
              cursor: pointer;
            }
            .context-menu{
              position: absolute;
              right: -10px;
              bottom: 20px;
              z-index: 10;
            }
          }
        }
      }
    }
  }
}

index.vue

<template>
  <div shadow style="height: 100%;width: 100%;overflow:hidden">
    <div class="department-outer">
      <div class="view-box">
        <org-view
          v-if="data"
          :data="data"
          :zoom-handled="zoomHandled"
          @on-menu-click="handleMenuClick"
        ></org-view>
      </div>
    </div>
  </div>
</template>

<script>
import OrgView from './components/org-view.vue'
import ZoomController from './components/zoom-controller.vue'
import './index.less'
const menuDic = {
  edit: '编辑按钮',
  detail: '查看按钮',
  new: '新增按钮',
  delete: '删除按钮'
}
export default {
  name: 'org_tree_page',
  components: {
    OrgView,
    ZoomController
  },
  data () {
    return {
      // TODO 目前暂时为假数据,可调用接口方法赋予实时数据
      data: {
        id: 0,
        label: 'XX科技有限公司',
        children: [
          {
            id: 2,
            label: '产品研发部',
            children: [
              {
                id: 5,
                label: '研发-前端'
              }, {
                id: 6,
                label: '研发-后端'
              }, {
                id: 9,
                label: 'UI设计'
              }, {
                id: 10,
                label: '产品经理'
              }
            ]
          },
          {
            id: 3,
            label: '销售部',
            children: [
              {
                id: 7,
                label: '销售一部'
              }, {
                id: 8,
                label: '销售二部'
              }
            ]
          },
          {
            id: 4,
            label: '财务部'
          }, {
            id: 11,
            label: 'HR人事'
          }
        ]
      },
      zoom: 100
    }
  },
  computed: {
    zoomHandled () {
      return this.zoom / 100
    }
  },
  methods: {
    setDepartmentData (data) {
      data.isRoot = true
      return data
    },
    handleMenuClick ({ data, key }) {
      // this.submitSucc(`点击了《${data.label}》节点的'${menuDic[key]}'菜单`)
      this.$message(`点击了《${data.label}》节点的'${menuDic[key]}'菜单`);
    },
  }
}
</script>

<style></style>
(5)最终实现效果图

npm run dev 运行
在这里插入图片描述
两种菜单中都默认定义了点击菜单弹出消息框,具体的业务根据你们个人情况修改~
在这里插入图片描述

三、总结

在这里插入图片描述

部分文件说明
  1. org-view.vue:封装v-org-tree组件功能
  2. index.less:样式
  3. index.vue:正式使用组件编写业务
遇坑问题
  1. 注意引入文件位置
    在这里插入图片描述
  2. 从iview-admin源码中直接拷贝过来修改组件时注意修改标签为elment的标签
    在这里插入图片描述
  3. 如上图中org-view.vue文件中的事件方法写法
    element-ui中 el-dropdown 点击菜单项触发的事件回调 @command变为on-command才会生效!
    
    在这里插入图片描述
    调用方法为 { this.方法名 }
    如:
    on-command={this.handleContextMenuClick.bind(this, data)}
    
  4. 由于小编是Java工程师,对前端vue这块不是太熟,部分修改并不是那么完善,还请见谅 ~
Logo

前往低代码交流专区

更多推荐