1、v-org-tree
1-1、安装
cnpm install v-org-tree --save
cnpm install v-click-outside-x --save
1-2、注册
//main.js
import OrgTree from 'v-org-tree'
import 'v-org-tree/dist/v-org-tree.css'
Vue.use(OrgTree)
import { directive as clickOutside } from 'v-click-outside-x'
Vue.directive('clickOutside', clickOutside)
//下拉框用iview
import ViewUI from 'view-design';
import 'view-design/dist/styles/iview.css';
Vue.use(ViewUI);
1-3、使用
HTML
<template>
<div class="view-box">
<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>
</div>
</template>
JS
<script>
import { on, off } from "@/utils/tools";
const menuList = [
{
key: "edit",
label: "编辑部门",
},
{
key: "detail",
label: "查看部门",
},
{
key: "new",
label: "新增子部门",
},
{
key: "delete",
label: "删除部门",
},
];
export default {
name: "OrgView",
data() {
return {
data: {
id: 0,
label: "XXX科技有限公司",
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人事",
},
],
},
currentContextMenuId: "",
orgTreeOffsetLeft: 0,
orgTreeOffsetTop: 0,
initPageX: 0,
initPageY: 0,
oldMarginLeft: 0,
oldMarginTop: 0,
canMove: false,
};
},
computed: {
orgTreeStyle() {
return {
transform: `translate(-50%, -50%) scale(${this.zoomHandled}, ${this.zoomHandled})`,
marginLeft: `${this.orgTreeOffsetLeft}px`,
marginTop: `${this.orgTreeOffsetTop}px`,
};
},
},
methods: {
handleNodeClick(e, data, expand) {
expand();
},
closeMenu() {
this.currentContextMenuId = "";
},
getBgColor(data) {
return this.currentContextMenuId === data.id
? data.isRoot
? "#0d7fe8"
: "#5d6c7b"
: "";
},
nodeRender(h, data) {
return (
<div
class={[
"custom-org-node",
data.children && data.children.length ? "has-children-label" : "",
]}
on-mousedown={(event) => event.stopPropagation()}
on-contextmenu={this.contextmenu.bind(this, data)}
>
{data.label}
<dropdown
trigger="custom"
class="context-menu"
visible={this.currentContextMenuId === data.id}
nativeOn-click={this.handleDropdownClick}
on-on-click={this.handleContextMenuClick.bind(this, data)}
style={{
transform: `scale(${1 / this.zoomHandled}, ${
1 / this.zoomHandled
})`,
}}
v-click-outside={this.closeMenu}
>
<dropdown-menu slot="list">
{menuList.map((item) => {
return (
<dropdown-item name={item.key}>{item.label}</dropdown-item>
);
})}
</dropdown-menu>
</dropdown>
</div>
);
},
contextmenu(data, $event) {
let event = $event || window.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) {
console.log(data,key,'点击节点')
},
},
mounted() {
on(document, "contextmenu", this.handleDocumentContextmenu);
},
beforeDestroy() {
off(document, "contextmenu", this.handleDocumentContextmenu);
},
};
</script>
Css
<style scoped lang='scss'>
.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: 0;
top: 20%;
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;
}
}
}
}
}
}
</style>
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)
}
}
}
})()
2、vue-tree-chart
2-1、安装
cnpm install vue-tree-chart --save
2-2、全局过滤器
//main.js
Vue.directive('drag',el=>{
let l=0;
let t=0;
el.onmousedown=function(e){
console.log(e);
//计算出元素距离上边和左边的距离(鼠标点击的位置-元素的位置)
//这个应该能理解吧
var disX=e.clientX-el.offsetLeft;
var disY=e.clientY-el.offsetTop;
document.onmousemove = function(e){
//鼠标要按住不松开移动才行,松开就会触发onmouseup的事件
//计算出元素移动后的位置(鼠标位置-元素初始的disX/disY)
l=e.clientX-disX;
t=e.clientY-disY;
el.style.left=l+'px';
el.style.top=t+'px';
}
document.onmouseup=function(e){
document.onmousemove=null;
document.onmouseup=null;
el.style.left=l+'px';
el.style.top=t+'px';
}
}
})
2-3、使用
2-3-1、在components中新建TreeChart.vue组件
<template>
<div class="tree_box" >
<table v-if="treeData.name" >
<tr>
<td :colspan="Array.isArray(treeData.children) ? treeData.children.length * 2 : 1"
:class="{parentLevel: Array.isArray(treeData.children) && treeData.children.length, extend: Array.isArray(treeData.children) && treeData.children.length && treeData.extend}"
>
<div :class="{node: true, hasMate: treeData.mate}">
<div class="person"
:class="Array.isArray(treeData.class) ? treeData.class : []"
>
<div class="avat">
<img :src="treeData.image_url" @contextmenu="$emit('click-node', treeData)"/>
</div>
<!-- <div class="name">{{treeData.name}}</div> -->
</div>
<div class="paeson_name">{{treeData.name}}</div>
<template v-if="Array.isArray(treeData.mate) && treeData.mate.length">
<div class="person" v-for="(mate, mateIndex) in treeData.mate" :key="treeData.name+mateIndex"
:class="Array.isArray(mate.class) ? mate.class : []"
@click="$emit('click-node', mate)"
>
<div class="avat">
<img :src="mate.image_url" />
</div>
<!-- <div class="name">{{mate.name}}</div> -->
</div>
<div class="paeson_name">{{treeData.name}}</div>
</template>
</div>
<div class="extend_handle" v-if="Array.isArray(treeData.children) && treeData.children.length" @click="toggleExtend(treeData)">
<Icon v-if="open" class="open" type="ios-add-circle-outline" />
<Icon v-else class="close" type="ios-remove-circle-outline" />
</div>
</td>
</tr>
<tr v-if="Array.isArray(treeData.children) && treeData.children.length && treeData.extend">
<td v-for="(children, index) in treeData.children" :key="index" colspan="2" class="childLevel">
<TreeChart :node-render="nodeRender" :json="children" @click-node="$emit('click-node', $event)"/>
</td>
</tr>
</table>
</div>
</template>
<script>
import TreeChart from "vue-tree-chart";
// import draggable from 'vuedraggable'
export default {
name: "TreeChart",
props: ["json"],
components: {
TreeChart
},
data() {
return {
open:false,
treeData: {}
}
},
watch: {
json: {
handler: function(Props){
let extendKey = function(jsonData){
jsonData.extend = (jsonData.extend===void 0 ? true: !!jsonData.extend);
if(Array.isArray(jsonData.children)){
jsonData.children.forEach(c => {
extendKey(c)
})
}
return jsonData;
}
if(Props){
this.treeData = extendKey(Props);
}
},
immediate: true
}
},
methods: {
toggleExtend: function(treeData){
this.open=!this.open;
treeData.extend = !treeData.extend;
this.$forceUpdate();
},
nodeRender(h, data) {
return (
<div
class={[
"custom-org-node",
data.children && data.children.length ? "has-children-label" : "",
]}
on-mousedown={(event) => event.stopPropagation()}
on-contextmenu={this.contextmenu.bind(this, data)}
>
{data.label}
<dropdown
trigger="custom"
class="context-menu"
visible={this.currentContextMenuId === data.id}
nativeOn-click={this.handleDropdownClick}
on-on-click={this.handleContextMenuClick.bind(this, data)}
style={{
transform: `scale(${1 / this.zoomHandled}, ${
1 / this.zoomHandled
})`,
}}
v-click-outside={this.closeMenu}
>
<dropdown-menu slot="list">
{menuList.map((item) => {
return (
<dropdown-item name={item.key}>{item.label}</dropdown-item>
);
})}
</dropdown-menu>
</dropdown>
</div>
);
},
}
}
</script>
<style scoped>
.tree_box{
width: 100%;
cursor: move;
}
table{border-collapse: separate!important;border-spacing: 0!important;width: 100%;}
td{position: relative; vertical-align: top;padding:0 0 60px 0;text-align: center; }
.extend_handle{position: absolute;left:50%;bottom:30px;transform: translate3d(-10px,0,0);cursor: pointer;}
.extend_handle .open,.extend_handle .close{width: 20px;height: 20px;border-radius: 50%;text-align: center;line-height: 20px;font-size: 20px;}
/* .extend_handle:before{content:""; display: block; width:100%;height: 100%;box-sizing: border-box; border:2px solid;border-color:#ccc #ccc transparent transparent;
transform: rotateZ(135deg);transform-origin: 50% 50% 0;transition: transform ease 300ms;}
/* .extend_handle:hover:before{border-color:#333 #333 transparent transparent;} */
/* .extend .extend_handle:before{transform: rotateZ(-45deg);} */
.extend::after{content: "";position: absolute;left:50%;bottom:15px;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
.childLevel::before{content: "";position: absolute;left:50%;bottom:100%;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
.childLevel::after{content: "";position: absolute;left:0;right:0;top:-15px;border-top:2px solid #ccc;}
.childLevel:first-child:before, .childLevel:last-child:before{display: none;}
.childLevel:first-child:after{left:50%;height:15px; border:2px solid;border-color:#ccc transparent transparent #ccc;border-radius: 6px 0 0 0;transform: translate3d(1px,0,0)}
.childLevel:last-child:after{right:50%;height:15px; border:2px solid;border-color:#ccc #ccc transparent transparent;border-radius: 0 6px 0 0;transform: translate3d(-1px,0,0)}
.childLevel:first-child.childLevel:last-child::after{left:auto;border-radius: 0;border-color:transparent #ccc transparent transparent;transform: translate3d(1px,0,0)}
.node{position: relative; display: inline-block;margin: 0 1em;box-sizing: border-box; text-align: center;}
.node:hover{color: #2d8cf0;cursor: pointer;}
.node .person{position: relative; display: inline-block;z-index: 2;width:6em; }
.node .person .avat{display: block;width:4em;height: 4em;margin:auto;overflow:hidden; background:#fff;box-sizing: border-box;}
.node .person .avat img{width:100%;height: 100%;}
.node .person .name{height:2em;line-height: 2em;overflow: hidden;width:100%;}
.node.hasMate::after{content: "";position: absolute;left:2em;right:2em;top:2em;border-top:2px solid #ccc;z-index: 1;}
.node .paeson_name{position: absolute; top: 55px;right: 0;width: 88px;text-align: center;text-overflow: ellipsis; overflow: hidden; white-space: nowrap;}
.landscape{transform:translate(-100%,0) rotate(-90deg);transform-origin: 100% 0;}
.landscape .node{text-align: left;height: 8em;width:8em;right: 18px;}
.landscape .person{position: absolute; height: 4em;top:4em;left: 2.5em;}
.landscape .person .avat{position: absolute;left: 0;border-radius: 2em;border-width:2px;}
.landscape .person .name{height: 4em; line-height: 4em;}
.landscape .hasMate{position: relative;}
.landscape .hasMate .person{position: absolute; }
.landscape .hasMate .person:first-child{left:auto; right:-4em;}
.landscape .hasMate .person:last-child{left: -4em;margin-left:0;}
</style>
2-3-2、组件使用
<template>
<div class="about" v-drag>
<TreeChart :json="jsonData" @click-node="clickNode" />
<div class="gl_prs_ctn" :style='[contextstyle]'>
<ul class='gl_prs_li'>
<li @click="handleClick(1)">添加</li>
<li @click="handleClick(2)">详情</li>
<li @click="handleClick(3)">编辑</li>
<li @click="handleClick(4)">删除</li>
</ul>
</div>
</div>
</template>
<script>
import TreeChart from "@/components/TreeChart";
export default {
components: {
TreeChart
},
data() {
return {
selectId:0,
jsonData: {
id:0,
name: 'root',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
class: ["rootNode"],
children: [
{
id:1,
name: 'children1',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
children: [
{
id:3,
name: 'grandchild',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
},
{
id:4,
name: 'grandchild2',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
},
{
id:5,
name: 'grandchild3',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
}
]
},
{
id:2,
name: 'children2',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
children: [
{
id:6,
name: 'grandchild',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
},
{
id:7,
name: 'grandchild2',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
},
{
id:8,
name: 'grandchild3',
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
}
]
}
]
},
contextstyle: {
display: 'none',
right: '0px',
top: '0px',
left: '0px',
bottom: '0px',
},
}
},
created(){
document.oncontextmenu = ()=>{return false}
document.addEventListener("click", () => {
if(this.contextstyle.display == 'block'){
this.contextstyle.display = 'none'
}
})
},
methods: {
handleClick(type){
console.log(type,this.selectId)
},
clickNode(e){
this.selectId=e.id;
if(window.event.x + 188 > document.documentElement.clientWidth){
this.contextstyle.left = 'unset';
this.contextstyle.right = document.documentElement.clientWidth - window.event.x + 'px';
}else{
this.contextstyle.left = window.event.x + 'px';
}
if(window.event.y + 166 > document.documentElement.clientHeight){
this.contextstyle.top = 'unset';
this.contextstyle.bottom = document.documentElement.clientHeight - window.event.y + 'px';
}else{
this.contextstyle.top = window.event.y + 'px';
}
this.contextstyle.display = 'block';
},
}
}
</script>
<style>
.about {
width: 100%;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.gl_prs_ctn{
width: 188px;
background: rgb(255, 255, 255);
box-shadow: rgba(0, 0, 0, 0.075) 0px 1px 1px inset, rgba(102, 175, 233, 0.6) 0px 0px 8px;
z-index: 99999;
position: fixed;
padding: 10px;
box-sizing: content-box;
height: 142px;
border-radius: 10px;
}
.gl_prs_li{padding: unset;margin: unset;}
.gl_prs_li>li{
cursor: pointer;
list-style: none;
border-bottom: 1px solid #efefef;
padding: 7px 10px;
}
li:last-child { border: unset }
li:hover{
background: #ccc;
color: #fff;
}
</style>
3、需求实现
<template>
<div class="box" v-drag>
<TreeChart :json="treeData" @click-node='clickNode' />
</div>
</template>
<script>
import TreeChart from "vue-tree-chart";
export default {
data() {
return {
treeData: {
name: "父亲",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
mate: {
name: "母亲",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
children: [
{
name: "儿子",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
mate: {
name: "儿媳",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
children:[
{
name: "孙子",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
name: "孙子",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
]
},
{
name: "兄弟",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
name: "兄弟",
image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
],
},
};
},
components: {
TreeChart,
},
methods: {
clickNode(node) {
console.log(node,'2222')
}
},
};
</script>
<style lang="scss" scoped>
.box{
position: relative;
cursor: move;
/deep/ .extend_handle{
display: none;
}
/deep/ .node .person .avat{
border: none;
}
/deep/ .extend:after{
height: 35px;
}
/deep/ .node .person{
cursor: pointer;
}
}
</style>