Browse Source

feat: 分离结构实现、子节点跟随等

liaojiaxing 6 months ago
parent
commit
7834358334

+ 6 - 4
apps/designer/src/components/CustomInput.tsx

@@ -1,5 +1,5 @@
-import React, { useEffect, useMemo } from "react";
-import { Input } from "antd";
+import React, { useEffect, useMemo, useRef } from "react";
+import { Input, InputRef } from "antd";
 import { Node } from "@antv/x6";
 import { useSafeState } from "ahooks";
 export default function CustomInput(props: {
@@ -16,6 +16,7 @@ export default function CustomInput(props: {
 }) {
   const { value, styles, node, placeholder, txtStyle } = props;
   const [isEditing, setIsEditing] = useSafeState(!node.data?.lock);
+  const inputRef = useRef<InputRef>(null);
   
   const style = useMemo(() => {
     const top = styles.textVAlign === 'top' ? 0 : styles.textVAlign === 'middle' ? '50%' : undefined;
@@ -29,7 +30,7 @@ export default function CustomInput(props: {
       top,
       bottom,
     }
-  }, [styles])
+  }, [styles]);
 
   const handleChange = (val: string) => {
     node.setData({ label: val });
@@ -57,6 +58,7 @@ export default function CustomInput(props: {
     <div className="absolute w-full h-full w-full" style={txtStyle}>
       {isEditing ? (
         <Input.TextArea
+          ref={inputRef}
           placeholder={placeholder}
           value={value}
           variant="borderless"
@@ -64,8 +66,8 @@ export default function CustomInput(props: {
           style={style}
           onChange={(e) => handleChange(e.target.value)}
           onBlur={() => handleSetEditing(false)}
-          autoSize
           autoFocus
+          autoSize
         />
      ) : (
         <div

+ 4 - 4
apps/designer/src/components/mindMap/Topic.tsx

@@ -6,9 +6,9 @@ import CustomInput from "../CustomInput";
 import { useEffect, useState } from "react";
 import { PlusOutlined } from "@ant-design/icons";
 import { TopicType } from "@/enum";
-import { addTopic } from "@/utils/mindMap";
+import { addTopic } from "@/pages/mindmap/mindMap";
 const component = ({ node, graph }: { node: Node; graph: Graph }) => {
-  const { fill, stroke, opacity, label, text, borderSize } = node.getData();
+  const { fill, stroke, opacity, label, text, borderSize, setMindProjectInfo } = node.getData();
   const { size, ref } = useSizeHook();
   const { fillContent, strokeColor, strokeWidth, strokeDasharray } =
     useShapeProps(fill, size, stroke);
@@ -32,9 +32,9 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   const handleAddBranch = () => {
     const data = node.getData();
     if( data.type === TopicType.main) {
-      addTopic(node, TopicType.branch);
+      addTopic(TopicType.branch, setMindProjectInfo, node);
     } else {
-      addTopic(node, TopicType.sub);
+      addTopic(TopicType.sub, setMindProjectInfo, node);
     }
   }
   

+ 3 - 2
apps/designer/src/config/data.ts

@@ -1,6 +1,6 @@
-import { BorderSize, ImageFillType, TopicType } from "@/enum"
+import { BorderSize, ImageFillType, StructureType, TopicType } from "@/enum"
 import { MindMapProjectInfo } from "@/types";
-import { buildTopic } from "@/utils/mindMap";
+import { buildTopic } from "@/pages/mindmap/mindMap";
 
 // 通用连接桩
 export const ports = {
@@ -140,6 +140,7 @@ export const defaultProject: MindMapProjectInfo = {
   desc: "",
   version: "",
   author: "",
+  mapType: StructureType.right,
   pageSetting: {
     fillType: "color",
     fill: "#ffffff",

+ 23 - 0
apps/designer/src/enum/index.ts

@@ -1,3 +1,4 @@
+// 图片填充方式
 export enum ImageFillType {
   Fill, // 填充
   Auto, // 自动
@@ -6,12 +7,14 @@ export enum ImageFillType {
   Tiled, // 平铺
 }
 
+// 连线样式
 export enum ConnectorType {
   Rounded, // 圆角
   Smooth,  // 平滑
   Normal,  // 直线
 }
 
+// 边线类型
 export enum LineType {
   solid = "",
   dashed = "5,5",
@@ -19,14 +22,34 @@ export enum LineType {
   dashdot = "5,5,1,5",
 }
 
+// 边框圆角大小
 export enum BorderSize {
   none = 0,
   medium = 5,
   large = 30,
 }
 
+// 主题类型
 export enum TopicType {
   main = 'main',
   branch = 'branch',
   sub = 'sub',
+}
+
+// 思维导图结构类型 左右分布、自由分布、右侧分布、左侧分布、树状图结构、组织结构、左侧鱼骨图、右侧鱼骨图、横向时间轴、向上时间轴、向下时间轴、树形图、右侧树形图、左侧树形图、右侧括号图、左侧括号图
+export enum StructureType {
+  leftRight = 'leftRight',
+  free = 'free',
+  right = 'right',
+  left = 'left',
+  tree = 'tree',
+  organization = 'organization',
+  leftFishbone = 'leftFishbone',
+  rightFishbone = 'rightFishbone',
+  horizontalTime = 'horizontalTime',
+  upwardTime = 'upwardTime',
+  downwardTime = 'downwardTime',
+  treeShape = 'treeShape',
+  rightTreeShape = 'rightTreeShape',
+  leftTreeShape = 'leftTreeShape',
 }

apps/designer/src/events/index.ts → apps/designer/src/events/flowEvent.ts


+ 9 - 15
apps/designer/src/events/mindMapEvent.ts

@@ -1,9 +1,7 @@
 import { Graph, Node } from "@antv/x6";
-import Topic from "@/components/mindMap/Topic";
 import { BorderSize, TopicType } from "@/enum";
-import { addTopic } from "@/utils/mindMap";
+import { addTopic, updateTopic } from "@/pages/mindmap/mindMap";
 import { MindMapProjectInfo } from "@/types";
-import { topicData } from "@/config/data";
 
 export const bindMindMapEvents = (
   graph: Graph,
@@ -11,23 +9,19 @@ export const bindMindMapEvents = (
   setMindProjectInfo?: (info: MindMapProjectInfo) => void
 ) => {
   graph.on("node:click", ({ cell }) => {
-    console.log("node:click", cell);
   });
 
-  // 双击-新增自由主题
+  // 双击画布空白-新增自由主题
   graph.on("blank:dblclick", (args) => {
-    graph.addNode({
-      ...Topic,
-      x: args.x,
-      y: args.y,
-      width: 104,
-      height: 40,
-      data: {
-        ...topicData,
+    if(setMindProjectInfo) {
+      addTopic(TopicType.branch, setMindProjectInfo, undefined, {
+        x: args.x,
+        y: args.y,
+        setMindProjectInfo,
         type: TopicType.branch,
         label: "自由主题",
         borderSize: BorderSize.medium,
-      },
-    });
+      });
+    }
   });
 };

+ 1 - 1
apps/designer/src/models/graphModel.ts

@@ -10,7 +10,7 @@ import { Keyboard } from '@antv/x6-plugin-keyboard'
 import { Export } from '@antv/x6-plugin-export'
 import { useModel } from 'umi'
 import '@/components/PageContainer'
-import { handleGraphEvent } from '@/events'
+import { handleGraphEvent } from '@/events/flowEvent'
 import { pageMenu, nodeMenu} from '@/utils/contentMenu';
 import { bindKeys } from '@/utils/fastKey'
 export default function GraphModel() {

+ 16 - 6
apps/designer/src/models/mindMapModel.ts

@@ -10,8 +10,9 @@ import { Scroller } from "@antv/x6-plugin-scroller";
 import { MindMapProjectInfo } from "@/types";
 import { bindMindMapEvents } from "@/events/mindMapEvent";
 import { useLocalStorageState } from "ahooks";
-import { renderMindMap } from "@/utils/mindMap";
+import { renderMindMap } from "@/pages/mindmap/mindMap";
 import { defaultProject } from "@/config/data";
+import { TopicType } from "@/enum";
 
 type RightToolbarType =
   | "style"
@@ -43,11 +44,7 @@ export default function mindMapModel() {
   }
 
   useEffect(() => {
-    window.addEventListener("storage", (e) => {
-      console.log('storage change', e)
-    })
-    if(!graph || !mindProjectInfo) return;
-    console.log('准备渲染', mindProjectInfo);
+    if (!graph || !mindProjectInfo) return;
     renderMindMap(graph, setMindProjectInfo);
   }, [mindProjectInfo, graph]);
 
@@ -73,9 +70,22 @@ export default function mindMapModel() {
       width: document.documentElement.clientWidth,
       height: document.documentElement.clientHeight,
       autoResize: true,
+      async: false,
       connecting: {
         connectionPoint: "anchor",
       },
+      interacting: {
+        nodeMovable: (view) => {
+          const data = view.cell.getData<{
+            ignoreDrag: boolean;
+            lock: boolean;
+            type: TopicType;
+            parentId: string;
+          }>();
+          if (data?.ignoreDrag || data?.lock) return false;
+          return data?.type === TopicType.branch && !data?.parentId;
+        },
+      },
     });
 
     instance.use(new Selection());

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

@@ -79,7 +79,7 @@ export default function PageStyle() {
             onChange={(val) => handleSetPageSetting("branchX", val)}
           />
         </div>
-        <div className="text-12px color-#6c7d8f my-4px">子主题</div>
+        {/* <div className="text-12px color-#6c7d8f my-4px">子主题</div>
         <div className="flex justify-between gap-8px">
           <InputNumber
             className="flex-1"
@@ -101,7 +101,7 @@ export default function PageStyle() {
             value={pageSetting?.subTopicX}
             onChange={(val) => handleSetPageSetting("subTopicX", val)}
           />
-        </div>
+        </div> */}
       </section>
       <section className="px-16px my-8px">
         <div>

+ 0 - 0
apps/designer/src/pages/mindmap/components/Config/Structure.tsx


+ 38 - 0
apps/designer/src/pages/mindmap/hierarchy.ts

@@ -0,0 +1,38 @@
+import { StructureType, TopicType } from "@/enum";
+import { MindMapProjectInfo, TopicItem } from "@/types";
+import Hierarchy from "@antv/hierarchy";
+
+// 思维导图结构实现
+export const hierarchyMethodMap: Record<
+  string,
+  (topic: TopicItem, pageSetting: MindMapProjectInfo["pageSetting"]) => any
+> = {
+  // 右侧图
+  [StructureType.right]: <T>(
+    topic: TopicItem,
+    pageSetting: MindMapProjectInfo["pageSetting"]
+  ): T => {
+    return Hierarchy.mindmap(topic, {
+      direction: "H",
+      getHeight(d: TopicItem) {
+        return d.height;
+      },
+      getWidth(d: TopicItem) {
+        return d.width;
+      },
+      getHGap(d: TopicItem) {
+        if(d.type === TopicType.main) return pageSetting.branchX || 20;
+        if(d.type === TopicType.branch) return pageSetting.subTopicX || 20;
+        if(d.type === TopicType.sub) return pageSetting.subTopicX || 20;
+        return pageSetting.branchX || 40;
+      },
+      getVGap() {
+        return pageSetting.branchY || 20;
+      },
+      getSide: () => {
+        return "right";
+      },
+    });
+  },
+
+};

+ 93 - 66
apps/designer/src/utils/mindMap.tsx

@@ -1,11 +1,10 @@
-import { BorderSize, TopicType } from "@/enum";
+import { TopicType } from "@/enum";
 import { MindMapProjectInfo, TopicItem } from "@/types";
 import { Graph, Cell, Node } from "@antv/x6";
 import TopicComponent from "@/components/mindMap/Topic";
-import Hierarchy from "@antv/hierarchy";
 import { topicData } from "@/config/data";
 import { uuid } from "@/utils";
-import { SetState } from "ahooks/lib/useSetState";
+import { hierarchyMethodMap } from "@/pages/mindmap/hierarchy";
 
 interface HierarchyResult {
   id: string;
@@ -28,52 +27,33 @@ export const renderMindMap = (
   const cells: Cell[] = [];
   topics.forEach((topic) => {
     // 遍历出层次结构
-    const result: HierarchyResult = Hierarchy.mindmap(topic, {
-      direction: "H",
-      getHeight(d: TopicItem) {
-        return d.height;
-      },
-      getWidth(d: TopicItem) {
-        return d.width;
-      },
-      getHGap(d: TopicItem) {
-        if (d.type === TopicType.main) return pageSetting.branchX;
-        if (d.type === TopicType.branch) return pageSetting.subTopicX;
-        if (d.type === TopicType.sub) return pageSetting.subTopicX;
-        return 40;
-      },
-      getVGap(d: TopicItem) {
-        if (d.type === TopicType.main) return pageSetting.subTopicY;
-        if (d.type === TopicType.branch) return pageSetting.subTopicY;
-        if (d.type === TopicType.sub) return pageSetting.subTopicY;
-        return 20;
-      },
-      getSide: () => {
-        return "right";
-      },
-    });
+    const result: HierarchyResult = hierarchyMethodMap[projectInfo.mapType]?.(topic, pageSetting);
+    const originPosition = { x: topic?.x ?? -10, y: topic?.y ?? -10 };
+    const offsetX = originPosition.x - result.x;
+    const offsetY = originPosition.y - result.y;
     const traverse = (
-      hierarchyItem: HierarchyResult
+      hierarchyItem: HierarchyResult,
+      parent?: Node
     ) => {
       if (hierarchyItem) {
         const { data, children, x, y } = hierarchyItem;
         const id = data?.id || uuid();
         // 创建主题
-        cells.push(
-          graph.createNode({
-            ...TopicComponent,
-            width: data.width,
-            height: data.height,
-            data: {
-              ...data,
-              // 节点内部执行数据更新方法
-              setMindProjectInfo,
-            },
-            id,
-            x,
-            y,
-          })
-        );
+        const node = graph.createNode({
+          ...TopicComponent,
+          width: data.width,
+          height: data.height,
+          data: {
+            ...data,
+            // 节点内部执行数据更新方法
+            setMindProjectInfo,
+          },
+          id,
+          x: offsetX + x,
+          y: offsetY + y,
+        });
+        cells.push(node);
+        parent && parent.addChild(node);
 
         if (children) {
           children.forEach((item: HierarchyResult) => {
@@ -99,7 +79,7 @@ export const renderMindMap = (
               })
             );
             // 递归遍历
-            traverse(item);
+            traverse(item, node);
           });
         }
       }
@@ -107,8 +87,8 @@ export const renderMindMap = (
     
     traverse(result);
   });
-
-  cells.forEach((cell) => {
+  // 处理节点
+  cells.filter(cell => cell.isNode()).forEach((cell) => {
     // 存在更新位置,否则添加
     if (graph.hasCell(cell.id) && cell.isNode()) {
       const oldCell = graph.getCellById(cell.id);
@@ -117,6 +97,16 @@ export const renderMindMap = (
       graph.addCell(cell);
     }
   });
+  cells.filter(cell => cell.isEdge()).forEach((cell) => {
+    graph.addCell(cell);
+  })
+  const oldCells = graph.getCells();
+  // 移除不存在的节点
+  oldCells.forEach((cell) => {
+    if (!cells.find(item => cell.id === item.id)) {
+      graph.removeCell(cell);
+    }
+  });
   // graph.centerContent();
 };
 
@@ -124,33 +114,41 @@ export const renderMindMap = (
  * 添加分支主题
  */
 export const addTopic = (
-  node: Node,
   type: TopicType,
-  otherData: Record<string, any> = {}
+  setMindProjectInfo: (info: MindMapProjectInfo) => void,
+  node?: Node,
+  otherData: Record<string, any> = {},
 ) => {
   const projectInfo = getMindMapProjectByLocal();
-  if (!projectInfo) return;
+  if (!projectInfo || !setMindProjectInfo) return;
 
-  const topic = buildTopic(type, otherData);
-  const parentData = node.getData();
-  const traverse = (topics: TopicItem[]) => {
-    topics.forEach((item) => {
-      if (item.id === parentData.id) {
+  const topic = buildTopic(type, {
+    ...(otherData || {}),
+    parentId: node?.id
+  });
+
+  if( node) {
+    const parentData = node.getData();
+    const traverse = (topics: TopicItem[]) => {
+      topics.forEach((item) => {
+        if (item.id === parentData?.id) {
+          if (item.children) {
+            item.children?.push(topic);
+          } else {
+            item.children = [topic];
+          }
+        }
         if (item.children) {
-          item.children?.push(topic);
-        } else {
-          item.children = [topic];
+          traverse(item.children);
         }
-      }
-      if (item.children) {
-        traverse(item.children);
-      }
-    });
-  };
-
-  traverse(projectInfo?.topics || []);
+      });
+    };
+    traverse(projectInfo?.topics || []);
+  } else {
+    projectInfo.topics.push(topic);
+  }
 
-  parentData?.setMindProjectInfo(projectInfo);
+  setMindProjectInfo(projectInfo);
 };
 
 const topicMap = {
@@ -202,3 +200,32 @@ export const buildTopic = (
 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<TopicItem>,
+  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);
+}

+ 2 - 1
apps/designer/src/types.d.ts

@@ -1,5 +1,5 @@
 import { Node } from "@antv/x6";
-import { TopicType } from "@/enum";
+import { StructureType, TopicType } from "@/enum";
 import { topicData } from "@/config/data";
 export interface CompoundedComponent {
   name: string;
@@ -63,6 +63,7 @@ export interface MindMapProjectInfo{
   version: string;
   author: string;
   theme: Record<string, any>;
+  mapType: StructureType;
   pageSetting: {
     fillType: 'color' | 'image',
     fill: string,