diff --git a/src/views/monitor/topology-build/graph.ts b/src/views/monitor/topology-build/graph.ts deleted file mode 100644 index 99f295b2..00000000 --- a/src/views/monitor/topology-build/graph.ts +++ /dev/null @@ -1,1742 +0,0 @@ -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; - const neStateShape = 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', - }); - - // 添加动画 - neStateShape.animate( - { - // Magnifying and disappearing - r: 5, - opacity: 0.3, - }, - { - duration: 1000, - easing: 'easeCubic', - delay: 0, - repeat: true, // repeat - } - ); - } - - 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. 隐藏节点 -
-
- `; - } - } 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-build/hooks/useEdge.ts b/src/views/monitor/topology-build/hooks/useEdge.ts new file mode 100644 index 00000000..1929525a --- /dev/null +++ b/src/views/monitor/topology-build/hooks/useEdge.ts @@ -0,0 +1,151 @@ +import { message, Form } from 'ant-design-vue/lib'; +import { reactive, watch } from 'vue'; +import { graphG6 } from './useGraph'; + +/**图边内置边类型 */ +export const edgeTypeOptions = [ + { + value: 'line', + label: '直线,连接两个节点的直线', + }, + { + value: 'polyline', + label: '折线,多段线段构成的折线,连接两个端点', + }, + { + value: 'arc', + label: '圆弧线,连接两个节点的一段圆弧', + }, + { + value: 'quadratic', + label: '二阶贝塞尔曲线,只有一个控制点的曲线', + }, + { + value: 'cubic', + label: '三阶贝塞尔曲线,有两个控制点的曲线', + }, + { + value: 'cubic-vertical', + label: '垂直方向的三阶贝塞尔曲线', + }, + { + value: 'cubic-horizontal', + label: '水平方向的三阶贝塞尔曲线', + }, + { + value: 'loop', + label: '自环', + }, +]; + +/**图边标签文本位置 */ +export const edgePositionOptions = [ + { + value: 'start', + label: '开头', + }, + { + value: 'middle', + label: '中间', + }, + { + value: 'end', + label: '末尾', + }, +]; + +/**边信息状态类型 */ +type EdgeStateType = { + /**图边原始数据 */ + origin: Record; + /**图边表单数据 */ + form: Record; +}; + +/**边信息状态 */ +export let edgeState: EdgeStateType = reactive({ + origin: {}, + form: { + id: '', + source: '', + target: '', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, +}); + +/**图边对话框内表单属性和校验规则 */ +export const edgeStateForm = Form.useForm( + edgeState.form, + reactive({ + id: [{ required: true, message: '边唯一 ID' }], + source: [{ required: true, message: '起始点 id' }], + target: [{ required: true, message: '结束点 id' }], + type: [{ required: true, message: 'line' }], + }) +); + +/**图边编辑监听更新视图 */ +watch(edgeState.form, edge => { + const info = JSON.parse(JSON.stringify(edge)); + const edgeId = info.id; + if (edgeId) { + graphG6.value.clearItemStates(edgeId, 'selected'); + graphG6.value.updateItem(edgeId, info); + } +}); + +/**图边新增或更新 */ +export function handleOkEdge() { + const edge = JSON.parse(JSON.stringify(edgeState.form)); + if (!edge.id) { + message.warn({ + content: `边元素ID错误`, + duration: 2, + }); + return false; + } + // graphG6.value.removeItem(edge.id); + // edge.id = `${edge.source}~${Date.now()}~${edge.target}`; + // graphG6.value.addItem('edge', edge); + const item = graphG6.value.findById(edge.id); + if (item) { + graphG6.value.updateItem(item, edge); + } else { + edge.id = `${edge.source}~${Date.now()}~${edge.target}`; + graphG6.value.addItem('edge', edge); + } + edgeStateForm.resetFields(); + edgeState.origin = {}; + return true; +} + +/**图边取消还原 */ +export function handleCancelEdge() { + const origin = JSON.parse(JSON.stringify(edgeState.origin)); + if (origin.id) { + graphG6.value.updateItem(origin.id, origin); + // graphG6.value.removeItem(edgeOrigin.id); + // graphG6.value.addItem('edge', edgeOrigin); + edgeStateForm.resetFields(); + edgeState.origin = {}; + } +} diff --git a/src/views/monitor/topology-build/hooks/useGraph.ts b/src/views/monitor/topology-build/hooks/useGraph.ts new file mode 100644 index 00000000..a3f86d89 --- /dev/null +++ b/src/views/monitor/topology-build/hooks/useGraph.ts @@ -0,0 +1,422 @@ +import { + Graph, + GraphData, + ICanvas, + IShapeBase, + Item, + Menu, + Tooltip, +} from '@antv/g6'; +import { ref } from 'vue'; + +/**图实例对象 */ +export const graphG6 = ref(null); + +/**图事件变更 */ +export const graphEvent = ref<{ + type: string; + target: HTMLElement | (IShapeBase & ICanvas); + item: Item | null; +}>(); + +/**图画布右击菜单 */ +const graphCanvasMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['canvas'], + getContent(evt) { + return ` +
+
+ 1. 显示所有隐藏项 +
+
`; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'show': + // 显示节点 + graphG6.value.getNodes().forEach((node: any) => { + if (!node.isVisible()) { + graphG6.value.showItem(node); + graphG6.value.refreshItem(node); + } + }); + // 显示边 + graphG6.value.getEdges().forEach((edge: any) => { + if (!edge.isVisible()) { + graphG6.value.showItem(edge); + graphG6.value.refreshItem(edge); + } + }); + break; + } + }, +}); + +/**图节点右击菜单 */ +const graphNodeMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['node'], + getContent(evt) { + console.log(evt); + return ` +
+
+ 1. 编辑 +
+
+ 2. 隐藏 +
+
+ `; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'edit': + graphEvent.value = { type: `nodeMenu-${targetId}`, target, item }; + break; + case 'hide': + graphG6.value.hideItem(item); + break; + } + }, +}); + +/**图节点展示 */ +const graphNodeTooltip = new Tooltip({ + offsetX: 10, + offsetY: 20, + getContent(e: any) { + const outDiv = document.createElement('div'); + outDiv.style.width = '180px'; + outDiv.innerHTML = ` +

自定义tooltip

+
    +
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • +
`; + return outDiv; + }, + itemTypes: ['node'], +}); + +/**图边右击菜单 */ +const graphEdgeMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['edge'], + getContent(evt) { + console.log(evt); + return ` +
+
+ 1. 编辑 +
+
+ 2. 隐藏 +
+
+ `; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'edit': + graphEvent.value = { type: `edgeMenu-${targetId}`, target, item }; + break; + case 'hide': + graphG6.value.hideItem(item); + break; + } + }, +}); + +/**图边展示 */ +const graphEdgeTooltip = new Tooltip({ + offsetX: 10, + offsetY: 20, + getContent(e: any) { + const outDiv = document.createElement('div'); + outDiv.style.width = '180px'; + outDiv.innerHTML = ` +

