目录
一、实现目标
实现一个功能关系图形工具,可以用点和线的形式描节点(分为有用节点和有害节点)之间的关系,并对节点和连线进行增删改查等操作。
具体功能实现成果如下:
1. 从根节点衍生功能部件
2. 从功能部件中衍生后继功能部件
3. 编辑功能部件
4. 删除功能部件
5. 选择节点,另其与其他节点建立联系
6. 删除联系
7. 重置图形
8. 按部件类型分类显示
二、组件的使用
组件引入:
npm install --save relation-graph
或
yarn add relation-graph --save
在Vue Component中引入
import SeeksRelationGraph from 'relation-graph'
export default {
components: {
SeeksRelationGraph
},
}
组件的使用
<!-- 作图工具 -->
<div style="width: calc(100% - 20px); height:calc(100vh - 100px); padding-top: 20px">
<SeeksRelationGraph
ref="seeksRelationGraph"
:options="graphOptions"
:on-node-click="onNodeClick"
:on-line-click="onLineClick"/>
</div>
组件库文档
relation-graphhttp://www.relation-graph.com/#/docs/start
三、功能的具体实现
图像的初始化
relation-graph的图形结构由三部分组成:根节点、节点列表、连接列表,其中根节点和节点列表中根节点信息是初始化时必不可少的,是整个图形的基础。
setGraphData() {
let init_graph_data = {
rootId: 'root',
nodes: [{id: 'root', name: '你的发明创造', color: '#c9c0d3', nodeShape: 1, width: 130, height: 60}],
links: []
}
this.$refs.seeksRelationGraph.setJsonData(init_graph_data, (seeksRGGraph) => {
// 这些写上当图谱初始化完成后需要执行的代码
this.graphs = init_graph_data
})
},
根节点是整个图形中不可删除的部分,因此,根据需要,若不希望显示根节点,可以设置根节点的isHide属性为true。
(上述处理方式可能会在图像存取方面出现问题,若没有存取反复使用的需求可忽略)
另一方面,在初始化图形数据(或进行节点、关系的操作)后,图形会自动添加上一些其他的属性,这使得官方的getJSONData获得的并不是单纯的图形数据。因此,需要单独使用一个JSON对象记录当前的图形数据(可选择存入session中)。
产生新节点
这里的产生新节点指的是从根节点衍生节点,作为整个功能模型的基础功能。
addNewNode() {
if (this.nodeName === '' || this.radio === ''){
this.$message.error('请完善节点信息');
return
}
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.nodeName === this.graphs.nodes[i].name){
this.$message.error('该功能部件已存在');
return
}
}
const graph = this.$refs.seeksRelationGraph;
let node_color = '';
if (this.radio === 1)
node_color = '#89B8CA'
else
node_color = '#ca8989'
const __graph_json_data = {
nodes: [
{
id: this.nodeName,
name: this.nodeName,
color: node_color,
x: (graph.getNodes().length + 4) * 150,
y: 200,
}
],
links: [
{from: 'root', to: this.nodeName, text: '需要'}
]
};
graph.appendJsonData(__graph_json_data, true, (seeksRGGraph) => {
// 这些写上当图谱初始化完成后需要执行的代码
this.$notification.open({
title: '成功添加功能部件',
type: 'success',
message: '添加功能部件:' + this.nodeName
})
this.nodeName = ''
this.radio = ''
this.visible = false
this.graphs.nodes.push(__graph_json_data.nodes[0])
this.graphs.links.push(__graph_json_data.links[0])
// console.log(this.graphs)
this.selectingNode = false
sessionStorage.setItem('graph', JSON.stringify(this.graphs))
})
},
这里设定了节点的id和名称相同,并限定创建节点时,节点名称不得相同。
官方文档中使用的初始化方式为根据 当前节点列表长度+1 作为下一个节点的id,这样的处理方式会在删除某个节点后重新添加节点时出现问题。
另一方面,节点初始化后产生的位置是重叠的,若存在一次添加多个节点的情况,可以设定节点坐标分散优化用户体验。
衍生后继结点
addPostposition() {
if (this.nodeName === '' || this.radio === ''){
this.$message.error('请完善节点信息');
return
}
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.nodeName === this.graphs.nodes[i].name){
this.$message.error('该功能部件已存在');
return
}
}
const graph = this.$refs.seeksRelationGraph;
let node_color = '';
if (this.radio === 1)
node_color = '#89B8CA'
else
node_color = '#ca8989'
const __graph_json_data = {
nodes: [
{
id: this.nodeName,
name: this.nodeName,
color: node_color,
x: (graph.getNodes().length + 4) * 150,
y: 200,
}
],
links: [
{from: this.currentNode.id, to: this.nodeName, text: '产生'}
]
};
graph.appendJsonData(__graph_json_data, true, (seeksRGGraph) => {
// 这些写上当图谱初始化完成后需要执行的代码
this.$notification.open({
title: '成功添加功能部件',
type: 'success',
message: '添加功能部件:' + this.nodeName
})
this.nodeName = ''
this.radio = ''
this.visible = false
this.graphs.nodes.push(__graph_json_data.nodes[0])
this.graphs.links.push(__graph_json_data.links[0])
this.selectingNode = false
this.checkContradiction(this.currentNode)
sessionStorage.setItem('graph', JSON.stringify(this.graphs))
})
},
这一功能的实现与创建新节点十分类似,只需要将连接的from设定为当前选定的节点即可。
节点编辑
editNode() {
if (this.nodeName === '' || this.radio === ''){
this.$message.error('请完善节点信息');
return
}
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.currentNode.id !== this.graphs.nodes[i].id &&
this.nodeName === this.graphs.nodes[i].name)
{
this.$message.error('该功能部件已存在');
return
}
}
const graph = this.$refs.seeksRelationGraph;
let node_color = '';
if (this.radio === 1)
node_color = '#89B8CA'
else
node_color = '#ca8989'
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.currentNode.id === this.graphs.nodes[i].id){
this.graphs.nodes[i].id = this.nodeName
this.graphs.nodes[i].name = this.nodeName
this.graphs.nodes[i].color = node_color
this.graphs.nodes[i].text = this.nodeName
}
}
for (let i = 0; i < this.graphs.links.length; i++){
if (this.currentNode.id === this.graphs.links[i].from){
this.graphs.links[i].from = this.nodeName
}
if (this.currentNode.id === this.graphs.links[i].to){
this.graphs.links[i].to = this.nodeName
}
}
this.$refs.seeksRelationGraph.setJsonData(this.getUsableData(this.graphs))
sessionStorage.setItem('graph',JSON.stringify(this.getUsableData(this.graphs)))
this.visible = false
this.selectingNode = false
},
由于前面设定了节点的名称和id相同,因此在编辑节点名称时,节点id也会随之变化。因此需要对该节点相关的连接信息进行重新设定。
根据节点类型分别显示
根据节点颜色区分,通过设定透明度实现。
showNodes(nodeType) {
const graph = this.$refs.seeksRelationGraph
for (let i = 0; i < this.graphs.nodes.length; i++){
graph.getNodeById(this.graphs.nodes[i].id).opacity = 1
}
let avilColor = ''
if (nodeType === 'useful')
avilColor = '#ca8989'
else if (nodeType === 'harmful')
avilColor = '#89B8CA'
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.graphs.nodes[i].color === avilColor){
graph.getNodeById(this.graphs.nodes[i].id).opacity = 0.3
}
}
},
删除节点
removeNode(){
let tmpNodes = []
for (let i = 0; i < this.graphs.nodes.length; i++){
if (this.currentNode.id !== this.graphs.nodes[i].id){
tmpNodes.push(this.graphs.nodes[i])
}
}
this.graphs.nodes = tmpNodes
for (let i = 0; i < this.graphs.links.length; i++){
if (this.currentNode.id === this.graphs.links[i].from){
this.graphs.links[i].from = 'root'
this.graphs.links[i].isHide = true
// 删除后自动把悬浮点挂在根节点上
// this.graphs.links[i].text = '需要'
}
if (this.currentNode.id === this.graphs.links[i].to){
this.graphs.links[i].to = 'root'
this.graphs.links[i].isHide = true
}
}
this.$refs.seeksRelationGraph.setJsonData(this.getUsableData(this.graphs))
sessionStorage.setItem('graph',JSON.stringify(this.getUsableData(this.graphs)))
this.selectingNode = false
// this.selectingLine = false
},
这是功能中实现起来较为复杂的部分,实现的方式也并不理想。
在官方文档中有删除节点方法,但是调用后再次读取图形时,与该节点相关的节点坐标会被设定为无穷大(由连接关系导致)。无奈之下,只能重写该方法。
直接删除节点会导致关系目标缺失,导致节点无法正确显示,因此这里采取下策,在节点列表中删除节点,然后将与该节点关联的关系隐藏,并将与该节点相关的节点(包括前继节点和后继节点)连接到根节点上。
删除关系
与上述处理类似,这里选择将关系隐藏,否则会出现同样的节点坐标无穷大。
removeRelation(){
for (let i = 0; i < this.graphs.links.length; i++){
if (this.currentLink.fromNode.id === this.graphs.links[i].from
&& this.currentLink.toNode.id === this.graphs.links[i].to){
this.graphs.links[i].isHide = true
}
}
this.$refs.seeksRelationGraph.setJsonData(this.getUsableData(this.graphs))
sessionStorage.setItem('graph',JSON.stringify(this.getUsableData(this.graphs)))
this.selectingLine = false
},
建立连接关系
在建立连接关系之前,需要判断符合建立连接关系的条件:不能是当前节点的前继节点、不能是当前节点的后继节点、不能是根节点。
因此在选中节点时,需要计算出符合建立条件的节点列表。
onNodeClick(nodeObject) {
const graph = this.$refs.seeksRelationGraph
this.$notification.open({
type: 'success',
message: '选择节点:' + nodeObject.text
})
this.currentNode = nodeObject
this.selectingNode = this.currentNode.id !== 'root'
this.selectingLine = false
let posNodes = this.getPosNodes(nodeObject)
let frontNodes = this.getFrontNodes(nodeObject)
this.selectableNodes = []
// 找可建立关系节点
for (let i = 0; i < this.graphs.nodes.length; i++){
let nodeItem = {}
if (this.graphs.nodes[i].id !== 'root' && this.graphs.nodes[i].id !== nodeObject.id
&& !this.array_contain(posNodes, this.graphs.nodes[i].id)
&& !this.array_contain(frontNodes, this.graphs.nodes[i].id)
)
{
nodeItem.id = this.graphs.nodes[i].id
nodeItem.label = graph.getNodeById(nodeItem.id).text
this.selectableNodes.push(nodeItem)
}
}
sessionStorage.setItem('selectableNodes', JSON.stringify(this.selectableNodes))
this.selectableNodes = []
},
getPosNodes(nodeObject){
// 找后置节点
let posNodes = []
console.log(this.graphs.links)
// 可见的连接
let visibleLinks = []
for (let i = 0; i < this.graphs.links.length; i++){
if (this.graphs.links[i].isHide !== true){
visibleLinks.push(this.graphs.links[i])
}
}
// console.log(visibleLinks)
for (let i = 0; i < visibleLinks.length; i++){
if (visibleLinks[i].from === nodeObject.id){
posNodes.push(visibleLinks[i].to)
}
}
// console.log(posNodes)
return posNodes
},
getFrontNodes(nodeObject){
// 找前置节点
let frontNodes = []
// 可见的连接
let visibleLinks = []
for (let i = 0; i < this.graphs.links.length; i++){
if (this.graphs.links[i].isHide !== true){
visibleLinks.push(this.graphs.links[i])
}
}
for (let i = 0; i < visibleLinks.length; i++){
if (visibleLinks[i].to === nodeObject.id){
frontNodes.push(visibleLinks[i].from)
}
}
return frontNodes
},
建立连接关系
addNewRelation() {
if (this.selectedNodes.length === 0){
this.$message.error('请选择关联节点');
return
}
for (let i = 0; i < this.selectedNodes.length; i++){
const graph = this.$refs.seeksRelationGraph;
const __graph_json_data = {
nodes: [],
links: [
{from: this.currentNode.id, to: this.selectedNodes[i].key, text: '产生'}
]
};
graph.appendJsonData(__graph_json_data, true, (seeksRGGraph) => {
// 这些写上当图谱初始化完成后需要执行的代码
this.$notification.open({
title: '成功添加关系',
type: 'success',
message: '添加关系:' + graph.getNodeById(this.currentNode.id).text + ' 产生 ' + graph.getNodeById(this.selectedNodes[i].key).text
})
this.selectVisible = false
this.graphs.links.push(__graph_json_data.links[0])
this.checkContradiction(this.currentNode)
// console.log(this.graphs)
this.selectingNode = false
sessionStorage.setItem('graph', JSON.stringify(this.graphs))
})
}
this.selectedNodes = []
},
以上便是重要功能的实现,后续将上传完整代码资源。