import { Graph, Cell, Node, Edge, EventArgs } from "@antv/x6"; import { BorderSize, StructureType, TopicType } from "@/enum"; import { addTopic, updateTopic } from "@/pages/mindmap/mindMap"; import { cellStyle, MindMapProjectInfo, TopicItem } from "@/types"; import { Dnd } from "@antv/x6-plugin-dnd"; import { selectTopic } from "@/utils/mindmapHander"; import { uuid } from "@repo/utils"; import { getTheme } from "@/pages/mindmap/theme"; import { traverseNode } from "@/utils/mindmapHander"; import { EditMindMapElement, AddMindMapElement } from "@/api/systemDesigner"; import { debounce, isEqual } from "lodash-es"; enum positionType { left = "left", right = "right", outside = "outside", } // 拖拽主题时指示器 let indicatorNode: Node | undefined; let indicatorEdge: Edge | undefined; // 插入的节点位置 let insertNode: Node | undefined; let moveStart: { x: number; y: number } | undefined; let dragging = false; let currentShadowNode: Node | undefined; export const bindMindMapEvents = ( graph: Graph, setMindProjectInfo?: ( info: MindMapProjectInfo, init?: boolean, isSetting?: boolean, ignoreRender?: boolean ) => void, setSelectedCell?: (cell: Cell[]) => void, dndRef?: React.MutableRefObject ) => { graph.on("selection:changed", ({ selected }: { selected: Cell[] }) => { setSelectedCell?.(selected); }); graph.on("node:mouseenter", (args) => { if (args.node.data?.type !== TopicType.main) { graph.disablePanning(); } }); graph.on("node:mouseleave", (args) => { graph.enablePanning(); }); /*********************************** 拖拽开始 *********************************/ graph.on("node:mousedown", (args) => { moveStart = { x: args.x, y: args.y, }; }); graph.on("node:mousemove", (args) => { if (!args.node.data?.parentId || args.node.data?.shadow) { return; } if ( !dragging && moveStart && (Math.abs(args.x - moveStart.x) > 5 || Math.abs(args.y - moveStart.y) > 5) ) { // 判断开始点是否有值,有值时当拖拽距离>5时开启影子节点 dragging = true; const node = args.node; const shadowNode = graph.createNode({ shape: "rect", width: node.size().width, height: node.size().height, x: args.x, y: args.y, label: node.data?.label, attrs: { body: { rx: node.data?.borderSize, ry: node.data?.borderSize, stroke: "#666", strokeWidth: 1, }, }, data: { ...node.data, shadow: true, }, }); setShadowMode(true, node, graph); currentShadowNode = shadowNode; dndRef?.current?.init(); dndRef?.current?.start(shadowNode, args.e as unknown as MouseEvent); } }); graph.on("node:mousemove", (args) => { // 节点拖拽-处理影子节点吸附问题 if (currentShadowNode && setMindProjectInfo) { topicDragHander(graph, { x: args.x, y: args.y }, args.node); } }); graph.on("node:mouseup", (args) => { // 拖拽结束 if (indicatorNode && insertNode) { graph.removeCell(args.node.id + "-edge"); setMindProjectInfo && handleSwitchPosition( setMindProjectInfo, args.node.id, insertNode.id, undefined, graph ); } // 成为自由节点 if ( currentShadowNode && !indicatorNode && args.node.data?.parentId && canBeFreeNode(args.x, args.y, args.node) ) { setMindProjectInfo && handleSwitchPosition( setMindProjectInfo, args.node.id, undefined, { x: args.x, y: args.y, }, graph ); } currentShadowNode && setShadowMode(false, args.node, graph); moveStart = undefined; dragging = false; currentShadowNode = undefined; dndRef?.current?.remove(); if (indicatorEdge && indicatorNode) { graph.removeCells([indicatorEdge, indicatorNode]); indicatorEdge = undefined; indicatorNode = undefined; } }); graph.on("node:move", (args) => { // 自由节点拖拽 if (!args.node.data?.parentId) { setShadowMode(true, args.node, graph); } }); graph.on("node:moving", (args) => { if (!args.node.data?.parentId && setMindProjectInfo) { setShadowMode(false, args.node, graph); topicDragHander(graph, { x: args.x, y: args.y }, args.node); } }); graph.on("node:moved", (args) => { if (!args.node.data?.parentId) { setShadowMode(false, args.node, graph); } }); /*********************************** 拖拽结束 *********************************/ // 双击画布空白-新增自由主题 graph.on("blank:dblclick", (args) => { if (setMindProjectInfo) { const topic = addTopic( TopicType.branch, setMindProjectInfo, graph, undefined, { x: args.x, y: args.y, setMindProjectInfo, type: TopicType.branch, label: "自由主题", borderSize: BorderSize.medium, } ); selectTopic(graph, topic); } }); /** * 节点数据更改 */ graph.on("node:change:data", (args) => { const { current, previous } = args; console.log("数据变更:", current, previous); // 收折子项 setMindProjectInfo更新会重新渲染 if (current.collapsed !== previous.collapsed) { setMindProjectInfo && updateTopic( args.cell.id, { collapsed: current.collapsed }, setMindProjectInfo, graph ); return; } if (current?.links && current.links.length !== previous?.links?.length) { setMindProjectInfo && updateTopic( args.cell.id, { links: current.links }, setMindProjectInfo, graph ); } if (current?.border !== previous?.border) { setMindProjectInfo && updateTopic( args.cell.id, { border: current.border }, setMindProjectInfo, graph ); } if (current?.summary !== previous?.summary) { setMindProjectInfo && updateTopic( args.cell.id, { summary: current.summary }, setMindProjectInfo, graph ); } if (current?.extraModules !== previous?.extraModules) { setMindProjectInfo && updateTopic( args.cell.id, { extraModules: current?.extraModules }, setMindProjectInfo, graph ); } // 改线段 if (current?.edge && !isEqual(current.edge, previous?.edge)) { setMindProjectInfo && updateTopic( args.cell.id, { edge: current.edge }, setMindProjectInfo, graph ); } // 本地缓存更新不会重新渲染 if (args.cell.id.includes("-border")) { updateTopic( args.current.origin, { border: current }, (info) => { setMindProjectInfo?.(info, false, false, true); }, graph ); } else { updateTopic( args.cell.id, current, (info) => { setMindProjectInfo?.(info, false, false, true); }, graph ); } }); graph.on("node:resized", (args) => { args.node.setData({ fixedWidth: true, width: args.node.size().width, height: args.node.size().height, }); }); graph.on( "node:change:position", debounce((args) => { const { current } = args; if ( args.cell.isNode() && !args.cell.data?.parentId && args.cell.data.type !== TopicType.main ) { updateTopic( args.cell.id, { ...args.cell.data, x: current?.x, y: current?.y }, (info) => { // localStorage.setItem("minMapProjectInfo", JSON.stringify(info)); setMindProjectInfo && setMindProjectInfo(info); }, graph ); EditMindMapElement({ ...args.cell.data, ...args.current, graphId: sessionStorage.getItem("projectId"), tools: "", }); } }, 500) ); /** * 连接线更改 */ graph.on("edge:change:*", (args) => { if (args.key === "vertices" || args.key === "labels") { const link = args.edge.toJSON(); const source = args.edge.getSourceCell(); source?.setData({ links: source.data?.links.map((item: Edge.Properties) => { if (item.id === link.id) return link; return item; }), }); } }); /** * 连接线删除 */ graph.on("edge:removed", (args) => { if (args.edge.data?.isLink) { // @ts-ignore const source = graph.getCellById(args.edge.source?.cell as string); source?.setData( { links: source.data?.links?.filter( (item: Edge.Properties) => item.id !== args.edge.id ), }, { deep: false, } ); } }); }; const canBeFreeNode = (x: number, y: number, node: Node): boolean => { if (!moveStart) return false; return Math.abs(x - moveStart.x) > 50 || Math.abs(y - moveStart.y) > 50; }; // 判断当前点在主图的位置 const atNodePosition = ( position: { x: number; y: number }, x: number, y: number, width: number, height: number ): positionType => { if ( position.x < x + width / 2 && position.x > x && position.y < y + height && position.y > y ) { return positionType.left; } if ( position.x < x + width && position.x > x + width / 2 && position.y < y + height && position.y > y ) { return positionType.right; } return positionType.outside; }; // 创建一个指示器 const addIndicator = ( x: number, y: number, graph: Graph, targetNode: Node, atPosition: positionType ) => { if (indicatorEdge && indicatorNode) { graph.removeCells([indicatorEdge, indicatorNode]); } indicatorNode = graph.addNode({ shape: "rect", width: 100, height: 26, x, y, attrs: { body: { fill: "#fecb99", stroke: "#fc7e00", strokeWidth: 1, rx: 2, ry: 2, style: { opacity: 0.6, }, }, }, }); indicatorEdge = graph.addEdge({ source: { cell: targetNode.id, anchor: { name: atPosition === positionType.left ? "left" : "right", args: { dx: atPosition === positionType.left ? 3 : -3, }, }, }, target: { cell: indicatorNode.id, anchor: { name: atPosition === positionType.left ? "right" : "left", args: { dx: atPosition === positionType.left ? -3 : 3, }, }, }, connector: "normal", attrs: { line: { stroke: "#fc7e00", strokeWidth: 2, sourceMarker: { name: "", }, targetMarker: { name: "", }, style: { opacity: 0.6, }, }, }, }); }; // 添加指示器 const setIndicator = ( atPosition: positionType, targetNode: Node, graph: Graph ) => { switch (atPosition) { // 1、左侧位置 case positionType.left: { const x = targetNode.position().x - 120; const y = targetNode.position().y + targetNode.size().height / 2 - 13; // 判断是左侧节点还是右侧节点 if (targetNode.data?.parentId) { const parentNode = graph.getCellById(targetNode.data.parentId); // 左侧朝向的结构 if ( parentNode?.isNode() && targetNode.position().x < parentNode.position().x ) { addIndicator(x, y, graph, targetNode, atPosition); } } else { // @ts-ignore const mindProjectInfo = graph.extendAttr.getMindProjectInfo(); if (mindProjectInfo?.structure === StructureType.left) { addIndicator(x, y, graph, targetNode, atPosition); } } break; } // 2、右侧位置 case positionType.right: { const x = targetNode.position().x + targetNode.size().width + 20; const y = targetNode.position().y + targetNode.size().height / 2 - 13; // 判断是左侧节点还是右侧节点 if (targetNode.data?.parentId) { const parentNode = graph.getCellById(targetNode.data.parentId); // 右侧朝向的结构 if ( parentNode?.isNode() && targetNode.position().x > parentNode.position().x ) { addIndicator(x, y, graph, targetNode, atPosition); } } else { // @ts-ignore const mindProjectInfo = graph.extendAttr.getMindProjectInfo(); if (mindProjectInfo?.structure === StructureType.right) { addIndicator(x, y, graph, targetNode, atPosition); } } break; } // 外部位置 case positionType.outside: { } } }; // 判断目标节点是不是后代节点 const isDescendantNode = (targetNode: Node, originNode: Node, graph: Graph) => { if (targetNode.data?.parentId === originNode.id) { return true; } let isChild = false; // 是后代节点 const findParent = (parentId: string) => { const cell = graph.getCellById(parentId); if (cell?.isNode() && cell.data.parentId) { if (cell.data.parentId === originNode.id) { isChild = true; } else { findParent(cell.data.parentId); } } }; targetNode.data?.parentId && findParent(targetNode.data.parentId); return isChild; }; /** * 主题拖拽放置 * @param graph * @param setMindProjectInfo */ export const topicDragHander = ( graph: Graph, position: { x: number; y: number }, originNode: Node ) => { if (indicatorEdge) graph.removeCell(indicatorEdge); if (indicatorNode) graph.removeCell(indicatorNode); indicatorEdge = undefined; indicatorNode = undefined; // 1、 获取位置的地方是否有节点 const nodes = graph.getNodes(); nodes.forEach((targetNode) => { // 目标节点是自己、自己的后代、节点不做处理 if ( targetNode.id === originNode.id || isDescendantNode(targetNode, originNode, graph) ) { return; } const { x, y } = targetNode.position(); const { width, height } = targetNode.size(); // 2、 找到是在节点哪个区域内 const atPosition = atNodePosition(position, x, y, width, height); // 3、添加插入指示 setIndicator(atPosition, targetNode, graph); // 4、 根据位置确定插入位置 if ([positionType.left, positionType.right].includes(atPosition)) { insertNode = targetNode; } }); }; /** * 拖拽完毕,切换主题位置 * @param setMindProjectInfo * @param sourceId 拖拽的节点 * @param targetId 放置的节点 * @param position 自由节点放置的位置 * @returns */ const handleSwitchPosition = ( setMindProjectInfo: (info: MindMapProjectInfo) => void, sourceId: string, targetId?: string, position?: { x: number; y: number }, graph?: Graph ) => { // @ts-ignore const mindmapProjectInfo: MindMapProjectInfo = graph.extendAttr.getMindProjectInfo(); if (!mindmapProjectInfo) return; // 找到要拖拽的节点并删除 let source: (TopicItem & cellStyle) | undefined; mindmapProjectInfo.topics.forEach((topic) => { if (topic.id === sourceId) { source = topic; } mindmapProjectInfo.topics = mindmapProjectInfo.topics.filter( (item) => item.id !== sourceId ); }); const topics = source ? mindmapProjectInfo.topics : traverseNode(mindmapProjectInfo.topics, (topic) => { const findItem = topic?.children?.find((item) => item.id === sourceId); if (findItem) { source = findItem; topic.children = topic.children?.filter( (item) => item.id !== sourceId ); } }); if (!source) return; // 处理节点样式 const targetNode = targetId ? graph?.getCellById(targetId) : undefined; const theme = getTheme( mindmapProjectInfo.theme, targetNode?.isNode() && targetNode.data.type !== TopicType.main ? targetNode.data : undefined ); source.type = targetNode?.data?.type === TopicType.main ? TopicType.branch : TopicType.sub; const themeObj = targetId ? theme[source.type] : theme[TopicType.branch]; source.fill = { ...source.fill, ...themeObj.fill, }; source.text = { ...source.text, ...themeObj.text, }; source.stroke = { ...source.stroke, ...themeObj.stroke, }; source.edge = { ...source.edge, ...themeObj.edge, }; // 后代节点样式 if (source.children) source.children = traverseNode(source.children, (topic) => { const theme = getTheme(mindmapProjectInfo.theme, source); topic.type = TopicType.sub; topic.fill = { ...topic.fill, ...theme[topic.type].fill, }; topic.text = { ...topic.text, ...theme[topic.type].text, }; topic.stroke = { ...topic.stroke, ...theme[topic.type].stroke, }; topic.edge = { ...topic.edge, ...theme[topic.type].edge, }; }); if (targetId) { // 加入到目标节点下 mindmapProjectInfo.topics = traverseNode(topics, (topic) => { if (topic.id === targetId) { if (!topic.children) { topic.children = []; } source && topic.children.push({ ...source, parentId: topic.id, }); } }); } else { const id = uuid(); // 成为自由节点 const freeTopic = { ...source, id, children: (source.children || []).map((item) => { item.parentId = id; return item; }), opacity: 100, x: position?.x, y: position?.y, type: TopicType.branch, parentId: null, links: (source.links || []).map((item) => { // 修改sourceId item.source = { ...item.source, id, }; return item; }), }; mindmapProjectInfo.topics.push(freeTopic); AddMindMapElement({ ...freeTopic, graphId: sessionStorage.getItem("projectId"), }); } setMindProjectInfo(mindmapProjectInfo); }; /** * 设置影子模式-拖拽时变灰 * @param node * @param graph */ export const setShadowMode = (enable: boolean, node: Node, graph: Graph) => { const data = node.getData(); node.setData({ opacity: enable ? 60 : 100, }); if (data?.children?.length) { traverseNode(data.children, (topic) => { const cell = graph.getCellById(topic.id); const edge = graph.getCellById(topic.id + "-edge"); if (cell && cell.isNode()) { cell.setData({ opacity: enable ? 60 : 100, }); } if (edge && edge.isEdge()) { edge.setAttrs({ line: { style: { opacity: enable ? 0.6 : 1, }, }, }); } }); } };