graphEdgeTooltip

+
    +
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • +
`; + return outDiv; + }, + itemTypes: ['edge'], +}); + +/**图绑定事件 */ +function fnGraphEvent(graph: Graph) { + // 调用 graph.add / graph.addItem 方法之后触发 + graph.on('afteradditem', evt => { + fnSelectSourceTargetOptionsData(); + }); + + // 鼠标进入节点事件 + graph.on('edge:mouseenter', (ev: any) => { + // 获得鼠标当前目标边 + const edge = ev.item; + // 该边的起始点 + const source = edge.getSource(); + // 该边的结束点 + const target = edge.getTarget(); + // 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。 + // edge.toFront(); + // source.toFront(); + // target.toFront(); + }); + + graph.on('edge:mouseleave', (ev: any) => { + // 获得图上所有边实例 + const edges = graph.getEdges(); + // 遍历边,将所有边的层级放置在后方,以恢复原样 + // edges.forEach(edge => { + // edge.toBack(); + // }); + }); + + graph.on('node:mouseenter', evt => { + // 获得鼠标当前目标节点 + const node = evt.item; + // 获取该节点的所有相关边 + const edges = node && graph.getEdges(); + // 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果 + // edges.forEach((edge: any) => { + // edge.toFront(); + // edge.getSource().toFront(); + // edge.getTarget().toFront(); + // }); + // graphEvent.value = { + // type: 'node:mouseenter', + // target: evt.target, + // item: evt.item, + // }; + }); + + graph.on('node:mouseleave', (ev: any) => { + // 获得图上所有边实例 + const edges = graph.getEdges(); + // 遍历边,将所有边的层级放置在后方,以恢复原样 + // edges.forEach(edge => { + // edge.toBack(); + // }); + }); +} + +/**图元素选择开始结束点 */ +export const selectSourceTargetOptions = ref[]>([]); + +/** + * 图元素选择开始结束点数据获取 + */ +function fnSelectSourceTargetOptionsData() { + // 节点 + graphG6.value.getNodes().forEach((node: any) => { + const info = JSON.parse(JSON.stringify(node.getModel())); + selectSourceTargetOptions.value.push({ + value: info.id, + label: info.label, + info, + }); + }); + // 框 + graphG6.value.getCombos().forEach((combo1: any) => { + const info = JSON.parse(JSON.stringify(combo1.getModel())); + selectSourceTargetOptions.value.push({ + value: info.id, + label: info.label, + info, + }); + }); +} + +/**图数据渲染 */ +export function handleRanderGraph(container: HTMLElement, data: GraphData) { + if (!container) return; + const { clientHeight, clientWidth } = container; + + const graph = new Graph({ + container: container, + width: clientWidth, + height: clientHeight, + animate: true, + fitCenter: true, + modes: { + // default: [ + // // 允许拖拽画布、放缩画布、拖拽节点 + // 'drag-canvas', + // 'zoom-canvas', + // 'drag-node', + // ], + default: [ + { + type: 'click-select', + selectEdge: true, + }, + 'drag-combo', + { + type: 'drag-node', + onlyChangeComboSize: true, + }, + 'drag-canvas', + 'zoom-canvas', + 'collapse-expand-combo', + ], + edit: [ + { + type: 'click-select', + selectEdge: true, + }, + { + type: 'drag-node', + shouldEnd: (e: any) => { + return true; + }, + }, + { type: 'drag-combo' }, + 'drag-canvas', + 'zoom-canvas', + { type: 'create-edge', key: 'alt' }, + ], + }, + groupByTypes: false, + // layout: { + // type: 'dagre', + // sortByCombo: false, + // ranksep: 10, + // nodesep: 10, + // }, + // 全局节点 矩形 + defaultNode: { + type: 'rect', + size: [80, 40], + style: { + radius: 8, + // fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, // triangle 特有 + }, + direction: 'up', // triangle 三角形的方向 + }, + // 全局边 三次贝塞尔曲线 + defaultEdge: { + type: 'polyline', + style: { + offset: 20, // 拐弯处距离节点最小距离 + radius: 2, // 拐弯处的圆角弧度,若不设置则为直角 + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + // defaultEdge: { + // type: 'line', + // }, + // 全局框节点 矩形 + defaultCombo: { + type: 'rect', // Combo 类型 + size: [40, 40], + style: { + fillOpacity: 0.1, + }, + }, + plugins: [ + graphCanvasMenu, + graphNodeMenu, + graphNodeTooltip, + graphEdgeMenu, + graphEdgeTooltip, + ], + }); + graph.data(data); + graph.render(); + + // 图绑定事件 + fnGraphEvent(graph); + + graphG6.value = graph; + + // 图元素选择开始结束点数据 + fnSelectSourceTargetOptionsData(); + + return graph; +} + +/**图模式选择项 */ +export const graphModeOptions = [ + { + value: 'default', + label: '默认', + }, + { + value: 'edit', + label: '编辑', + }, +]; + +/**图模式选择项 */ +export const graphMode = ref('default'); + +/**图模式改变 default | edit */ +export function handleChangeMode(value: any) { + console.log(value, JSON.parse(JSON.stringify(graphG6.value.save()))); + graphG6.value.setMode(value); + graphMode.value = graphG6.value.getCurrentMode(); +} diff --git a/src/views/monitor/topology-build/hooks/useNode.ts b/src/views/monitor/topology-build/hooks/useNode.ts new file mode 100644 index 00000000..ee4d1085 --- /dev/null +++ b/src/views/monitor/topology-build/hooks/useNode.ts @@ -0,0 +1,294 @@ +import { message, Form } from 'ant-design-vue/lib'; +import { reactive, watch } from 'vue'; +import { graphG6 } from './useGraph'; + +/**图节点内置边类型 */ +export const nodeTypeOptions = [ + { + value: 'circle', + label: '圆形', + }, + { + value: 'rect', + label: '矩形', + }, + { + value: 'ellipse', + label: '椭圆', + }, + { + value: 'diamond', + label: '菱形', + }, + { + value: 'triangle', + label: '三角形', + }, + { + value: 'star', + label: '星形', + }, + { + value: 'image', + label: '图片', + }, + { + value: 'donut', + label: '面包圈', + }, +]; + +/**图节点标签文本位置 */ +export const nodePositionOptions = [ + { + value: 'top', + label: '上', + }, + { + value: 'left', + label: '左', + }, + { + value: 'right', + label: '右', + }, + { + value: 'bottom', + label: '下', + }, + { + value: 'center', + label: '居中', + }, +]; + +/**图节点三角形方向 */ +export const nodeDirectionOptions = [ + { value: 'up', label: '向上' }, + { value: 'down', label: '向下' }, + { value: 'left', label: '向左' }, + { value: 'right', label: '向右' }, +]; + +/**图节点图片裁剪的形状 */ +export const nodeImageClipCfgOptions = [ + { value: 'circle', label: '圆形' }, + { value: 'rect', label: '矩形' }, + { value: 'ellipse', label: '椭圆' }, +]; + +/**图节点图片来源 */ +export const nodeImageOptions = [ + { value: '/svg/base.svg', label: '基站' }, + { value: '/svg/cloud.svg', label: '云' }, + { value: '/svg/service.svg', label: '服务器' }, + { value: '/svg/service_db.svg', label: '数据服务器' }, +]; + +/**图节点信息状态类型 */ +type NodeStateType = { + /**图节点原始数据 */ + origin: Record; + /**图节点表单数据 */ + form: Record; +}; + +/**图节点信息状态 */ +export let nodeState: NodeStateType = reactive({ + origin: {}, + form: { + id: '', + x: 0, + y: 0, + type: 'circle', + size: [0], + anchorPoints: false, + style: { + fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + }, + label: '', + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + }, +}); + +/**图节点对话框内表单属性和校验规则 */ +export const nodeStateForm = Form.useForm( + nodeState.form, + reactive({ + id: [{ required: true, message: '节点唯一 ID' }], + type: [{ required: true, message: 'line' }], + }) +); + +/**图节点编辑监听更新视图 */ +watch(nodeState.form, node => { + const info = JSON.parse(JSON.stringify(node)); + const nodeId = info.id; + if (nodeId) { + // 图片类型需要移除style属性,避免填充 + if (info.type === 'image') { + Reflect.deleteProperty(info, 'style'); + } + graphG6.value.clearItemStates(nodeId, 'selected'); + graphG6.value.updateItem(nodeId, info); + // 三角和图片的样式变更需要重绘才生效 + if (info.type === 'triangle' || info.type === 'image') { + graphG6.value.read(graphG6.value.save()); + } + } +}); + +/**图节点类型输入限制 */ +export function handleTypeChange(type: any) { + // 设置图标属性 + if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) { + const origin = nodeState.origin; + if (origin.icon) { + nodeState.form = Object.assign(nodeState.form, { + icon: origin.icon, + }); + } else { + nodeState.form = Object.assign(nodeState.form, { + icon: { + show: false, + img: '', + width: 25, + height: 25, + }, + }); + } + } else if (type === 'triangle') { + // 三角 + const origin = nodeState.origin; + if (origin.icon) { + nodeState.form = Object.assign(nodeState.form, { + direction: origin.direction || 'up', // triangle 三角形的方向 + icon: Object.assign({ offset: 20 }, origin.icon), + }); + } else { + nodeState.form = Object.assign(nodeState.form, { + direction: 'up', // triangle 三角形的方向 + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, // triangle 特有 + }, + }); + } + } + // 设置图片属性 + if (type === 'image') { + const origin = nodeState.origin; + if (origin.img) { + nodeState.form = Object.assign(nodeState.form, { + img: origin.img, + clipCfg: origin.clipCfg, + }); + } else { + nodeState.form = Object.assign(nodeState.form, { + img: '/svg/service.svg', + clipCfg: { + show: false, + width: 0, + height: 0, + type: 'circle', + }, + }); + } + Reflect.deleteProperty(nodeState.form, 'style'); + } else { + // 当切换非图片时补充style属性 + if (!Reflect.has(nodeState.form, 'style')) { + nodeState.form = Object.assign(nodeState.form, { + style: { + fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + }, + }); + } + } +} + +/**图节点大小输入限制 */ +export function handleSizeChange(value: any) { + // 处理格式 + let intArr: number[] = []; + for (const v of value) { + const intV = parseInt(v); + if (!isNaN(intV)) { + intArr.push(intV); + } + } + // 节点类型限制size + const nodeType = nodeState.form.type; + switch (nodeType) { + case 'circle': + case 'star': + case 'donut': + intArr = intArr.slice(0, 1); + break; + case 'rect': + case 'ellipse': + case 'diamond': + case 'triangle': + case 'image': + intArr = intArr.slice(0, 2); + break; + } + nodeState.form.size = intArr; +} + +/**图节点新增或更新 */ +export function handleOkNode() { + const node = JSON.parse(JSON.stringify(nodeState.form)); + if (!node.id) { + message.warn({ + content: `节点元素ID错误`, + duration: 2, + }); + return false; + } + // graphG6.value.removeItem(node.id); + // graphG6.value.addItem('node', node); + const item = graphG6.value.findById(node.id); + if (item) { + graphG6.value.updateItem(item, node); + } else { + graphG6.value.addItem('node', node); + } + // 三角和图片的样式变更需要重绘才生效 + if (node.type === 'triangle' || node.type === 'image') { + graphG6.value.read(graphG6.value.save()); + } + nodeStateForm.resetFields(); + nodeState.origin = {}; + return true; +} + +/**图节点取消还原 */ +export function handleCancelNode() { + const origin = JSON.parse(JSON.stringify(nodeState.origin)); + if (origin.id) { + graphG6.value.updateItem(origin.id, origin); + // 三角和图片的样式变更需要重绘才生效 + if (origin.type === 'triangle' || origin.type === 'image') { + graphG6.value.read(graphG6.value.save()); + } + console.log(JSON.parse(JSON.stringify(nodeState.form))); + nodeStateForm.resetFields(); + nodeState.origin = {}; + } +} diff --git a/src/views/monitor/topology-build/index.vue b/src/views/monitor/topology-build/index.vue index 43a514d5..78797adf 100644 --- a/src/views/monitor/topology-build/index.vue +++ b/src/views/monitor/topology-build/index.vue @@ -2,28 +2,68 @@ import { reactive, onMounted, ref, watch } from 'vue'; import { PageContainer } from 'antdv-pro-layout'; import useI18n from '@/hooks/useI18n'; -import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; -import { listNe, stateNe } from '@/api/ne/ne'; -import { parseDateToStr } from '@/utils/date-utils'; -import { Graph, Menu, Tooltip } from '@antv/g6'; -import { message, Modal, Form, notification } from 'ant-design-vue/lib'; +import { + handleRanderGraph, + handleChangeMode, + graphEvent, + graphG6, + selectSourceTargetOptions, + graphMode, + graphModeOptions, +} from './hooks/useGraph'; +import { + edgeTypeOptions, + edgePositionOptions, + handleOkEdge, + handleCancelEdge, + edgeState, + edgeStateForm, +} from './hooks/useEdge'; +import { + nodeTypeOptions, + nodePositionOptions, + nodeDirectionOptions, + nodeImageClipCfgOptions, + nodeImageOptions, + handleTypeChange, + handleSizeChange, + handleOkNode, + handleCancelNode, + nodeState, + nodeStateForm, +} from './hooks/useNode'; const { t } = useI18n(); /**图DOM节点实例对象 */ const graphG6Dom = ref(undefined); -/**图实例对象 */ -const graphG6 = ref(null); +/**图监听事件变更 */ +watch(graphEvent, v => { + if (!v) return; + const { type, target, item } = v; + console.log(type, target, item); -/**图实例状态 */ -const graphG6State = reactive>({ - mode: 'default', - editEdge: {}, + if (type === 'edgeMenu-edit' && item) { + const edge = item.getModel(); + edgeState.origin = JSON.parse(JSON.stringify(edge)); + edgeState.form = Object.assign(edgeState.form, edge); + modalState.title = '边信息编辑'; + modalState.formType = 'edge'; + modalState.visible = true; + } + if (type === 'nodeMenu-edit' && item) { + const node = item.getModel(); + nodeState.origin = JSON.parse(JSON.stringify(node)); + nodeState.form = Object.assign(nodeState.form, node); + modalState.title = '节点信息编辑'; + modalState.formType = 'node'; + modalState.visible = true; + } }); /**图数据 */ -const graphG6Data = reactive>({ +const graphG6Data2 = reactive>({ nodes: [ // 0 基站 { @@ -320,288 +360,186 @@ const graphG6Data = reactive>({ ], }); -/**图绑定事件 */ -function graphEvent(graph: Graph) { - // 调用 graph.add / graph.addItem 方法之后触发 - graph.on('afteradditem', evt => { - fnSelectSourceTargetOptionsData(); - }); - - // 鼠标进入节点事件 - graph.on('edge:mouseenter', (ev: any) => { - // 获得鼠标当前目标边 - const edge = ev.item; - // 该边的起始点 - const source = edge.getSource(); - // 该边的结束点 - const target = edge.getTarget(); - // 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。 - // edge.toFront(); - // source.toFront(); - // target.toFront(); - }); - - graph.on('edge:mouseleave', (ev: any) => { - // 获得图上所有边实例 - const edges = graph.getEdges(); - // 遍历边,将所有边的层级放置在后方,以恢复原样 - // edges.forEach(edge => { - // edge.toBack(); - // }); - }); - - graph.on('node:mouseenter', (ev: any) => { - // 获得鼠标当前目标节点 - const node = ev.item; - // 获取该节点的所有相关边 - const edges = node.getEdges(); - // 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果 - // edges.forEach((edge: any) => { - // edge.toFront(); - // edge.getSource().toFront(); - // edge.getTarget().toFront(); - // }); - }); - - graph.on('node:mouseleave', (ev: any) => { - // 获得图上所有边实例 - const edges = graph.getEdges(); - // 遍历边,将所有边的层级放置在后方,以恢复原样 - // edges.forEach(edge => { - // edge.toBack(); - // }); - }); -} - -/**图画布右击菜单 */ -const graphCanvasMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['canvas'], - getContent(evt) { - return ` -
-
- 1. 显示所有隐藏项 -
-
`; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'show': - // 显示节点 - graphG6.value.getNodes().forEach((node: any) => { - if (!node.isVisible()) { - graphG6.value.showItem(node); - graphG6.value.refreshItem(node); - } - }); - // 显示边 - graphG6.value.getEdges().forEach((edge: any) => { - if (!edge.isVisible()) { - graphG6.value.showItem(edge); - graphG6.value.refreshItem(edge); - } - }); - break; - } - }, -}); - -/**图节点右击菜单 */ -const graphNodeMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['node'], - getContent(evt) { - console.log(evt); - return ` -
-
- 1. 编辑 -
-
- 2. 隐藏 -
-
- `; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'edit': - const node = item.getModel(); - modalState.title = '节点信息编辑'; - modalState.formNodeOrigin = JSON.parse(JSON.stringify(node)); - modalState.formNode = Object.assign(modalState.formNode, node); - modalState.formType = 'node'; - modalState.visible = true; - console.log(JSON.parse(JSON.stringify(modalState.formNode))); - break; - case 'hide': - graphG6.value.hideItem(item); - break; - } - }, -}); - -/**图节点展示 */ -const graphNodeTooltip = new Tooltip({ - offsetX: 10, - offsetY: 20, - getContent(e: any) { - const outDiv = document.createElement('div'); - outDiv.style.width = '180px'; - outDiv.innerHTML = ` -

