最近在写后台管理系统的时候,遇到一个需求,就是给我一些节点,让我渲染到页面上,效果图如下:
之前写过一篇文章关于relation-graph关系图组件
http://t.csdnimg.cn/7BGYm的用法
还有一篇关于relation-graph——实现右击节点显示详情+点击展开折叠操作——技能提升
http://t.csdnimg.cn/K3rzf
关于插件的安装和使用,在此不再赘述。可以参照上面的两个链接。
下面给我的数据结构及想要的效果:
原始数据结构
this.taskRecords = {
"nodes": [
{
"taskName": "完善客诉",
"taskNodeName": "WanShanKeSu"
},
{
"taskName": "PCB判责",
"taskNodeName": "PCBPanZe"
},
{
"taskName": "PCBA判责",
"taskNodeName": "PCBAPanZe"
},
{
"taskName": "方案确定并处理",
"taskNodeName": "FangAnQuDingBingChuLi"
}
],
"connections": [
{
"from": "",
"to": "WanShanKeSu",
"depth": null
},
{
"from": "",
"to": "PCBPanZe",
"depth": null
},
{
"from": "",
"to": "PCBAPanZe",
"depth": null
},
{
"from": "WanShanKeSu",
"to": "PCBPanZe",
"depth": null
},
{
"from": "WanShanKeSu",
"to": "PCBAPanZe",
"depth": null
},
{
"from": "PCBPanZe",
"to": "FangAnQuDingBingChuLi",
"depth": null
},
{
"from": "PCBAPanZe",
"to": "FangAnQuDingBingChuLi",
"depth": null
}
]
}
可以看出提供的节点有4个
完善客诉
PCB判责
PCBA判责
方案确定并处理
给出的连线有:
1.空 ——> 完善客诉
2.空 ——> PCB判责
3.空 ——> PCBA判责
4.完善客诉 ——> PCB判责
5.完善客诉 ——> PCBA判责
6.PCB判责 ——> 方案确定并处理
7.PCBA判责 ——> 方案确定并处理
如果不进行任何的处理,直接通过 渲染,则会出现下面的情况
上图乍一看,没啥问题,但是通过拖动【完善客诉】节点,就会发现问题了,就是【完善客诉】指向【PCBA判责】与【完善客诉】指向【PCB判责】的连线重合了,就会有【完善客诉】指向【PCB判责】再指向【PCBA判责】的错觉。这样的效果不是我们想要的。
想要实现【完善客诉】在【PCBA判责】与【PCB判责】节点中间,则需要指定排列的顺序。
比如上面的nodes
节点更改顺序如下:
"nodes": [
{
"taskName": "PCB判责",
"taskNodeName": "PCBPanZe"
},
{
"taskName": "完善客诉",
"taskNodeName": "WanShanKeSu"
},
{
"taskName": "PCBA判责",
"taskNodeName": "PCBAPanZe"
},
{
"taskName": "方案确定并处理",
"taskNodeName": "FangAnQuDingBingChuLi"
}
],
这样就是我们想要的效果了
考虑到还有多层路径的情况,所以要通过递归来排列节点的顺序
我的思路
根据connections
将空的节点填充为start开始节点
,然后将没有任何from
引申的节点,通通相当于指向end结束节点
1.开始 ——> 完善客诉
2.开始 ——> PCB判责
3.开始 ——> PCBA判责
4.完善客诉 ——> PCB判责
5.完善客诉 ——> PCBA判责
6.PCB判责 ——> 方案确定并处理
7.PCBA判责 ——> 方案确定并处理
8.方案确定并处理 ——> 结束
1.给from为空的节点赋值为start
let endArr = [];
let nodeObj = {};
let nodeArr = [];
this.taskRecords.connections.forEach((item) => {
if (!item.from) {
item.from = 'start';
}
endArr.push(item.from);
});
上面的endArr
就是所有连线的开始节点,比如现在的endArr=['完善客诉','PCB判责','PCBA判责']
所有节点的集合:
this.taskRecords.nodes &&this.taskRecords.nodes.forEach((item) => {
nodeArr.push(item.taskNodeName);
nodeObj[item.taskNodeName] = [];
});
目前nodeArr=['完善客诉','PCB判责','PCBA判责','方案确定并处理']
this.taskRecords.connections &&this.taskRecords.connections.forEach((item) => {
nodeObj[item.from].push(item.to);
});
经过上面的处理,nodeObj
内容如下:
nodeObj = {
'开始':['完善客诉','PCB判责','PCBA判责'],
'完善客诉':['PCB判责','PCBA判责',],
'PCB判责':['方案确定并处理'],
'PCBA判责':['方案确定并处理'],
'方案确定并处理':[],
'结束':[]
}
我的思路是:遍历nodeObj
,如果节点对应的数组长度大于1,则表示有好几个分支,则分支的排序尤为重要。比如【开始】节点,指向三个节点,我需要再次遍历,每一个子节点是否有好几个分支,如果有,则需要将该节点,位置安排在分支中间。
比如【完善客诉】的子节点【PCB判责】【PCBA判责】,则【完善客诉】位置应该是位于【PCB判责】和【PCBA判责】中间。
下面的代码可以实现这一操作:
for (let key in nodeObj) {
if (nodeObj[key].length) {
nodeObj[key].forEach((item) => {
if (nodeObj[item].length > 1) {
let arr = nodeObj[item].filter(
(n) => nodeObj[key].indexOf(n) > -1
);
let len = Math.floor(arr.length / 2);
let centerIndex = this.taskRecords.connections.findIndex(
(no) => no.from == key && no.to == item
);
let currentObj = this.taskRecords.connections[centerIndex];
this.taskRecords.connections.splice(centerIndex, 1);
this.taskRecords.connections.splice(len, 0, currentObj);
}
});
}
}
经过上面的操作:
endArr=['完善客诉','PCB判责','PCBA判责']
nodeArr=['完善客诉','PCB判责','PCBA判责','方案确定并处理']
所以存在于nodeArr
中,但是不存在于endArr
中的【方案确定并处理】应该有一条线是指向【结束】的
nodeArr && nodeArr.forEach((item) => {
if (endArr.indexOf(item) == -1) {
this.taskRecords.connections.push({
from: item,
to: 'end',
});
}
});
重新组装nodes
节点数据:
let nodes = [
{
text: '开始',
id: 'start',
color: this.info.taskList.length ? '#f90' : null,
},
];
this.taskRecords.nodes &&
this.taskRecords.nodes.forEach((item) => {
nodes.push({
id: item.taskNodeName,
text: item.taskName,
color: item.color,
...item,
});
});
nodes.push({
text: '结束',
id: 'end',
});
上面的步骤基本能实现想要的效果了。
//需要指定 节点参数和连接线的参数
this.graph_json_data = {
rootId: 'start',
nodes: nodes,
lines: this.taskRecords.connections,
};
this.$refs.seeksRelationGraph.setJsonData(
this.graph_json_data,
(seeksRGGraph) => {}
);
我的效果图中,还有节点变亮,以及变亮节点中间的连线也是变亮的。这个就是给对应的节点和连线中添加color
即可。
自定义插槽
下面要讲的是自定义插槽:
鼠标移入到节点上时,可以i显示其他的内容,此时需要使用插槽了。
<RelationGraph
ref="seeksRelationGraph"
style="
height: 300px;
width: 80%;
margin: 0 auto;
border: 1px solid #666;
"
:options="graphOptions"
>
<template #node="{ node }">
<div class="my-node">
<div class="my-node-text">{{ node.text }}</div>
<div
class="my-node-detail"
v-if="node.data && node.data.creatorName"
>
<div @dblclick="handleCopy(node.data)">
{{ node.data.taskOwnerName || node.data.creatorName }}:{{
(node.data.completedTime || node.data.creationTime) | moment
}}
</div>
</div>
</div>
</template>
</RelationGraph>
上面中的graphOptions
就是一些普通的配置项,具体的可以在https://relation-graph.com/#/options-tools链接中在线配置好后,拷贝到本地使用。
自定义插槽,一定要注意:node
中识别内容只有id``text``data
,其中的data
可以是个对象,一开始我使用的是detail
对象,则没有显示出来。改成data
就可以了。
.my-node {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.my-node-detail {
display: none;
}
&:hover {
.my-node-detail {
display: block;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -50%;
width: 250px;
height: auto;
min-height: 60px;
line-height: 30px;
background: #fff;
padding: 10px 0;
border: 3px solid #f90;
color: #000;
z-index: 1;
font-size: 18px;
user-select: all;
}
}
}
监听全屏/取消全屏——保证关系图在页面中间
我的思路就是,全屏/取消全屏时,重新渲染
监听页面的全屏操作
mounted() {
// 添加全屏变化的事件监听器
document.addEventListener('fullscreenchange', this.onFullScreenChange);
},
方法:
onFullScreenChange() {
this.$refs.seeksRelationGraph.setJsonData(
this.graph_json_data,
(seeksRGGraph) => {}
);
},