效果图
源码
<template>
<div
class="cp_container tree-container"
:style="{
flexDirection:direction=='level'?'row':'column',
'--my-gap':gap || '15px',
}">
<div
v-if="!isStart && parentNode.child && parentNode.child.length>1"
:class="{
location_el:true,
'start_location_el-vertical':direction=='vertical',
'start_location_el-level':direction=='level',
}">
</div>
<div
v-if="!isEnd && parentNode.child && parentNode.child.length>1"
:class="{
location_el:true,
'end_location_el-vertical':direction=='vertical',
'end_location_el-level':direction=='level',
}">
</div>
<div
class="name tree-name-container"
@click="onItem(nodeData,parentNode,zIndex)">
<svg-icon :icon-class="nodeData.icon" v-if="nodeData.icon"></svg-icon>
{{nodeData.name}}
<div class="bt_list">
<i
class="el-icon-plus"
@click.stop="onAdd(nodeData,parentNode,zIndex)">
</i>
<i
class="el-icon-minus"
@click.stop="onDelete(nodeData,parentNode,zIndex)">
</i>
<i
class="el-icon-edit-outline"
@click.stop="onUpdate(nodeData,parentNode,zIndex)">
</i>
</div>
<div
v-if="parentNode.name"
:class="{
center_el:true,
'start_center_el-vertical':direction=='vertical',
'start_center_el-level':direction=='level',
}">
<span></span>
</div>
<div
v-if="nodeData.child && nodeData.child.length>0"
:class="{
center_el:true,
'end_center_el-vertical':direction=='vertical',
'end_center_el-level':direction=='level',
}">
<span></span>
</div>
</div>
<div
class="container tree-childs-container"
v-if="nodeData.child && nodeData.child.length>0"
:style="{
gridTemplateColumns:direction!='level'?`repeat(${nodeData.child.length},max-content)`:'',
gridTemplateRows:direction=='level'?`repeat(${nodeData.child.length},max-content)`:'',
paddingTop:direction!='level'?'calc(var(--my-gap) * 2)':'',
paddingLeft:direction=='level'?'calc(var(--my-gap) * 2)':'',
}">
<MyTree
v-for="(item,index) in nodeData.child"
:key="index"
:zIndex="zIndex+1"
:direction="direction"
:parentNode="nodeData"
:nodeData='item'
:isStart='index == 0 && nodeData.child.length>1'
:isEnd='index == (nodeData.child.length - 1) && nodeData.child.length>1'
@onClick="onItem($event.nodeData,$event.parentNode,$event.zIndex)"
@onAdd="onAdd($event.nodeData,$event.parentNode,$event.zIndex)"
@onUpdate="onUpdate($event.nodeData,$event.parentNode,$event.zIndex)"
@onDelete="onDelete($event.nodeData,$event.parentNode,$event.zIndex)">
</MyTree>
</div>
</div>
</template>
<script>
//部门树结构组件 提供css让外部修改
export default {
name:'MyTree',
props:{
zIndex:{ //结构层级
type:Number,
default:0,
},
direction:{ //结构方向
type:String,
default:'vertical',
},
nodeData:{ //节点数据对象
type:Object,
default:()=>{
return {};
},
},
parentNode:{ //父节点
type:Object,
default:()=>{
return {};
},
},
isStart:{ //是左侧
type:Boolean,
default:false,
},
isEnd:{ //是右侧
type:Boolean,
default:false,
},
gap:{ //间隔
type:String,
default:'15px',
},
},
data() {
return {
};
},
methods:{
onItem(nodeData,parentNode,zIndex){ //点击事件
this.$emit('onClick',{
parentNode,
nodeData,
zIndex,
});
},
onAdd(nodeData,parentNode,zIndex){
this.$emit('onAdd',{
parentNode,
nodeData,
zIndex,
});
},
onUpdate(nodeData,parentNode,zIndex){
this.$emit('onUpdate',{
parentNode,
nodeData,
zIndex,
});
},
onDelete(nodeData,parentNode,zIndex){
this.$emit('onDelete',{
nodeData,
parentNode,
zIndex,
});
},
},
};
</script>
<style scoped lang="scss">
.cp_container{
width: fit-content;
height: fit-content;
display: flex;
align-items: center;
position: relative;
--border-color: #ccc; //边框颜色
>.location_el{
position: absolute;
}
>.start_location_el-vertical{
height: var(--my-gap);
width: calc(50% + var(--my-gap));
left: calc(0px - var(--my-gap));
top: calc(0px - var(--my-gap));
border-top: 1px solid var(--border-color);
}
>.start_location_el-level{
width: var(--my-gap);
height: calc(50% + var(--my-gap));
left: calc(0px - var(--my-gap));
top: calc(0px - var(--my-gap));
border-left: 1px solid var(--border-color);
}
>.end_location_el-vertical{
height: var(--my-gap);
width: calc(50% + var(--my-gap));
right: calc(0px - var(--my-gap));
top: calc(0px - var(--my-gap));
border-top: 1px solid var(--border-color);
}
>.end_location_el-level{
width: var(--my-gap);
height: calc(50% + var(--my-gap));
left: calc(0px - var(--my-gap));
bottom: calc(0px - var(--my-gap));
border-left: 1px solid var(--border-color);
}
>.name{
max-width: 400px;
padding:15px;
border-radius: 3px;
font-size: 20px;
box-sizing: border-box;
background-color: rgb(255, 255, 255);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
position: relative;
box-shadow: 0 1px 4px #00152936;
color: #3e4761;
>svg{
margin-right: 5px;
min-width: 20px;
width: 20px;
height: 20px;
}
&:hover{
.bt_list{
opacity: 1;
}
}
>.bt_list{
opacity: 0;
display: grid;
position: absolute;
height: 30px;
padding: 0 5px;
border-radius: 2px;
background-color: rgb(255, 255, 255);
bottom: -35px;
border: 1px solid #dadada;
box-sizing: border-box;
z-index: 999;
grid-gap: 5px;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
transition: all 0.2s;
box-shadow: 0 1px 4px #00152936;
&:hover{
opacity: 1;
}
}
//center 居中的四个边框
>.center_el{
position: absolute;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
>span{
background-color: var(--border-color);
}
}
>.start_center_el-vertical{
width: 100%;
height: var(--my-gap);
right: 0;
top: calc(0px - var(--my-gap));
>span{
width: 1px;
height: 100%;
}
}
>.start_center_el-level{
width: var(--my-gap);
height: 100%;
left: calc(0px - var(--my-gap));
top: 0;
>span{
height: 1px;
width: 100%;
}
}
>.end_center_el-vertical{
width: 100%;
height: var(--my-gap);
right: 0;
bottom: calc(0px - var(--my-gap));
>span{
width: 1px;
height: 100%;
}
}
>.end_center_el-level{
width: var(--my-gap);
height: 100%;
right: calc(0px - var(--my-gap));
top: 0;
>span{
height: 1px;
width: 100%;
}
}
}
>.container{
display: grid;
grid-gap: var(--my-gap);
justify-content: center;
width: fit-content;
}
}
</style>
使用方法
<MyTree
:nodeData="treeData"
direction="level"
@onAdd="onAdd"
@onUpdate="onUpdate"
@onDelete="onDelete">
</MyTree>
//数据
treeData:{
name:'客户申报',
child:[],
},
默认横向排列,非常简单。
其中 svg-icon
为我自定义的全局组件,不用直接删除就是了。