import { StructureType, TopicType } from "@/enum"; import { MindMapProjectInfo, TopicItem, HierarchyResult } from "@/types"; import { Graph, Cell, Node } from "@antv/x6"; import TopicComponent from "@/components/mindMap/Topic"; import TopicBorder from "@/components/mindMap/Border"; import SummaryBorder from "@/components/mindMap/SummaryBorder"; import { topicData } from "@/config/data"; import { uuid } from "@/utils"; import { hierarchyMethodMap } from "@/pages/mindmap/hierarchy"; import { createEdge } from "./edge"; import { getTheme } from "./theme"; import { topicMenu } from "@/utils/contentMenu"; import { cacluculateExtremeValue, getBorderPositionAndSize, } from "@/utils/mindmapHander"; /** * 渲染思维导图项目 * @param graph */ export const renderMindMap = ({ topics, pageSetting, structure, theme, graph, setMindProjectInfo, returnCells = false }: { topics: TopicItem[]; pageSetting: MindMapProjectInfo["pageSetting"]; structure: StructureType; theme: string; graph: Graph; setMindProjectInfo: (info: MindMapProjectInfo) => void; returnCells?: boolean; }) => { const cells: Cell[] = []; topics.forEach((topic) => { // 遍历出层次结构 const result: HierarchyResult = hierarchyMethodMap[structure]?.( topic, pageSetting ); let originPosition = { x: topic?.x ?? -10, y: topic?.y ?? -10 }; if (graph.hasCell(topic.id)) { const node = graph.getCellById(topic.id); if (node.isNode()) { originPosition = node.position(); } } const offsetX = originPosition.x - result.x; const offsetY = originPosition.y - result.y; const traverse = (hierarchyItem: HierarchyResult, parent?: Node) => { if (hierarchyItem) { const { data, children, x, y } = hierarchyItem; const id = data?.id || uuid(); // 创建主题 const node = graph.createNode({ ...TopicComponent, width: data.width, height: data.height, data: { ...data, opacity: 100, // 节点内部执行数据更新方法 setMindProjectInfo, }, id, x: offsetX + x, y: offsetY + y, tools: [ { name: "contextmenu", args: { menu: topicMenu, }, }, ], }); // 渲染边框 if (data.border) { cells.push( createBorderComponent(hierarchyItem, offsetX, offsetY, graph) ); } // 渲染概要 if (data.summary) { const summaryCells = createSummaryCells( hierarchyItem, data.summary, structure, pageSetting, theme, graph, setMindProjectInfo, offsetX, offsetY ); cells.push(...(summaryCells || [])); } cells.push(node); parent && parent.addChild(node); if (data?.links) { cells.push(...data.links.map((item) => graph.createEdge(item))); } if (children) { children.forEach((item: HierarchyResult, index) => { const isBracket = [ StructureType.leftBracket, StructureType.rightBracket, ].includes(structure); // 括号图不绘制中间连线 if (!isBracket || index === 0 || index === children.length - 1) { const edge = createEdge(graph, id, item, structure, theme, { onlyOneChild: children.length === 1, }); cells.push(edge); node.addChild(edge); } // 递归遍历 traverse(item, node); }); } } }; traverse(result); }); if(returnCells) return cells; const oldCells = graph.getCells(); // 移除不要的节点及对应的边 oldCells.forEach((cell) => { if (!cells.find((item) => cell.id === item.id)) { graph.removeCell(cell.id + "-edge"); graph.removeCell(cell); } }); // 添加或删除节点 cells .filter((cell) => cell.isNode() && !graph.hasCell(cell.id)) .forEach((cell) => { graph.addCell(cell); }); // 更新老的节点 cells .filter((cell) => cell.isNode() && graph.hasCell(cell.id)) .forEach((cell) => { cell.isNode() && updateNode(cell, graph); }); // 添加所需的节点 const edgeCells = cells.filter((cell) => cell.isEdge()); graph.removeCells(edgeCells); graph.addCell(edgeCells); }; // 渲染概要 const createSummaryCells = ( hierarchyItem: HierarchyResult, summary: TopicItem['summary'], structure: StructureType, pageSetting: MindMapProjectInfo['pageSetting'], theme: string, graph: Graph, setMindProjectInfo: (info: MindMapProjectInfo) => void, offsetX: number, offsetY: number ): Cell[] => { let cells: Cell[] = []; if (summary) { const positionAndSize = cacluculateExtremeValue( hierarchyItem, hierarchyItem.children ); const totalHeight = positionAndSize.maxY - positionAndSize.minY; const totalWidth = positionAndSize.maxX - positionAndSize.minX; // 概要边框 const node = graph.createNode({ ...SummaryBorder, data: summary.border, id: summary.topic.id + "-border", zIndex: 0, position: { x: offsetX + positionAndSize.minX - 2, y: offsetY + positionAndSize.minY - 2, }, size: { width: totalWidth + 4, height: totalHeight + 4, }, }); cells.push(node); // 概要节点 cells.push(...renderMindMap({ topics: [{ ...summary.topic, x: offsetX + hierarchyItem.x + totalWidth + 40, y: offsetY + hierarchyItem.y }], pageSetting, structure, theme, graph, setMindProjectInfo, returnCells: true }) || []); } return cells; } // 创建外框组件 const createBorderComponent = ( hierarchyItem: HierarchyResult, offsetX: number, offsetY: number, graph: Graph ) => { const positionAndSize = getBorderPositionAndSize(hierarchyItem); return graph.createNode({ ...TopicBorder, id: hierarchyItem.id + "-border", data: { ...hierarchyItem.data.border, origin: hierarchyItem.id, }, zIndex: 0, position: { x: offsetX + positionAndSize.x, y: offsetY + positionAndSize.y, }, size: { width: positionAndSize.width, height: positionAndSize.height, }, }); }; const updateNode = (node: Node, graph: Graph) => { const oldCell = graph.getCellById(node.id); if (oldCell.isNode()) { oldCell.setData(node.data); oldCell.position(node.position().x, node.position().y); oldCell.setSize(node.size().width, node.size().height); // oldCell.setAttrs(node.attrs); // const cells = node.children?.map(item => graph.getCellById(item.id)); // oldCell.setChildren(cells ?? null); } }; /** * 添加分支主题 */ export const addTopic = ( type: TopicType, setMindProjectInfo: (info: MindMapProjectInfo) => void, node?: Node, otherData: Record = {} ): TopicItem | undefined => { const projectInfo = getMindMapProjectByLocal(); if (!projectInfo || !setMindProjectInfo) return; const topic = buildTopic( type, { ...(otherData || {}), parentId: node?.id, isSummary: node?.data?.isSummary, summarySource: node?.data?.summarySource }, node ); if (node) { const parentId = node.id; const traverse = (topics: TopicItem[]) => { topics.forEach((item) => { if (item.id === parentId) { if (item.children) { item.children?.push(topic); } else { item.children = [topic]; } } if (item.children) { traverse(item.children); } if (item.summary) { traverse([item.summary.topic]) } }); }; traverse(projectInfo?.topics || []); } else { projectInfo.topics.push(topic); } setMindProjectInfo(projectInfo); return topic; }; const topicMap = { [TopicType.main]: { label: "中心主题", width: 206, height: 70, }, [TopicType.branch]: { label: "分支主题", width: 104, height: 40, }, [TopicType.sub]: { label: "子主题", width: 76, height: 27, }, }; /** * 构建一个主题数据 * @param type 主题类型 * @param options 配置项 * @returns */ export const buildTopic = ( type: TopicType, options: Record = {}, parentNode?: Node ): TopicItem => { const projectInfo = getMindMapProjectByLocal(); const theme = getTheme( projectInfo?.theme, type === TopicType.sub ? parentNode : undefined ); const id = uuid(); return { ...topicData, id, type, label: topicMap[type].label || "自由主题", width: topicMap[type].width || 206, height: topicMap[type].height || 70, fill: { ...topicData.fill, ...theme[type]?.fill, }, text: { ...topicData.text, ...theme[type]?.text, }, stroke: { ...topicData.stroke, ...theme[type]?.stroke, }, edge: { ...topicData.edge, color: theme[type]?.edge.color, }, ...options, children: (options?.children || topicData.children || []).map( (item: TopicItem) => { return { ...item, parentId: id, }; } ), }; }; /** * 从本地获取项目信息 * @returns */ export const getMindMapProjectByLocal = (): MindMapProjectInfo | null => { return JSON.parse(localStorage.getItem("minMapProjectInfo") || "null"); }; /** * 更新主题数据 * @param id 主题id * @param value 更新的数据 * @param setMindProjectInfo 更新项目信息方法 */ export const updateTopic = ( id: string, value: Partial, setMindProjectInfo: (info: MindMapProjectInfo) => void ) => { const projectInfo = getMindMapProjectByLocal(); if (!projectInfo || !setMindProjectInfo) return; const traverse = (topics: TopicItem[]) => { topics.forEach((item) => { if (item.id === id) { Object.assign(item, value); } if (item.children) { traverse(item.children); } }); }; traverse(projectInfo?.topics || []); setMindProjectInfo(projectInfo); };