/* * This file is part of the kernelstudio package. * * (c) 2014-2025 zlin * * 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; readonly: boolean; eventListener: EventListener; currentZoom: Ref; graph: ComputedRef; 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(null); const container = ref(null); const eventListener = new EventListener(); const currentZoom = ref(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) => { console.info('node click', ctx); emitGraphEvent('node:click', ctx); }); graph.value.on('node:dblclick', (ctx: NodeViewPositionEventArgs) => { 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; };