问题描述:如标题所示(粗略的写了一版,没有细化)
UI
红框的地方本来想用canvas自己写,但是找资料的时候发现了一个插件LeaderLine,非常符合要求,然后发现了一篇文章vue横向树级组件(流程图、组件递归)套用了一下,然后按照自己的需求修改了一下。
注意点
1.弹窗显示的数据,所以有一个层级显示,需要把svg的z-index的值写大一点
2. 数据多的时候,弹窗会显示滚动条,这个时候,线条会跟着滚动条移动,需要调用postion()方法,重新定位一下
3. 需求里还有一个节点下展示的数据,这个图是用css画的,可以收起或者开始,点击的时候,线条也是会乱的,所以手动触发resize事件
解决方法:
安装插件:npm i leader-line-vue
package.json
“leader-line-vue”: “^2.1.1”
child.vue
<template>
<div v-loading="loading">
<right-tree
v-if="list && list.length"
:list="list"
></right-tree>
</div>
</template>
<script>
import RightTree from './RightTree'
export default {
components: {
RightTree,
},
data() {
return {
list: [],
flag: true,
}
},
watch: {},
methods: {
/**
* 获取列表数据
*/
async getNodeList(flag) {
this.loading = true
try {
this.list.splice(0)
setTimeout(() => {
if (flag) {
this.list = [
{
id: '1',
name: '标题',
checked: true,
children: [
{
id: '1-1',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:2'
}
]
},
{
id: '1-2',
name: '标题',
checked: false,
},
{
id: '1-3',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:1'
}, {
name: '1:2'
}
]
},
{
id: '1-4',
name: '点击更多',
checked: false,
}
],
},
]
} else {
this.list = [
{
id: '1',
name: '标题',
checked: true,
children: [
{
id: '1-1',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:2'
}
]
},
{
id: '1-2',
name: '标题',
checked: false,
},
{
id: '1-3',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:1'
}, {
name: '1:2'
}
]
},
{
id: '1-4',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:1'
}, {
name: '1:2'
}
]
},
{
id: '1-5',
name: '标题',
checked: true,
child: [
{
name: '1:1'
}, {
name: '1:1'
}, {
name: '1:2'
}
]
}
],
},
]
}
}, 1000)
} catch (e) {
console.log('e', e)
}
this.loading = false
},
},
mounted() {
this.getNodeList(this.flag)
this.$eventBus.$on("changeData", value => {
this.getNodeList(value)
})
},
created() {
}
}
</script>
RightTree.vue
<template>
<div
class="FaultRegion"
>
<div class="fault_content ">
<div
class="child"
v-for="(dataItem,index) in list"
:key="dataItem.id +'-child-'+index"
>
<div
class="column"
:style="{marginRight: dataItem.children && dataItem.children.length > 1 ? '20px' :''}"
>
<div
class="column_name"
:id="dataItem"
:style="dataItem.children&& dataItem.children.length?'justify-content: center;':''"
>
<div
class="column_box"
:class="dataItem.children&& dataItem.children.length?'father':'child'"
:ref="dataItem.id"
:id="dataItem.id"
style="width:110px"
>
<!-- 集群or节点的名称-->
<div class="name">
<!--集群-->
<div v-if="dataItem.children&& dataItem.children.length" style="display: flex">
<div class="father_title">{{ dataItem.name }}</div>
</div>
<!--节点-->
<div v-else :class="dataItem.checked?'active':'noActive'">
<div class="line" v-if="!dataItem.children"></div>
<div class="child_title">{{ dataItem.name }}</div>
</div>
<p class="icon" @click="iconClcik(dataItem)" :class="dataItem.name==='点击更多'?'more':''"
style="margin-left:10px;">
<i :class="dataItem.checked?'el-icon-minus':'el-icon-plus'"
v-if="dataItem.name!='点击更多'"></i>
<i v-else class="el-icon-d-arrow-right"></i>
</p>
</div>
</div>
</div>
<!-- 节点的下一级 -->
<div v-if="dataItem.name !='集群'">
<div class="child_list" v-show="dataItem.checked">
<div class="child_icon" v-show="dataItem.checked &&dataItem.child && dataItem.child.length">
<div class="child_line"></div>
<div class="child_brackets"></div>
</div>
<div class="child_content">
<div v-for="(item,index) in dataItem.child" class="child_list_row">
<div class="child_circle"></div>
{{ item.name }}
</div>
</div>
</div>
</div>
</div>
<!-- 递归组件展示子节点 -->
<div
class="node_list"
v-if="dataItem.children && dataItem.children.length && dataItem.checked"
>
<RightTree
:list="dataItem.children"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import LeaderLine from 'leader-line-vue'
export default {
name: 'RightTree',
components: {},
data() {
return {
lines: [],
}
},
props: {
list: {
type: Array,
default: () => [],
},
},
mounted() {
this.$nextTick(() => {
this.drawLeaderLine(this.list)
})
// 检测滚动事件
const dom = document.getElementsByClassName('f_con')[0]
if (dom) {
dom.onscroll = () => {
this.drawLine()
this.doResize();
}
}
},
beforeDestroy() {
/**
* 离开页面时销毁所有line
*/
if (this.lines && this.lines.length) {
this.lines.forEach(line => {
line.remove()
})
}
},
methods: {
/**
* 销毁所有线条
*/
destoryLine() {
if (this.lines && this.lines.length) {
this.lines.forEach(line => {
line.remove()
})
}
},
/**
* 根据上下级关系绘制线条
*/
drawLeaderLine(list) {
this.lines.splice(0)
list.forEach(element => {
let start = document.getElementById(element.id)
if (element.children && element.children.length) {
element.children.forEach(child => {
let line = LeaderLine.setLine(start, document.getElementById(child.id))
line.color = '#1890ff'
line.size = 2
line.setOptions({
solid: {animation: true},
})
this.lines.push(line)
})
}
})
},
/**
* icon点击
*/
iconClcik(dataItem) {
dataItem.checked = !dataItem.checked
if (dataItem.name === '点击更多') {
if (dataItem.checked) {
this.$eventBus.$emit('changeData', false)
} else {
this.$eventBus.$emit('changeData', true)
}
} else {
// 集群
if (dataItem.children && dataItem.children.length) {
if (dataItem.checked) {
this.$nextTick(() => { // 划线
this.drawLeaderLine(this.list)
})
} else {
this.destoryLine() // 清除线
}
} else {// 子节点
this.doResize();
}
}
},
/**
* 手动触发resize事件
*/
doResize() {
setTimeout(function () {
//手动触发窗口resize事件
if (document.createEvent) {
var event = document.createEvent("HTMLEvents");
event.initEvent("resize", true, true);
window.dispatchEvent(event);
} else if (document.createEventObject) {
window.fireEvent("onresize");
}
}, 100);
},
/**
* 渲染线条
*/
drawLine() {
if (this.lines && this.lines.length) {
this.lines.forEach(line => {
line.position()
})
}
},
},
}
</script>
<style lang="scss" scoped>
.FaultRegion {
.fault_content {
margin-top: 20px;
.child {
display: flex;
background-color: #fff;
.column {
display: flex;
align-items: center;
margin: 10px 0;
.column_name {
cursor: pointer;
height: 100%;
display: flex;
align-items: center;
width: 180px;
text-align: center;
justify-content: end;
position: relative;
padding: 10px 0;
//background-color: red;
.column_box {
text-align: left;
width: 32px;
font-size: 16px;
font-family: MicrosoftYaHei;
color: #333333;
display: flex;
justify-content: end;
.name {
display: flex;
justify-content: end;
align-items: center;
font-family: MicrosoftYaHei;
color: #333333;
.line {
width: 2px;
height: 14px;
background: #E05E5E;
margin-right: 5px;
}
.father_title {
font-size: 15px;
max-width: 56px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.child_title {
font-size: 14px;
width: 56px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.noActive {
padding: 10px;
display: flex;
}
.active {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
height: 33px;
background: rgba(223, 239, 255, 0.7);
border-radius: 4px;
border: 1px solid #64A9EF;
}
}
.icon {
width: 14px;
height: 14px;
border-radius: 2px;
border: 1px solid #D4D4D4;
color: #D4D4D4;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
//margin-left: 30px;
//margin-right: 60px;
i {
font-size: 4px;
}
}
.more {
border: none;
i {
transform: rotate(90deg);
}
}
}
.father {
display: flex;
justify-content: end;
align-items: center;
}
}
}
.node_list {
display: flex;
flex-direction: column;
justify-content: center;
}
.child_list {
display: flex;
.child_icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
.child_line {
width: 45px;
height: 2px;
background-color: #1B70CA;
}
.child_brackets {
width: 2px;
height: 100%;
background: #1B70CA;
}
&::before {
content: "";
width: 2px;
height: 7px;
position: absolute;
right: -2px;
bottom: -5px;
background: #1B70CA;
transform: rotate(-45deg);
}
&::after {
content: "";
width: 2px;
height: 7px;
position: absolute;
right: -2px;
top: -5px;
background: #1B70CA;
transform: rotate(45deg);
}
}
.child_content {
display: flex;
flex-direction: column;
.child_list_row {
display: flex;
align-items: center;
margin-left: 10px;
margin-bottom: 10px;
.child_circle {
width: 4px;
height: 4px;
background: #A7D2FF;
border-radius: 50%;
margin-right: 10px;
}
}
}
.child_content :last-child {
margin-bottom: 0;
}
}
}
}
}
</style>
father.vue
<template>
<div class="box FaultRegion">
<div class="con">
<div class="row">
<div class="label">标题:</div>
<div class="value" @click="FaultRegionShow = true">点击我,显示弹窗</div>
</div>
<Modal :show.sync="FaultRegionShow" title="标题" class="faultRegionModal" :hideBottom=false>
<div class="menu">
<div title="缩小" class="minus">
<i class="el-icon-minus"></i>
</div>
<div title="放大" class="plus">
<i class="el-icon-rank"></i>
</div>
<div title="刷新" class="refresh">
<i class="el-icon-refresh"></i>
</div>
</div>
<div class="f_con">
<Test/>
</div>
</Modal>
</div>
</template>
<script>
import Modal from "@/components/XcdataModal/XcdataModal";
import Test from "views/systemConfig/SystemConfig/Test/child";
export default {
name: "index",
data() {
return {
FaultRegionShow: false,
}
},
components: {
Modal,
Test
},
}
</script>
<style scoped lang="scss">
.FaultRegion {
.con {
.row {
display: flex;
align-items: center;
font-size: 14px;
font-family: MicrosoftYaHei;
margin-bottom: 38px;
.label {
padding: 0 12px 0 0;
width: 160px;
text-align: right;
}
.value {
color: var(--primary-color);
cursor: pointer;
display: flex;
flex-direction: row;
.value_box {
height: 34px;
border-radius: 2px;
border: 1px solid #1B70CA;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
color: #333333;
font-size: 14px;
font-family: MicrosoftYaHei;
padding: 10px 8px;
position: relative;
cursor: pointer;
}
.checked {
color: #fff;
border-style: solid;
border-width: 0px 0px 18px 17px;
border-color: transparent transparent var(--primary-color) transparent;
position: absolute;
bottom: 0;
right: 0;
}
.checked::after {
content: "";
width: 4px;
height: 8px;
border-color: #fff;
border-style: solid;
border-width: 0 1px 1px 0;
transform: rotate(45deg);
position: absolute;
bottom: -16px;
right: 3px;
}
.active {
background: rgba(27, 112, 202, 0.11);
}
}
}
& > div:nth-child(3) {
margin-bottom: 18px;
}
}
}
</style>
<style lang="scss">
.faultRegionModal {
.modal {
width: 622px;
height: 477px;
.con_wrap {
display: flex;
flex-direction: column;
.menu {
display: flex;
justify-content: end;
margin-top: 10px;
margin-right: 20px;
.minus, .plus, .refresh, {
width: 16px;
height: 16px;
background: #EFEFEF;
border-radius: 2px;
margin-left: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
i {
font-size: 6px;
color: var(--primary-color);
}
}
}
.f_con {
height: 400px;
overflow-y: auto;
}
}
}
}
</style>
<style lang="scss">
.leader-line {
z-index: 999999;
}
</style>