feat: 新增三个自定义边/三个自定义节点

This commit is contained in:
TsMask
2024-01-06 16:41:34 +08:00
parent 0509e05e50
commit abbe51c521
8 changed files with 548 additions and 9 deletions

View File

@@ -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",

View File

@@ -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: "右",

View File

@@ -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')"
>
<a-form-item
:label="t('views.monitor.topologyBuild.nodeFormSize')"
@@ -573,7 +573,7 @@ function fnModalCancel() {
:lg="12"
:md="12"
:xs="24"
v-else-if="nodeState.form.type === 'star'"
v-else-if="nodeState.form.type.startsWith('star')"
>
<a-form-item
:label="t('views.monitor.topologyBuild.nodeFormSize')"

View File

@@ -0,0 +1,183 @@
import { registerEdge } from '@antv/g6';
/**
* cubic 三阶贝塞尔曲线,虚线运动
* @key cubic-animate-line-dash
*/
export function edgeCubicAnimateLineDash() {
registerEdge(
'cubic-animate-line-dash',
{
afterDraw(cfg, group) {
if (!group) return;
// 获取组中的第一个形状
const shape = group.get('children')[0];
// 定义动画
let index = 0;
shape.animate(
() => {
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' // 扩展内置边
);
}

View File

@@ -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'
);
}

View File

@@ -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) {

View File

@@ -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<any>(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();

View File

@@ -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],
});