From fa1bb055481e1468b97ceff5982e2606111635d6 Mon Sep 17 00:00:00 2001 From: TsMask <340112800@qq.com> Date: Thu, 21 Dec 2023 14:45:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=93=E6=89=91=E5=9B=BE=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E6=95=B0=E6=8D=AE=E6=98=BE=E7=A4=BA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/svg/base.svg | 1 + public/svg/cloud.svg | 1 + public/svg/service.svg | 1 + public/svg/service_db.svg | 1 + src/api/ne/ne.ts | 29 + src/views/monitor/topology/graph.ts | 1730 ++++++++++++++++++++++++++ src/views/monitor/topology/index.vue | 638 ++++------ 7 files changed, 1980 insertions(+), 421 deletions(-) create mode 100644 public/svg/base.svg create mode 100644 public/svg/cloud.svg create mode 100644 public/svg/service.svg create mode 100644 public/svg/service_db.svg create mode 100644 src/api/ne/ne.ts create mode 100644 src/views/monitor/topology/graph.ts diff --git a/public/svg/base.svg b/public/svg/base.svg new file mode 100644 index 00000000..1ea6656f --- /dev/null +++ b/public/svg/base.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svg/cloud.svg b/public/svg/cloud.svg new file mode 100644 index 00000000..24764060 --- /dev/null +++ b/public/svg/cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svg/service.svg b/public/svg/service.svg new file mode 100644 index 00000000..53425975 --- /dev/null +++ b/public/svg/service.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svg/service_db.svg b/public/svg/service_db.svg new file mode 100644 index 00000000..8db11e0a --- /dev/null +++ b/public/svg/service_db.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/api/ne/ne.ts b/src/api/ne/ne.ts new file mode 100644 index 00000000..3aa05cea --- /dev/null +++ b/src/api/ne/ne.ts @@ -0,0 +1,29 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询网元列表 + * @param query 查询参数 + * @returns object + */ +export function listNe(query: Record) { + return request({ + url: '/ne/list', + method: 'get', + params: query, + timeout: 60_000, + }); +} + +/** + * 查询网元状态 + * @param neType 网元类型 + * @param neId 网元ID + * @returns object + */ +export function stateNe(neType: string, neId: string) { + return request({ + url: '/ne/state', + method: 'get', + params: { neType, neId }, + }); +} diff --git a/src/views/monitor/topology/graph.ts b/src/views/monitor/topology/graph.ts new file mode 100644 index 00000000..b8868f66 --- /dev/null +++ b/src/views/monitor/topology/graph.ts @@ -0,0 +1,1730 @@ +import { + Algorithm, + Util, + registerNode, + registerEdge, + Layout, + Menu, + Graph, + Tooltip, +} from '@antv/g6'; + +const { labelPropagation, louvain, findShortestPath } = Algorithm; +const { uniqueId } = Util; + +const NODESIZEMAPPING = 'degree'; +const SMALLGRAPHLABELMAXLENGTH = 12; +let labelMaxLength = SMALLGRAPHLABELMAXLENGTH; +const DEFAULTNODESIZE = 30; +const DEFAULTAGGREGATEDNODESIZE = 56; +const NODE_LIMIT = 40; // TODO: find a proper number for maximum node number on the canvas + +let graph: any = null; +let currentUnproccessedData = { nodes: [], edges: [] }; +let nodeMap: any = {}; +let aggregatedNodeMap: any = {}; +let hiddenItemIds: any[] = []; // 隐藏的元素 id 数组 +let largeGraphMode = true; +let cachePositions: any = {}; +let manipulatePosition: any = undefined; +let descreteNodeCenter: any; +let layout: any = { + type: '', + instance: null, + destroyed: true, +}; +let expandArray: any = []; +let collapseArray: any = []; +let shiftKeydown = false; +let CANVAS_WIDTH = 800, + CANVAS_HEIGHT = 800; + +const duration = 2000; +const animateOpacity = 0.6; +const animateBackOpacity = 0.1; +const virtualEdgeOpacity = 0.1; +const realEdgeOpacity = 0.2; + +const darkBackColor = 'rgb(43, 47, 51)'; +const disableColor = '#777'; +const theme = 'dark'; // 'dark'; +const subjectColors = [ + '#5F95FF', // blue + '#61DDAA', + '#65789B', + '#F6BD16', + '#7262FD', + '#78D3F8', + '#9661BC', + '#F6903D', + '#008685', + '#F08BB4', +]; + +const colorSets = Util.getColorSetsBySubjectColors( + subjectColors, + darkBackColor, + theme, + disableColor +); + +const globalStyle = { + node: { + style: { + fill: '#2B384E', + }, + labelCfg: { + style: { + fill: '#acaeaf', + stroke: '#191b1c', + }, + }, + stateStyles: { + focus: { + fill: '#2B384E', + }, + }, + }, + edge: { + style: { + stroke: '#acaeaf', + realEdgeStroke: '#acaeaf', //'#f00', + realEdgeOpacity, + strokeOpacity: realEdgeOpacity, + }, + labelCfg: { + style: { + fill: '#acaeaf', + realEdgeStroke: '#acaeaf', //'#f00', + realEdgeOpacity: 0.5, + stroke: '#191b1c', + }, + }, + stateStyles: { + focus: { + stroke: '#fff', // '#3C9AE8', + }, + }, + }, +}; + +// Custom super node +registerNode( + 'aggregated-node', + { + draw(cfg, group) { + let width = 53, + height = 27; + const style = cfg.style || {}; + const colorSet = cfg.colorSet || colorSets[0]; + + // halo for hover + group.addShape('rect', { + attrs: { + x: -width * 0.55, + y: -height * 0.6, + width: width * 1.1, + height: height * 1.2, + fill: colorSet.mainFill, + opacity: 0.9, + lineWidth: 0, + radius: (height / 2 || 13) * 1.2, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'halo-shape', + visible: false, + }); + + // focus stroke for hover + group.addShape('rect', { + attrs: { + x: -width * 0.55, + y: -height * 0.6, + width: width * 1.1, + height: height * 1.2, + fill: colorSet.mainFill, // '#3B4043', + stroke: '#AAB7C4', + lineWidth: 1, + lineOpacty: 0.85, + radius: (height / 2 || 13) * 1.2, + }, + name: 'stroke-shape', + visible: false, + }); + + const keyShape = group.addShape('rect', { + attrs: { + x: -width / 2, + y: -height / 2, + width, + height, + fill: colorSet.mainFill, // || '#3B4043', + stroke: colorSet.mainStroke, + lineWidth: 2, + cursor: 'pointer', + radius: height / 2 || 13, + lineDash: [2, 2], + ...style, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'aggregated-node-keyShape', + }); + + let labelStyle = {}; + if (cfg.labelCfg) { + labelStyle = Object.assign(labelStyle, cfg.labelCfg.style); + } + group.addShape('text', { + attrs: { + text: `${cfg.count}`, + x: 0, + y: 0, + textAlign: 'center', + textBaseline: 'middle', + cursor: 'pointer', + fontSize: 12, + fill: '#fff', + opacity: 0.85, + fontWeight: 400, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'count-shape', + className: 'count-shape', + draggable: true, + }); + + // tag for new node + // if (cfg.new) { + // group.addShape('circle', { + // attrs: { + // x: width / 2 - 3, + // y: -height / 2 + 3, + // r: 4, + // fill: '#6DD400', + // lineWidth: 0.5, + // stroke: '#FFFFFF', + // }, + // // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + // name: 'typeNode-tag-circle', + // }); + // } + return keyShape; + }, + setState: (name, value, item: any) => { + const group = item.get('group'); + if (name === 'layoutEnd' && value) { + const labelShape = group.find( + (e: any) => e.get('name') === 'text-shape' + ); + if (labelShape) labelShape.set('visible', true); + } else if (name === 'hover') { + if (item.hasState('focus')) { + return; + } + const halo = group.find((e: any) => e.get('name') === 'halo-shape'); + const keyShape = item.getKeyShape(); + const colorSet = item.getModel().colorSet || colorSets[0]; + if (value) { + halo && halo.show(); + keyShape.attr('fill', colorSet.activeFill); + } else { + halo && halo.hide(); + keyShape.attr('fill', colorSet.mainFill); + } + } else if (name === 'focus') { + const stroke = group.find((e: any) => e.get('name') === 'stroke-shape'); + const keyShape = item.getKeyShape(); + const colorSet = item.getModel().colorSet || colorSets[0]; + if (value) { + stroke && stroke.show(); + keyShape.attr('fill', colorSet.selectedFill); + } else { + stroke && stroke.hide(); + keyShape.attr('fill', colorSet.mainFill); + } + } + }, + update: undefined, + }, + 'single-node' +); + +// Custom real node +registerNode( + 'real-node', + { + draw(cfg, group) { + let r = 30; + if (typeof cfg.size === 'number') { + r = cfg.size / 2; + } else if (Array.isArray(cfg.size)) { + r = cfg.size[0] / 2; + } + const style = cfg.style || {}; + const colorSet = cfg.colorSet || colorSets[0]; + console.log('=== ', cfg); + + // halo for hover + group.addShape('circle', { + attrs: { + x: 0, + y: 0, + r: r + 5, + fill: style.fill || colorSet.mainFill || '#2B384E', + opacity: 0.9, + lineWidth: 0, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'halo-shape', + visible: false, + }); + + // focus stroke for hover + group.addShape('circle', { + attrs: { + x: 0, + y: 0, + r: r + 5, + fill: style.fill || colorSet.mainFill || '#2B384E', + stroke: '#fff', + strokeOpacity: 0.85, + lineWidth: 1, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'stroke-shape', + visible: false, + }); + + // 默认形状 + const keyShape = group.addShape('circle', { + attrs: { + x: 0, + y: 0, + r, + fill: colorSet.mainFill, + stroke: colorSet.mainStroke, + lineWidth: 2, + cursor: 'pointer', + ...style, + }, + // 设置 draggable 以允许响应鼠标的图拽事件 + draggable: true, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'aggregated-node-keyShape', + }); + + // 网元状态标识 + if (cfg.info) { + const neInfo: any = cfg.info; + const hasNeState = neInfo.serverState.neId; + group.addShape('circle', { + attrs: { + x: r - 3, + y: -r + 3, + r: 5, + fill: hasNeState ? '#6DD400' : 'red', + lineWidth: 0.5, + stroke: '#FFFFFF', + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'ne-state', + }); + } + + if (cfg.label) { + const text = cfg.label; + let labelStyle: any = { + textAlign: 'center', + textBaseLine: 'alphabetic', + cursor: 'pointer', + fontSize: 8, + fill: '#fff', + opacity: 0.85, + fontWeight: 400, + }; + let refY = 0; + if (cfg.labelCfg) { + labelStyle = Object.assign(labelStyle, cfg.labelCfg.style); + refY += cfg.labelCfg.offset || 0; + } + labelStyle.fontSize = labelStyle.fontSize < 8 ? 8 : labelStyle.fontSize; + + group.addShape('text', { + attrs: { + text, + x: 0, + y: r + refY + 15, + ...labelStyle, + }, + // 设置 draggable 以允许响应鼠标的图拽事件 + draggable: true, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'text-shape', + className: 'text-shape', + }); + } + + if (cfg.icon) { + // left icon + group.addShape('image', { + attrs: { + x: 0, + cursor: 'pointer', + ...cfg.icon, + }, + // 设置 draggable 以允许响应鼠标的图拽事件 + draggable: true, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'icon-shape', + }); + } + + return keyShape; + }, + setState: (name, value, item: any) => { + const group = item.get('group'); + if (name === 'layoutEnd' && value) { + const labelShape = group.find( + (e: any) => e.get('name') === 'text-shape' + ); + if (labelShape) labelShape.set('visible', true); + } else if (name === 'neState') { + // 设置网元状态 + const neState = group.find((e: any) => e.get('name') === 'ne-state'); + if (value) { + neState.attr('fill', '#52c41a'); + } else { + neState.attr('fill', '#f5222d'); + } + } else if (name === 'hover') { + if (item.hasState('focus')) { + return; + } + const halo = group.find((e: any) => e.get('name') === 'halo-shape'); + const keyShape = item.getKeyShape(); + const colorSet = item.getModel().colorSet || colorSets[0]; + if (value) { + halo && halo.show(); + keyShape.attr('fill', colorSet.activeFill); + } else { + halo && halo.hide(); + keyShape.attr('fill', colorSet.mainFill); + } + } else if (name === 'focus') { + const stroke = group.find((e: any) => e.get('name') === 'stroke-shape'); + const label = group.find((e: any) => e.get('name') === 'text-shape'); + const keyShape = item.getKeyShape(); + const colorSet = item.getModel().colorSet || colorSets[0]; + if (value) { + stroke && stroke.show(); + keyShape.attr('fill', colorSet.selectedFill); + label && label.attr('fontWeight', 800); + } else { + stroke && stroke.hide(); + keyShape.attr('fill', colorSet.mainFill); // '#2B384E' + label && label.attr('fontWeight', 400); + } + } + // const model = item.getModel(); + // const neState = group.find( + // (e: any) => e.get('name') === 'ne-state-circle' + // ); + // if (neState) { + // console.log('neState', neState); + // item.getKeyShape().attr('fill', '#6DD400'); + // } + }, + update: undefined, + }, + 'aggregated-node' +); // 这样可以继承 aggregated-node 的 setState + +// Custom the quadratic edge for multiple edges between one node pair +registerEdge( + 'custom-quadratic', + { + setState: (name, value, item: any) => { + const group = item.get('group'); + const model = item.getModel(); + if (name === 'focus') { + const back = group.find((ele: any) => ele.get('name') === 'back-line'); + if (back) { + back.stopAnimate(); + back.remove(); + back.destroy(); + } + const keyShape = group.find( + (ele: any) => ele.get('name') === 'edge-shape' + ); + const arrow = model.style.endArrow; + if (value) { + if (keyShape.cfg.animation) { + keyShape.stopAnimate(true); + } + keyShape.attr({ + strokeOpacity: animateOpacity, + opacity: animateOpacity, + stroke: '#fff', + endArrow: { + ...arrow, + stroke: '#fff', + fill: '#fff', + }, + }); + if (model.isReal) { + const { lineWidth, path, endArrow, stroke } = keyShape.attr(); + const back = group.addShape('path', { + attrs: { + lineWidth, + path, + stroke, + endArrow, + opacity: animateBackOpacity, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'back-line', + }); + back.toBack(); + const length = keyShape.getTotalLength(); + keyShape.animate( + (ratio: any) => { + // the operations in each frame. Ratio ranges from 0 to 1 indicating the prograss of the animation. Returns the modified configurations + const startLen = ratio * length; + // Calculate the lineDash + const cfg = { + lineDash: [startLen, length - startLen], + }; + return cfg; + }, + { + repeat: true, // Whether executes the animation repeatly + duration, // the duration for executing once + } + ); + } else { + let index = 0; + const lineDash = keyShape.attr('lineDash'); + const totalLength = lineDash[0] + lineDash[1]; + keyShape.animate( + () => { + index++; + if (index > totalLength) { + index = 0; + } + const res = { + lineDash, + lineDashOffset: -index, + }; + // returns the modified configurations here, lineDash and lineDashOffset here + return res; + }, + { + repeat: true, // whether executes the animation repeatly + duration, // the duration for executing once + } + ); + } + } else { + keyShape.stopAnimate(); + const stroke = '#acaeaf'; + const opacity = model.isReal ? realEdgeOpacity : virtualEdgeOpacity; + keyShape.attr({ + stroke, + strokeOpacity: opacity, + opacity, + endArrow: { + ...arrow, + stroke, + fill: stroke, + }, + }); + } + } + }, + }, + 'quadratic' +); + +// Custom the line edge for single edge between one node pair +registerEdge( + 'custom-line', + { + setState: (name, value, item: any) => { + const group = item.get('group'); + const model = item.getModel(); + if (name === 'focus') { + const keyShape = group.find( + (ele: any) => ele.get('name') === 'edge-shape' + ); + const back = group.find((ele: any) => ele.get('name') === 'back-line'); + if (back) { + back.stopAnimate(); + back.remove(); + back.destroy(); + } + const arrow = model.style.endArrow; + if (value) { + if (keyShape.cfg.animation) { + keyShape.stopAnimate(true); + } + keyShape.attr({ + strokeOpacity: animateOpacity, + opacity: animateOpacity, + stroke: '#fff', + endArrow: { + ...arrow, + stroke: '#fff', + fill: '#fff', + }, + }); + if (model.isReal) { + const { path, stroke, lineWidth } = keyShape.attr(); + const back = group.addShape('path', { + attrs: { + path, + stroke, + lineWidth, + opacity: animateBackOpacity, + }, + // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type + name: 'back-line', + }); + back.toBack(); + const length = keyShape.getTotalLength(); + keyShape.animate( + (ratio: any) => { + // the operations in each frame. Ratio ranges from 0 to 1 indicating the prograss of the animation. Returns the modified configurations + const startLen = ratio * length; + // Calculate the lineDash + const cfg = { + lineDash: [startLen, length - startLen], + }; + return cfg; + }, + { + repeat: true, // Whether executes the animation repeatly + duration, // the duration for executing once + } + ); + } else { + const lineDash = keyShape.attr('lineDash'); + const totalLength = lineDash[0] + lineDash[1]; + let index = 0; + keyShape.animate( + () => { + index++; + if (index > totalLength) { + index = 0; + } + const res = { + lineDash, + lineDashOffset: -index, + }; + // returns the modified configurations here, lineDash and lineDashOffset here + return res; + }, + { + repeat: true, // whether executes the animation repeatly + duration, // the duration for executing once + } + ); + } + } else { + keyShape.stopAnimate(); + const stroke = '#acaeaf'; + const opacity = model.isReal ? realEdgeOpacity : virtualEdgeOpacity; + keyShape.attr({ + stroke, + strokeOpacity: opacity, + opacity: opacity, + endArrow: { + ...arrow, + stroke, + fill: stroke, + }, + }); + } + } + }, + }, + 'single-edge' +); + +const descendCompare = (p: any) => { + // 这是比较函数 + return function (m: any, n: any) { + const a = m[p]; + const b = n[p]; + return b - a; // 降序 + }; +}; + +const clearFocusItemState = (graph: any) => { + if (!graph) return; + clearFocusNodeState(graph); + clearFocusEdgeState(graph); +}; + +// 清除图上所有节点的 focus 状态及相应样式 +const clearFocusNodeState = (graph: any) => { + const focusNodes = graph.findAllByState('node', 'focus'); + focusNodes.forEach((fnode: any) => { + graph.setItemState(fnode, 'focus', false); // false + }); +}; + +// 清除图上所有边的 focus 状态及相应样式 +const clearFocusEdgeState = (graph: any) => { + const focusEdges = graph.findAllByState('edge', 'focus'); + focusEdges.forEach((fedge: any) => { + graph.setItemState(fedge, 'focus', false); + }); +}; + +// 截断长文本。length 为文本截断后长度,elipsis 是后缀 +const formatText = (text: any, length = 5, elipsis = '...') => { + if (!text) return ''; + if (text.length > length) { + return `${text.substr(0, length)}${elipsis}`; + } + return text; +}; + +const labelFormatter = (text: any, minLength = 10) => { + if (text && text.split('').length > minLength) + return `${text.substr(0, minLength)}...`; + return text; +}; + +const processNodesEdges = ( + nodes: any, + edges: any, + width: any, + height: any, + largeGraphMode: any, + edgeLabelVisible: any +) => { + if (!nodes || nodes.length === 0) return {}; + const currentNodeMap: any = {}; + let maxNodeCount = -Infinity; + const paddingRatio = 0.3; + const paddingLeft = paddingRatio * width; + const paddingTop = paddingRatio * height; + nodes.forEach((node: any) => { + node.type = node.level === 0 ? 'real-node' : 'aggregated-node'; + node.isReal = node.level === 0 ? true : false; + node.label = formatText(node.label, labelMaxLength, '...'); + node.degree = 0; + node.inDegree = 0; + node.outDegree = 0; + if (currentNodeMap[node.id]) { + console.warn('node exists already!', node.id); + node.id = `${node.id}${Math.random()}`; + } + currentNodeMap[node.id] = node; + if (node.count > maxNodeCount) maxNodeCount = node.count; + const cachePosition = cachePositions ? cachePositions[node.id] : undefined; + if (cachePosition) { + node.x = cachePosition.x; + node.y = cachePosition.y; + } else { + if (manipulatePosition && !node.x && !node.y) { + node.x = + manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); + node.y = + manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); + } + } + }); + + let maxCount = -Infinity; + let minCount = Infinity; + // let maxCount = 0; + edges.forEach((edge: any) => { + // to avoid the dulplicated id to nodes + if (!edge.id) edge.id = uniqueId('edge'); + else if (edge.id.split('-')[0] !== 'edge') edge.id = `edge-${edge.id}`; + // TODO: delete the following line after the queried data is correct + if (!currentNodeMap[edge.source] || !currentNodeMap[edge.target]) { + console.warn( + 'edge source target does not exist', + edge.source, + edge.target, + edge.id + ); + return; + } + const sourceNode = currentNodeMap[edge.source]; + const targetNode = currentNodeMap[edge.target]; + + if (!sourceNode || !targetNode) + console.warn( + 'source or target is not defined!!!', + edge, + sourceNode, + targetNode + ); + + // calculate the degree + sourceNode.degree++; + targetNode.degree++; + sourceNode.outDegree++; + targetNode.inDegree++; + + if (edge.count > maxCount) maxCount = edge.count; + if (edge.count < minCount) minCount = edge.count; + }); + + nodes.sort(descendCompare(NODESIZEMAPPING)); + const maxDegree = nodes[0].degree || 1; + + const descreteNodes: any = []; + nodes.forEach((node: any, i: any) => { + // assign the size mapping to the outDegree + const countRatio = node.count / maxNodeCount; + const isRealNode = node.level === 0; + node.size = isRealNode + ? node.size || DEFAULTNODESIZE + : DEFAULTAGGREGATEDNODESIZE; + node.isReal = isRealNode; + node.labelCfg = node.labelCfg || { + position: 'bottom', + offset: 5, + style: { + fill: globalStyle.node.labelCfg.style.fill, + fontSize: 6 + countRatio * 6 || 12, + stroke: globalStyle.node.labelCfg.style.stroke, + lineWidth: 3, + }, + }; + + if (!node.degree) { + descreteNodes.push(node); + } + }); + + const countRange = maxCount - minCount; + const minEdgeSize = 1; + const maxEdgeSize = 12; + const edgeSizeRange = maxEdgeSize - minEdgeSize; + edges.forEach((edge: any) => { + // set edges' style + const targetNode = currentNodeMap[edge.target]; + + const size = + ((edge.count - minCount) / countRange) * edgeSizeRange + minEdgeSize || 1; + edge.size = size; + + const arrowWidth = Math.max(size / 2 + 2, 3); + const arrowLength = 10; + const arrowBeging = targetNode.size + arrowLength; + let arrowPath: any = `M ${arrowBeging},0 L ${ + arrowBeging + arrowLength + },-${arrowWidth} L ${arrowBeging + arrowLength},${arrowWidth} Z`; + let d = targetNode.size / 2 + arrowLength; + if (edge.source === edge.target) { + edge.type = 'loop'; + arrowPath = undefined; + } + const sourceNode = currentNodeMap[edge.source]; + const isRealEdge = targetNode.isReal && sourceNode.isReal; + edge.isReal = isRealEdge; + const stroke = isRealEdge + ? globalStyle.edge.style.realEdgeStroke + : globalStyle.edge.style.stroke; + const opacity = isRealEdge + ? globalStyle.edge.style.realEdgeOpacity + : globalStyle.edge.style.strokeOpacity; + const dash = Math.max(size, 2); + const lineDash = isRealEdge ? undefined : [dash, dash]; + edge.style = { + stroke, + strokeOpacity: opacity, + cursor: 'pointer', + lineAppendWidth: Math.max(edge.size || 5, 5), + fillOpacity: 1, + lineDash, + endArrow: arrowPath + ? { + path: arrowPath, + d, + fill: stroke, + strokeOpacity: 0, + } + : false, + }; + edge.labelCfg = { + autoRotate: true, + style: { + stroke: globalStyle.edge.labelCfg.style.stroke, + fill: globalStyle.edge.labelCfg.style.fill, + lineWidth: 4, + fontSize: 12, + lineAppendWidth: 10, + opacity: 1, + }, + }; + // 标签名称 + if (!edge.oriLabel) edge.oriLabel = edge.label; + if (largeGraphMode || !edgeLabelVisible) edge.label = ''; + else { + edge.label = labelFormatter(edge.label, labelMaxLength); + } + + // arrange the other nodes around the hub + const sourceDis = sourceNode.size / 2 + 20; + const targetDis = targetNode.size / 2 + 20; + if (sourceNode.x && !targetNode.x) { + targetNode.x = + sourceNode.x + sourceDis * Math.cos(Math.random() * Math.PI * 2); + } + if (sourceNode.y && !targetNode.y) { + targetNode.y = + sourceNode.y + sourceDis * Math.sin(Math.random() * Math.PI * 2); + } + if (targetNode.x && !sourceNode.x) { + sourceNode.x = + targetNode.x + targetDis * Math.cos(Math.random() * Math.PI * 2); + } + if (targetNode.y && !sourceNode.y) { + sourceNode.y = + targetNode.y + targetDis * Math.sin(Math.random() * Math.PI * 2); + } + + if (!sourceNode.x && !sourceNode.y && manipulatePosition) { + sourceNode.x = + manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); + sourceNode.y = + manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); + } + if (!targetNode.x && !targetNode.y && manipulatePosition) { + targetNode.x = + manipulatePosition.x + 30 * Math.cos(Math.random() * Math.PI * 2); + targetNode.y = + manipulatePosition.y + 30 * Math.sin(Math.random() * Math.PI * 2); + } + }); + + descreteNodeCenter = { + x: width - paddingLeft, + y: height - paddingTop, + }; + descreteNodes.forEach((node: any) => { + if (!node.x && !node.y) { + node.x = + descreteNodeCenter.x + 30 * Math.cos(Math.random() * Math.PI * 2); + node.y = + descreteNodeCenter.y + 30 * Math.sin(Math.random() * Math.PI * 2); + } + }); + + Util.processParallelEdges(edges, 12.5, 'custom-quadratic', 'custom-line'); + return { + maxDegree, + edges, + }; +}; + +const getForceLayoutConfig = ( + graph: any, + largeGraphMode: any, + configSettings?: any +) => { + let { + linkDistance, + edgeStrength, + nodeStrength, + nodeSpacing, + preventOverlap, + nodeSize, + collideStrength, + alpha, + alphaDecay, + alphaMin, + } = configSettings || { preventOverlap: true }; + + if (!linkDistance && linkDistance !== 0) linkDistance = 525; + if (!edgeStrength && edgeStrength !== 0) edgeStrength = 50; + if (!nodeStrength && nodeStrength !== 0) nodeStrength = 200; + if (!nodeSpacing && nodeSpacing !== 0) nodeSpacing = 5; + + const config: any = { + type: 'gForce', + minMovement: 0.01, + maxIteration: 5000, + preventOverlap, + damping: 0.99, + linkDistance: (d: any) => { + let dist = linkDistance; + const sourceNode = nodeMap[d.source] || aggregatedNodeMap[d.source]; + const targetNode = nodeMap[d.target] || aggregatedNodeMap[d.target]; + // // 两端都是聚合点 + // if (sourceNode.level && targetNode.level) dist = linkDistance * 3; + // // 一端是聚合点,一端是真实节点 + // else if (sourceNode.level || targetNode.level) dist = linkDistance * 1.5; + if (!sourceNode?.level && !targetNode?.level) dist = linkDistance * 0.3; + return dist; + }, + edgeStrength: (d: any) => { + const sourceNode = nodeMap[d.source] || aggregatedNodeMap[d.source]; + const targetNode = nodeMap[d.target] || aggregatedNodeMap[d.target]; + // 聚合节点之间的引力小 + if (sourceNode?.level && targetNode?.level) return edgeStrength / 2; + // 聚合节点与真实节点之间引力大 + if (sourceNode?.level || targetNode?.level) return edgeStrength; + return edgeStrength; + }, + nodeStrength: (d: any) => { + // 给离散点引力,让它们聚集 + if (d.degree === 0) return -10; + // 聚合点的斥力大 + if (d.level) return nodeStrength * 2; + return nodeStrength; + }, + nodeSize: (d: any) => { + if (!nodeSize && d.size) return d.size; + return 50; + }, + nodeSpacing: (d: any) => { + if (d.degree === 0) return nodeSpacing * 2; + if (d.level) return nodeSpacing; + return nodeSpacing; + }, + onLayoutEnd: () => { + if (largeGraphMode) { + graph.getEdges().forEach((edge: any) => { + console.log('onLayoutEnd', edge.oriLabel); + if (!edge.oriLabel) return; + edge.update({ + label: labelFormatter(edge.oriLabel, labelMaxLength), + }); + }); + } + }, + tick: () => { + graph.refreshPositions(); + }, + }; + + if (nodeSize) config['nodeSize'] = nodeSize; + if (collideStrength) config['collideStrength'] = collideStrength; + if (alpha) config['alpha'] = alpha; + if (alphaDecay) config['alphaDecay'] = alphaDecay; + if (alphaMin) config['alphaMin'] = alphaMin; + + return config; +}; + +const hideItems = (graph: any) => { + hiddenItemIds.forEach(id => { + graph.hideItem(id); + }); +}; + +const showItems = (graph: any) => { + graph.getNodes().forEach((node: any) => { + if (!node.isVisible()) graph.showItem(node); + }); + graph.getEdges().forEach((edge: any) => { + if (!edge.isVisible()) edge.showItem(edge); + }); + hiddenItemIds = []; +}; + +const handleRefreshGraph = ( + graph: any, + graphData: any, + width: any, + height: any, + largeGraphMode: any, + edgeLabelVisible: any +) => { + if (!graphData || !graph) return; + clearFocusItemState(graph); + // reset the filtering + graph.getNodes().forEach((node: any) => { + if (!node.isVisible()) node.show(); + }); + graph.getEdges().forEach((edge: any) => { + if (!edge.isVisible()) edge.show(); + }); + + let nodes = [], + edges = []; + + nodes = graphData.nodes; + const processRes = processNodesEdges( + nodes, + graphData.edges || [], + width, + height, + largeGraphMode, + edgeLabelVisible + ); + + edges = processRes.edges; + + graph.changeData({ nodes, edges }); + + hideItems(graph); + graph.getNodes().forEach((node: any) => { + node.toFront(); + }); + + // layout.instance.stop(); + // force 需要使用不同 id 的对象才能进行全新的布局,否则会使用原来的引用。因此复制一份节点和边作为 force 的布局数据 + layout.instance.init({ + nodes: graphData.nodes, + edges, + }); + + layout.instance.minMovement = 0.0001; + // layout.instance.getCenter = d => { + // const cachePosition = cachePositions[d.id]; + // if (!cachePosition && (d.x || d.y)) return [d.x, d.y, 10]; + // else if (cachePosition) return [cachePosition.x, cachePosition.y, 10]; + // return [width / 2, height / 2, 10]; + // } + layout.instance.getMass = (d: any) => { + const cachePosition = cachePositions[d.id]; + if (cachePosition) return 5; + return 1; + }; + layout.instance.execute(); + return { nodes, edges }; +}; + +const getMixedGraph = ( + aggregatedData: any, + originData: any, + nodeMap: any, + aggregatedNodeMap: any, + expandArray: any, + collapseArray: any +) => { + let nodes: any = [], + edges: any = []; + + const expandMap: any = {}, + collapseMap: any = {}; + expandArray.forEach((expandModel: any) => { + expandMap[expandModel.id] = true; + }); + collapseArray.forEach((collapseModel: any) => { + collapseMap[collapseModel.id] = true; + }); + + aggregatedData.clusters.forEach((cluster: any, i: any) => { + if (expandMap[cluster.id]) { + nodes = nodes.concat(cluster.nodes); + aggregatedNodeMap[cluster.id].expanded = true; + } else { + nodes.push(aggregatedNodeMap[cluster.id]); + aggregatedNodeMap[cluster.id].expanded = false; + } + }); + + originData.edges.forEach((edge: any) => { + const isSourceInExpandArray = expandMap[nodeMap[edge.source].clusterId]; + const isTargetInExpandArray = expandMap[nodeMap[edge.target].clusterId]; + if (isSourceInExpandArray && isTargetInExpandArray) { + edges.push(edge); + } else if (isSourceInExpandArray) { + const targetClusterId = nodeMap[edge.target].clusterId; + const vedge = { + source: edge.source, + target: targetClusterId, + id: uniqueId('edge'), + label: '', + }; + edges.push(vedge); + } else if (isTargetInExpandArray) { + const sourceClusterId = nodeMap[edge.source].clusterId; + const vedge = { + target: edge.target, + source: sourceClusterId, + id: uniqueId('edge'), + label: '', + }; + edges.push(vedge); + } + }); + aggregatedData.clusterEdges.forEach((edge: any) => { + if (expandMap[edge.source] || expandMap[edge.target]) return; + else edges.push(edge); + }); + return { nodes, edges }; +}; + +const examAncestors = ( + model: any, + expandedArray: any, + length: any, + keepTags: any +) => { + for (let i = 0; i < length; i++) { + const expandedNode = expandedArray[i]; + if (!keepTags[i] && model.parentId === expandedNode.id) { + keepTags[i] = true; // 需要被保留 + examAncestors(expandedNode, expandedArray, length, keepTags); + break; + } + } +}; + +// 展开节点数据控制 +const manageExpandCollapseArray = ( + nodeNumber: any, + model: any, + collapseArray: any, + expandArray: any +) => { + manipulatePosition = { x: model.x, y: model.y }; + + // 维护 expandArray,若当前画布节点数高于上限,移出 expandedArray 中非 model 祖先的节点) + if (nodeNumber > NODE_LIMIT) { + // 若 keepTags[i] 为 true,则 expandedArray 的第 i 个节点需要被保留 + const keepTags: any = {}; + const expandLen = expandArray.length; + // 检查 X 的所有祖先并标记 keepTags + examAncestors(model, expandArray, expandLen, keepTags); + // 寻找 expandedArray 中第一个 keepTags 不为 true 的点 + let shiftNodeIdx = -1; + for (let i = 0; i < expandLen; i++) { + if (!keepTags[i]) { + shiftNodeIdx = i; + break; + } + } + // 如果有符合条件的节点,将其从 expandedArray 中移除 + if (shiftNodeIdx !== -1) { + let foundNode = expandArray[shiftNodeIdx]; + if (foundNode.level === 2) { + let foundLevel1 = false; + // 找到 expandedArray 中 parentId = foundNode.id 且 level = 1 的第一个节点 + for (let i = 0; i < expandLen; i++) { + const eNode = expandArray[i]; + if (eNode.parentId === foundNode.id && eNode.level === 1) { + foundLevel1 = true; + foundNode = eNode; + expandArray.splice(i, 1); + break; + } + } + // 若未找到,则 foundNode 不变, 直接删去 foundNode + if (!foundLevel1) expandArray.splice(shiftNodeIdx, 1); + } else { + // 直接删去 foundNode + expandArray.splice(shiftNodeIdx, 1); + } + // const removedNode = expandedArray.splice(shiftNodeIdx, 1); // splice returns an array + const idSplits = foundNode.id.split('-'); + let collapseNodeId; + // 去掉最后一个后缀 + for (let i = 0; i < idSplits.length - 1; i++) { + const str = idSplits[i]; + if (collapseNodeId) collapseNodeId = `${collapseNodeId}-${str}`; + else collapseNodeId = str; + } + const collapseNode = { + id: collapseNodeId, + parentId: foundNode.id, + level: foundNode.level - 1, + }; + collapseArray.push(collapseNode); + } + } + + const currentNode = { + id: model.id, + level: model.level, + parentId: model.parentId, + }; + + // 加入当前需要展开的节点 + expandArray.push(currentNode); + + graph.get('canvas').setCursor('default'); + return { expandArray, collapseArray }; +}; + +const cacheNodePositions = (nodes: any) => { + const positionMap: any = {}; + const nodeLength = nodes.length; + for (let i = 0; i < nodeLength; i++) { + const node = nodes[i].getModel(); + positionMap[node.id] = { + x: node.x, + y: node.y, + level: node.level, + }; + } + return positionMap; +}; + +// 布局停止动画 +const stopLayout = () => { + layout.instance.stop(); +}; + +// 图监听事件 +const bindListener = (graph: any) => { + graph.on('node:mouseenter', (evt: any) => { + const { item } = evt; + graph.setItemState(item, 'hover', true); + item.toFront(); + }); + + graph.on('node:mouseleave', (evt: any) => { + const { item } = evt; + graph.setItemState(item, 'hover', false); + }); + + graph.on('edge:mouseenter', (evt: any) => { + const { item } = evt; + const model = item.getModel(); + const currentLabel = model.label; + item.update({ + label: model.oriLabel, + }); + model.oriLabel = currentLabel; + item.toFront(); + item.getSource().toFront(); + item.getTarget().toFront(); + }); + + graph.on('edge:mouseleave', (evt: any) => { + const { item } = evt; + const model = item.getModel(); + const currentLabel = model.label; + item.update({ + label: model.oriLabel, + }); + model.oriLabel = currentLabel; + }); + // click node to show the detail drawer + graph.on('node:click', (evt: any) => { + stopLayout(); + if (!shiftKeydown) clearFocusItemState(graph); + else clearFocusEdgeState(graph); + const { item } = evt; + + // highlight the clicked node, it is down by click-select + graph.setItemState(item, 'focus', true); + + if (!shiftKeydown) { + // 将相关边也高亮 + const relatedEdges = item.getEdges(); + relatedEdges.forEach((edge: any) => { + graph.setItemState(edge, 'focus', true); + }); + } + }); + + // click edge to show the detail of integrated edge drawer + graph.on('edge:click', (evt: any) => { + stopLayout(); + if (!shiftKeydown) clearFocusItemState(graph); + const { item } = evt; + console.log(item); + // highlight the clicked edge + graph.setItemState(item, 'focus', true); + }); + + // click canvas to cancel all the focus state + graph.on('canvas:click', (evt: any) => { + clearFocusItemState(graph); + console.log( + graph.getGroup(), + graph.getGroup().getBBox(), + graph.getGroup().getCanvasBBox() + ); + }); +}; + +// 导出渲染函数 +export function randerGroph(graphG6Dom: HTMLElement | undefined, data: any) { + if (!graphG6Dom) return; + + // graphG6Dom.value.style.backgroundColor = '#2b2f33'; + + CANVAS_WIDTH = graphG6Dom.scrollWidth; + CANVAS_HEIGHT = (graphG6Dom.scrollHeight || 500) - 30; + + const nodeMap: any = {}; + const clusteredData = louvain(data, false, 'weight'); + const aggregatedData: any = { nodes: [], edges: [] }; + clusteredData.clusters.forEach((cluster, i) => { + cluster.nodes.forEach(node => { + const readNode = data.nodes.find((item: any) => item.id === node.id); + node = Object.assign(node, readNode); + node.level = 0; + // node.label = node.id; + node.type = ''; + node.colorSet = colorSets[i]; + nodeMap[node.id] = node; + }); + const cnode = { + id: cluster.id, + type: 'aggregated-node', + count: cluster.nodes.length, + level: 1, + label: cluster.id, + colorSet: colorSets[i], + idx: i, + }; + aggregatedNodeMap[cluster.id] = cnode; + aggregatedData.nodes.push(cnode); + }); + clusteredData.clusterEdges.forEach(clusterEdge => { + const cedge: any = { + ...clusterEdge, + size: Math.log(clusterEdge.count), + // label: '', + id: uniqueId('edge'), + }; + // 自旋 + if (cedge.source === cedge.target) { + cedge.type = 'loop'; + cedge.loopCfg = { + dist: 20, + }; + } else { + cedge.type = 'line'; + } + aggregatedData.edges.push(cedge); + }); + + // data.edges.forEach((edge: any) => { + // edge.label = `${edge.source}-${edge.target}`; + // edge.id = uniqueId('edge'); + // }); + + currentUnproccessedData = aggregatedData; + + // 节点上下文菜单 + const contextMenu = new Menu({ + shouldBegin(evt: any) { + if (evt.target && evt.target.isCanvas && evt.target.isCanvas()) + return true; + if (evt.item) return true; + return false; + }, + getContent(evt: any) { + const { item } = evt; + if (evt.target && evt.target.isCanvas && evt.target.isCanvas()) { + return ` +
+
+ 1. 显示所有隐藏项 +
+
+ 2. 折叠所有集群 +
+
`; + } + if (!item) return ''; + const itemType = item.getType(); + const model = item.getModel(); + if (itemType && model) { + if (itemType === 'node') { + if (model.level !== 0) { + return ` +
+
+ 1. 展开集群 +
+
+ 2. 隐藏节点 +
+
+ `; + } else { + return ` +
+
+ 1. 折叠集群 +
+
+ 2. 隐藏节点 +
+
+ 3. 重启 +
+
+ `; + } + } else { + return ` +
+
+ 1. 隐藏边 +
+
+ `; + } + } + return ''; + }, + handleMenuClick: (target, item) => { + const model: any = item && item.getModel(); + const liIdStrs = target.id.split('-'); + let mixedGraphData; + switch (liIdStrs[0]) { + case 'hide': + graph.hideItem(item); + hiddenItemIds.push(model.id); + break; + case 'expand': + const newArray = manageExpandCollapseArray( + graph.getNodes().length, + model, + collapseArray, + expandArray + ); + expandArray = newArray.expandArray; + collapseArray = newArray.collapseArray; + mixedGraphData = getMixedGraph( + clusteredData, + data, + nodeMap, + aggregatedNodeMap, + expandArray, + collapseArray + ); + break; + case 'collapse': + const aggregatedNode = aggregatedNodeMap[model.clusterId]; + manipulatePosition = { x: aggregatedNode.x, y: aggregatedNode.y }; + collapseArray.push(aggregatedNode); + for (let i = 0; i < expandArray.length; i++) { + if (expandArray[i].id === model.clusterId) { + expandArray.splice(i, 1); + break; + } + } + mixedGraphData = getMixedGraph( + clusteredData, + data, + nodeMap, + aggregatedNodeMap, + expandArray, + collapseArray + ); + break; + case 'collapseAll': + expandArray = []; + collapseArray = []; + mixedGraphData = getMixedGraph( + clusteredData, + data, + nodeMap, + aggregatedNodeMap, + expandArray, + collapseArray + ); + break; + case 'restart': + console.log('restart'); + break; + case 'show': + showItems(graph); + break; + default: + break; + } + if (mixedGraphData) { + cachePositions = cacheNodePositions(graph.getNodes()); + currentUnproccessedData = mixedGraphData; + handleRefreshGraph( + graph, + currentUnproccessedData, + CANVAS_WIDTH, + CANVAS_HEIGHT, + largeGraphMode, + true + ); + } + }, + // offsetX and offsetY include the padding of the parent container + // 需要加上父级容器的 padding-left 16 与自身偏移量 10 + offsetX: 16 + 10, + // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10 + offsetY: 0, + // the types of items that allow the menu show up + // 在哪些类型的元素上响应 + itemTypes: ['node', 'edge', 'canvas'], + }); + + // 节点提示菜单 + const tooltip = new Tooltip({ + offsetX: 20, + offsetY: 20, + getContent(e: any) { + const node = e.item.getModel(); + const neInfo = node.info; + if (!neInfo) { + return `
ID:${node.id}
`; + } + const serverState = neInfo.serverState; + console.log(neInfo); + const hasNeID = serverState.neId; + if (!hasNeID) { + return '
状态:异常
'; + } + return ` +
+
状态: + ${hasNeID ? '正常' : '异常'} +
+
刷新时间: + ${serverState.refreshTime ?? '--'} +
+
ID:${hasNeID}
+
名称:${serverState.neName ?? '--'}
+
版本: + ${serverState.version ?? '--'} +
+
SN:${serverState.sn ?? '--'}
+
有效期: + ${serverState.expire ?? '--'} +
+
+ `; + }, + itemTypes: ['node'], + }); + + const { edges: processedEdges } = processNodesEdges( + currentUnproccessedData.nodes, + currentUnproccessedData.edges, + CANVAS_WIDTH, + CANVAS_HEIGHT, + true, + true + ); + + graph = new Graph({ + container: graphG6Dom, + width: graphG6Dom?.clientWidth, + height: graphG6Dom?.clientHeight, + linkCenter: true, + minZoom: 0.1, + groupByTypes: false, + modes: { + default: [ + { + type: 'drag-canvas', + enableOptimize: true, + }, + { + type: 'zoom-canvas', + enableOptimize: true, + optimizeZoom: 0.01, + }, + 'drag-node', + ], + lassoSelect: [ + { + type: 'zoom-canvas', + enableOptimize: true, + optimizeZoom: 0.01, + }, + { + type: 'lasso-select', + selectedState: 'focus', + trigger: 'drag', + }, + ], + fisheyeMode: [], + }, + defaultNode: { + type: 'aggregated-node', + size: DEFAULTNODESIZE, + }, + plugins: [contextMenu, tooltip], + }); + + graph.get('canvas').set('localRefresh', false); + + const layoutConfig = getForceLayoutConfig(graph, largeGraphMode); + layoutConfig.center = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2]; + layout.instance = new Layout['gForce'](layoutConfig); + layout.instance.init({ + nodes: aggregatedData.nodes, + edges: processedEdges, + }); + layout.instance.execute(); + // console.log(processedEdges) + bindListener(graph); + // graph.data({ nodes: aggregatedData.nodes, edges: processedEdges }); + graph.data({ nodes: aggregatedData.nodes, edges: processedEdges }); + graph.render(); + + // 节点展开 + for (const model of aggregatedData.nodes) { + const newArray = manageExpandCollapseArray( + graph.getNodes().length, + model, + collapseArray, + expandArray + ); + expandArray = newArray.expandArray; + collapseArray = newArray.collapseArray; + let mixedGraphData = getMixedGraph( + clusteredData, + data, + nodeMap, + aggregatedNodeMap, + expandArray, + collapseArray + ); + if (mixedGraphData) { + cachePositions = cacheNodePositions(graph.getNodes()); + let currentUnproccessedData = mixedGraphData; + handleRefreshGraph( + graph, + currentUnproccessedData, + CANVAS_WIDTH, + CANVAS_HEIGHT, + largeGraphMode, + true + ); + } + } + + return graph; +} diff --git a/src/views/monitor/topology/index.vue b/src/views/monitor/topology/index.vue index 8566e536..87a97a64 100644 --- a/src/views/monitor/topology/index.vue +++ b/src/views/monitor/topology/index.vue @@ -3,445 +3,161 @@ import { reactive, onMounted, ref } from 'vue'; import { PageContainer } from 'antdv-pro-layout'; import ChartGraphG6 from '@/components/ChartGraphG6/index.vue'; import useI18n from '@/hooks/useI18n'; -import useNeInfoStore from '@/store/modules/neinfo'; -import { Graph, GraphData } from '@antv/g6'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; +import { listNe, stateNe } from '@/api/ne/ne'; import message from 'ant-design-vue/lib/message'; -const neInfoStore = useNeInfoStore(); +import { randerGroph } from './graph'; +import { parseDateToStr } from '@/utils/date-utils'; const { t } = useI18n(); /**图DOM节点实例对象 */ const graphG6Dom = ref(undefined); /**图实例对象 */ -let graphChart: any = null; +const graphG6 = ref(null); -/**图DOM节点实例对象 */ -const graphData = reactive({ +/**图数据 */ +const graphG6Data = reactive>({ nodes: [], edges: [], - combos: [], }); -const data = { - nodes: [ - // 0 基站 - { - id: '0', - x: 50, - y: 150, - size: 48, - type: 'circle', - label: '基站', - labelCfg: { - position: 'bottom', - offset: 10, - style: { - fill: '#333', - stroke: '#fff', - lineWidth: 10, - }, - }, - style: { - fill: '#9EC9FF', - stroke: '#5B8FF9', - lineWidth: 2, - }, - icon: { - show: true, - // 可更换为其他图片地址 - img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', - width: 24, - height: 24, - }, - }, - // 1 DM - { - id: '1', - x: 450, - y: 450, - label: 'DM', - labelCfg: { - position: 'center', - }, - style: { - fill: '#00b050', - stroke: '#00b050', - lineWidth: 1, - }, - }, - // 2 O&M - { - id: '2', - x: 50, - y: 450, - label: 'O&M', - }, - // 100 EMS - { - id: '100', - label: 'EMS', - comboId: 'combo-ems', - x: 300, - y: 450, - }, - // 190 UPF - { - id: '190', - comboId: 'combo-upf', - x: 300, - y: 350, - label: 'UPF', - labelCfg: { - position: 'center', - }, - style: { - fill: '#d580ff', - stroke: '#d580ff', - lineWidth: 1, - }, - }, - // EP-IMS - { - id: '110', - comboId: 'combo-ims', - x: 600, - y: 350, - label: 'IMS', - labelCfg: { - position: 'center', - }, - style: { - fill: '#ed7d31', - stroke: '#ed7d31', - lineWidth: 1, - }, - }, - // 5GC控制面 - { - id: '170', - label: 'NSSF', - comboId: 'combo-5gc', - x: 300, - y: 50, - }, - { - id: '130', - label: 'AUSF', - comboId: 'combo-5gc', - x: 450, - y: 50, - }, - { - id: '140', - label: 'UDM', - comboId: 'combo-5gc', - x: 600, - y: 50, - }, - { - id: '120', - label: 'AMF', - comboId: 'combo-5gc', - x: 300, - y: 150, - }, - { - id: '180', - label: 'NRF', - comboId: 'combo-5gc', - x: 450, - y: 150, - }, - { - id: '150', - label: 'SMF', - comboId: 'combo-5gc', - x: 300, - y: 250, - }, - { - id: '160', - label: 'PCF', - comboId: 'combo-5gc', - x: 700, - y: 250, - }, - ], - edges: [ - { - id: '0-5gc', - source: '0', - target: 'combo-5gc', - }, - { - id: '0-upf', - source: '0', - target: 'combo-upf', - }, - { - id: 'upf-1', - source: 'combo-upf', - target: '1', - }, - - { - id: 'ems-2', - source: 'combo-ems', - target: '2', - }, - { - id: '170-120', - source: '170', - target: '120', - }, - { - id: '130-120', - source: '130', - target: '120', - }, - { - id: '140-120', - source: '140', - target: '120', - }, - { - id: '140-180', - source: '140', - target: '180', - }, - { - id: '120-180', - source: '120', - target: '180', - }, - { - id: '130-180', - source: '130', - target: '180', - }, - { - id: '140-150', - source: '140', - target: '150', - }, - { - id: '140-110', - source: '140', - target: '110', - }, - { - id: '120-150', - source: '120', - target: '150', - data: {}, - }, - { - id: '150-180', - source: '150', - target: '180', - data: {}, - }, - { - id: '150-160', - source: '150', - target: '160', - }, - { - id: '160-120', - source: '160', - target: '120', - }, - { - id: '160-180', - source: '160', - target: '180', - }, - { - id: '160-110', - source: '160', - target: '110', - }, - - { - id: '150-190', - source: '150', - target: '190', - }, - - { - id: 'upf-ims', - source: 'combo-upf', - target: 'combo-ims', - }, - { - id: 'ems-5gc', - source: 'combo-ems', - target: 'combo-5gc', - }, - { - id: 'ems-upf', - source: 'combo-ems', - target: 'combo-upf', - }, - { - id: 'ems-ims', - source: 'combo-ems', - target: 'combo-ims', - }, - ], - combos: [ - { - id: 'combo-5gc', - data: { - text: '5GC控制面', - }, - }, - { - id: 'combo-upf', - data: { - keyShape: { - opacity: 0.8, - padding: [20, 20, 20, 20], - radius: 4, - lineWidth: 1, - stroke: '#d580ff', - }, - }, - }, - { - id: 'combo-ims', - data: {}, - }, - { - id: 'combo-ems', - data: {}, - }, - ], -}; - -/**初始化渲染图表 */ -function initChart() { +/**查询全部网元数据列表 */ +function fnRanderData() { if (!graphG6Dom.value) return; - console.log(graphG6Dom.value.offsetWidth, graphG6Dom.value.offsetHeight); - console.log(graphG6Dom.value.clientWidth, graphG6Dom.value.clientHeight); - graphChart = new Graph({ - container: graphG6Dom.value, - height: graphG6Dom.value.clientHeight, - width: graphG6Dom.value.clientWidth, - fitCenter: true, - modes: { - default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点 - }, - // 全局节点 矩形 - defaultNode: { - type: 'rect', - size: [80, 40], - style: { - fill: '#fff', - lineWidth: 1, - radius: 8, - }, - labelCfg: {}, - }, - // 全局边 三次贝塞尔曲线 - defaultEdge: { - type: 'polyline', - style: { - offset: 20, // 拐弯处距离节点最小距离 - radius: 4, // 拐弯处的圆角弧度,若不设置则为直角 - lineWidth: 1, - stroke: '#87e8de', - }, - }, - // 全局框节点 矩形 - defaultCombo: { - type: 'rect', // Combo 类型 - size: [40, 40], - // ... 其他配置 - style: { - lineWidth: 1, - }, - }, - }); + graphG6.value = randerGroph(graphG6Dom.value, graphG6Data); +} - graphChart.data(data); // 加载数据 - graphChart.render(); // 渲染 +/**查询网元状态 */ +async function fnGetState() { + for (const node of graphG6Data.nodes) { + const ne = node.info; + if (ne.neType === 'OMC') continue; + const result = await stateNe(ne.neType, ne.neId); + if (result.code === RESULT_CODE_SUCCESS) { + ne.serverState = result.data; + ne.serverState.refreshTime = parseDateToStr( + ne.serverState.refreshTime, + 'HH:mm:ss' + ); + const node = graphG6.value.findById(ne.neName); + console.log('查询网元状态', node); + graphG6.value.setItemState(node, 'neState', true); + } + } } /**查询全部网元数据列表 */ -function fnGetList() {} +function fnGetList(refresh: boolean = false) { + listNe({ + bandStatus: false, + }) + .then(res => { + if ( + res.code === RESULT_CODE_SUCCESS && + Array.isArray(res.data) && + res.data.length > 0 + ) { + let rootNode = 'OMC'; + const nodes = []; + const edges = []; + for (const item of res.data) { + item.serverState = {}; + const nodeIndex = nodes.findIndex(v => v.id === item.neName); + if (nodeIndex === -1) { + // 根网管 + if (item.neType === 'OMC') { + rootNode = item.neName; + item.serverState = { + neId: item.neId, + neName: item.neName, + neType: item.neType, + expire: '2024-03-31', + refreshTime: '10:31:47', + sn: '13770707', + version: '2.2312.8', + }; + nodes.push({ + id: item.neName, + label: item.neName, + info: item, + labelCfg: { + position: 'bottom', + offset: 8, + style: { + fill: '#fff', + fontSize: 14, + }, + }, + size: 60, + icon: { + x: -30, + y: -30, + // 可更换为其他图片地址 + img: '/svg/service_db.svg', + width: 60, + height: 60, + }, + }); + } else { + nodes.push({ + id: item.neName, + label: item.neName, + info: item, + size: 48, + icon: { + x: -24, + y: -24, + img: '/svg/service.svg', + width: 48, + height: 48, + }, + }); + } + } + + if (item.neType !== 'OMC') { + const edgeIndex = edges.findIndex(v => v.source === item.neName); + if (edgeIndex === -1) { + edges.push({ + source: item.neName, + target: rootNode, + label: `${item.neType}-${rootNode}`, + }); + } + } + } + graphG6Data.nodes = nodes; + graphG6Data.edges = edges; + console.log(graphG6Data); + return true; + } else { + message.warning({ + content: t('common.noData'), + duration: 2, + }); + return false; + } + }) + .then(hasNeList => { + if (!hasNeList) return; + if (refresh) { + // graphG6.value.get('canvas').set('localRefresh', true); + graphG6.value.destroy(); + // graphG6.value.clear(); + } + fnRanderData(); + fnGetState(); + }); +} + +function fnAdd() { + fnGetList(true); +} onMounted(() => { - // 获取网元网元列表 - neInfoStore.fnNelist().then(res => { - if ( - res.code === RESULT_CODE_SUCCESS && - Array.isArray(res.data) && - res.data.length > 0 - ) { - console.log(res.data); - console.log(neInfoStore.neCascaderOptions); - - const itemNode = neInfoStore.neCascaderOptions; - for (const item of itemNode) { - // 圈 - const comboId = `combo-${item.value}`; - graphData.combos?.push({ - id: comboId, - data: { - label: item.label, - keyShape: { - opacity: 0.8, - padding: [20, 20, 20, 20], - radius: 4, - lineWidth: 1, - stroke: '#d580ff', - }, - }, - }); - - for (const itemChild of item.children) { - // 点 - const nodeId = `node-${itemChild.label}`; - graphData.nodes?.push({ - id: nodeId, - data: { - label: itemChild.label, - parentId: comboId, - keyShape: { - width: 80, - height: 40, - radius: 8, - fill: '#00b050', - }, - labelShape: { - position: 'center', - text: itemChild.label, - fill: '#fff', // 节点标签文字颜色 - }, - }, - }); - // 边 - // const edgeId = `edge-${itemChild.label}`; - // graphData.edges?.push({ - // id: edgeId, - // source: '0', - // target: 'combo-5gc', - // data: {}, - // }); - } - } - - fnGetList(); - initChart(); - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - } - }); + // 获取网元列表 + fnGetList(); }); @@ -455,7 +171,7 @@ onMounted(() => {