This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
cyber-narrator-cn-ui/src/views/entityExplorer/EntityGraph.vue

1351 lines
54 KiB
Vue
Raw Normal View History

2023-06-16 17:18:58 +08:00
<template>
<div class="entity-graph">
2024-06-13 10:25:52 +08:00
<div class="entity-graph__chart" id="entityGraph" ></div>
<div class="entity-graph__right-box" id="rightBox">
2023-07-02 22:38:59 +08:00
<el-drawer
v-model="rightBox.show"
direction="rtl"
class="entity-graph__detail"
:close-on-click-modal="true"
:modal="false"
:size="rightBoxWidth"
:with-header="false"
destroy-on-close>
<graph-entity-list
v-if="rightBox.mode === 'list'"
:node="rightBox.node"
:loading="rightBox.loading"
@expandList="expandList"
@closeBlock="onCloseBlock"
>
</graph-entity-list>
<graph-entity-detail
v-else-if="rightBox.mode === 'detail'"
:node="rightBox.node"
:currentEntityType="entity.entityType"
:title="entity.entityName"
:loading="rightBox.loading"
@expandDetailList="expandDetailList"
@closeBlock="onCloseBlock"
>
</graph-entity-detail>
2023-07-02 22:38:59 +08:00
</el-drawer>
2023-06-16 17:18:58 +08:00
</div>
</div>
2024-06-13 10:25:52 +08:00
<div id="toolbarContainer" class="graph-toolbar" style="height:50px;">
<ul class="toolbar__tools">
<li @click="handleToolsbarClick('zoomOut')" :title="$t('overall.zoomOut')"><i class="cn-icon cn-icon-zoom-out"></i></li>
<li @click="handleToolsbarClick('zoomIn')" :title="$t('overall.zoomIn')"><i class="cn-icon cn-icon-zoom-in"></i></li>
<li @click="handleToolsbarClick('autoZoom')" :title="$t('overall.autoZoom')"><i class="cn-icon cn-icon-reset"></i></li>
<li @click="handleToolsbarClick('undo')" :title="$t('overall.preStep')" id="preStep" class="toolbar--unactivated"><i class="cn-icon cn-icon-pre-step"></i></li>
<li @click="handleToolsbarClick('redo')" :title="$t('overall.nextStep')" id="nextStep" class="toolbar--unactivated"><i class="cn-icon cn-icon-next-step"></i></li>
<li @click="handleToolsbarClick('toDefault')" :title="$t('overall.reset')"><i class="cn-icon cn-icon-to-default"></i></li>
2024-06-13 10:25:52 +08:00
</ul>
</div>
2023-06-16 17:18:58 +08:00
</template>
<script>
import GraphEntityList from '@/views/entityExplorer/entityGraph/GraphEntityList'
import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDetail'
import { useRoute } from 'vue-router'
2023-06-29 14:40:50 +08:00
import { ref, shallowRef } from 'vue'
import ForceGraph from 'force-graph'
2024-06-14 14:59:01 +08:00
import { Algorithm } from '@antv/g6'
import * as d3 from 'd3'
2024-06-13 10:25:52 +08:00
import Node, { nodeType } from './entityGraph/node'
import Link, { linkType, linkDistance } from './entityGraph/link'
2024-06-13 10:25:52 +08:00
import { builtTooltip } from '@/views/entityExplorer/entityGraph/utils'
2023-06-29 14:40:50 +08:00
import _ from 'lodash'
2023-06-16 17:18:58 +08:00
export default {
name: 'EntityRelationship',
components: {
GraphEntityList,
GraphEntityDetail
2023-06-16 17:18:58 +08:00
},
data () {
return {
debounceFunc: null,
center: {},
2024-06-13 10:25:52 +08:00
initialData: null, // 初始化数据,用于重置
nodes: [],
links: [],
graph: shallowRef(null),
defaultChargeStrength: -50, // 之前的设置-20
2024-06-18 15:40:20 +08:00
defaultLinkDistance: 80,
2024-06-13 10:25:52 +08:00
defaultMargin: 2, // 图像与箭头的距离
rootNode: null,
isClicking: false,
isDraggingEntityNode: false,
defaultArrowSize: 3, // 箭头大小
/* 自己实现stack操作 */
stackData: {
undo: [], // 后退
justUndo: false, // 是否刚后退了
redo: [], // 前进
justRedo: false // 是否刚前进了
},
centerPonit:{
x:200,
y:0
},
rightBoxWidth: 400
}
},
methods: {
2023-06-29 14:40:50 +08:00
async init () {
try {
const initialData = await this.generateInitialData()
this.initialData = _.cloneDeep(initialData) // 初始化数据
this.initForceGraph(initialData) // 初始化拓展图
} catch (e) {
console.error(e)
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
}
},
getCoordinate (startNode, endNode) {
const startX = startNode.x
const startY = startNode.y
const endX = endNode.drawEndX
const endY = endNode.drawEndY
if (endX && startX) {
const diffVal = Math.round(Math.sqrt(Math.pow((endX - startX), 2) + Math.pow((endY - startY), 2)))
const step = 5
const i = endNode.nextI ? endNode.nextI : 0
if (i < diffVal / step) {
endNode.nextI = i + 1
const dx = endX - startX
const dy = endY - startY
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
const cx = startX + step * Math.cos(angle) * i
const cy = this.linearInterpolate([startX, startY], [endX, endY]).getY(cx)
return {
x: cx,
y: cy
}
} else {
return null
}
} else {
return null
}
},
getShortenedLength (end) {
return end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理
},
initForceGraph (initialData) {
let hoverNode = null
const canvasHeight = document.body.clientHeight - 100
//const canvasWidth = document.body.clientWidth - this.rightBoxWidth
this.graph = ForceGraph()(document.getElementById('entityGraph'))
.height(canvasHeight)
//.width(canvasWidth)
.graphData(initialData)
.nodeCanvasObject((node, ctx) => {
if(node.type === nodeType.entityNode) {
let sourceNode = node.sourceNode
if(!node.isInit && sourceNode && sourceNode.fx !== null) {
const sourceNodeNeighbors = sourceNode.getNeighbors(this.graph.graphData())
sourceNode.childCount = sourceNodeNeighbors.targetNodes.length//设置每个list节点的子节点个数
if(!sourceNode.currentChildIndex) {
sourceNode.currentChildIndex = 0
}
let angle = this.getEntityNodeAngle(sourceNode.childCount, sourceNode.currentChildIndex++)
const entityFirstPoint = this.pointOfLine({ x: sourceNode.sourceNode.x, y: sourceNode.sourceNode.y}, {x:sourceNode.fx,y:sourceNode.fy}, linkDistance.normal + linkDistance.root)// start,end,lineLength
const toPoint = this.pointOfRotate({x:entityFirstPoint.x,y:entityFirstPoint.y},{x:sourceNode.fx,y:sourceNode.fy},angle)//y如果设置为0则展示的位置不对所以设置一个较小的y为1
node.x = toPoint.x
node.y = toPoint.y
node.isInit = true
}
/*else if(node.x === null){
console.log('node.x === null ========================'+node.isInit)
} else if(!node.isInit && sourceNode.fx === null){ //如果节点离list节点太远的话也进行重新处理
console.log('未初始化list节点的fx为null**********'+sourceNode)
}*/
}
/*
* 共有4种 nodeType3 entityType
* */
const nodeStyle = this.getNodeStyle(node.type, node.data.entityType)
switch (node.type) {
case nodeType.rootNode: {
// 如果是鼠标点击高亮的,最外层加上第三层圆环
if (node.id === this.clickNode.id) {
2024-06-18 15:40:20 +08:00
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.selectedShadowR, 0, 2 * Math.PI, false)
2024-06-18 15:40:20 +08:00
ctx.closePath()
ctx.fillStyle = nodeStyle.selectedShadowColor
2024-06-13 10:25:52 +08:00
ctx.fill()
}
// 第二层圆环
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = node === this.clickNode || node === hoverNode
? nodeStyle.hoveredShadowColor
: nodeStyle.shadowColor
ctx.fill()
// 内部挖空
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
ctx.fill()
2024-06-18 15:40:20 +08:00
// 第一层圆环
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.lineWidth = 1
ctx.strokeStyle = nodeStyle.borderColor
ctx.stroke()
// 图片
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
// 文字
ctx.font = nodeStyle.font
const textWidth = ctx.measureText(node.label).width
const bckgDimensions = [textWidth, 9].map(n => n + 9 * 0.2)
ctx.fillStyle = nodeStyle.fontBgColor
ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 31, ...bckgDimensions) // 文字的白底
2024-06-18 15:40:20 +08:00
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = nodeStyle.fontColor
ctx.fillText(node.label, node.x, node.y + 30)
break
}
case nodeType.listNode: {
// 如果是鼠标点击或者悬停的,有一层圆环
if (node === this.clickNode || node === hoverNode) {
2024-06-13 10:25:52 +08:00
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false)
2024-06-18 15:40:20 +08:00
ctx.closePath()
if (node === this.currentSelectedNode) {
ctx.fillStyle = nodeStyle.selectedShadowColor
} else {
ctx.fillStyle = nodeStyle.hoveredShadowColor
}
2024-06-13 10:25:52 +08:00
ctx.fill()
}
// 内部填白
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
ctx.fill()
// 第一层圆环
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.lineWidth = 1
ctx.strokeStyle = nodeStyle.borderColor
ctx.stroke()
// 图片
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
// 文字
ctx.font = nodeStyle.font
const textWidth = ctx.measureText(node.label).width
const bckgDimensions = [textWidth, 8].map(n => n + 8 * 0.2)
ctx.fillStyle = nodeStyle.fontBgColor
ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 24, ...bckgDimensions) // 文字的白底
2024-06-18 15:40:20 +08:00
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = nodeStyle.fontColor
ctx.fillText(node.label, node.x, node.y + 24)
break
}
case nodeType.entityNode: {
// 先画个白底圆环,避免 link 穿过不好看
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
ctx.fill()
// 如果是鼠标点击或者悬停的,有一层圆环
if (node === this.clickNode || node === hoverNode) {
2024-06-13 10:25:52 +08:00
ctx.beginPath()
ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false)
2024-06-18 15:40:20 +08:00
ctx.closePath()
if (node === this.clickNode) {
ctx.fillStyle = nodeStyle.selectedShadowColor
} else {
ctx.fillStyle = nodeStyle.hoveredShadowColor
}
ctx.fill()
}
// 图片
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
break
}
case nodeType.tempNode: {
// 先画个白底圆环,避免 link 穿过不好看
ctx.beginPath()
const tempNodeDistance = this.getShortenedLength(node)// 临时节点展示动画时临时节点的初始位置距离entity节点的距离
// 计算箭头角度
const dx = node.fx - node.sourceNode.x
const dy = node.fy - node.sourceNode.y
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
const startNodeX = node.sourceNode.x + tempNodeDistance * Math.cos(angle)
const startNodeY = node.sourceNode.y + tempNodeDistance * Math.sin(angle)
node.drawEndX = node.realEndX
node.drawEndY = node.realEndY
const nodeCoordinate = this.getCoordinate({ x: startNodeX, y: startNodeY }, node)
if (nodeCoordinate && !this.isDraggingEntityNode) {
ctx.arc(nodeCoordinate.x, nodeCoordinate.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
2024-06-18 15:40:20 +08:00
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
2024-06-13 10:25:52 +08:00
ctx.fill()
// 画透明度0.7的图标
ctx.globalAlpha = 0.7
ctx.drawImage(node.img, nodeCoordinate.x - nodeStyle.iconWidth / 2, nodeCoordinate.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
ctx.globalAlpha = 1
} else {
2024-06-18 15:40:20 +08:00
ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false)
ctx.closePath()
ctx.fillStyle = nodeStyle.fillStyle
2024-06-13 10:25:52 +08:00
ctx.fill()
2024-06-18 15:40:20 +08:00
// 画透明度0.7的图标
ctx.globalAlpha = 0.7
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
ctx.globalAlpha = 1
}
break
2024-06-13 10:25:52 +08:00
}
}
})
.linkCanvasObject((link, ctx) => {
const start = link.source
const end = link.target
let width = 1 // 线宽
const arrowSize = this.defaultArrowSize // 箭头大小
const shortenedLength = this.getShortenedLength(end) // link 末端缩短长度,root节点特殊处理
2024-06-13 10:25:52 +08:00
// 计算箭头角度
const dx = end.x - start.x
const dy = end.y - start.y
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
let lineEndX = end.x - shortenedLength * Math.cos(angle)
let lineEndY = end.y - shortenedLength * Math.sin(angle)
let arrowEndX = lineEndX + arrowSize * Math.cos(angle)
let arrowEndY = lineEndY + arrowSize * Math.sin(angle)
2024-06-18 15:40:20 +08:00
// 绘制线
let color
if (link.type === linkType.temp) {
ctx.setLineDash([2, 2])
color = 'rgba(119,131,145,0.2)' // 虚线颜色
if (end.realEndX) {
end.drawEndX = lineEndX
end.drawEndY = lineEndY
}
const nodeCoordinate = this.getCoordinate(start, end)
if (nodeCoordinate && !this.isDraggingEntityNode) {
lineEndX = nodeCoordinate.x
lineEndY = nodeCoordinate.y
arrowEndX = nodeCoordinate.x + arrowSize * Math.cos(angle)
arrowEndY = nodeCoordinate.y + arrowSize * Math.sin(angle)
2024-06-18 15:40:20 +08:00
}
} else {
ctx.setLineDash([])
if (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id) {
color = 'rgba(119,131,145,0.9)' // 高亮线颜色
width = 1.2
} else {
color = 'rgba(119,131,145,0.3)' // 普通线颜色
}
}
ctx.beginPath()
ctx.moveTo(start.x, start.y)
ctx.lineTo(lineEndX, lineEndY)
ctx.strokeStyle = color
ctx.lineWidth = width
ctx.stroke()
2024-06-13 10:25:52 +08:00
ctx.save() // 保存当前状态以便之后恢复
ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端
ctx.rotate(angle) // 根据链接方向旋转坐标系
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边
ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边
ctx.closePath()
ctx.fillStyle = color
ctx.fill()
ctx.restore() // 恢复之前保存的状态
})
.autoPauseRedraw(false) // keep redrawing after engine has stopped如果您有依赖于画布的不断重绘的自定义动态对象建议关闭此选项。
.onNodeHover(node => {
hoverNode = node || null
if (node) {
node.name = builtTooltip(node)
}
})
.centerAt(this.centerPonit.x, this.centerPonit.y)// 设置中心节点位置
.zoom(0.9999)
.onNodeClick(async (node, e) => {
this.isClicking = true
this.clickNode = node || null
if (node.type !== 'tempNode') {
this.rightBox.show = true
this.cleanTempItems()
// 点击entityNode查询数据并根据数据生成tempNode
if (node.type === nodeType.entityNode) {
node.fx = node.x
node.fy = node.y
this.rightBox.loading = true
try {
// 若已查过数据,不重复查询
if (!node.data.relatedEntities) {
await node.queryDetailData()
}
const toAddNodes = []
const toAddLinks = []
let keyCount = 0
Object.keys(node.data.relatedEntities).forEach((k, i) => {
if (node.data.relatedEntities[k].total) {
keyCount++
}
})
let nodeIndex = 0// 临时节点需要total大于0的所以不可以用循环中的i角度计算index
Object.keys(node.data.relatedEntities).forEach((k) => {
if (node.data.relatedEntities[k].total) {
// 若已有同级同类型的listNode不生成此tempNode
const neighbors = node.getNeighbors(this.graph.graphData())
const hasListNode = neighbors.nodes.some(b => b.data.entityType === k)
if (!hasListNode) {
const tempNode = new Node(
nodeType.tempNode,
`${node.realId}__${k}__temp`,
{
entityType: k,
...this.generateTempNodeCoordinate(node.getSourceNode(this.graph.graphData()), e)
},
node,
this.getIconUrl(k, false, false)
)
// 临时节点的初始固定坐标为其对应的entity节点坐标展示直线动画
const tempNodePoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x, y: node.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度
const finalTempNodePoint = this.pointOfLine({ x: node.x, y: node.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength
2024-06-13 10:25:52 +08:00
if (tempNodePoint.x && tempNodePoint.y) {
const tempNodeDistance = this.getShortenedLength(tempNode) + this.defaultArrowSize// 临时节点展示动画时临时节点的初始位置距离entity节点的距离
// 计算箭头角度
const dx = finalTempNodePoint.x - node.x
const dy = finalTempNodePoint.y - node.y
const angle = Math.atan2(dy, dx) // 计算与x轴的角度弧度
tempNode.fx = node.x + tempNodeDistance * Math.cos(angle)
tempNode.fy = node.y + tempNodeDistance * Math.sin(angle)
tempNode.realEndX = finalTempNodePoint.x
tempNode.realEndY = finalTempNodePoint.y
}
const tempLink = new Link(node, tempNode, linkType.temp)
toAddNodes.push(tempNode)
toAddLinks.push(tempLink)
}
}
})
if (toAddNodes.length || toAddLinks.length) {
this.addItems(toAddNodes, toAddLinks, false)
}
this.rightBox.node = node
this.rightBox.mode = 'detail'
} catch (e) {
console.error(e)
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
2024-06-13 10:25:52 +08:00
}
} else if (node.type === nodeType.listNode) {
this.rightBox.node = node
this.rightBox.mode = 'list'
} else if (node.type === nodeType.rootNode) {
this.rightBox.node = node
this.rightBox.mode = 'detail'
2024-06-13 10:25:52 +08:00
}
} else {
// 点击tempNode根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。
// 若已达第六层则只生成listNode不再展开entityNode
const nodes = []
const links = []
const stackNodes = []
const stackLinks = []
const sourceNode = node.getSourceNode(this.graph.graphData())
const k1 = (node.x - sourceNode.x) / linkDistance.normal
const k2 = (node.y - sourceNode.y) / linkDistance.normal
const listNode = new Node(
nodeType.listNode,
`${sourceNode.realId}__${node.data.entityType}-list`,
{
entityType: node.data.entityType,
fx: sourceNode.x + k1 * linkDistance.entityToList,
fy: sourceNode.y + k2 * linkDistance.entityToList
},
sourceNode,
this.getIconUrl(node.data.entityType, false, false)
)
nodes.push(listNode)
links.push(new Link(sourceNode, listNode))
stackNodes.push(_.cloneDeep(listNode))
stackLinks.push(_.cloneDeep(links[0]))
this.addItems(nodes, links, false)
this.cleanTempItems()
this.clickNode = listNode
this.rightBox.mode = 'list'
setTimeout(async () => {
// 判断listNode的sourceNode层级若大于等于10即第6层listNode则不继续拓展entity node并给用户提示。否则拓展entity node
const level = this.getNodeLevel(listNode.sourceNode.id)
if (level < 10) {
2024-06-14 14:59:01 +08:00
this.rightBox.loading = true
nodes.splice(0, nodes.length)
links.splice(0, links.length)
2024-06-13 10:25:52 +08:00
try {
const entities = await sourceNode.queryRelatedEntities(listNode.data.entityType)
sourceNode.data.relatedEntities[listNode.data.entityType].list.push(...entities.list)
const curNodes = this.graph.graphData().nodes
entities.list.forEach(entity => {
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
entityType: listNode.data.entityType,
entityName: entity.vertex,
x: listNode.x + Math.random() * 10 - 5,
y: listNode.y + Math.random() * 10 - 5
}, listNode, this.getIconUrl(listNode.data.entityType, false, false))
nodes.push(entityNode)
const link = new Link(listNode, entityNode)
links.push(link)
if (curNodes.findIndex(node => node.id === entityNode.id) === -1) { // 过滤掉已有的节点
stackNodes.push(_.cloneDeep(entityNode))
}
stackLinks.push(_.cloneDeep(link))
2024-06-13 10:25:52 +08:00
})
this.addItems(nodes, links, false)
// 由于点击临时节点后增加节点分为了2步所以需要将2步的所有节点一起缓存
this.stackData.undo.push({ nodes: stackNodes, links: stackLinks })
setTimeout(() => {
listNode.fx = null
listNode.fy = null
this.rightBox.node = _.cloneDeep(listNode)
}, 100)
2024-06-13 10:25:52 +08:00
} catch (e) {
2024-06-14 14:59:01 +08:00
console.error(e)
this.$message.error(this.errorMsgHandler(e))
2024-06-13 10:25:52 +08:00
} finally {
2024-06-14 14:59:01 +08:00
this.rightBox.loading = false
}
} else {
this.$message.warning(this.$t('tip.maxExpandDepth'))
}
if (this.stackData.justUndo) {
this.stackData.justUndo = false
this.stackData.redo = []
}
if (this.stackData.justRedo) {
this.stackData.justRedo = false
this.stackData.redo = []
}
}, 200)
}
})
.d3Force('link', d3.forceLink().id(link => link.id)
.distance(link => {
if (link.source.type === nodeType.rootNode) {
return linkDistance.root
} else if (link.source.type === nodeType.entityNode && link.target.type === nodeType.listNode) {
return linkDistance.entityToList
}
return linkDistance.normal
2024-06-13 10:25:52 +08:00
})
.strength(link => {
return link.strength
}))// 设置线对点的力?
.d3Force('center', d3.forceCenter().strength(0))// 设置力导图点阵中心位置, 向心力设置为0以后d3.forceCenter(-50,-70)不起作用了
.d3Force('charge', d3.forceManyBody().strength(this.defaultChargeStrength))
.onNodeDrag((node, translate) => {
const { nodes, links } = this.graph.graphData()
// 拖动entity节点时如果此entity节点有临时节点则同时移动临时节点
if (node.type === nodeType.entityNode) {
this.isDraggingEntityNode = true
// 查询点击entity节点对应的list节点
const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x
const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y
const from = { x: fromX, y: fromY }
const centerX = node.preDragX ? node.preDragX : node.x
const centerY = node.preDragY ? node.preDragY : node.y
const center = { x: centerX, y: centerY }
2024-06-13 10:25:52 +08:00
const tempLinks = links.filter(link => link.source.id === node.id && link.type === linkType.temp)
tempLinks.forEach(link => {
const tempNodeGroup = nodes.filter(node => node.id === link.target.id)
tempNodeGroup.forEach(tempNode => {
const toX = tempNode.fx ? tempNode.fx : tempNode.x
const toY = tempNode.fy ? tempNode.fy : tempNode.y
const to = { x: toX, y: toY }
if (!tempNode.angle) { // 每次拖拽,每个临时节点计算一次角度即可,因为角度不会发生改变
tempNode.angle = this.angleOfRotate(from, to, center)
}
const toPoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x + translate.x, y: node.y + translate.y }, tempNode.angle)
// 因为在拖拽的过长中list节点到entity节点之间的距离是变化的且我们要求的是临时节点到entity节点之间的距离不发生变化所以此处需要再次进行计算转换得到最终的坐标
// 已知2个点的坐标求从起点开始指定长度线段的终点坐标
if (!tempNode.lineLength) { // 每次拖拽,每个临时节点计算一次与实体节点的距离即可,因为距离在当前拖拽中不会发生改变
tempNode.lineLength = Math.sqrt(Math.pow(node.x - tempNode.fx, 2) + Math.pow(node.y - tempNode.fy, 2))
}
const finalTo = this.pointOfLine({ x: node.x + translate.x, y: node.y + translate.y }, toPoint, tempNode.lineLength)
tempNode.fx = finalTo.x
tempNode.fy = finalTo.y
})
})
}
// 拖动list节点时如果此list节点对应的entity节点有临时节点则同时移动临时节点
if (node.type === nodeType.listNode) {
// 根据list节点找到list节点连接的线
const listLinks = links.filter(link => link.source.id === node.id)
const fromX = node.preDragX ? node.preDragX : node.x
const fromY = node.preDragY ? node.preDragY : node.y
listLinks.forEach(link => {
// 找到连接临时节点的虚线
const tempLinks = links.filter(entityLink => entityLink.source.id === link.target.id && entityLink.type === linkType.temp)
if (tempLinks && tempLinks.length > 0) {
tempLinks.forEach(tempLink => {
// 找到entity节点
const entityNode = nodes.find(normalNode => normalNode.id === tempLink.source.id && normalNode.type === nodeType.entityNode)
if (entityNode) {
const entityNodeX = entityNode.fx ? entityNode.fx : entityNode.x
const entityNodeY = entityNode.fy ? entityNode.fy : entityNode.y
const angle = this.angleOfRotate({ x: fromX, y: fromY }, { x: node.x + translate.x, y: node.y + translate.y }, { x: entityNodeX, y: entityNodeY })
const tempNodes = nodes.filter(normalNode => normalNode.id === tempLink.target.id && normalNode.type === nodeType.tempNode)// 找到临时节点
tempNodes.forEach(tempNode => {
const tempNodeX = tempNode.fx ? tempNode.fx : tempNode.x
const tempNodeY = tempNode.fy ? tempNode.fy : tempNode.y
const sourceNodeX = tempLink.source.fx ? tempLink.source.fx : tempLink.source.x
const sourceNodeY = tempLink.source.fy ? tempLink.source.fy : tempLink.source.y
// rotate为list节点旋转的角度根据旋转角度及临时节点的位置及entity节点的坐标得到临时节点旋转后的坐标
const to = this.pointOfRotate({ x: tempNodeX, y: tempNodeY }, { x: sourceNodeX, y: sourceNodeY }, angle)
tempNode.fx = to.x
tempNode.fy = to.y
})
}
})
}
})
}
// 记录上次拖拽时节点的坐标,否则计算时出现误差。
node.preDragX = node.x + translate.x
node.preDragY = node.y + translate.y
})
.cooldownTime(2000)// 到时间后才执行onEngineStop
.onNodeDragEnd((node, translate) => { // 修复拖动节点
this.isDraggingEntityNode = false
node.fx = node.x
node.fy = node.y
// 拖拽结束时把所有临时节点的的angle置为null,便于拖动其它节点时的计算
this.graph.graphData().nodes.forEach(node => {
if (node.type === nodeType.tempNode) {
node.angle = null
node.lineLength = null
}
})
})
.onEngineStop(() => {
this.releaseNodes(this.graph.graphData().nodes,nodeType.listNode)
})
.onEngineTick(() => {
if (this.isClicking) {
const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode)
if (tempNodeGroup.length > 0) {
const keyCount = tempNodeGroup.length
let nodeIndex = 0// 临时节点需要total大于0的所以不可以用循环中的i角度计算index
tempNodeGroup.forEach((tempNode) => {
const tempNodeSource = tempNode.sourceNode
if (tempNodeSource) {
const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度为以entity节点为centerlist节点为from旋转到临时节点的角度
if (tempNodePoint.x && tempNodePoint.y) {
tempNode.fx = tempNode.realEndX
tempNode.fy = tempNode.realEndY
}
}
})
this.isClicking = false
}
}
})
},
/*
sleep() {
for (let i = 0; i < 100000000*2; i++) {
let num = i + i
}
},*/
/*
* 线性插值: 已知两点坐标计算已知x或y的C点坐标且C点在AB连线上
*
* */
linearInterpolate (coord1, coord2) {
const scale = (coord1[0] - coord2[0]) / (coord1[1] - coord2[1])
return {
getY: (x) => (x - coord1[0]) / scale + coord1[1],
getX: (y) => (y - coord1[1]) / scale + coord1[0]
}
2023-07-09 21:51:05 +08:00
},
2024-06-27 10:19:28 +08:00
getTempNodeAngle (nodeCount, i) {
switch (nodeCount) {
case 1:
return 180
case 2:
2024-06-27 10:19:28 +08:00
return 150 + i * 60
case 3:
2024-06-27 10:19:28 +08:00
return 150 + i * 30
}
},
getEntityNodeAngle (nodeCount, i) {
let defaultAngle = 15
let remainder = i%2
if(i === 0) {
return 0
} else if(remainder === 0) {
return i/2 * defaultAngle * -1
} else {
return (i+1)/2 * defaultAngle
}
},
getListNodeAngle (nodeCount, i) {
switch (nodeCount) {
case 1:
return 0
case 2:
return i * 140 * -1
case 3:
return i * 120
}
},
2024-06-27 10:19:28 +08:00
// 根据3个点坐标计算节点旋转的角度
angleOfRotate (from, to, center) {
const ab = {}
const ac = {}
ab.x = from.x - center.x
ab.y = from.y - center.y
ac.x = to.x - center.x
ac.y = to.y - center.y
2024-06-27 10:19:28 +08:00
const direct = (ab.x * ac.y) - (ab.y * ac.x)// 求节点是顺时针还是逆时针旋转
const lengthAb = Math.sqrt(Math.pow(center.x - from.x, 2) + Math.pow(center.y - from.y, 2))
const lengthAc = Math.sqrt(Math.pow(center.x - to.x, 2) + Math.pow(center.y - to.y, 2))
const lengthBc = Math.sqrt(Math.pow(from.x - to.x, 2) + Math.pow(from.y - to.y, 2))
const cosA = (Math.pow(lengthAb, 2) + Math.pow(lengthAc, 2) - Math.pow(lengthBc, 2)) / (2 * lengthAb * lengthAc)// 余弦定理求出旋转角
let angleA = Math.acos(cosA) * 180 / Math.PI
2024-06-27 10:19:28 +08:00
if (direct < 0) { // 负数表示逆时针旋转,正数表示顺时针旋转
angleA = -angleA
}
return angleA
},
// from: 圆上某点(初始点);
// center: 圆心点;
// angle: 旋转角度° -- [angle * M_PI / 180]:将角度换算为弧度
// 【注意】angle 逆时针为正,顺时针为负
2024-06-27 10:19:28 +08:00
pointOfRotate (from, center, angle) {
const a = center.x
const b = center.y
const x0 = from.x
const y0 = from.y
const rx = a + (x0 - a) * Math.cos(angle * Math.PI / 180) - (y0 - b) * Math.sin(angle * Math.PI / 180)
const ry = b + (x0 - a) * Math.sin(angle * Math.PI / 180) + (y0 - b) * Math.cos(angle * Math.PI / 180)
if (rx && ry) {
return { x: rx, y: ry }
} else {
return from
}
},
2024-06-27 10:19:28 +08:00
// 已知2个点的坐标求从起点开始指定长度线段的终点坐标
pointOfLine (start, end, lineLength) {
// 计算总距离
2024-06-27 10:19:28 +08:00
const totalDistance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2))
const cosA = (end.x - start.x) / totalDistance
const sinA = (end.y - start.y) / totalDistance
const finalX = start.x + cosA * lineLength
const finalY = start.y + sinA * lineLength
2024-06-27 10:19:28 +08:00
return { x: finalX, y: finalY }
},
async handleToolsbarClick (code) {
2024-06-27 10:19:28 +08:00
if (code === 'undo') { // 上一步
2024-06-13 10:25:52 +08:00
const data = this.stackData.undo.pop()
this.stackData.justUndo = true
2024-06-27 10:19:28 +08:00
if (data) {
const { nodes, links } = this.graph.graphData()
data.nodes.forEach(popNode => {
const popNodeIndex = nodes.findIndex(item => item.id === popNode.id)
if (popNodeIndex > -1) {
2024-06-27 10:19:28 +08:00
nodes.splice(popNodeIndex, 1)
}
})
data.links.forEach(link => {
const linksIndex = links.findIndex(item => item.source.id === link.source && item.target.id === link.target)
if (linksIndex > -1) {
2024-06-27 10:19:28 +08:00
links.splice(linksIndex, 1)
}
})
this.cleanTempItems()
this.stackData.redo.push(data)
if (this.stackData.justRedo) {
this.stackData.justRedo = false
2024-06-13 10:25:52 +08:00
}
this.onCloseBlock()
2024-06-13 10:25:52 +08:00
}
2024-06-27 10:19:28 +08:00
} else if (code === 'redo') { // 下一步
2024-06-13 10:25:52 +08:00
const data = this.stackData.redo.pop()
2024-06-27 10:19:28 +08:00
if (data) {
this.stackData.justRedo = true
this.addItems(data.nodes, data.links)
this.cleanTempItems()
if (this.stackData.justUndo) {
this.stackData.justUndo = false
}
this.onCloseBlock()
2024-06-13 10:25:52 +08:00
}
} else if (code === 'autoZoom') {
2024-06-27 10:19:28 +08:00
this.graph.zoomToFit(100, 100)
if(this.rightBox.show) {
this.graph.width(document.body.clientWidth - this.rightBoxWidth)
} else {
this.graph.width(document.body.clientWidth)
}
2024-06-13 10:25:52 +08:00
} else if (code === 'zoomOut') {
this.graph.zoom(this.graph.zoom() + 0.2)
} else if (code === 'zoomIn') {
this.graph.zoom(this.graph.zoom() - 0.2)
} else {
this.clickNode = this.rootNode
this.stackData = {
undo: [],
redo: [],
justUndo: false,
justRedo: false
}
let initData = _.cloneDeep(this.initialData)
/*
initData.nodes.forEach(node => {
if(node.type === nodeType.entityNode) {
node.x = null
node.y = null
}
})*/
this.initForceGraph(initData) // 初始化拓展图
this.rightBox.show = true
this.rightBox.node = this.rootNode
this.rightBox.mode = 'detail'
2024-06-13 10:25:52 +08:00
}
},
getNodeStyle (curNodeType, entityType) {
switch (curNodeType) {
case nodeType.rootNode: {
2024-06-14 14:59:01 +08:00
return this.getRootNodeStyle(entityType)
2024-06-13 10:25:52 +08:00
}
case nodeType.listNode: {
2024-06-14 14:59:01 +08:00
return this.getListNodeStyle(entityType)
2024-06-13 10:25:52 +08:00
}
case nodeType.entityNode: {
2024-06-14 14:59:01 +08:00
return this.getEntityNodeStyle(entityType)
2024-06-13 10:25:52 +08:00
}
case nodeType.tempNode: {
2024-06-18 15:40:20 +08:00
return this.getEntityNodeStyle(entityType)
2024-06-13 10:25:52 +08:00
}
}
},
getRootNodeStyle (entityType) {
const nodeStyle = {
2024-06-18 15:40:20 +08:00
innerR: 19,
fillStyle: '#FFFFFF',
shadowR: 25,
selectedShadowR: 36,
font: '9px NotoSansSChineseRegular',
fontColor: '#353636',
fontBgColor: 'rgba(255,255,255,0.8)'
2024-06-13 10:25:52 +08:00
}
switch (entityType) {
case 'ip': {
return {
...nodeStyle,
2024-06-18 15:40:20 +08:00
iconWidth: 24,
iconHeight: 21,
2024-06-13 10:25:52 +08:00
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
}
case 'domain': {
return {
...nodeStyle,
2024-06-18 15:40:20 +08:00
iconWidth: 24,
2024-06-13 10:25:52 +08:00
iconHeight: 24,
borderColor: 'rgba(56,172,210,1)',
shadowColor: 'rgba(56,172,210,0.21)',
hoveredShadowColor: 'rgba(56,172,210,0.36)',
selectedShadowColor: 'rgba(56,172,210,0.1)'
}
}
case 'app': {
return {
...nodeStyle,
2024-06-18 15:40:20 +08:00
iconWidth: 21,
iconHeight: 24,
2024-06-13 10:25:52 +08:00
borderColor: 'rgba(229,162,25,1)',
shadowColor: 'rgba(229,162,25,0.21)',
hoveredShadowColor: 'rgba(229,162,25,0.36)',
selectedShadowColor: 'rgba(229,162,25,0.1)'
}
}
}
return {
...nodeStyle,
2024-06-18 15:40:20 +08:00
iconWidth: 24,
iconHeight: 21,
2024-06-13 10:25:52 +08:00
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
},
getListNodeStyle (entityType) {
2024-06-18 15:40:20 +08:00
let iconWidth = 20
let iconHeight = 18
2024-06-13 10:25:52 +08:00
switch (entityType) {
case 'ip': {
2024-06-18 15:40:20 +08:00
iconWidth = 20
iconHeight = 18
break
2024-06-13 10:25:52 +08:00
}
case 'domain': {
2024-06-18 15:40:20 +08:00
iconWidth = 20
iconHeight = 20
break
2024-06-13 10:25:52 +08:00
}
case 'app': {
2024-06-18 15:40:20 +08:00
iconWidth = 18
iconHeight = 20
break
2024-06-13 10:25:52 +08:00
}
}
return {
2024-06-18 15:40:20 +08:00
innerR: 16,
shadowR: 22,
fillStyle: '#FFFFFF',
font: '9px NotoSansSChineseRegular',
fontColor: '#353636',
fontBgColor: 'rgba(255,255,255,0.8)',
iconWidth,
iconHeight,
borderColor: 'rgba(119,131,145,0.5)',
selectedBorderColor: 'rgba(119,131,145,0.8)',
2024-06-13 10:25:52 +08:00
hoveredShadowColor: 'rgba(151,151,151,0.21)',
2024-06-18 15:40:20 +08:00
selectedShadowColor: 'rgba(151,151,151,0.4)'
2024-06-13 10:25:52 +08:00
}
},
getEntityNodeStyle (entityType) {
2024-06-18 15:40:20 +08:00
let iconWidth = 20
let iconHeight = 18
2024-06-13 10:25:52 +08:00
switch (entityType) {
case 'ip': {
2024-06-18 15:40:20 +08:00
iconWidth = 20
iconHeight = 18
break
2024-06-13 10:25:52 +08:00
}
case 'domain': {
2024-06-18 15:40:20 +08:00
iconWidth = 20
iconHeight = 20
break
2024-06-13 10:25:52 +08:00
}
case 'app': {
2024-06-18 15:40:20 +08:00
iconWidth = 18
iconHeight = 20
break
2024-06-13 10:25:52 +08:00
}
}
return {
2024-06-18 15:40:20 +08:00
shadowR: 15,
innerR: 10,
fillStyle: '#FFFFFF',
iconWidth,
iconHeight,
hoveredShadowColor: 'rgba(151,151,151,0.12)',
selectedShadowColor: 'rgba(151,151,151,0.24)'
2024-06-13 10:25:52 +08:00
}
},
/**
* 求直线与圆的交点坐标
* x2, y2, r
* 线段 x1, y1, x2, y2
* */
2024-06-13 10:25:52 +08:00
findCircleLineIntersection (x2, y2, r, x1, y1) {
r = r + +this.defaultMargin
const xx = x1 - x2
const yy = y1 - y2
let n = Math.sqrt(Math.pow(r, 2) / [Math.pow(yy / xx, 2) + 1])
let m = Math.sqrt(Math.pow(r, 2) - Math.pow(n, 2))
if (xx > 0) {
if (yy > 0) { // 第四象限
} else { // 第一象限
m *= -1
}
} else {
n *= -1
if (yy > 0) { // 第三象限
} else { // 第二象限
m *= -1
}
}
const x = x2 + n
const y = y2 + m
return [{ x, y }]
},
/**
ctx Canvas绘图环境
fromX, fromY 起点坐标也可以换成 p1 只不过它是一个数组
toX, toY 终点坐标 (也可以换成 p2 只不过它是一个数组)
theta 三角斜边一直线夹角
headlen 三角斜边长度
width 箭头线宽度
color 箭头颜色
*/
2024-06-13 10:25:52 +08:00
drawArrow (ctx, fromX, fromY, toX, toY, theta, headlen) {
theta = typeof (theta) != 'undefined' ? theta : 30
headlen = typeof (theta) != 'undefined' ? headlen : 10
// 计算各角度和对应的P2,P3坐标
const angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI
const angle1 = (angle + theta) * Math.PI / 180
const angle2 = (angle - theta) * Math.PI / 180
const topX = headlen * Math.cos(angle1)
const topY = headlen * Math.sin(angle1)
const botX = headlen * Math.cos(angle2)
const botY = headlen * Math.sin(angle2)
let arrowX = fromX - topX
let arrowY = fromY - topY
arrowX = toX + topX
arrowY = toY + topY
ctx.moveTo(arrowX, arrowY)
ctx.lineTo(toX, toY)
arrowX = toX + botX
arrowY = toY + botY
ctx.lineTo(arrowX, arrowY)
ctx.lineTo(toX + topX, toY + topY)
},
getNodeLevel (id) {
return 5
2024-06-14 14:59:01 +08:00
const { findShortestPath } = Algorithm
const { nodes, links } = this.graph.graphData()
const g6FormatData = { nodes: _.cloneDeep(nodes), edges: _.cloneDeep(links) }
g6FormatData.edges.forEach(l => {
l.source = l.source.id
l.target = l.target.id
})
const info = findShortestPath(g6FormatData, this.entity.entityName, id)
2024-06-14 14:59:01 +08:00
return info.length
2024-06-13 10:25:52 +08:00
},
generateTempNodeCoordinate (sourceNode, event) {
const sx = sourceNode.x
const sy = sourceNode.y
const cx = event.x
const cy = event.y
return {
x: cx + cx - sx + Math.random() * 30 - 15,
y: cy + cy - sy + Math.random() * 30 - 15
}
},
2024-06-27 10:19:28 +08:00
addItems (toAddNodes, toAddLinks, stack = true) {
2024-06-14 14:59:01 +08:00
if (toAddNodes.length || toAddLinks.length) {
const { nodes, links } = this.graph.graphData()
const nodes2 = toAddNodes.filter(n => !nodes.some(n2 => n.id === n2.id))
nodes.push(...nodes2)
links.push(...toAddLinks)
this.graph.graphData({ nodes, links })
2024-06-27 10:19:28 +08:00
if (stack) {
this.stackData.undo.push(_.cloneDeep({ nodes: nodes2, links: toAddLinks }))
}
2024-06-13 10:25:52 +08:00
}
},
cleanTempItems () {
const { nodes, links } = this.graph.graphData()
const newNodes = nodes.filter(n => n.type !== nodeType.tempNode)
2024-06-18 16:32:27 +08:00
const newLinks = links.filter(l => l.type !== linkType.temp)
if (newNodes.length !== nodes.length || newLinks.length !== links.length) {
this.graph.graphData({ nodes: newNodes, links: newLinks })
}
2024-06-27 10:19:28 +08:00
// 清理临时节点时同时释放临时节点对应的entity节点,不再固定位置
const tempNodes = nodes.filter(n => n.type === nodeType.tempNode)
tempNodes.forEach(n => {
2024-06-27 10:19:28 +08:00
if (n.sourceNode) {
n.sourceNode.fx = null
n.sourceNode.fy = null
}
})
2024-06-27 10:19:28 +08:00
// 点击节点的时候把所有list节点的的preDragX和preDragY置为null,便于拖动其它节点时的计算
nodes.forEach(node => {
2024-06-27 10:19:28 +08:00
// if(node.type === nodeType.listNode) {
node.preDragX = null
node.preDragY = null
// }
})
2024-06-13 10:25:52 +08:00
},
async generateInitialData (clickNode) {
const nodes = []
const links = []
2024-06-13 10:25:52 +08:00
2024-06-25 18:04:10 +08:00
const rootNode = clickNode || new Node(nodeType.rootNode, this.entity.entityName, this.entity, null, this.getIconUrl(this.entity.entityType, true, true))
await rootNode.queryDetailData()
nodes.push(rootNode)
2024-06-13 10:25:52 +08:00
this.clickNode = rootNode
this.rootNode = rootNode
2024-06-13 10:25:52 +08:00
// 生成listNode和entityNode及edge
if (rootNode.data.relatedEntities) {
// listNode
const listNodes = []
2024-06-13 10:25:52 +08:00
const keys = Object.keys(rootNode.data.relatedEntities)
let keyCount = 0
keys.forEach((k, i) => {//确定root节点有几个分支
if (rootNode.data.relatedEntities[k].total) {
keyCount++
}
})
let nodeIndex = 0// list节点需要total大于0的所以不可以用循环中的i角度计算index
keys.forEach(k => {
2024-06-13 10:25:52 +08:00
if (rootNode.data.relatedEntities[k].total) {
let angle = this.getListNodeAngle(keyCount, nodeIndex++)
const toPoint = this.pointOfRotate({x:linkDistance.root,y:1},{x:0,y:0},angle)//y如果设置为0则展示的位置不对所以设置一个较小的y为1
const listNode = new Node(
2024-06-13 10:25:52 +08:00
nodeType.listNode,
`${rootNode.realId}__${k}-list`,
{
entityType: k,
fx: toPoint.x,
fy: toPoint.y
},
rootNode,
this.getIconUrl(k, false, false)
)
listNodes.push(listNode)
2024-06-18 16:32:27 +08:00
links.push(new Link(rootNode, listNode))
}
})
// entityNode
const entityNodes = []
2024-06-13 10:25:52 +08:00
for (const listNode of listNodes) {
const entities = await rootNode.queryRelatedEntities(listNode.data.entityType)
rootNode.data.relatedEntities[listNode.data.entityType].list = entities.list
entities.list.forEach(entity => {
const entityNode = new Node(
2024-06-13 10:25:52 +08:00
nodeType.entityNode,
entity.vertex,
{
entityType: listNode.data.entityType,
entityName: entity.vertex
},
listNode,
2024-06-18 16:32:27 +08:00
this.getIconUrl(listNode.data.entityType, false, false)
)
entityNodes.push(entityNode)
2024-06-18 16:32:27 +08:00
links.push(new Link(listNode, entityNode))
})
}
nodes.push(...listNodes, ...entityNodes)
this.releaseNodes(nodes,nodeType.listNode)
}
this.rightBox.node = rootNode
return {
2024-06-13 10:25:52 +08:00
nodes,
links
}
2023-07-09 21:51:05 +08:00
},
//释放节点
releaseNodes(allNodes,nodeType) {
setTimeout(() => {
// 释放list节点,不再固定位置
const filterNodes = allNodes.filter(n => n.type === nodeType)
filterNodes.forEach(n => {
n.fx = null
n.fy = null
})
}, 100)
},
2024-06-13 10:25:52 +08:00
async expandList (nodeId) {
2024-06-14 14:59:01 +08:00
const { nodes } = this.graph.graphData()
const node = nodes.find(n => n.id === nodeId)
const sourceNode = nodes.find(n => n.id === node.sourceNode.id)
const expandType = node.data.entityType
if (sourceNode.data.relatedEntities[expandType].list.length >= sourceNode.data.relatedEntities[expandType].total) {
2024-06-13 10:25:52 +08:00
return
}
2024-06-14 14:59:01 +08:00
if (sourceNode.data.relatedEntities[expandType].list.length < 50) {
2024-06-27 10:19:28 +08:00
// this.rightBox.loading = true
2024-06-13 10:25:52 +08:00
try {
2024-06-14 14:59:01 +08:00
const entities = await sourceNode.queryRelatedEntities(expandType)
sourceNode.data.relatedEntities[expandType].list.push(...entities.list)
const toAddNodes = []
const toAddLinks = []
2024-06-13 10:25:52 +08:00
entities.list.forEach(entity => {
2024-06-14 14:59:01 +08:00
const toAddNode = new Node(nodeType.entityNode, entity.vertex, {
2024-06-13 10:25:52 +08:00
entityType: expandType,
entityName: entity.vertex,
x: node.x + Math.random() * 10 - 5,
y: node.y + Math.random() * 10 - 5
2024-06-25 18:04:10 +08:00
}, node, this.getIconUrl(node.data.entityType, false, false))
2024-06-14 14:59:01 +08:00
toAddNodes.push(toAddNode)
2024-06-18 16:32:27 +08:00
const toAddLink = new Link(node, toAddNode)
2024-06-14 14:59:01 +08:00
toAddLinks.push(toAddLink)
2024-06-13 10:25:52 +08:00
})
2024-06-14 14:59:01 +08:00
this.addItems(toAddNodes, toAddLinks)
this.rightBox.node = _.cloneDeep(node)
2024-06-13 10:25:52 +08:00
} catch (e) {
2024-06-14 14:59:01 +08:00
console.error(e)
2024-06-13 10:25:52 +08:00
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
}
} else {
this.$message.warning(this.$t('tip.maxExpandCount'))
}
2023-07-09 21:51:05 +08:00
},
2024-06-13 10:25:52 +08:00
async expandDetailList (nodeId, expandType) {
2024-06-14 14:59:01 +08:00
const { nodes } = this.graph.graphData()
const node = nodes.find(n => n.id === nodeId)
2024-06-13 10:25:52 +08:00
if (node) {
2024-06-14 14:59:01 +08:00
if (node.data.relatedEntities[expandType].list.length >= node.data.relatedEntities[expandType].total) {
2024-06-13 10:25:52 +08:00
return
}
2024-06-14 14:59:01 +08:00
if (node.data.relatedEntities[expandType].list.length < 50) {
const toAddNodes = []
const toAddLinks = []
2024-06-27 10:19:28 +08:00
// this.rightBox.loading = true
2024-06-13 10:25:52 +08:00
try {
2024-06-14 14:59:01 +08:00
const entities = await node.queryRelatedEntities(expandType)
node.data.relatedEntities[expandType].list.push(...entities.list)
// 移除 tempNode 和 tempEdge
this.cleanTempItems()
const neighbors = node.getNeighbors(this.graph.graphData())
2024-06-14 14:59:01 +08:00
let listNode = neighbors.targetNodes.find(n => n.data.entityType === expandType)
if (!listNode) {
2024-06-25 18:04:10 +08:00
listNode = new Node(nodeType.listNode, `${node.id}__${expandType}-list`, { entityType: expandType }, node, this.getIconUrl(expandType, false, false))
2024-06-18 16:32:27 +08:00
const link = new Link(node, listNode)
2024-06-14 14:59:01 +08:00
toAddNodes.push(listNode)
toAddLinks.push(link)
2024-06-13 10:25:52 +08:00
}
entities.list.forEach(entity => {
2024-06-14 14:59:01 +08:00
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
2024-06-13 10:25:52 +08:00
entityType: expandType,
entityName: entity.vertex,
x: listNode.x + Math.random() * 10 - 5,
y: listNode.y + Math.random() * 10 - 5
2024-06-25 18:04:10 +08:00
}, listNode, this.getIconUrl(expandType, false, false))
2024-06-14 14:59:01 +08:00
toAddNodes.push(entityNode)
2024-06-18 16:32:27 +08:00
toAddLinks.push(new Link(listNode, entityNode))
2024-06-13 10:25:52 +08:00
})
2024-06-14 14:59:01 +08:00
this.addItems(toAddNodes, toAddLinks)
this.rightBox.node = _.cloneDeep(node)
2024-06-13 10:25:52 +08:00
} catch (e) {
2024-06-14 14:59:01 +08:00
console.error(e)
2024-06-13 10:25:52 +08:00
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
}
2024-06-13 10:25:52 +08:00
} else {
this.$message.warning(this.$t('tip.maxExpandCount'))
2023-07-14 16:50:30 +08:00
}
2023-07-09 21:51:05 +08:00
}
},
2024-06-13 10:25:52 +08:00
onCloseBlock () {
this.rightBox.mode = ''
this.rightBox.show = false
},
resize () {
},
2024-06-13 10:25:52 +08:00
getIconUrl (entityType, colored, isRoot) {
2024-06-14 14:59:01 +08:00
const suffix = colored ? '-colored' : ''
2024-06-13 10:25:52 +08:00
const img = new Image()
img.src = require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
return img
}
},
watch: {
stackData: {
deep: true,
handler (n) {
if (n) {
if (n.undo.length > 0) {
document.getElementById('preStep').classList.remove('toolbar--unactivated')
} else {
document.getElementById('preStep').classList.add('toolbar--unactivated')
}
if (n.redo.length > 0) {
document.getElementById('nextStep').classList.remove('toolbar--unactivated')
} else {
document.getElementById('nextStep').classList.add('toolbar--unactivated')
}
}
}
},
'rightBox.show' : {
handler (n) {
if(!n) {
const canvasWidth = document.body.clientWidth
this.graph.width(canvasWidth)
}
}
}
2023-06-29 14:40:50 +08:00
},
async mounted () {
if (this.entity.entityType && this.entity.entityName) {
2024-06-14 14:59:01 +08:00
await this.init()
2024-06-13 10:25:52 +08:00
this.debounceFunc = this.$_.debounce(this.resize, 300)
window.addEventListener('resize', this.debounceFunc)
2023-06-29 14:40:50 +08:00
}
},
unmounted () {
window.removeEventListener('resize', this.debounceFunc)
2023-06-29 14:40:50 +08:00
},
setup () {
const route = useRoute()
const { entityType, entityName } = route.query
const entity = {
2023-06-29 14:40:50 +08:00
entityType,
entityName
}
// 关系图
2023-07-09 21:51:05 +08:00
const graph = shallowRef(null)
2023-07-02 22:38:59 +08:00
const rightBox = ref({
mode: 'detail', // list | detail
2024-06-18 16:32:27 +08:00
show: true,
node: null,
loading: true
2023-07-02 22:38:59 +08:00
})
2023-06-29 14:40:50 +08:00
return {
entity,
2023-07-09 21:51:05 +08:00
rightBox,
graph
2023-06-29 14:40:50 +08:00
}
2023-06-16 17:18:58 +08:00
}
}
</script>