293 lines
8.0 KiB
TypeScript
293 lines
8.0 KiB
TypeScript
/*
|
|
* This file is part of the kernelstudio package.
|
|
*
|
|
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE file
|
|
* that was distributed with this source code.
|
|
*/
|
|
|
|
import { computed, type ComputedRef, ref, type Ref, nextTick } from 'vue';
|
|
import { type Dom, Graph, Node } from '@antv/x6';
|
|
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
|
import { createGraphCanvas } from './canvas';
|
|
import { EventListener } from '@/utils/event';
|
|
import type { ModelElement } from './element';
|
|
|
|
// import {createLineOptions} from './line'
|
|
|
|
export interface UseGraphCanvas {
|
|
container: Ref<HTMLDivElement | null>;
|
|
readonly: boolean;
|
|
eventListener: EventListener;
|
|
currentZoom: Ref<number>;
|
|
graph: ComputedRef<Graph>;
|
|
zoomIn: () => void;
|
|
zoomOut: () => void;
|
|
destroyGraph: () => void;
|
|
clearGraph: () => void;
|
|
fitToScreen: () => void;
|
|
centerContent: () => void;
|
|
resizeCanvas: () => void;
|
|
handleGraphEvent: (name: string, fn: Function) => void;
|
|
emitGraphEvent: (name: string, options?: any) => void;
|
|
createCanvas: (c: HTMLDivElement) => Graph;
|
|
}
|
|
|
|
export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|
|
|
const graph = ref<Graph | null>(null);
|
|
const container = ref<HTMLDivElement | null>(null);
|
|
const eventListener = new EventListener();
|
|
const currentZoom = ref<number>(0);
|
|
|
|
|
|
const clearGraph = ()=> {
|
|
if (graph.value) {
|
|
try {
|
|
// graph.value.off();
|
|
graph.value.clearCells();
|
|
} catch (e) {
|
|
console.error('清空画布失败:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
const destroyGraph = ()=> {
|
|
eventListener.clear();
|
|
if (graph.value) {
|
|
clearGraph();
|
|
|
|
// 同步销毁 Graph 实例,避免异步导致的竞态条件
|
|
graph.value.dispose();
|
|
graph.value = null;
|
|
if (container.value) {
|
|
container.value.innerHTML = ''; // 清空容器内容
|
|
}
|
|
} else if (container.value) {
|
|
container.value.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
const handleGraphEvent = (name: string, fn: Function) => {
|
|
eventListener.on(name, fn);
|
|
};
|
|
|
|
const emitGraphEvent = (name: string, options?: any): void => {
|
|
eventListener.emit(name, options);
|
|
};
|
|
|
|
const zoomIn = (): void => {
|
|
if (graph.value) {
|
|
const zoom = graph.value.zoom();
|
|
graph.value.zoom(zoom + 0.1);
|
|
}
|
|
};
|
|
|
|
const zoomOut = (): void => {
|
|
if (graph.value) {
|
|
const zoom = graph.value.zoom();
|
|
graph.value.zoom(Math.max(0.1, zoom - 0.1));
|
|
}
|
|
};
|
|
|
|
const fitToScreen = () => {
|
|
if (graph.value) {
|
|
graph.value.zoomToFit({ padding: 20 });
|
|
}
|
|
};
|
|
|
|
const centerContent = () => {
|
|
if (graph.value) {
|
|
graph.value.centerContent();
|
|
}
|
|
};
|
|
|
|
const resizeCanvas = () => {
|
|
if (graph.value) {
|
|
graph.value?.resize();
|
|
}
|
|
};
|
|
|
|
const initEvents = () => {
|
|
if (!graph.value) {
|
|
return;
|
|
}
|
|
graph.value.on('keydown', ({ e }: any) => {
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
const cells = graph.value?.getSelectedCells();
|
|
if (cells && cells.length) {
|
|
graph.value?.removeCells(cells);
|
|
// 通知父组件更新状态
|
|
emitGraphEvent('cells:removed', cells);
|
|
}
|
|
}
|
|
});
|
|
|
|
graph.value.on('scale', (ctx) => {
|
|
currentZoom.value = ctx.sx;
|
|
emitGraphEvent('scale', ctx);
|
|
});
|
|
|
|
// 监听画布空白点击
|
|
graph.value.on('blank:click', () => {
|
|
graph.value?.cleanSelection();
|
|
emitGraphEvent('blank:click');
|
|
});
|
|
|
|
// 监听连线删除
|
|
graph.value.on('edge:removed', ({ edge }) => {
|
|
emitGraphEvent('edge:removed', edge); // 添加此行
|
|
});
|
|
|
|
graph.value.on('edge:added', ({ edge }) => {
|
|
// const lineOptions = createLineOptions();
|
|
// edge.setAttrs(lineOptions.attrs);
|
|
// edge.set(lineOptions.animation);
|
|
// edge.setMarkup(lineOptions.markup);
|
|
emitGraphEvent('edge:added', edge); // 添加此行
|
|
});
|
|
|
|
graph.value.on('edge:contextmenu', (ctx) => {
|
|
emitGraphEvent('edge:contextmenu', ctx);
|
|
});
|
|
|
|
graph.value.on('edge:connect:abort', () => {
|
|
// 当连线被拖拽到空白区域后释放,自动移除这条无效连线
|
|
const edges = graph.value?.getEdges();
|
|
const invalidEdges = (edges ?? []).filter(edge =>
|
|
!edge.getSourceCell() || !edge.getTargetCell(),
|
|
);
|
|
if (invalidEdges.length) {
|
|
graph.value?.removeCells(invalidEdges);
|
|
}
|
|
});
|
|
|
|
// 监听连接失败
|
|
graph.value.on('edge:connect:invalid', () => {
|
|
emitGraphEvent('edge:connect:invalid');
|
|
});
|
|
|
|
graph.value.on('edge:click', (edge) => {
|
|
emitGraphEvent('edge:connect:invalid', edge);
|
|
});
|
|
|
|
graph.value.on('edge:connected', ({ edge }) => {
|
|
const sourceNode = edge.getSourceCell() as Node;
|
|
const targetNode = edge.getTargetCell() as Node;
|
|
|
|
if (sourceNode && targetNode) {
|
|
const sourceData = sourceNode.getData() as ModelElement;
|
|
const targetData = targetNode.getData() as ModelElement;
|
|
|
|
// 将连线存储到节点数据中
|
|
const sourceEdges = sourceData.edges || [];
|
|
const existingEdge = sourceEdges.find(e => e.target === targetNode.id);
|
|
|
|
if (!existingEdge) {
|
|
sourceEdges.push({
|
|
id: sourceData.id,
|
|
key: edge.id,
|
|
source: sourceNode.id,
|
|
sourceName: sourceData.name,
|
|
connector: {},
|
|
router: {},
|
|
attrs: {},
|
|
target: targetNode.id,
|
|
targetName: targetData.name,
|
|
});
|
|
sourceNode.replaceData({ ...sourceData, edges: sourceEdges });
|
|
}
|
|
}
|
|
|
|
edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色
|
|
edge.attr('line/strokeWidth', 2); // 显式设置宽度
|
|
// edge.refresh() // 刷新连线
|
|
// graph.paint() // 重绘画布
|
|
|
|
emitGraphEvent('edge:connected', edge);
|
|
});
|
|
|
|
graph.value.on('node:click', (ctx: NodeViewPositionEventArgs<Dom.ClickEvent>) => {
|
|
console.info('node click', ctx);
|
|
emitGraphEvent('node:click', ctx);
|
|
});
|
|
|
|
graph.value.on('node:dblclick', (ctx: NodeViewPositionEventArgs<Dom.ClickEvent>) => {
|
|
console.info('node:dblclick', ctx);
|
|
emitGraphEvent('node:dblclick', ctx);
|
|
});
|
|
|
|
// 监听节点选中事件
|
|
graph.value.on('node:selected', ({ node }) => {
|
|
console.info('node select', node,node.getData());
|
|
|
|
emitGraphEvent('node:selected', node);
|
|
});
|
|
|
|
// 监听节点取消选中
|
|
graph.value.on('node:unselected', () => {
|
|
emitGraphEvent('node:unselected');
|
|
});
|
|
|
|
// 监听节点鼠标移入,显示连接点
|
|
graph.value.on('node:mouseenter', (ctx) => {
|
|
emitGraphEvent('node:mouseenter', ctx);
|
|
});
|
|
|
|
// 监听节点鼠标移出,隐藏连接点
|
|
graph.value.on('node:mouseleave', (ctx) => {
|
|
emitGraphEvent('node:mouseleave', ctx);
|
|
});
|
|
|
|
// 监听节点状态变化
|
|
graph.value.on('node:change:data', (ctx) => {
|
|
const edges = graph.value?.getIncomingEdges(ctx.node);
|
|
const { status } = ctx.node.getData();
|
|
edges?.forEach((edge) => {
|
|
if (status === 'running') {
|
|
edge.attr('line/strokeDasharray', 5);
|
|
edge.attr('line/style/animation', 'running-line 30s infinite linear');
|
|
} else {
|
|
edge.attr('line/strokeDasharray', '');
|
|
edge.attr('line/style/animation', '');
|
|
}
|
|
});
|
|
emitGraphEvent('node:change:data', ctx);
|
|
});
|
|
|
|
};
|
|
|
|
const createCanvas = (c: HTMLDivElement): Graph => {
|
|
container.value = c;
|
|
|
|
graph.value = createGraphCanvas(c, readonly);
|
|
initEvents();
|
|
|
|
emitGraphEvent('created', graph.value);
|
|
|
|
return graph.value as Graph;
|
|
};
|
|
|
|
const graphInstance = computed(() => graph.value);
|
|
|
|
return {
|
|
container,
|
|
readonly,
|
|
destroyGraph,
|
|
clearGraph,
|
|
graph: graphInstance,
|
|
eventListener,
|
|
currentZoom,
|
|
|
|
emitGraphEvent,
|
|
handleGraphEvent,
|
|
|
|
zoomIn,
|
|
zoomOut,
|
|
fitToScreen,
|
|
centerContent,
|
|
resizeCanvas,
|
|
createCanvas,
|
|
} as UseGraphCanvas;
|
|
}; |