From abbe51c52137d0cbebb2802de97b2380841eb77a Mon Sep 17 00:00:00 2001 From: TsMask <340112800@qq.com> Date: Sat, 6 Jan 2024 16:41:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E8=BE=B9/=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/locales/en-US.ts | 6 + src/i18n/locales/zh-CN.ts | 6 + .../components/GraphEditModal.vue | 4 +- .../topologyBuild/hooks/registerEdge.ts | 183 ++++++++++++ .../topologyBuild/hooks/registerNode.ts | 280 ++++++++++++++++++ .../monitor/topologyBuild/hooks/useEdge.ts | 18 ++ .../monitor/topologyBuild/hooks/useGraph.ts | 36 ++- .../monitor/topologyBuild/hooks/useNode.ts | 24 +- 8 files changed, 548 insertions(+), 9 deletions(-) create mode 100644 src/views/monitor/topologyBuild/hooks/registerEdge.ts create mode 100644 src/views/monitor/topologyBuild/hooks/registerNode.ts diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index e6443af2..f85a69e2 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -1153,6 +1153,9 @@ export default { edgeTypeCubicV: "Vertical third-order Bessel curve", edgeTypeCubicH: "Horizontal third-order Bessel curve", edgeTypeLoop: "Self-Loop", + edgeTypeCubicAnimateLineDash: "Third-order Bessel curve, dashed motion", + edgeTypeCubicAnimateCircleMove: "Third-order Bézier curves with dots moving along edges", + edgeTypeLineAnimateState: "Straight line with state animations", edgeLabelPositionStart: "Start", edgeLabelPositionMiddle: "Middle", edgeLabelPositionEnd: "End", @@ -1163,6 +1166,9 @@ export default { nodeTypeTriangle: "Triangle", nodeTypeStar: "Star", nodeTypeImage: "Image", + nodeTypeCircleAnimateShapeR: "Circle, node zoom in and out animation", + nodeTypeCircleAnimateShapeStroke: "Circular, node edge spread animation", + nodeTypeRectAnimateState: "Rectangle with state animations", nodeLabelPositionTop: "Top", nodeLabelPositionLeft: "Left", nodeLabelPositionRight: "Right", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 81d9866f..e9fb6f7f 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -1153,6 +1153,9 @@ export default { edgeTypeCubicV: "垂直方向的三阶贝塞尔曲线", edgeTypeCubicH: "水平方向的三阶贝塞尔曲线", edgeTypeLoop: "自环", + edgeTypeCubicAnimateLineDash: "三阶贝塞尔曲线,虚线运动", + edgeTypeCubicAnimateCircleMove: "三阶贝塞尔曲线,圆点沿边运动", + edgeTypeLineAnimateState: "直线,含有状态动画", edgeLabelPositionStart: "开头", edgeLabelPositionMiddle: "中间", edgeLabelPositionEnd: "末尾", @@ -1163,6 +1166,9 @@ export default { nodeTypeTriangle: "三角形", nodeTypeStar: "星形", nodeTypeImage: "图片", + nodeTypeCircleAnimateShapeR: "圆形,节点放大缩小动画", + nodeTypeCircleAnimateShapeStroke: "圆形,节点边缘扩散动画", + nodeTypeRectAnimateState: "矩形,含有状态动画", nodeLabelPositionTop: "上", nodeLabelPositionLeft: "左", nodeLabelPositionRight: "右", diff --git a/src/views/monitor/topologyBuild/components/GraphEditModal.vue b/src/views/monitor/topologyBuild/components/GraphEditModal.vue index e87464c5..0f7c68df 100644 --- a/src/views/monitor/topologyBuild/components/GraphEditModal.vue +++ b/src/views/monitor/topologyBuild/components/GraphEditModal.vue @@ -555,7 +555,7 @@ function fnModalCancel() { :lg="12" :md="12" :xs="24" - v-if="nodeState.form.type === 'circle'" + v-if="nodeState.form.type.startsWith('circle')" > { + index++; + if (index > 8) { + index = 0; + } + return { + lineDash: [4, 2, 1, 2], + lineDashOffset: -index, + }; + }, + { + repeat: true, // 是否重复执行动画 + duration: 3000, // 执行一次的持续时间 + } + ); + }, + }, + 'cubic' // 扩展内置边 + ); +} + +/** + * cubic 三阶贝塞尔曲线,圆点沿边运动 + * @key cubic-animate-circle-move + */ +export function edgeCubicAnimateCircleMove() { + registerEdge( + 'cubic-animate-circle-move', + { + afterDraw(cfg, group) { + if (!group) return; + // 获取组中的第一个形状 + const shape = group.get('children')[0]; + // 边缘路径的起始位置 + const startPoint = shape.getPoint(0); + const fillColor = cfg?.labelCfg?.style?.fill || '#1890ff'; + // 添加圆圈形状 + const circle = group.addShape('circle', { + attrs: { + x: startPoint.x, + y: startPoint.y, + fill: fillColor, + r: 3, + }, + // 在 G6 3.3 及更高版本中必须指定。它可以是你想要的任何字符串,但在自定义项目类型中应该是唯一的 + name: 'circle-shape', + }); + + // 定义动画 + circle.animate( + (ratio: any) => { + // 每帧中的操作。比率范围从 0 到 1,表示动画的进度。返回修改后的配置 + // 根据比率获取边缘上的位置 + const tmpPoint = shape.getPoint(ratio); + // 在此处返回修改后的配置,在此处返回 x 和 y + return { + x: tmpPoint.x, + y: tmpPoint.y, + }; + }, + { + repeat: true, // 是否重复执行动画 + duration: 3000, // 执行一次的持续时间 + } + ); + }, + }, + 'cubic' // 扩展内置边 + ); +} + +/** + * line 直线,含有状态动画 + * @key line-animate-state + * @name line-dash 虚线运动 + * @name line-path 线路径加载运动 + */ +export function edgeLineAnimateState() { + registerEdge( + 'line-animate-state', + { + setState: (name, value, item: any) => { + const group = item.get('group'); + const model = item.getModel(); + const keyShape = group.find( + (ele: any) => ele.get('name') === 'edge-shape' + ); + // line-dash 虚线运动 + if (name === 'line-dash') { + if (value) { + let index = 0; + keyShape.animate( + () => { + index++; + if (index > 8) { + index = 0; + } + return { + lineDash: [4, 2, 1, 2], + lineDashOffset: -index, + }; + }, + { + repeat: true, // 是否重复执行动画 + duration: 3000, // 执行一次的持续时间 + } + ); + } else { + keyShape.stopAnimate(); + keyShape.attr({ + lineDash: null, + lineDashOffset: null, + }); + } + return; + } + // line-path 线路径加载运动 + if (name === 'line-path') { + // 线路径 + let back = group.find((ele: any) => ele.get('name') === 'line-path'); + if (back) { + back.remove(); + back.destroy(); + } + const { path, stroke, lineWidth } = keyShape.attr(); + back = group.addShape('path', { + attrs: { + path, + stroke, + lineWidth, + opacity: 0.2, + }, + name: 'line-path', + }); + back.toBack(); // 置于底层 + + if (value) { + // 直线加载 + const length = keyShape.getTotalLength(); + keyShape.animate( + (ratio: any) => { + const startLen = ratio * length; + return { + lineDash: [startLen, length - startLen], + }; + }, + { + repeat: true, + duration: 2000, + } + ); + } else { + keyShape.stopAnimate(); + keyShape.attr({ + lineDash: null, + }); + back.remove(); + back.destroy(); + } + return; + } + }, + }, + 'line' // 扩展内置边 + ); +} diff --git a/src/views/monitor/topologyBuild/hooks/registerNode.ts b/src/views/monitor/topologyBuild/hooks/registerNode.ts new file mode 100644 index 00000000..84492cbd --- /dev/null +++ b/src/views/monitor/topologyBuild/hooks/registerNode.ts @@ -0,0 +1,280 @@ +import { registerNode } from '@antv/g6'; + +/** + * cubic 圆形,节点放大缩小动画 + * @type circle-animate-shape-r + */ +export function nodeCircleAnimateShapeR() { + registerNode( + 'circle-animate-shape-r', + { + afterDraw(cfg, group) { + if (!group) return; + const shape = group.get('children')[0]; + const r = Number(cfg?.size) || 2; + shape.animate( + (ratio: any) => { + const diff = ratio <= 0.5 ? ratio * 10 : (1 - ratio) * 10; + return { + r: r / 2 + diff, + }; + }, + { + repeat: true, // 是否重复执行动画 + duration: 3000, // 执行一次的持续时间 + easing: 'easeCubic', + } + ); + }, + }, + 'circle' + ); +} + +/** + * cubic 圆形,节点边缘扩散动画 + * @type circle-animate-shape-stroke + */ +export function nodeCircleAnimateShapeStroke() { + registerNode( + 'circle-animate-shape-stroke', + { + afterDraw(cfg, group) { + if (!group) return; + + const size = Array.isArray(cfg?.size) ? cfg?.size[0] : cfg?.size; + let r = Number(size) || 2; + r = r / 2; + + const fillColor = cfg?.style?.fill || '#1890ff'; + + // 第一个背景圆 + const back1 = group.addShape('circle', { + zIndex: -3, + attrs: { + x: 0, + y: 0, + r, + fill: fillColor, + opacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'circle-shape1', + }); + // 第二个背景圆 + const back2 = group.addShape('circle', { + zIndex: -2, + attrs: { + x: 0, + y: 0, + r, + fill: fillColor, + opacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'circle-shape2', + }); + // 第三个背景圆 + const back3 = group.addShape('circle', { + zIndex: -1, + attrs: { + x: 0, + y: 0, + r, + fill: fillColor, + opacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'circle-shape3', + }); + group.sort(); // 排序,根据 zIndex 排序 + + // 第一个背景圆逐渐放大,并消失 + back1.animate( + { + r: r + 10, + opacity: 0.1, + }, + { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 0, // 无延迟 + } + ); + + // 第二个背景圆逐渐放大,并消失 + back2.animate( + { + r: r + 10, + opacity: 0.1, + }, + { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 1000, // 1 秒延迟 + } + ); + + // 第三个背景圆逐渐放大,并消失 + back3.animate( + { + r: r + 10, + opacity: 0.1, + }, + { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 2000, // 2 秒延迟 + } + ); + }, + }, + 'circle' + ); +} + +/** + * rect 矩形,含有状态动画 + * @key rect-animate-state + * @name stroke 边缘扩散动画 + */ +export function nodeRectAnimateState() { + registerNode( + 'rect-animate-state', + { + afterDraw(cfg, group) { + if (!group) return; + + const size = Array.isArray(cfg?.size) ? cfg?.size : [40, 40]; + const fillColor = cfg?.style?.fill || '#1783ff'; + const radius = cfg?.style?.radius || 2; + const lineWidth = cfg?.style?.lineWidth || 1; + + // 矩形边,边缘扩散动画 =============Start + // 第一个矩形边 + const back1 = group.addShape('rect', { + zIndex: -3, + attrs: { + x: -size[0] / 2, + y: -size[1] / 2, + width: size[0], + height: size[1], + stroke: fillColor, + lineWidth: lineWidth, + radius: radius, + strokeOpacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'rect-stroke1', + }); + back1.hide(); + // 第二个矩形边 + const back2 = group.addShape('rect', { + zIndex: -2, + attrs: { + x: -size[0] / 2, + y: -size[1] / 2, + width: size[0], + height: size[1], + stroke: fillColor, + lineWidth: lineWidth, + radius: radius, + opacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'rect-stroke2', + }); + back2.hide(); + // 第三个矩形边 + const back3 = group.addShape('rect', { + zIndex: -1, + attrs: { + x: -size[0] / 2, + y: -size[1] / 2, + width: size[0], + height: size[1], + stroke: fillColor, + lineWidth: lineWidth, + radius: radius, + opacity: 0.6, + }, + // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 + name: 'rect-stroke3', + }); + back3.hide(); + // 矩形边,边缘扩散动画 =============End + + group.sort(); // 排序,根据 zIndex 排序 + }, + setState: (name, value, item: any) => { + const group = item.get('group'); + const model = item.getModel(); + // 原始图形 + const keyShape = group.find( + (ele: any) => ele.get('name') === 'rect-animate-state-keyShape' + ); + + // 选中状态 + if (name === 'selected') { + if (value) { + const { fill, lineWidth, stroke, shadowBlur, shadowColor } = + item.getStateStyle('selected'); + keyShape.attr({ fill, lineWidth, stroke, shadowBlur, shadowColor }); + } else { + const { fill, lineWidth, stroke } = model.style; + keyShape.attr({ + fill, + lineWidth, + stroke, + shadowBlur: null, + shadowColor: null, + }); + } + return; + } + + // 矩形边,边缘扩散动画 + if (name === 'stroke') { + const backArr = group.findAll((ele: any) => + ele.get('name').startsWith('rect-stroke') + ); + if (!Array.isArray(backArr)) return; + + if (value) { + const { lineWidth } = keyShape.attr(); + for (let i = 0; i < backArr.length; i++) { + const back = backArr[i]; + back.show(); + back.animate( + { + lineWidth: lineWidth + 10, + strokeOpacity: 0.1, + }, + { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: i * 1000, // 逐渐延迟 + } + ); + } + } else { + for (const back of backArr) { + back.stopAnimate(); + back.hide(); + back.attr({ + lineWidth: 1, + strokeOpacity: 1, + }); + } + } + return; + } + }, + }, + 'rect' + ); +} diff --git a/src/views/monitor/topologyBuild/hooks/useEdge.ts b/src/views/monitor/topologyBuild/hooks/useEdge.ts index 3fccf947..0406ee87 100644 --- a/src/views/monitor/topologyBuild/hooks/useEdge.ts +++ b/src/views/monitor/topologyBuild/hooks/useEdge.ts @@ -40,6 +40,18 @@ export default function useEdge() { value: 'loop', label: t('views.monitor.topologyBuild.edgeTypeLoop'), }, + { + value: 'cubic-animate-line-dash', + label: t('views.monitor.topologyBuild.edgeTypeCubicAnimateLineDash'), + }, + { + value: 'cubic-animate-circle-move', + label: t('views.monitor.topologyBuild.edgeTypeCubicAnimateCircleMove'), + }, + { + value: 'line-animate-state', + label: t('views.monitor.topologyBuild.edgeTypeLineAnimateState'), + }, ]; /**图边标签文本位置 */ @@ -160,6 +172,12 @@ export default function useEdge() { if (edge.source === edge.target) { edge.type = 'loop'; } + // 不存在fontWeight会触发异常 + if(!edge.labelCfg.style.fontWeight){ + console.log(edge) + debugger + edge.labelCfg.style.fontWeight = 500 + } // 存在更新,新增id是#不监听变化 const item = graphG6.value.findById(edge.id); if (item) { diff --git a/src/views/monitor/topologyBuild/hooks/useGraph.ts b/src/views/monitor/topologyBuild/hooks/useGraph.ts index 13d00b82..2d8e2002 100644 --- a/src/views/monitor/topologyBuild/hooks/useGraph.ts +++ b/src/views/monitor/topologyBuild/hooks/useGraph.ts @@ -9,6 +9,16 @@ import { Tooltip, } from '@antv/g6'; import { ref } from 'vue'; +import { + edgeCubicAnimateCircleMove, + edgeCubicAnimateLineDash, + edgeLineAnimateState, +} from './registerEdge'; +import { + nodeCircleAnimateShapeR, + nodeCircleAnimateShapeStroke, + nodeRectAnimateState, +} from './registerNode'; /**图实例对象 */ export const graphG6 = ref(null); @@ -400,7 +410,7 @@ export default function useGraph() { const info = JSON.parse(JSON.stringify(node.getModel())); selectSourceTargetOptions.value.push({ value: info.id, - label: info.label, + label: info.label || info.id, info, }); }); @@ -408,14 +418,14 @@ export default function useGraph() { selectComboOptions.value = [ { value: '', - label: '未分配', + label: '#', }, ]; graphG6.value.getCombos().forEach((combo: any) => { const info = JSON.parse(JSON.stringify(combo.getModel())); const comboInfo = { value: info.id, - label: info.label, + label: info.label || info.id, info, }; selectSourceTargetOptions.value.push(comboInfo); @@ -423,6 +433,18 @@ export default function useGraph() { }); } + /**注册自定义边或节点 */ + function registerEdgeNode() { + // 边 + edgeCubicAnimateLineDash(); + edgeCubicAnimateCircleMove(); + edgeLineAnimateState(); + // 节点 + nodeCircleAnimateShapeR(); + nodeCircleAnimateShapeStroke(); + nodeRectAnimateState(); + } + /**图数据渲染 */ function handleRanderGraph( container: HTMLElement | undefined, @@ -431,11 +453,12 @@ export default function useGraph() { if (!container) return; const { clientHeight, clientWidth } = container; + registerEdgeNode(); + const graph = new Graph({ container: container, width: clientWidth, height: clientHeight, - animate: true, fitCenter: true, modes: { default: [ @@ -559,6 +582,11 @@ export default function useGraph() { graphEdgeMenu, graphEdgeTooltip, ], + animate: true, // 是否使用动画过度,默认为 false + animateCfg: { + duration: 500, // Number,一次动画的时长 + easing: 'linearEasing', // String,动画函数 + }, }); graph.data(data); graph.render(); diff --git a/src/views/monitor/topologyBuild/hooks/useNode.ts b/src/views/monitor/topologyBuild/hooks/useNode.ts index 08427850..e1595e68 100644 --- a/src/views/monitor/topologyBuild/hooks/useNode.ts +++ b/src/views/monitor/topologyBuild/hooks/useNode.ts @@ -40,6 +40,18 @@ export default function useNode() { // value: 'donut', // label: '面包圈', // }, + { + value: 'circle-animate-shape-r', + label: t('views.monitor.topologyBuild.nodeTypeCircleAnimateShapeR'), + }, + { + value: 'circle-animate-shape-stroke', + label: t('views.monitor.topologyBuild.nodeTypeCircleAnimateShapeStroke'), + }, + { + value: 'rect-animate-state', + label: t('views.monitor.topologyBuild.nodeTypeRectAnimateState'), + }, ]; /**图节点标签文本位置 */ @@ -197,9 +209,15 @@ export default function useNode() { /**图节点类型输入限制 */ function handleNodeTypeChange(type: any) { // 设置图标属性 - if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) { + if ( + ['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type) || + type.startsWith('circle') + ) { let size: number[] | number = [40, 30]; - if (['circle', 'star', 'donut'].includes(type)) { + if ( + ['circle', 'star', 'donut'].includes(type) || + type.startsWith('circle') + ) { size = 60; } const origin = nodeState.origin; @@ -277,7 +295,7 @@ export default function useNode() { } } // 设置矩形大小 - if (type === 'rect') { + if (type.startsWith('rect')) { nodeState.form = Object.assign(nodeState.form, { size: [80, 40], });