自定义tooltip

-
    -
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • -
`; - return outDiv; - }, - itemTypes: ['node'], -}); - -/**图边右击菜单 */ -const graphEdgeMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['edge'], - getContent(evt) { - console.log(evt); - return ` -
-
- 1. 编辑 -
-
- 2. 隐藏 -
-
- `; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'edit': - const edge = item.getModel(); - modalState.title = '边信息编辑'; - modalState.formEdgeOrigin = JSON.parse(JSON.stringify(edge)); - modalState.formEdge = Object.assign(modalState.formEdge, edge); - modalState.formType = 'edge'; - modalState.visible = true; - break; - case 'hide': - graphG6.value.hideItem(item); - break; - } - }, -}); - -/**图边展示 */ -const graphEdgeTooltip = new Tooltip({ - offsetX: 10, - offsetY: 20, - getContent(e: any) { - const outDiv = document.createElement('div'); - outDiv.style.width = '180px'; - outDiv.innerHTML = ` -

graphEdgeTooltip

-
    -
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • -
`; - return outDiv; - }, - itemTypes: ['edge'], -}); - -/**查询全部网元数据列表 */ -function fnRanderGraph() { - if (!graphG6Dom.value) return; - const { clientHeight, clientWidth } = graphG6Dom.value; - - const graph = new Graph({ - container: graphG6Dom.value, - width: clientWidth, - height: clientHeight, - animate: true, - fitCenter: true, - modes: { - // default: [ - // // 允许拖拽画布、放缩画布、拖拽节点 - // 'drag-canvas', - // 'zoom-canvas', - // 'drag-node', - // ], - default: [ - { - type: 'click-select', - selectEdge: true, +const graphG6Data = reactive>({ + nodes: [ + { + id: '0', + x: 415, + y: 482, + size: 48, + type: 'circle', + label: '基站', + labelCfg: { + position: 'bottom', + offset: 10, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, }, - 'drag-combo', - { - type: 'drag-node', - onlyChangeComboSize: true, + }, + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, }, - 'drag-canvas', - 'zoom-canvas', - 'collapse-expand-combo', - ], - edit: [ - { - type: 'click-select', - selectEdge: true, - }, - { - type: 'drag-node', - shouldEnd: (e: any) => { - return true; + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, }, }, - { type: 'drag-combo' }, - 'drag-canvas', - 'zoom-canvas', - { type: 'create-edge', key: 'alt' }, - ], + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#5B8FF9', + lineWidth: 2, + cursor: 'pointer', + fill: '#9EC9FF', + }, + icon: { + show: true, + img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + width: 24, + height: 24, + offset: 20, + }, + direction: 'up', }, - groupByTypes: false, - // layout: { - // type: 'dagre', - // sortByCombo: false, - // ranksep: 10, - // nodesep: 10, - // }, - // 全局节点 矩形 - defaultNode: { + { + id: '1', + x: 132, + y: 42, + label: 'DN', + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#00b050', + lineWidth: 1, + cursor: 'pointer', + fill: '#00b050', + }, + type: 'rect', + size: [80, 40], + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '2', + x: 747, + y: 431, + label: 'O&M', type: 'rect', size: [80, 40], style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, radius: 8, - // fill: '#ffffff', stroke: '#ffffff', lineWidth: 1, cursor: 'pointer', @@ -620,20 +558,683 @@ function fnRanderGraph() { img: '/svg/service.svg', width: 25, height: 25, - offset: 20, // triangle 特有 + offset: 20, }, - direction: 'up', // triangle 三角形的方向 + direction: 'up', }, - // 全局边 三次贝塞尔曲线 - defaultEdge: { - type: 'polyline', + { + id: '100', + label: 'EMS', + comboId: 'combo-ems', + x: 106, + y: 450, + type: 'rect', + size: [80, 40], style: { - offset: 20, // 拐弯处距离节点最小距离 - radius: 2, // 拐弯处的圆角弧度,若不设置则为直角 + radius: 8, stroke: '#ffffff', lineWidth: 1, cursor: 'pointer', }, + depth: 50, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '190', + comboId: 'combo-upf', + x: 119, + y: 338, + label: 'UPF', + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#d580ff', + lineWidth: 1, + cursor: 'pointer', + fill: '#d580ff', + }, + type: 'rect', + size: [80, 40], + depth: 28, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '110', + comboId: 'combo-ims', + x: 600, + y: 350, + label: 'IMS', + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ed7d31', + lineWidth: 1, + cursor: 'pointer', + fill: '#ed7d31', + }, + type: 'rect', + size: [80, 40], + depth: 39, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '170', + label: 'NSSF', + comboId: 'combo-5gc', + x: 300, + y: 50, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 17, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '130', + label: 'AUSF', + comboId: 'combo-5gc', + x: 450, + y: 50, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 16, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '140', + label: 'UDM', + comboId: 'combo-5gc', + x: 600, + y: 50, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 15, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '120', + label: 'AMF', + comboId: 'combo-5gc', + x: 300, + y: 150, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 14, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '180', + label: 'NRF', + comboId: 'combo-5gc', + x: 450, + y: 150, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 13, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '150', + label: 'SMF', + comboId: 'combo-5gc', + x: 300, + y: 250, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 12, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + { + id: '160', + label: 'PCF', + comboId: 'combo-5gc', + x: 700, + y: 250, + type: 'rect', + size: [80, 40], + style: { + active: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + }, + selected: { + fill: 'rgb(255, 255, 255)', + stroke: 'rgb(95, 149, 255)', + lineWidth: 4, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + fill: 'rgb(223, 234, 255)', + stroke: '#4572d9', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + fill: 'rgb(247, 250, 255)', + stroke: 'rgb(191, 213, 255)', + lineWidth: 1, + }, + disable: { + fill: 'rgb(250, 250, 250)', + stroke: 'rgb(224, 224, 224)', + lineWidth: 1, + }, + radius: 8, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + depth: 11, + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + icon: { + show: false, + img: '/svg/service.svg', + width: 25, + height: 25, + offset: 20, + }, + direction: 'up', + }, + ], + edges: [ + { + id: '170-120', + source: '170', + target: '120', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 259.5, + y: 50, + anchorIndex: 0, + id: '259.5|||50', + }, + endPoint: { + x: 259.5, + y: 150, + anchorIndex: 0, + id: '259.5|||150', + }, labelCfg: { refX: 0, refY: 0, @@ -646,169 +1247,1482 @@ function fnRanderGraph() { }, }, }, - // defaultEdge: { - // type: 'line', - // }, - // 全局框节点 矩形 - defaultCombo: { - type: 'rect', // Combo 类型 - size: [40, 40], + { + id: '140-120', + source: '140', + target: '120', + type: 'polyline', style: { - fillOpacity: 0.1, + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 559.5, + y: 50, + anchorIndex: 0, + id: '559.5|||50', + }, + endPoint: { + x: 340.5, + y: 150, + anchorIndex: 1, + id: '340.5|||150', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, }, }, - plugins: [ - graphCanvasMenu, - graphNodeMenu, - graphNodeTooltip, - graphEdgeMenu, - graphEdgeTooltip, - ], - }); - graph.data(graphG6Data); - graph.render(); + { + id: '120-180', + source: '120', + target: '180', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 340.5, + y: 150, + anchorIndex: 1, + id: '340.5|||150', + }, + endPoint: { + x: 409.5, + y: 150, + anchorIndex: 0, + id: '409.5|||150', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '140-110', + source: '140', + target: '110', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 559.5, + y: 50, + anchorIndex: 0, + id: '559.5|||50', + }, + endPoint: { + x: 559.5, + y: 350, + anchorIndex: 0, + id: '559.5|||350', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '120-150', + source: '120', + target: '150', + data: {}, + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 259.5, + y: 150, + anchorIndex: 0, + id: '259.5|||150', + }, + endPoint: { + x: 259.5, + y: 250, + anchorIndex: 0, + id: '259.5|||250', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '150-180', + source: '150', + target: '180', + data: {}, + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 340.5, + y: 250, + anchorIndex: 1, + id: '340.5|||250', + }, + endPoint: { + x: 409.5, + y: 150, + anchorIndex: 0, + id: '409.5|||150', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '160-120', + source: '160', + target: '120', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 659.5, + y: 250, + anchorIndex: 0, + id: '659.5|||250', + }, + endPoint: { + x: 340.5, + y: 150, + anchorIndex: 1, + id: '340.5|||150', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '160-180', + source: '160', + target: '180', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 659.5, + y: 250, + anchorIndex: 0, + id: '659.5|||250', + }, + endPoint: { + x: 490.5, + y: 150, + anchorIndex: 1, + id: '490.5|||150', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '160-110', + source: '160', + target: '110', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 659.5, + y: 250, + anchorIndex: 0, + id: '659.5|||250', + }, + endPoint: { + x: 640.5, + y: 350, + anchorIndex: 1, + id: '640.5|||350', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '150-190', + source: '150', + target: '190', + type: 'polyline', + style: { + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + startPoint: { + x: 259.5, + y: 250, + anchorIndex: 0, + id: '259.5|||250', + }, + endPoint: { + x: 159.5, + y: 338, + anchorIndex: 1, + id: '159.5|||338', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: 'ems-upf', + source: 'combo-ems', + target: 'combo-upf', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + isComboEdge: true, + startPoint: { + x: 167, + y: 445, + anchorIndex: 1, + id: '167|||445', + }, + endPoint: { + x: 58, + y: 333, + anchorIndex: 0, + id: '58|||333', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: 'ems-ims', + source: 'combo-ems', + target: 'combo-ims', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + isComboEdge: true, + startPoint: { + x: 167, + y: 445, + anchorIndex: 1, + id: '167|||445', + }, + endPoint: { + x: 539, + y: 345, + anchorIndex: 0, + id: '539|||345', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: '0~1703575912428~combo-upf', + source: '0', + target: 'combo-upf', + type: 'cubic-vertical', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#eeff00', + lineWidth: 13, + cursor: 'pointer', + startArrow: true, + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 415, + y: 457, + }, + endPoint: { + x: 58, + y: 333, + anchorIndex: 0, + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: 'combo-upf~1703575945004~1', + source: 'combo-upf', + target: '1', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#4202f2', + lineWidth: 12, + cursor: 'pointer', + endArrow: true, + startArrow: true, + }, + label: '3分3发', + labelCfg: { + refX: 0, + refY: 0, + position: 'end', + autoRotate: true, + style: { + fill: '#fa9e00', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 180, + y: 333, + anchorIndex: 1, + id: '180|||333', + }, + endPoint: { + x: 91.5, + y: 42, + anchorIndex: 0, + id: '91.5|||42', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: 'combo-ems~1703575966436~2', + source: 'combo-ems', + target: '2', + type: 'quadratic', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#20cb81', + lineWidth: 12, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 167, + y: 445, + anchorIndex: 1, + }, + endPoint: { + x: 706.5, + y: 431, + anchorIndex: 0, + }, + curvePosition: 0.5, + minCurveOffset: [0, 0], + curveOffset: -20, + }, + { + id: 'combo-ems~1703575974507~combo-5gc', + source: 'combo-ems', + target: 'combo-5gc', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#cc1e1e', + lineWidth: 1, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 167, + y: 445, + anchorIndex: 1, + id: '167|||445', + }, + endPoint: { + x: 239, + y: 145, + anchorIndex: 0, + id: '239|||145', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '150~1703575985251~160', + source: '150', + target: '160', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#5eab4f', + lineWidth: 5, + cursor: 'pointer', + endArrow: true, + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 340.5, + y: 250, + anchorIndex: 1, + id: '340.5|||250', + }, + endPoint: { + x: 659.5, + y: 250, + anchorIndex: 0, + id: '659.5|||250', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '0-5gc', + source: '0', + target: 'combo-5gc', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#ffffff', + lineWidth: 1, + cursor: 'pointer', + }, + isComboEdge: true, + startPoint: { + x: 421.20060794856374, + y: 457.78115483619814, + id: '421.20060794856374|||457.78115483619814', + }, + endPoint: { + x: 239, + y: 145, + anchorIndex: 0, + id: '239|||145', + }, + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 12, + fontWeight: 500, + }, + }, + }, + { + id: 'combo-upf~1703576027131~combo-ims', + source: 'combo-upf', + target: 'combo-ims', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#7f5757', + lineWidth: 5, + cursor: 'pointer', + }, + label: '3分3发', + labelCfg: { + refX: 2, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 180, + y: 333, + anchorIndex: 1, + id: '180|||333', + }, + endPoint: { + x: 539, + y: 345, + anchorIndex: 0, + id: '539|||345', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '130~1703576145468~120', + source: '130', + target: '120', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#492727', + lineWidth: 1, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 409.5, + y: 50, + anchorIndex: 0, + id: '409.5|||50', + }, + endPoint: { + x: 340.5, + y: 150, + anchorIndex: 1, + id: '340.5|||150', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '130~1703576154675~180', + source: '130', + target: '180', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#9a0e0e', + lineWidth: 7, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 409.5, + y: 50, + anchorIndex: 0, + id: '409.5|||50', + }, + endPoint: { + x: 409.5, + y: 150, + anchorIndex: 0, + id: '409.5|||150', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '140~1703576175859~150', + source: '140', + target: '150', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#a63636', + lineWidth: 3, + cursor: 'pointer', + }, + label: '3分3发', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 559.5, + y: 50, + anchorIndex: 0, + id: '559.5|||50', + }, + endPoint: { + x: 340.5, + y: 250, + anchorIndex: 1, + id: '340.5|||250', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + { + id: '140~1703575996915~180', + source: '140', + target: '180', + type: 'polyline', + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(234, 234, 234)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(245, 245, 245)', + lineWidth: 1, + }, + offset: 20, + radius: 2, + stroke: '#3de6e3', + lineWidth: 4, + cursor: 'pointer', + }, + label: '', + labelCfg: { + refX: 0, + refY: 0, + position: 'middle', + autoRotate: false, + style: { + fill: '#ffffff', + fontSize: 14, + fontWeight: 400, + }, + }, + isComboEdge: true, + startPoint: { + x: 559.5, + y: 50, + anchorIndex: 0, + id: '559.5|||50', + }, + endPoint: { + x: 490.5, + y: 150, + anchorIndex: 1, + id: '490.5|||150', + }, + curvePosition: [0.5, 0.5], + minCurveOffset: [0, 0], + }, + ], + combos: [ + { + id: 'combo-5gc', + label: 'combo 5GC控制面', + children: [ + { + id: '170', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '130', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '140', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '120', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '180', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '150', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + { + id: '160', + comboId: 'combo-5gc', + itemType: 'node', + depth: 12, + }, + ], + depth: 10, + type: 'rect', + size: [40, 40], + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + fill: 'rgb(247, 250, 255)', + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: '#4572d9', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(224, 224, 224)', + fill: 'rgb(253, 253, 253)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(234, 234, 234)', + fill: 'rgb(250, 250, 250)', + lineWidth: 1, + }, + fillOpacity: 0.1, + r: 240.5, + width: 481, + height: 241, + }, + x: 500, + y: 150, + }, + { + id: 'combo-upf', + label: 'combo upf', + children: [ + { + id: '190', + comboId: 'combo-upf', + itemType: 'node', + depth: 29, + }, + ], + depth: 27, + type: 'rect', + size: [40, 40], + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + fill: 'rgb(247, 250, 255)', + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: '#4572d9', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(224, 224, 224)', + fill: 'rgb(253, 253, 253)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(234, 234, 234)', + fill: 'rgb(250, 250, 250)', + lineWidth: 1, + }, + fillOpacity: 0.1, + r: 40.5, + width: 81, + height: 41, + }, + x: 119, + y: 338, + }, + { + id: 'combo-ims', + label: 'combo ims', + children: [ + { + id: '110', + comboId: 'combo-ims', + itemType: 'node', + depth: 40, + }, + ], + depth: 38, + type: 'rect', + size: [40, 40], + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + fill: 'rgb(247, 250, 255)', + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: '#4572d9', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(224, 224, 224)', + fill: 'rgb(253, 253, 253)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(234, 234, 234)', + fill: 'rgb(250, 250, 250)', + lineWidth: 1, + }, + fillOpacity: 0.1, + r: 40.5, + width: 81, + height: 41, + }, + x: 600, + y: 350, + }, + { + id: 'combo-ems', + label: 'Combo ems', + children: [ + { + id: '100', + comboId: 'combo-ems', + itemType: 'node', + depth: 51, + }, + ], + depth: 49, + type: 'rect', + size: [40, 40], + style: { + active: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 1, + fill: 'rgb(247, 250, 255)', + }, + selected: { + stroke: 'rgb(95, 149, 255)', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + shadowColor: 'rgb(95, 149, 255)', + shadowBlur: 10, + 'text-shape': { + fontWeight: 500, + }, + }, + highlight: { + stroke: '#4572d9', + lineWidth: 2, + fill: 'rgb(253, 253, 253)', + 'text-shape': { + fontWeight: 500, + }, + }, + inactive: { + stroke: 'rgb(224, 224, 224)', + fill: 'rgb(253, 253, 253)', + lineWidth: 1, + }, + disable: { + stroke: 'rgb(234, 234, 234)', + fill: 'rgb(250, 250, 250)', + lineWidth: 1, + }, + fillOpacity: 0.1, + r: 40.5, + width: 81, + height: 41, + }, + x: 106, + y: 450, + }, + ], +}); - // 图绑定事件 - graphEvent(graph); - - graphG6.value = graph; - - // - fnSelectSourceTargetOptionsData(); -} - -/**查询网元状态 */ -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(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: 20, - 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: 20, - 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; - - return true; - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - return false; - } - }) - .then(hasNeList => { - if (!hasNeList) return; - if (refresh) { - graphG6.value.destroy(); - } - fnGetState(); - }); +// 将数据渲染到画布 +function fnRanderGraph(data: Record) { + // 获取DOM渲染数据 + if (!graphG6Dom.value) return; + handleRanderGraph(graphG6Dom.value, data); } onMounted(() => { - // 获取网元列表 - // fnGetList(); - fnRanderGraph(); + fnRanderGraph(graphG6Data); }); -/**改变图模式 */ -function fnChangeMode(value: any) { - console.log(value, JSON.parse(JSON.stringify(graphG6.value.save()))); - graphG6.value.setMode(value); -} - /**对话框对象信息状态类型 */ type ModalStateType = { /**对话框是否显示 */ @@ -817,12 +2731,6 @@ type ModalStateType = { title: string; /**图元素表单类型 */ formType: 'edge' | 'node'; - /**图节点表单数据 */ - formNodeOrigin: Record; - formNode: Record; - /**图边表单数据 */ - formEdgeOrigin: Record; - formEdge: Record; /**确定按钮 loading */ confirmLoading: boolean; }; @@ -832,432 +2740,29 @@ let modalState: ModalStateType = reactive({ visible: false, title: '图信息', formType: 'edge', - formNodeOrigin: {}, - formNode: { - id: '', - x: 0, - y: 0, - type: 'circle', - size: [0], - anchorPoints: false, - style: { - fill: '#ffffff', - stroke: '#ffffff', - lineWidth: 1, - }, - label: '', - labelCfg: { - position: 'center', - offset: 0, - style: { - fill: '#000000', - fontSize: 12, - fontWeight: 500, - }, - }, - }, - formEdgeOrigin: {}, - formEdge: { - id: '', - source: '', - target: '', - type: 'polyline', - style: { - offset: 20, - radius: 2, - stroke: '#ffffff', - lineWidth: 1, - cursor: 'pointer', - }, - label: '', - labelCfg: { - refX: 0, - refY: 0, - position: 'middle', - autoRotate: false, - style: { - fill: '#ffffff', - fontSize: 12, - fontWeight: 500, - }, - }, - }, confirmLoading: false, }); -/**图元素选择开始结束点 */ -let selectSourceTargetOptions = ref[]>([]); - -/** - * 图元素选择开始结束点数据获取 - */ -function fnSelectSourceTargetOptionsData() { - // 节点 - graphG6.value.getNodes().forEach((node: any) => { - const info = JSON.parse(JSON.stringify(node.getModel())); - selectSourceTargetOptions.value.push({ - value: info.id, - label: info.label, - info, - }); - }); - // 框 - graphG6.value.getCombos().forEach((combo1: any) => { - const info = JSON.parse(JSON.stringify(combo1.getModel())); - selectSourceTargetOptions.value.push({ - value: info.id, - label: info.label, - info, - }); - }); -} - -/**图边对话框内表单属性和校验规则 */ -const modalStateFormEdge = Form.useForm( - modalState.formEdge, - reactive({ - id: [{ required: true, message: '边唯一 ID' }], - source: [{ required: true, message: '起始点 id' }], - target: [{ required: true, message: '结束点 id' }], - type: [{ required: true, message: 'line' }], - }) -); - -/**图边内置边类型 */ -const edgeTypeOptions = [ - { - value: 'line', - label: '直线,连接两个节点的直线', - }, - { - value: 'polyline', - label: '折线,多段线段构成的折线,连接两个端点', - }, - { - value: 'arc', - label: '圆弧线,连接两个节点的一段圆弧', - }, - { - value: 'quadratic', - label: '二阶贝塞尔曲线,只有一个控制点的曲线', - }, - { - value: 'cubic', - label: '三阶贝塞尔曲线,有两个控制点的曲线', - }, - { - value: 'cubic-vertical', - label: '垂直方向的三阶贝塞尔曲线', - }, - { - value: 'cubic-horizontal', - label: '水平方向的三阶贝塞尔曲线', - }, - { - value: 'loop', - label: '自环', - }, -]; - -/**图边标签文本位置 */ -const edgePositionOptions = [ - { - value: 'start', - label: '开头', - }, - { - value: 'middle', - label: '中间', - }, - { - value: 'end', - label: '末尾', - }, -]; - -/**图边编辑监听更新视图 */ -watch(modalState.formEdge, edge => { - const info = JSON.parse(JSON.stringify(edge)); - const edgeId = info.id; - if (edgeId) { - graphG6.value.clearItemStates(edgeId, 'selected'); - graphG6.value.updateItem(edgeId, info); - } -}); - -/**图边新增 */ -function fnModalOkEdge(edge: any) { - console.log(JSON.parse(JSON.stringify(graphG6.value.save()))); - if (!edge.id) { - message.warn({ - content: `边元素ID错误`, - duration: 2, - }); - return; - } - // graphG6.value.removeItem(edge.id); - // edge.id = `${edge.source}~${Date.now()}~${edge.target}`; - // graphG6.value.addItem('edge', edge); - const item = graphG6.value.findById(edge.id); - if (item) { - graphG6.value.updateItem(item, edge); - } else { - edge.id = `${edge.source}~${Date.now()}~${edge.target}`; - graphG6.value.addItem('edge', edge); - } - modalState.visible = false; - modalStateFormEdge.resetFields(); - modalState.formEdgeOrigin = {}; -} - -/**图节点对话框内表单属性和校验规则 */ -const modalStateFormNode = Form.useForm( - modalState.formNode, - reactive({ - id: [{ required: true, message: '边唯一 ID' }], - source: [{ required: true, message: '起始点 id' }], - target: [{ required: true, message: '结束点 id' }], - type: [{ required: true, message: 'line' }], - }) -); - -/**图节点内置边类型 */ -const nodeTypeOptions = [ - { - value: 'circle', - label: '圆形', - }, - { - value: 'rect', - label: '矩形', - }, - { - value: 'ellipse', - label: '椭圆', - }, - { - value: 'diamond', - label: '菱形', - }, - { - value: 'triangle', - label: '三角形', - }, - { - value: 'star', - label: '星形', - }, - { - value: 'image', - label: '图片', - }, - { - value: 'donut', - label: '面包圈', - }, -]; - -/**图节点标签文本位置 */ -const nodePositionOptions = [ - { - value: 'top', - label: '上', - }, - { - value: 'left', - label: '左', - }, - { - value: 'right', - label: '右', - }, - { - value: 'bottom', - label: '下', - }, - { - value: 'center', - label: '居中', - }, -]; - -/**图节点三角形方向 */ -const nodeDirectionOptions = [ - { value: 'up', label: '向上' }, - { value: 'down', label: '向下' }, - { value: 'left', label: '向左' }, - { value: 'right', label: '向右' }, -]; - -/**图节点图片裁剪的形状 */ -const nodeImageClipCfgOptions = [ - { value: 'circle', label: '圆形' }, - { value: 'rect', label: '矩形' }, - { value: 'ellipse', label: '椭圆' }, -]; - -/**图节点类型输入限制 */ -function fnNodeTypeChange(type: any) { - console.log(type); - - // 设置图标属性 - if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) { - const nodeOrigin = modalState.formNodeOrigin; - if (nodeOrigin.icon) { - modalState.formNode = Object.assign(modalState.formNode, { - icon: nodeOrigin.icon, - }); - } else { - modalState.formNode = Object.assign(modalState.formNode, { - icon: { - show: false, - img: '', - width: 25, - height: 25, - }, - }); - } - } else if (type === 'triangle') { - // 三角 - const nodeOrigin = modalState.formNodeOrigin; - if (nodeOrigin.icon) { - modalState.formNode = Object.assign(modalState.formNode, { - direction: nodeOrigin.direction || 'up', // triangle 三角形的方向 - icon: Object.assign({ offset: 20 }, nodeOrigin.icon), - }); - } else { - modalState.formNode = Object.assign(modalState.formNode, { - direction: 'up', // triangle 三角形的方向 - icon: { - show: false, - img: '/svg/service.svg', - width: 25, - height: 25, - offset: 20, // triangle 特有 - }, - }); - } - } - // 设置图片属性 - if (type === 'image') { - const nodeOrigin = modalState.formNodeOrigin; - if (nodeOrigin.img) { - modalState.formNode = Object.assign(modalState.formNode, { - img: nodeOrigin.img, - clipCfg: nodeOrigin.clipCfg, - }); - } else { - modalState.formNode = Object.assign(modalState.formNode, { - img: '/svg/service.svg', - clipCfg: { - show: false, - width: 0, - height: 0, - type: 'circle', - }, - }); - } - Reflect.deleteProperty(modalState.formNode, 'style'); - } else { - // 当切换非图片时补充style属性 - if (!Reflect.has(modalState.formNode, 'style')) { - modalState.formNode = Object.assign(modalState.formNode, { - style: { - fill: '#ffffff', - stroke: '#ffffff', - lineWidth: 1, - }, - }); - } - } -} - -/**图节点大小输入限制 */ -function fnNodeSizeChange(value: any) { - // 处理格式 - let intArr: number[] = []; - for (const v of value) { - const intV = parseInt(v); - if (!isNaN(intV)) { - intArr.push(intV); - } - } - // 节点类型限制size - const nodeType = modalState.formNode.type; - switch (nodeType) { - case 'circle': - case 'star': - case 'donut': - intArr = intArr.slice(0, 1); - break; - case 'rect': - case 'ellipse': - case 'diamond': - case 'triangle': - case 'image': - intArr = intArr.slice(0, 2); - break; - } - modalState.formNode.size = intArr; -} - -/**图节点编辑监听更新视图 */ -watch(modalState.formNode, node => { - const info = JSON.parse(JSON.stringify(node)); - const nodeId = info.id; - console.log(info); - if (nodeId) { - if (info.type === 'image') { - Reflect.deleteProperty(info, 'style'); - } - graphG6.value.clearItemStates(nodeId, 'selected'); - graphG6.value.updateItem(nodeId, info); - if (info.type === 'triangle' || info.type === 'image') { - graphG6.value.read(graphG6.value.save()); - } - } -}); - -/**图节点新增 */ -function fnModalOkNode(node: any) { - console.log(JSON.parse(JSON.stringify(graphG6.value.save()))); - if (!node.id) { - message.warn({ - content: `节点元素ID错误`, - duration: 2, - }); - return; - } - // graphG6.value.removeItem(node.id); - // graphG6.value.addItem('node', node); - const item = graphG6.value.findById(node.id); - if (item) { - graphG6.value.updateItem(item, node); - } else { - graphG6.value.addItem('node', node); - } - if (node.type === 'triangle' || node.type === 'image') { - graphG6.value.read(graphG6.value.save()); - } - modalState.visible = false; - modalStateFormNode.resetFields(); - modalState.formNodeOrigin = {}; -} - /** * 对话框弹出确认执行函数 * 进行表达规则校验 */ function fnModalOk() { + modalState.confirmLoading = true; const type = modalState.formType; - console.log(type); + // 边编辑确认 if (type === 'edge') { - const edge = JSON.parse(JSON.stringify(modalState.formEdge)); - fnModalOkEdge(edge); + const result = handleOkEdge(); + console.log(type, result); + modalState.visible = !result; + modalState.confirmLoading = !result; } + // 节点编辑确认 if (type === 'node') { - const node = JSON.parse(JSON.stringify(modalState.formNode)); - fnModalOkNode(node); + const result = handleOkNode(); + console.log(type, result); + modalState.visible = !result; + modalState.confirmLoading = !result; } } @@ -1269,23 +2774,12 @@ function fnModalCancel() { modalState.visible = false; const type = modalState.formType; // 边编辑还原 - const edgeOrigin = JSON.parse(JSON.stringify(modalState.formEdgeOrigin)); - if (type === 'edge' && edgeOrigin.id) { - graphG6.value.updateItem(edgeOrigin.id, edgeOrigin); - // graphG6.value.removeItem(edgeOrigin.id); - // graphG6.value.addItem('edge', edgeOrigin); - modalStateFormEdge.resetFields(); - modalState.formEdgeOrigin = {}; + if (type === 'edge') { + handleCancelEdge(); } // 节点编辑还原 - const nodeOrigin = JSON.parse(JSON.stringify(modalState.formNodeOrigin)); - if (type === 'node' && nodeOrigin.id) { - graphG6.value.updateItem(nodeOrigin.id, nodeOrigin); - if (nodeOrigin.type === 'triangle' || nodeOrigin.type === 'image') { - graphG6.value.read(graphG6.value.save()); - } - modalStateFormNode.resetFields(); - modalState.formNodeOrigin = {}; + if (type === 'node') { + handleCancelNode(); } } @@ -1316,13 +2810,10 @@ function fnGraphLoad() {
- - 查看 - - 编辑 @@ -1351,7 +2842,7 @@ function fnGraphLoad() {