iview-admin中提供了 v-org-tree 这么一个vue组件可以实现树形菜单,下面小编来提供一下在element-ui中的使用教程
小编集成了el-dropdown下拉菜单(鼠标左击显示菜单),和右击自定义菜单,两种方式,效果图如下:
npm install clipboard
npm install v-click-outside-x
npm install v-org-tree
在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)
在项目目录下 -> 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个文件,比如我要写在user-group文件夹中
温馨小提示:注意在router.js路由中配置index访问页面哦~
<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>
npm run dev 运行
两种菜单中都默认定义了点击菜单弹出消息框,具体的业务根据你们个人情况修改~