import { BorderSize, TopicType } from "@/enum"; import { addTopic, buildTopic } from "@/utils/mindmap"; import { HierarchyResult, MindMapProjectInfo, TopicItem } from "@/types"; import { Cell, Graph, Node, Edge } from "@antv/x6"; import { message } from "antd"; import { cloneDeep } from "lodash-es"; import { uuid } from "@repo/utils"; import { ContextMenuTool } from "./contentMenu"; import { MutableRefObject } from "react"; import TopicBorder from "@/components/mindMap/Border"; import SummaryBorder from "@/components/mindMap/SummaryBorder"; import { openInsertImageModal } from "@/components/ImageModal"; import { BatchDeleteMindMapElement } from "@/api/systemDesigner"; export const selectTopic = (graph: Graph, topic?: TopicItem) => { if (topic?.id) { setTimeout(() => { graph.resetSelection(topic.id); const node = graph.getCellById(topic?.id); node?.isNode() && graph.createTransformWidget(node); }, 100); } }; export const selectTopics = (graph: Graph, topics?: TopicItem[]) => { setTimeout(() => { graph.resetSelection(topics?.map((item) => item.id)); topics?.forEach((item) => { const node = graph.getCellById(item.id); node?.isNode() && graph.createTransformWidget(node); }); }, 100); }; /** * 添加同级主题 * @param node * @param graph */ export const addPeerTopic = ( node: Cell, graph: Graph, setMindProjectInfo?: (info: MindMapProjectInfo) => void ) => { if (!setMindProjectInfo) return; const parentNode = node.data.type === TopicType.main || !node.data?.parentId ? node : graph.getCellById(node.data.parentId); const type = node.data.type === TopicType.main ? TopicType.branch : node.data.type; if (parentNode?.isNode()) { const topic = addTopic(type, setMindProjectInfo, graph, parentNode); selectTopic(graph, topic); } }; /** * 添加子主题 * @param node * @param setMindProjectInfo * @param graph */ export const addChildrenTopic = ( node: Node, graph: Graph, setMindProjectInfo?: (info: MindMapProjectInfo) => void, ) => { if (!setMindProjectInfo) return; const type = node.data?.type === TopicType.main ? TopicType.branch : TopicType.sub; const topic = addTopic(type, setMindProjectInfo, graph, node); graph && selectTopic(graph, topic); }; /** * 添加父主题 * @param node * @param setMindProjectInfo * @param graph */ export const addParentTopic = ( node: Node, setMindProjectInfo: (info: MindMapProjectInfo) => void, graph: Graph ) => { if (!setMindProjectInfo || !node.data?.parentId) return; const type = node.data?.type === TopicType.branch ? TopicType.branch : TopicType.sub; const parentNode = graph?.getCellById(node.data.parentId); // 删除原来的数据 deleteTopics([node.data.id], graph, setMindProjectInfo); // 加入新的父主题 const topic = addTopic(type, setMindProjectInfo, graph, parentNode as Node, { children: [ { ...node.data, }, ], }); graph && selectTopic(graph, topic); }; /** * 删除子主题 * @param ids * @param setMindProjectInfo */ export const deleteTopics = ( ids: string[], graph: Graph, setMindProjectInfo?: (info: MindMapProjectInfo) => void ) => { // @ts-ignore const mindProjectInfo: MindMapProjectInfo = graph.extendAttr.getMindProjectInfo(); if (!mindProjectInfo || !setMindProjectInfo) return; const topics = cloneDeep(mindProjectInfo.topics); const deleteIds: string[] = []; const filterTopics = (list: TopicItem[]): TopicItem[] => { const result: TopicItem[] = []; for (const item of list) { if(ids.includes(item.id) && item.type === TopicType.main) { item.children = []; } if (!ids.includes(item.id) || item.type === TopicType.main) { if (item.children?.length) { item.children = filterTopics(item.children); } result.push(item); } else { if(!item.parentId && !item.isSummary) { // 删除自由主题 deleteIds.push(item.id); } } } return result; }; mindProjectInfo.topics = filterTopics(topics); if(deleteIds.length) { BatchDeleteMindMapElement({ids: deleteIds}); } setMindProjectInfo(mindProjectInfo); localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo)); }; /** * 删除当前主题 * @param graph * @param nodes */ export const handleDeleteCurrentTopic = (graph: Graph, nodes: Node[]) => { // @ts-ignore const mindProjectInfo: MindMapProjectInfo = graph.extendAttr.getMindProjectInfo(); if (!mindProjectInfo) return; nodes.forEach((node) => { if (node.data.parentId) { traverseNode(mindProjectInfo.topics, (topic) => { if (topic.id === node.data.parentId) { const index = topic.children?.findIndex( (item) => item.id === node.id ); if (typeof index === "number" && index >= 0) { const newChildren = (node.data.children || []).map( (childNode: TopicItem) => { return { ...childNode, type: topic.type === TopicType.main ? TopicType.branch : TopicType.sub, parentId: topic.id, }; } ); (topic.children || []).splice(index, 1, ...newChildren); } } }); } // @ts-ignore graph?.extendAttr?.setMindProjectInfo?.(mindProjectInfo); localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo)); }); }; /** * 执行粘贴 * @param graph * @param setMindProjectInfo */ export const handleMindmapPaste = ( graph: Graph, setMindProjectInfo: (info: MindMapProjectInfo) => void ) => { // 读取剪切板数据 navigator.clipboard.read().then((items) => { console.log("剪切板内容:", items); const currentNode = graph.getSelectedCells().find((cell) => cell.isNode()); if (!currentNode) { message.warning("请先选择一个主题"); return; } const item = items?.[0]; if (item) { /**读取图片数据 */ if (item.types[0] === "image/png") { item.getType("image/png").then((blob) => { const reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = function (event) { const dataUrl = event.target?.result as string; // 获取图片大小 const img = new Image(); img.src = dataUrl; img.onload = function () { const width = img.width; const height = img.height; // 插入图片 currentNode.setData({ extraModules: { type: "image", data: { imageUrl: dataUrl, width, height, }, }, }); }; }; }); } /**读取文本数据 */ if (item.types[0] === "text/plain") { item.getType("text/plain").then((blob) => { const reader = new FileReader(); reader.readAsText(blob); reader.onload = function (event) { const text = event.target?.result as string; // 内部复制方法 if (text === " ") { const nodes = localStorage.getItem("mindmap-copy-data"); if (nodes) { JSON.parse(nodes)?.forEach((node: Node) => { const data = node.data; // 修改新的数据嵌套 data.id = uuid(); data.parentId = currentNode.id; if (data.children?.length) { data.children = traverseCopyData(data.children, data.id); } addTopic( currentNode.data?.type === TopicType.main ? TopicType.branch : TopicType.sub, setMindProjectInfo, graph, currentNode, { ...data } ); }); } } else { const topic = addTopic( currentNode.data?.type === TopicType.main ? TopicType.branch : TopicType.sub, setMindProjectInfo, graph, currentNode, { label: text } ); selectTopic(graph, topic); } }; }); } } }); }; const traverseCopyData = (list: TopicItem[], parentId: string): TopicItem[] => { return list.map((item) => { item.id = uuid(); item.parentId = parentId; if (item.children?.length) { item.children = traverseCopyData(item.children, item.id); } return item; }); }; /** * 遍历主题树 * @param topics * @param callback * @returns */ export const traverseNode = ( topics: TopicItem[], callback: (topic: TopicItem, index: number) => void ): TopicItem[] => { return topics.map((topic, index) => { callback && callback(topic, index); if (topic.children?.length) { topic.children = traverseNode(topic.children, callback); } // 遍历概要 if (topic?.summary?.topic) { topic.summary.topic = traverseNode([topic.summary.topic], callback)[0]; } return topic; }); }; // 关联线 const handleCorrelation = ( e: MouseEvent, correlationEdgeRef: MutableRefObject, graph: Graph ) => { if (correlationEdgeRef.current) { const point = graph?.clientToLocal(e.x, e.y); point && correlationEdgeRef.current?.setTarget(point); } }; /** * 添加关联线 * @param graph * @param correlationEdgeRef * @param sourceNode */ export const handleCreateCorrelationEdge = ( graph: Graph, correlationEdgeRef: MutableRefObject, sourceNode?: Node ) => { if (sourceNode) { correlationEdgeRef.current = graph?.addEdge({ source: { cell: sourceNode }, target: { x: sourceNode.position().x, y: sourceNode.position().y, }, connector: "normal", attrs: { line: { stroke: "#71cb2d", strokeWidth: 2, sourceMarker: { name: "", }, targetMarker: { name: "", }, style: { opacity: 0.6, }, }, }, data: { ignoreDrag: true, }, zIndex: 0, }); document.body.addEventListener("mousemove", (e) => handleCorrelation(e, correlationEdgeRef, graph) ); } else { document.body.removeEventListener("mousemove", (e) => handleCorrelation(e, correlationEdgeRef, graph) ); if (correlationEdgeRef.current) { graph?.removeCell(correlationEdgeRef.current); correlationEdgeRef.current = undefined; } } }; export const addBorder = (nodes: Node[]) => { // 判断节点是否在当前存在父级以上节点 nodes.forEach((node) => { let hasParent = false; traverseNode(node.data.children || [], (child) => { if (child.id === node.id) { hasParent = true; } }); // 添加边框数据 if (!hasParent && !node.data.border) { node.setData({ border: { ...TopicBorder.data, }, }); } }); }; /** * 计算当前节点总大小 * @param topItem * @param children * @returns */ export const cacluculateExtremeValue = (topItem: HierarchyResult, children: HierarchyResult[]) => { let minX = topItem.x; let minY = topItem.y; let maxX = minX + topItem.data.width; let maxY = minY + topItem.data.height; children.forEach((child) => { const childXY = cacluculateExtremeValue(child, child.children); minX = Math.min(minX, child.x, childXY.minX); minY = Math.min(minY, child.y, childXY.minY); maxX = Math.max(maxX, child.x + child.data.width, childXY.maxX); maxY = Math.max(maxY, child.y + child.data.height, childXY.maxY); }); return { minX, minY, maxX, maxY } }; /** * 获取边框位置及大小 * @param hierarchyItem * @returns */ export const getBorderPositionAndSize = (hierarchyItem: HierarchyResult) => { const firstChild = hierarchyItem?.children?.[0]; let totalHeigth = hierarchyItem?.totalHeight || 0; let totalWidth = hierarchyItem?.totalWidth || 0; let x = hierarchyItem?.x || 0; let y = hierarchyItem?.y || 0; // 是否存在子节点 if (firstChild) { const position = cacluculateExtremeValue(hierarchyItem, hierarchyItem.children || []); x = position.minX; y = position.minY; totalHeigth = position.maxY - position.minY; totalWidth = position.maxX - position.minX; } else { totalWidth = hierarchyItem.data.width; totalHeigth = hierarchyItem.data.height; } return { x: x - 10, y: y - 10, width: totalWidth + 20, height: totalHeigth + 20, }; }; /** * 添加概要 */ export const addSummary = (nodes: Node[], graph: Graph) => { // 判断节点是否在当前存在父级以上节点 nodes.forEach((node) => { let hasParent = false; traverseNode(node.data.children || [], (child) => { if (child.id === node.id) { hasParent = true; } }); // 添加边框数据 if (!hasParent && !node.data.summary) { const root = buildTopic(node.data.type, { setMindProjectInfo: node.data.setMindProjectInfo, type: TopicType.branch, label: "概要", borderSize: BorderSize.medium, isSummary: true, summarySource: node.id, }, graph); node.setData({ summary: { topic: root, range: [node.id], border: { ...SummaryBorder.data, origin: root.id, summarySource: node.id, } } }, { deep: false }); } }); } /** * 插入图片 */ export const insertImage = (node?: Node) => { if(!node) return; openInsertImageModal((url) => { console.log('图片地址:', url); node.setData({ extraModules: { type: "image", data: { imageUrl: url, width: 300, height: 300 } } }) }) } /** * 右键菜单处理方法 */ export const mindmapMenuHander = { addTopic(tool: ContextMenuTool) { const node = tool.cell; if (node.isNode()) addChildrenTopic(node, tool.graph, node.data.setMindProjectInfo,); }, addPeerTopic(tool: ContextMenuTool) { const node = tool.cell; if (node.isNode()) addPeerTopic(node, tool.graph, node.data.setMindProjectInfo); }, addParentTopic(tool: ContextMenuTool) { const node = tool.cell; if (node.isNode()) addParentTopic(node, node.data.setMindProjectInfo, tool.graph); }, addCorrelationEdge(tool: ContextMenuTool) { if (tool.cell.isNode()) { // @ts-ignore const correlationEdgeRef = tool.graph?.extendAttr?.correlationEdgeRef; handleCreateCorrelationEdge(tool.graph, correlationEdgeRef, tool.cell); } }, addRemark(tool: ContextMenuTool) { // @ts-ignore tool.graph?.extendAttr?.setRightToolbarActive("remark"); selectTopic(tool.graph, tool.cell.data); }, addHref(tool: ContextMenuTool) { // @ts-ignore tool.cell?.extendAttr?.showHrefConfig?.(); }, addTopicLink(tool: ContextMenuTool) { // todo }, addImage(tool: ContextMenuTool) { const cell = tool.cell; cell.isNode() && insertImage(cell); }, addTag(tool: ContextMenuTool) { // @ts-ignore tool.graph?.extendAttr?.setRightToolbarActive("tag"); selectTopic(tool.graph, tool.cell.data); }, addIcon(tool: ContextMenuTool) { // @ts-ignore tool.graph?.extendAttr?.setRightToolbarActive("icon"); selectTopic(tool.graph, tool.cell.data); }, addCode(tool: ContextMenuTool) { tool.cell.setData({ extraModules: { type: "code", data: { code: "", language: "javascript", }, }, }); }, chooseSameLevel(tool: ContextMenuTool) { const parentId = tool.cell.data?.parentId; if (!parentId) return; const parent = tool.graph.getCellById(parentId); selectTopics(tool.graph, parent.data?.children); }, chooseAllSameLevel(tool: ContextMenuTool) { // todo }, copy(tool: ContextMenuTool) { localStorage.setItem("mindmap-copy-data", JSON.stringify([tool.cell])); navigator.clipboard.writeText(" "); }, cut(tool: ContextMenuTool) { tool.graph.cut([tool.cell]); }, paste(tool: ContextMenuTool) { handleMindmapPaste(tool.graph, tool.cell.data.setMindProjectInfo); }, delete(tool: ContextMenuTool) { deleteTopics([tool.cell.id], tool.cell.data.setMindProjectInfo); }, deleteCurrent(tool: ContextMenuTool) { tool.cell.isNode() && handleDeleteCurrentTopic(tool.graph, [tool.cell]); }, exportImage(tool: ContextMenuTool) { tool.graph.exportPNG("", { quality: 1, copyStyles: false, }); }, copyImage(tool: ContextMenuTool) { // TODO复制为图片 }, };