Quellcode durchsuchen

feat: 添加快捷键

liaojiaxing vor 8 Monaten
Ursprung
Commit
7b06830ea1

+ 20 - 18
apps/designer/src/components/mindMap/Topic.tsx

@@ -3,7 +3,7 @@ import { EventArgs, Graph, Node, Path } from "@antv/x6";
 import { topicData } from "@/config/data";
 import { useSizeHook, useShapeProps } from "@/hooks";
 import CustomInput from "../CustomInput";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
 import { PlusOutlined } from "@ant-design/icons";
 import { TopicType } from "@/enum";
 import { addTopic } from "@/pages/mindmap/mindMap";
@@ -39,6 +39,10 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
     setSelected(cells.length === 1 && cells[0].id === node.id);
   };
   const [showCollapsePoint, setShowCollapsePoint] = useState(collapsed);
+  const extraModuleRef = useRef<HTMLDivElement>(null);
+  const titleRef = useRef<HTMLDivElement>(null);
+  const tagRef = useRef<HTMLDivElement>(null);
+  const remarkRef = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
     graph.createTransformWidget(node);
@@ -52,15 +56,11 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   }, []);
 
   useEffect(() => {
-    let maxWidth = size.width - 8;
-    let maxHeight = size.height;
-  }, [tags, label, icons]);
-
-  const getSize = useMemo(() => {
-    return {
-      contentHeight: node.size().height - (extraModules ? 40 : 0),
-    };
-  }, [icons, label, tags, extraModules, remark, link, size]);
+    // 动态计算出所需的宽高
+    let width = extraModules?.width || 0;
+    
+    
+  }, [extraModules, label, icons, tags, remarkRef]);
 
   const childrenCount = useMemo(() => {
     let count = 0;
@@ -110,15 +110,17 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
       >
         <div className="w-full h-full flex flex-col">
           {extraModules && (
-            <ExtraModule node={node} extraModules={extraModules} />
+            <div ref={extraModuleRef}>
+              <ExtraModule node={node} extraModules={extraModules} />
+            </div>
           )}
 
-          <div className="flex-1">
+          <div className="flex-1 flex">
             <div
               className="flex-1 flex flex-col justify-center"
-              style={{ height: getSize.contentHeight }}
+              // style={{ height: getSize.contentHeight }}
             >
-              <div className="flex justify-start items-center text-20px">
+              <div className="flex justify-start items-center text-20px" ref={titleRef}>
                 {icons?.map((icon: string) => {
                   return (
                     <svg key={icon} className="icon mr-6px" aria-hidden="true">
@@ -140,7 +142,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
                 />
               </div>
 
-              <div>
+              <div className="flex" ref={tagRef}>
                 {tags?.map((item: { name: string; color: string }) => {
                   return (
                     <CustomTag
@@ -156,14 +158,14 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
               </div>
             </div>
 
-            <div>
+            <div ref={remarkRef}>
               {link && <Link link={link} />}
               {remark && <i className="iconfont icon-pinglun1" />}
             </div>
           </div>
         </div>
 
-        {/* 添加主题 */}
+        {/* 添加主题按钮 */}
         {selected && !children?.length && (
           <div
             className={`
@@ -195,7 +197,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
             onMouseOut={() => !collapsed && setShowCollapsePoint(false)}
             />
         }
-        {/* 折叠子主题 */}
+        {/* 折叠按钮 */}
         {type !== TopicType.main && children?.length && showCollapsePoint && (
           <div
             className={`

+ 1 - 0
apps/designer/src/config/data.ts

@@ -138,6 +138,7 @@ export const topicData = {
     width: 3,
   },
   children: [],
+  extraModules: undefined
 }
 
 // 初始化项目

+ 7 - 0
apps/designer/src/models/mindMapModel.ts

@@ -7,6 +7,7 @@ import { Keyboard } from "@antv/x6-plugin-keyboard";
 import { History } from "@antv/x6-plugin-history";
 import { Transform } from "@antv/x6-plugin-transform";
 import { Scroller } from "@antv/x6-plugin-scroller";
+import { Clipboard } from "@antv/x6-plugin-clipboard";
 import { MindMapProjectInfo } from "@/types";
 import { bindMindMapEvents } from "@/events/mindMapEvent";
 import { useLocalStorageState } from "ahooks";
@@ -14,6 +15,7 @@ import { renderMindMap } from "@/pages/mindmap/mindMap";
 import { defaultProject } from "@/config/data";
 import { TopicType } from "@/enum";
 import { isEqual } from "lodash-es";
+import { bindMindmapKeys } from "@/utils/fastKey";
 
 type RightToolbarType =
   | "style"
@@ -118,6 +120,7 @@ export default function mindMapModel() {
     instance.use(new Selection());
     instance.use(new Keyboard());
     instance.use(new History());
+    instance.use(new Clipboard());
     instance.use(
       new Scroller({
         enabled: true,
@@ -139,7 +142,11 @@ export default function mindMapModel() {
       setCanUndo(instance.canUndo());
     });
 
+    // 绑定事件
     bindMindMapEvents(instance, mindProjectInfo, setMindProjectInfo, setSelectedCell);
+    // 绑定键盘
+    bindMindmapKeys(instance, mindProjectInfo, setMindProjectInfo);
+
     setGraph(instance);
     renderMindMap(instance, setMindProjectInfo);
     instance.centerContent();

+ 5 - 4
apps/designer/src/pages/mindmap/components/Config/NodeStyle.tsx

@@ -129,6 +129,7 @@ export default function GraphStyle() {
           text: model.text,
           edge: model.edge,
           BorderSize: model.borderSize,
+          fixedWidth: model.fixedWidth,
           fill: {
             ...model.fill,
             color1: model.isFill ? model.fill.color1 : "transparent"
@@ -250,7 +251,7 @@ export default function GraphStyle() {
             value={formModel.text.fontSize}
             onChange={(val) => handleSetFormModel("text.fontSize", val)}
           />
-          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+          <div className="bg-white rounded-s flex justify-between items-center">
             <Tooltip placement="bottom" title="字体加粗">
               <Button
                 type="text"
@@ -313,8 +314,8 @@ export default function GraphStyle() {
             </Tooltip>
           </div>
         </div>
-        <div className="flex items-center gap-12px mb-8px">
-          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+        <div className="flex items-center gap-12px">
+          <div className="bg-white rounded-s flex justify-between items-center">
             <Tooltip title="左对齐">
               <Button
                 type="text"
@@ -345,7 +346,7 @@ export default function GraphStyle() {
               />
             </Tooltip>
           </div>
-          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+          <div className="bg-white rounded-s flex justify-between items-center">
             <Tooltip title="顶部对齐">
               <Button
                 type="text"

+ 12 - 2
apps/designer/src/pages/mindmap/components/Config/index.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
 import { useModel } from "umi";
 import { Tabs } from "antd";
 import InsertCss from "insert-css"
@@ -16,13 +16,23 @@ InsertCss(`
   }
 `)
 export default function index() {
-  const { rightToobarActive } = useModel("mindMapModel");
+  const { rightToobarActive, selectedCell } = useModel("mindMapModel");
+  const [activeKey, setActiveKey] = useState("1");
+  useEffect(() => {
+    if(selectedCell.find(cell => cell.isNode())) {
+      setActiveKey("2");
+    } else {
+      setActiveKey("1");
+    }
+  }, [selectedCell]);
   return (
     <div>
       {/* 样式 */}
       {rightToobarActive === "style" && (
         <>
           <Tabs
+            activeKey={activeKey}
+            onChange={(key) => setActiveKey(key)}
             items={[
               {
                 key: "1",

+ 23 - 15
apps/designer/src/pages/mindmap/components/Footer/index.tsx

@@ -1,10 +1,11 @@
-import { CompressOutlined, ExpandOutlined, MinusOutlined, PlusOutlined, QuestionCircleFilled } from "@ant-design/icons";
+import { AimOutlined, CompressOutlined, ExpandOutlined, MinusOutlined, PlusOutlined, QuestionCircleFilled } from "@ant-design/icons";
 import { Button, ConfigProvider, Divider, Slider, Tooltip } from "antd";
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useMemo, useRef, useState } from "react";
 import { useFullscreen } from "ahooks";
 import { useModel } from "umi";
 import { MiniMap } from '@antv/x6-plugin-minimap';
 import insertCss from 'insert-css';
+import { TopicType } from "@/enum";
 
 insertCss(`
   .navigation-view {
@@ -23,10 +24,9 @@ insertCss(`
 export default function Footer() {
   const [isFullscreen, { toggleFullscreen }] = useFullscreen(document.body);
   const navigationViewRef = useRef(null);
-  const { graph } = useModel("mindMapModel");
   const [showNavigation, setShowNavigation] = useState(false);
-  const [countCell, setCountCell] = useState(0);
   const [scale, setScale] = useState(100);
+  const { selectedCell, graph} = useModel("mindMapModel");
 
   useEffect(() => {
     if(!graph || !navigationViewRef.current) return;
@@ -41,15 +41,15 @@ export default function Footer() {
     )
   }, [graph, navigationViewRef.current]);
 
-  useEffect(() => {
-    graph?.on('cell:change:*', () => {
-      const count = graph?.getCells().length || 0;
-      setCountCell(count > 0 ? count - 1 : 0)
-    });
-    graph?.on('scale', (scaleInfo) => {
-      setScale(parseInt(scaleInfo.sx * 100 + ''));
-    })
-  }, [graph]);
+
+  const countInfo = useMemo(() => {
+    return {
+      selectedNodeCount: selectedCell.filter((cell) => cell.isNode()).length,
+      selectedNodeTextCount: selectedCell.reduce((a, b) => a + b.data?.label?.length || 0 , 0),
+      nodeCount: graph?.getNodes().length || 0,
+      textCount: graph?.getNodes().reduce((a, b) => a + b.data?.label?.length || 0 , 0) || 0
+    };
+  }, [selectedCell, graph]);
 
   const handleZoom = (value: number) => {
     graph?.zoomTo(value / 100)
@@ -64,17 +64,25 @@ export default function Footer() {
     handleZoom(value)
   }
 
+  const handleFocusCenter = () => {
+    const center = graph?.getCells().find(cell => cell.data?.type === TopicType.main);
+    center && graph?.centerCell(center);
+  }
+
   return (
     <ConfigProvider componentSize="small">
       <div className="absolute w-full h-24px left-0 bottom-0 bg-white flex justify-between items-center px-16px">
         <div className="footer-left"></div>
         <div className="footer-right flex items-center">
-          <div>字数:{}/{countCell}</div>
-          <div>主题数:{}/{countCell}</div>
+          <div className="mr-8px">字数:{countInfo.selectedNodeCount ? `${countInfo.selectedNodeTextCount}/` : ''}{countInfo.textCount}</div>
+          <div>主题数:{countInfo.selectedNodeCount ? `${countInfo.selectedNodeCount}/` : ''}{countInfo.nodeCount}</div>
           <Divider type="vertical" />
           <Tooltip title="模板">
             <Button type="text" icon={<i className="iconfont icon-buju" />} />
           </Tooltip>
+          <Tooltip title="定位到中心主题">
+            <Button type="text" icon={<AimOutlined />} onClick={handleFocusCenter}/>
+          </Tooltip>
           <Tooltip title={showNavigation ? "关闭视图导航" : "显示视图导航"}>
             <Button type="text" icon={<i className="iconfont icon-map" />} onClick={() => setShowNavigation(!showNavigation)}/>
           </Tooltip>

+ 0 - 2
apps/designer/src/pages/mindmap/components/RightToolbar/index.tsx

@@ -54,8 +54,6 @@ export default function index() {
           type="text"
           disabled={!selectedCell.length}
           icon={<FileImageOutlined />}
-          className={rightToobarActive === "image" ? "active" : ""}
-          onClick={() => rightToolbarActive("image")}
         />
       </Tooltip>
       <Tooltip placement="bottom" title="标签">

+ 32 - 0
apps/designer/src/pages/mindmap/index.tsx

@@ -17,6 +17,38 @@ export default function MindMap() {
     graphRef.current && initMindMap(graphRef.current);
   }, []);
 
+  useEffect(() => {
+    document.addEventListener(
+      "mousewheel",
+      function (e: any) {
+        // 判断是否按下ctrl
+        if (e.ctrlKey) {
+          // 阻止默认事件
+          e.preventDefault();
+        }
+      },
+      { capture: false, passive: false }
+    );
+    document.addEventListener(
+      "keydown",
+      function (event) {
+        if (
+          (event.ctrlKey === true || event.metaKey === true) &&
+          (event.keyCode === 61 ||
+            event.keyCode === 107 ||
+            event.keyCode === 173 ||
+            event.keyCode === 109 ||
+            event.keyCode === 187 ||
+            event.keyCode === 189 ||
+            event.keyCode === 80)
+        ) {
+          event.preventDefault();
+        }
+      },
+      false
+    );
+  }, []);
+
   return (
     <ConfigProvider
       // componentSize="small"

+ 3 - 3
apps/designer/src/pages/mindmap/mindMap.tsx

@@ -30,6 +30,7 @@ export const renderMindMap = (
   topics.forEach((topic) => {
     // 遍历出层次结构
     const result: HierarchyResult = hierarchyMethodMap[projectInfo.structure]?.(topic, pageSetting);
+
     let originPosition = { x: topic?.x ?? -10, y: topic?.y ?? -10 };
     if(graph.hasCell(topic.id)) {
       const node = graph.getCellById(topic.id);
@@ -126,10 +127,10 @@ export const addTopic = (
   }, node);
 
   if( node) {
-    const parentData = node.getData();
+    const parentId = node.id;
     const traverse = (topics: TopicItem[]) => {
       topics.forEach((item) => {
-        if (item.id === parentData?.id) {
+        if (item.id === parentId) {
           if (item.children) {
             item.children?.push(topic);
           } else {
@@ -147,7 +148,6 @@ export const addTopic = (
   }
 
   setMindProjectInfo(projectInfo);
-
   return topic;
 };
 

+ 141 - 16
apps/designer/src/utils/fastKey.tsx

@@ -1,5 +1,7 @@
-import { Graph } from "@antv/x6";
+import { Cell, Graph } from "@antv/x6";
 import { printHandle } from "./index";
+import { TopicType } from "@/enum";
+import { MindMapProjectInfo } from "@/types";
 import {
   handleInsertText,
   handlePaste,
@@ -13,9 +15,16 @@ import {
   handleUnGroup,
   handleMove,
   handleLock,
-  handleUnLock
+  handleUnLock,
 } from "./hander";
 
+import {
+  addPeerTopic,
+  addChildrenTopic,
+  deleteTopics,
+  handleMindmapPaste,
+} from "./mindmapHander";
+
 export const bindKeys = (graph: Graph) => {
   // Ctrl + A 全选
   graph.bindKey("ctrl+a", (e: KeyboardEvent) => {
@@ -27,9 +36,7 @@ export const bindKeys = (graph: Graph) => {
   });
 
   // Ctrl + F 查找替换 todo
-  graph.bindKey("ctrl+f", (e: KeyboardEvent) => {
-    
-  });
+  graph.bindKey("ctrl+f", (e: KeyboardEvent) => {});
 
   // Ctrl + + 放大
   graph.bindKey("ctrl+=", (e: KeyboardEvent) => {
@@ -66,13 +73,11 @@ export const bindKeys = (graph: Graph) => {
 
   // Ctrl + K 插入链接
   graph.bindKey("ctrl+k", (e: KeyboardEvent) => {
-
     // todo
   });
 
   // Ctrl+Alt+M 插入评论
   graph.bindKey("ctrl+alt+m", (e: KeyboardEvent) => {
-    
     // todo
   });
 
@@ -88,7 +93,7 @@ export const bindKeys = (graph: Graph) => {
 
   // Ctrl+U 下划线
   graph.bindKey("ctrl+u", (e: KeyboardEvent) => {
-    handleSetAttr(graph, "text.textDecoration", 'underline');
+    handleSetAttr(graph, "text.textDecoration", "underline");
   });
 
   // Ctrl+c 复制
@@ -168,7 +173,7 @@ export const bindKeys = (graph: Graph) => {
 
   // ↑ 向上移动
   graph.bindKey("up", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "y", -10);
     }
@@ -176,7 +181,7 @@ export const bindKeys = (graph: Graph) => {
 
   // ↓ 向下移动
   graph.bindKey("down", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "y", 10);
     }
@@ -184,7 +189,7 @@ export const bindKeys = (graph: Graph) => {
 
   // ← 向左移动
   graph.bindKey("left", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "x", -10);
     }
@@ -192,7 +197,7 @@ export const bindKeys = (graph: Graph) => {
 
   // → 向右移动
   graph.bindKey("right", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "x", 10);
     }
@@ -200,7 +205,7 @@ export const bindKeys = (graph: Graph) => {
 
   // shift+← 向左微移
   graph.bindKey("shift+left", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "x", -1);
     }
@@ -208,7 +213,7 @@ export const bindKeys = (graph: Graph) => {
 
   // shift+→ 向右微动
   graph.bindKey("shift+right", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "x", 1);
     }
@@ -216,7 +221,7 @@ export const bindKeys = (graph: Graph) => {
 
   // shift+↑ 向上微动
   graph.bindKey("shift+up", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.stopPropagation();
       e.preventDefault();
       handleMove(graph, "y", -1);
@@ -225,7 +230,7 @@ export const bindKeys = (graph: Graph) => {
 
   // shift+↓ 向下微动
   graph.bindKey("shift+down", (e: KeyboardEvent) => {
-    if(graph.getSelectedCells().length) {
+    if (graph.getSelectedCells().length) {
       e.preventDefault();
       handleMove(graph, "y", 1);
     }
@@ -276,3 +281,123 @@ export const bindKeys = (graph: Graph) => {
     alignCell("v", graph.getSelectedCells());
   });
 };
+
+export const bindMindmapKeys = (
+  graph: Graph,
+  mindProjectInfo?: MindMapProjectInfo,
+  setMindProjectInfo?: (info: MindMapProjectInfo) => void
+) => {
+  // Enter 增加同级主题
+  graph.bindKey("enter", (e: KeyboardEvent) => {
+    e.preventDefault();
+    const branch = graph
+      .getSelectedCells()
+      .find((cell) => cell.data?.type !== TopicType.sub && cell.isNode());
+    if (branch) {
+      addPeerTopic(branch, graph, setMindProjectInfo);
+      return;
+    }
+    const sub = graph
+      .getSelectedCells()
+      .find((cell) => cell.data?.type === TopicType.sub && cell.isNode());
+    if (sub) {
+      sub.isNode() && addPeerTopic(sub, graph, setMindProjectInfo);
+    }
+  });
+
+  // Tab 增加子主题
+  graph.bindKey("tab", (e: KeyboardEvent) => {
+    e.preventDefault();
+    const node = graph.getSelectedCells().find((cell) => cell.isNode());
+    if (node) {
+      node.isNode() && addChildrenTopic(node, setMindProjectInfo);
+    }
+  });
+
+  // insert 增加子主题
+  graph.bindKey("insert", (e: KeyboardEvent) => {
+    e.preventDefault();
+    const node = graph.getSelectedCells().find((cell) => cell.isNode());
+    if (node) {
+      node.isNode() && addChildrenTopic(node, setMindProjectInfo);
+    }
+  });
+
+  // delete 删除
+  graph.bindKey("delete", (e: KeyboardEvent) => {
+    e.preventDefault();
+    const topicIds = graph
+      .getSelectedCells()
+      .filter((cell) => cell.isNode)
+      .map((cell) => cell.id);
+    deleteTopics(topicIds, setMindProjectInfo);
+  });
+
+  // Ctrl + A 全选
+  graph.bindKey("ctrl+a", (e: KeyboardEvent) => {
+    const cells = graph.getNodes();
+    graph.select(cells);
+  });
+
+  // Ctrl + + 放大
+  graph.bindKey("ctrl+=", (e: KeyboardEvent) => {
+    graph.zoomTo(graph.zoom() + 0.1);
+  });
+
+  // Ctrl + - 缩小
+  graph.bindKey("ctrl+-", (e: KeyboardEvent) => {
+    graph.zoomTo(graph.zoom() - 0.1);
+  });
+
+  // esc 取消
+  graph.bindKey("esc", (e: KeyboardEvent) => {
+    graph.cleanSelection();
+    graph.trigger("cancel");
+  });
+
+  // Ctrl+Alt+M 插入评论
+  graph.bindKey("ctrl+alt+m", (e: KeyboardEvent) => {
+    // todo
+  });
+
+  // Ctrl+B 加粗
+  graph.bindKey("ctrl+b", (e: KeyboardEvent) => {
+    handleSetAttr(graph, "text.bold", true);
+  });
+
+  // Ctrl+I 斜体
+  graph.bindKey("ctrl+i", (e: KeyboardEvent) => {
+    handleSetAttr(graph, "text.italic", true);
+  });
+
+  // Ctrl+U 下划线
+  graph.bindKey("ctrl+u", (e: KeyboardEvent) => {
+    handleSetAttr(graph, "text.textDecoration", "underline");
+  });
+
+  // Ctrl+c 复制
+  graph.bindKey("ctrl+c", (e: KeyboardEvent) => {
+    localStorage.setItem("mindmap-copy-data", JSON.stringify(graph.getSelectedCells()));
+    navigator.clipboard.writeText("  ");
+  });
+
+  // Ctrl+x 剪切
+  graph.bindKey("ctrl+x", (e: KeyboardEvent) => {
+    graph.cut(graph.getSelectedCells());
+  });
+
+  // Ctrl+v 粘贴
+  graph.bindKey("ctrl+v", (e: KeyboardEvent) => {
+    setMindProjectInfo && handleMindmapPaste(graph, setMindProjectInfo);
+  });
+
+  // Ctrl+z 撤销
+  graph.bindKey("ctrl+z", (e: KeyboardEvent) => {
+    graph.undo();
+  });
+
+  // Ctrl+y 重做
+  graph.bindKey("ctrl+y", (e: KeyboardEvent) => {
+    graph.redo();
+  });
+};

+ 1 - 1
apps/designer/src/utils/hander.tsx

@@ -553,7 +553,7 @@ export const handleSetAttr = (graph: Graph, path: string, value: any) => {
     } else {
       set(data, path, value);
     }
-    console.log(data);
+    
     cell.setData({
       ...data,
     });

+ 179 - 0
apps/designer/src/utils/mindmapHander.tsx

@@ -0,0 +1,179 @@
+import { TopicType } from "@/enum";
+import { addTopic, getMindMapProjectByLocal } from "@/pages/mindmap/mindMap";
+import { MindMapProjectInfo, TopicItem } from "@/types";
+import { Cell, Graph, Node } from "@antv/x6";
+import { message } from "antd";
+import { cloneDeep } from "lodash-es";
+import { uuid } from ".";
+
+/**
+ * 添加同级主题
+ * @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;
+  parentNode?.isNode() && addTopic(type, setMindProjectInfo, parentNode);
+};
+
+/**
+ * 添加子主题
+ * @param node
+ * @param setMindProjectInfo
+ */
+export const addChildrenTopic = (
+  node: Node,
+  setMindProjectInfo?: (info: MindMapProjectInfo) => void
+) => {
+  if (!setMindProjectInfo) return;
+
+  const type =
+    node.data?.type === TopicType.main ? TopicType.branch : TopicType.sub;
+  addTopic(type, setMindProjectInfo, node);
+};
+
+/**
+ * 删除子主题
+ * @param ids
+ * @param setMindProjectInfo
+ */
+export const deleteTopics = (
+  ids: string[],
+  setMindProjectInfo?: (info: MindMapProjectInfo) => void
+) => {
+  const mindProjectInfo = getMindMapProjectByLocal();
+  if (!mindProjectInfo || !setMindProjectInfo) return;
+  const topics = cloneDeep(mindProjectInfo.topics);
+  const filterTopics = (list: TopicItem[]): TopicItem[] => {
+    const result: TopicItem[] = [];
+    for (const item of list) {
+      if (!ids.includes(item.id) || item.type === TopicType.main) {
+        if (item.children?.length) {
+          item.children = filterTopics(item.children);
+        }
+        result.push(item);
+      }
+    }
+    return result;
+  };
+
+  mindProjectInfo.topics = filterTopics(topics);
+
+  setMindProjectInfo(mindProjectInfo); // TODO 这个方法删除更新有问题
+  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,
+                  currentNode,
+                  { ...data })
+                });
+              }
+            } else {
+              addTopic(
+                currentNode.data?.type === TopicType.main
+                  ? TopicType.branch
+                  : TopicType.sub,
+                setMindProjectInfo,
+                currentNode,
+                { label: text }
+              );
+            }
+          };
+        });
+      }
+    }
+  });
+};
+
+
+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;
+  })
+}