浏览代码

feat: 添加图片插入

liaojiaxing 6 月之前
父节点
当前提交
3e2523e450

+ 25 - 19
apps/designer/src/components/CustomInput.tsx

@@ -5,7 +5,7 @@ import { useSafeState } from "ahooks";
 export default function CustomInput(props: {
   value: string;
   styles: React.CSSProperties & {
-    textVAlign: 'top' | 'middle' | 'bottom';
+    textVAlign: "top" | "middle" | "bottom";
     bold: boolean;
     italic: boolean;
   };
@@ -17,40 +17,46 @@ export default function CustomInput(props: {
   const { value, styles, node, placeholder, txtStyle } = props;
   const [isEditing, setIsEditing] = useSafeState(false);
   const inputRef = useRef<InputRef>(null);
-  
+
   const style = useMemo(() => {
-    const top = styles.textVAlign === 'top' ? 0 : styles.textVAlign === 'middle' ? '50%' : undefined;
-    const bottom = styles.textVAlign === 'bottom' ? 0 : undefined;
+    const top =
+      styles.textVAlign === "top"
+        ? 0
+        : styles.textVAlign === "middle"
+          ? "50%"
+          : undefined;
+    const bottom = styles.textVAlign === "bottom" ? 0 : undefined;
     return {
       ...styles,
-      fontWeight: styles.bold ? 'bold' : undefined,
-      fontStyle: styles.italic ? 'italic' : undefined,
-      transform: styles.textVAlign === 'middle' ? 'translateY(-50%)' : undefined,
-      minHeight: '12px',
+      fontWeight: styles.bold ? "bold" : undefined,
+      fontStyle: styles.italic ? "italic" : undefined,
+      transform:
+        styles.textVAlign === "middle" ? "translateY(-50%)" : undefined,
+      minHeight: "12px",
       top,
       bottom,
-    }
+    };
   }, [styles]);
 
   const handleChange = (val: string) => {
     node.setData({ label: val });
     props.onChange?.(val);
-  }
+  };
 
   const handleSetEditing = (edit: boolean) => {
-    if(node.data?.lock) {
-      return
+    if (node.data?.lock) {
+      return;
     }
     node.setData({
-      ignoreDrag: edit
-    })
-    if(edit) {
+      ignoreDrag: edit,
+    });
+    if (edit) {
       setTimeout(() => {
-        inputRef.current?.focus({ cursor: 'all' });
-      }, 100)
+        inputRef.current?.focus({ cursor: "all" });
+      }, 100);
     }
     setIsEditing(edit);
-  }
+  };
 
   // useEffect(() => {
   //   // 处理字体加载
@@ -76,7 +82,7 @@ export default function CustomInput(props: {
           onBlur={() => handleSetEditing(false)}
           autoSize
         />
-     ) : (
+      ) : (
         <div
           className="absolute w-full"
           style={style}

+ 63 - 25
apps/designer/src/components/Editor.tsx

@@ -1,4 +1,3 @@
-import { CompoundedComponent } from "@/types";
 import { rosePineDawn } from "thememirror";
 import CodeMirror from "@uiw/react-codemirror";
 
@@ -14,8 +13,7 @@ import { sql } from "@codemirror/lang-sql";
 import { vue } from "@codemirror/lang-vue";
 import { xml } from "@codemirror/lang-xml";
 import { yaml } from "@codemirror/lang-yaml";
-import { Button, message, Select, Tooltip } from "antd";
-import { useState } from "react";
+import { Button, message, Select, Tooltip, Popover } from "antd";
 
 const langMap = {
   javascript: javascript(),
@@ -31,22 +29,35 @@ const langMap = {
   xml: xml(),
   yaml: yaml(),
 };
-export default function Editor({ 
-  code, 
+export default function Editor({
+  code,
   language,
   onChange,
   onLanguageChange,
   width,
-  height
+  height,
+  onDelete
 }: {
   code: string;
-  language: "javascript" | "css" | "go" | "html" | "java" | "php" | "python" | "rust" | "sql" | "vue" | "xml" | "yaml";
+  language:
+    | "javascript"
+    | "css"
+    | "go"
+    | "html"
+    | "java"
+    | "php"
+    | "python"
+    | "rust"
+    | "sql"
+    | "vue"
+    | "xml"
+    | "yaml";
   onChange: (code: string) => void;
   onLanguageChange: (language: string) => void;
   width: number;
   height: number;
+  onDelete?: () => void;
 }) {
-  const [showSetting, setShowSetting] = useState(false);
 
   const languageOptions = [
     { value: "javascript", label: "JavaScript" },
@@ -61,36 +72,63 @@ export default function Editor({
     { value: "typescript", label: "TypeScript" },
     { value: "xml", label: "XML" },
     { value: "yaml", label: "YAML" },
-  ]
+  ];
 
   const handleCopy = () => {
     navigator.clipboard.writeText(code).then(() => {
       message.success("复制成功");
-    })
+    });
+  };
+
+  const handleKeyDown = (e: KeyboardEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+  }
+
+  const handleClick = (e: MouseEvent) => {
+    e.stopPropagation();
+    e.preventDefault();
   }
 
   return (
     <>
-      <div className="relative w-full h-full" autoFocus onFocus={() => setShowSetting(true)}>
-        {
-          showSetting && (
-            <div className="absolute" style={{ left: 0, top: -35, zIndex: 1 }}>
+      <div className="relative w-full h-full">
+        <Popover
+          content={
+            <div className="w-190px flex gap-8px" style={{ left: 0, top: -35, zIndex: 1 }}>
               <Tooltip title="切换代码语言">
-                <Select style={{ width: 120 }} value={language} options={languageOptions} onChange={(l) => onLanguageChange?.(l)}/>
+                <Select
+                  style={{ width: 120 }}
+                  value={language}
+                  options={languageOptions}
+                  onChange={(l) => onLanguageChange?.(l)}
+                />
               </Tooltip>
               <Tooltip title="复制内容">
-                <Button icon={<i className="iconfont icon-fuzhi"/>} onClick={handleCopy}></Button>
+                <Button
+                  icon={<i className="iconfont icon-fuzhi" />}
+                  onClick={handleCopy}
+                ></Button>
+              </Tooltip>
+              <Tooltip title="删除代码">
+                <Button
+                  icon={<i className="iconfont icon-shanchu" />}
+                  onClick={() => onDelete?.()}
+                ></Button>
               </Tooltip>
             </div>
-          )
-        }
-        <CodeMirror
-          value={code}
-          height={height + "px"}
-          width={width + "px"}
-          extensions={[rosePineDawn, langMap[language]]}
-          onChange={onChange}
-        />
+          }
+        >
+          <CodeMirror
+            value={code}
+            height={height + "px"}
+            width={width + "px"}
+            extensions={[rosePineDawn, langMap[language]]}
+            onChange={onChange}
+            onKeyDown={handleKeyDown as any}
+            onClick={handleClick as any}
+          />
+        </Popover>
       </div>
     </>
   );

+ 53 - 0
apps/designer/src/components/ImageModal.tsx

@@ -0,0 +1,53 @@
+import { Button, Form, Input, Modal, FormInstance, FormRule } from "antd";
+import { useRef, useState } from "react";
+
+const ExportComponent = (props: {
+  callback: (url: string) => void;
+  getModal: () => { destroy: () => void };
+}) => {
+  const formRef = useRef<FormInstance>(null);
+  const [value, setValue] = useState("");
+  
+  const rules: FormRule[] = [
+    { required: true, message: "请输入图片链接" },
+    { type: "url", message: "请输入正确的图片链接" },
+    // { validateTrigger: ['change'],validator: (_rule: any, value: string) => {
+    //   if() {
+    //     return Promise.reject(new Error('123'));
+    //   }
+    //   return Promise.resolve();
+    // }}
+  ]
+  const handleChange = async () => {
+    props.callback(value);
+    props.getModal().destroy();
+  };
+
+  return (
+    <>
+      <div className="text-14px color-#666">网络图片</div>
+      <Form ref={formRef} onFinish={handleChange}>
+        <Form.Item name="val" rules={rules}>
+          <div className="flex gap-8px">
+            <Input
+              placeholder="请将图片链接地址粘贴在此处"
+              value={value}
+              onChange={(e) => setValue(e.target.value)}
+            />
+            <Button type="primary" htmlType="submit">插入图片</Button>
+          </div>
+        </Form.Item>
+      </Form>
+    </>
+  );
+};
+export const openInsertImageModal = (callback: (url: string) => void) => {
+  const modal = Modal.info({
+    title: "插入图片",
+    icon: <></>,
+    width: 440,
+    closable: true,
+    footer: <></>,
+    content: <ExportComponent getModal={() => modal} callback={callback} />,
+  });
+};

+ 46 - 36
apps/designer/src/components/mindMap/Border.tsx

@@ -10,34 +10,6 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   const { width, height } = node.size();
   const [showSetting, setShowSetting] = useState(false);
 
-  const path = useMemo(() => {
-    switch (type) {
-      case TopicBorderType.normal:
-        return `
-              M ${0},${0}
-              L ${width},${0}
-              L ${width},${height}
-              L ${0},${height} Z
-            `;
-      case TopicBorderType.rounded:
-        return `
-          M ${0},${line.width / 2}
-          L ${width - line.width / 2},${line.width / 2}
-          A ${line.width / 2},${line.width / 2} 0 0,1 ${width},${
-            height - line.width / 2
-          }
-          L ${line.width / 2},${height - line.width / 2}
-          A ${line.width / 2},${line.width / 2}0 0,1 ${0},${height}
-          L ${0},${0}
-          Z
-      `;
-      case TopicBorderType.trapezoid:
-        return ``;
-      case TopicBorderType.wavy:
-        return ``;
-    }
-  }, [line]);
-
   useEffect(() => {
     const handleSelect = (args: EventArgs["node:selected"]) => {
       setShowSetting(args.node.id === node.id);
@@ -69,6 +41,10 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
     });
   };
 
+  function generateWavePath() {
+    return '';
+  }
+
   const colors = [
     "#bf1e1b",
     "#63abf7",
@@ -185,21 +161,29 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
                     type="text"
                     size="small"
                     icon={<Icon icon="local:rect" width="16px" />}
+                    className={type === TopicBorderType.normal ? "active" : ""}
+                    onClick={() => handleChange("type", TopicBorderType.normal)}
                   ></Button>
                   <Button
                     type="text"
                     size="small"
                     icon={<Icon icon="local:rounded" width="16px" />}
+                    className={type === TopicBorderType.rounded ? "active" : ""}
+                    onClick={() => handleChange("type", TopicBorderType.rounded)}
                   ></Button>
                   <Button
                     type="text"
                     size="small"
                     icon={<Icon icon="local:wavy" width="16px" />}
+                    className={type === TopicBorderType.wavy ? "active" : ""}
+                    onClick={() => handleChange("type", TopicBorderType.wavy)}
                   ></Button>
                   <Button
                     type="text"
                     size="small"
                     icon={<Icon icon="local:trapezoid" width="16px" />}
+                    className={type === TopicBorderType.trapezoid ? "active" : ""}
+                    onClick={() => handleChange("type", TopicBorderType.trapezoid)}
                   ></Button>
                 </div>
               </div>
@@ -217,14 +201,40 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           viewBox={`0 0 ${width} ${height}`}
           xmlns="http://www.w3.org/2000/svg"
         >
-          <path
-            d={path}
-            fill={fill}
-            fillOpacity={0.1}
-            stroke={line.color}
-            strokeDasharray={line.style === "dashed" ? "5,5" : ""}
-            strokeWidth={line.width}
-          />
+          { type === TopicBorderType.normal && (
+            <rect
+              width={width}
+              height={height}
+              fill={fill}
+              fillOpacity={0.1}
+              stroke={line.color}
+              strokeDasharray={line.style === "dashed" ? "5,5" : ""}
+              strokeWidth={line.width}
+            />
+          )}
+          { type === TopicBorderType.rounded && (
+            <rect
+              rx={10}
+              ry={10}
+              width={width}
+              height={height}
+              fill={fill}
+              fillOpacity={0.1}
+              stroke={line.color}
+              strokeDasharray={line.style === "dashed" ? "5,5" : ""}
+              strokeWidth={line.width}
+            />
+          )}
+          { type === TopicBorderType.wavy && (
+            <path
+              d={generateWavePath()}
+              fill={fill}
+              fillOpacity={0.1}
+              stroke={line.color}
+              strokeDasharray={line.style === "dashed" ? "5,5" : ""}
+              strokeWidth={line.width}
+            />
+          )}
         </svg>
       </div>
     </>

+ 84 - 28
apps/designer/src/components/mindMap/ExtraModule.tsx

@@ -1,6 +1,6 @@
-import React from "react";
 import Editor from "@/components/Editor";
 import { Node } from "@antv/x6";
+import { InputNumber, Popover, Tooltip, Button } from "antd";
 export default function ExtraModule({
   node,
   extraModules,
@@ -8,36 +8,92 @@ export default function ExtraModule({
   node: Node;
   extraModules: { type: "image" | "code"; data: Record<string, any> };
 }) {
+  const handleChange = (key: string, value: number) => {
+    node.setData({
+      extraModules: {
+        ...extraModules,
+        data: {
+          ...extraModules.data,
+          [key]: value,
+        },
+      },
+    });
+  };
+  const handleDelete = () => {
+    node.setData({
+      extraModules: undefined,
+    }, {
+      deep: false
+    });
+  };
   return extraModules.type === "code" ? (
-    <Editor
-      width={node.size().width}
-      height={node.size().height / 2}
-      code={extraModules.data.code}
-      language={extraModules.data.language}
-      onChange={(code: string) => {
-        node.setData({
-          extraModules: {
-            type: "code",
-            data: {
-              code,
-              language: extraModules.data.language,
+    <div className="text-14px m-b-8px">
+      <Editor
+        width={200}
+        height={300}
+        code={extraModules.data.code}
+        language={extraModules.data.language}
+        onChange={(code: string) => {
+          node.setData({
+            extraModules: {
+              type: "code",
+              data: {
+                code,
+                language: extraModules.data.language,
+              },
             },
-          },
-        });
-      }}
-      onLanguageChange={(language: string) => {
-        node.setData({
-          extraModules: {
-            type: "code",
-            data: {
-              code: extraModules.data.code,
-              language,
+          });
+        }}
+        onLanguageChange={(language: string) => {
+          node.setData({
+            extraModules: {
+              type: "code",
+              data: {
+                code: extraModules.data.code,
+                language,
+              },
             },
-          },
-        });
-      }}
-    />
+          });
+        }}
+        onDelete={handleDelete}
+      />
+    </div>
   ) : (
-    <img src={extraModules.data.imageUrl} alt="" />
+    <div className="m-b-8px">
+      <Popover
+        content={
+          <div className="bg-#fff rounded-4px flex items-center py-2px px-4px">
+            <span className="text-14px m-r-4px">W</span>
+            <InputNumber
+              className="w-80px m-r-8px"
+              suffix="px"
+              min={100}
+              value={extraModules.data.width}
+              onChange={(val) => handleChange("width", val)}
+            />
+            <span className="text-14px m-r-4px">H</span>
+            <InputNumber
+              className="w-80px"
+              suffix="px"
+              min={100}
+              value={extraModules.data.height}
+              onChange={(val) => handleChange("height", val)}
+            />
+            <Tooltip title="删除代码">
+              <Button
+                icon={<i className="iconfont icon-shanchu" />}
+                onClick={handleDelete}
+              ></Button>
+            </Tooltip>
+          </div>
+        }
+      >
+        <img
+          src={extraModules.data.imageUrl}
+          width={extraModules.data.width}
+          height={extraModules.data.height}
+        />
+      </Popover>
+    </div>
   );
 }

+ 159 - 31
apps/designer/src/components/mindMap/SummaryBorder.tsx

@@ -21,10 +21,137 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
     };
   }, []);
 
-  const path = useMemo(() => {
-    switch (type) {
-      case 1:
-        return `
+  const linePosition = useMemo((): "left" | "right" | "top" | "bottom" => {
+    const originNode = graph.getCellById(origin);
+    if (originNode && originNode.isNode()) {
+      const x = originNode.position().x + originNode.size().width / 2;
+      const y = originNode.position().y + originNode.size().height / 2;
+      const { width, height } = node.size();
+      if (
+        x < node.position().x 
+        && y > node.position().y
+        && y < node.position().y + height
+      ) {
+        return "left";
+      }
+      if (
+        y > node.position().y
+        && x > node.position().x
+        && x < node.position().x + width
+      ) {
+        return "bottom";
+      }
+      if (
+        y < node.position().y
+        && x > node.position().x
+        && x < node.position().x + width
+      ) {
+        return "top";
+      }
+    }
+    return "right";
+  }, [origin]);
+
+  const lineStyle = useMemo(() => {
+    console.log(linePosition)
+    switch (linePosition) {
+      case "right":
+        return {
+          right: "-40px",
+        };
+      case "left":
+        return {
+          left: "-40px",
+        };
+      case "top":
+        return {
+          top: "-40px",
+        };
+      default:
+        return {
+          bottom: "-40px",
+        };
+    }
+  }, [linePosition]);
+
+  const rightLine = useMemo(() => {
+    if (type === 2) {
+      return `
+        M 20 ${line.width}
+        L 36 ${height / 2}
+        L 20 ${height - line.width}
+      `;
+    }
+    if (type === 3) {
+      return `
+        M 20 ${line.width}
+        A 10 ${height / 2} 0 0 1 20 ${height - line.width}
+        M 30 ${height / 2}
+        L 40 ${height / 2}
+        `;
+    }
+    if (type === 4) {
+      return `
+        M 20 ${line.width}
+        Q 30 ${line.width} 30 10
+        L 30 ${height / 2 - 10}
+        Q 30 ${height / 2} 40 ${height / 2}
+        Q 30 ${height / 2} 30 ${height / 2 + 10}
+        L 30 ${height - 10}
+        Q 30 ${height - line.width} 20 ${height - line.width}
+        `;
+    }
+    return `
+      M 20 ${line.width}
+      L 30 ${line.width}
+      L 30 ${height / 2}
+      L 40 ${height / 2}
+      L 30 ${height / 2}
+      L 30 ${height - line.width}
+      L 20 ${height - line.width}
+      `;
+  }, [type]);
+
+  const leftLine = useMemo(() => {
+    if (type === 2) {
+      return `
+        M 20 ${line.width}
+        L 4 ${height / 2}
+        L 20 ${height - line.width}
+      `;
+    }
+    if (type === 3) {
+      return `
+        M 20 ${line.width}
+        A 10 ${height / 2} 0 0 0 20 ${height - line.width}
+        M 10 ${height / 2}
+        L 0 ${height / 2}
+        `;
+    }
+    if (type === 4) {
+      return `
+        M 20 ${line.width}
+        Q 10 ${line.width} 10 10
+        L 10 ${height / 2 - 10}
+        Q 10 ${height / 2} 0 ${height / 2}
+        Q 10 ${height / 2} 10 ${height / 2 + 10}
+        L 10 ${height - 10}
+        Q 10 ${height - line.width} 20 ${height - line.width}
+        `;
+    }
+    return `
+      M 20 ${line.width}
+      L 10 ${line.width}
+      L 10 ${height / 2}
+      L 00 ${height / 2}
+      L 10 ${height / 2}
+      L 10 ${height - line.width}
+      L 20 ${height - line.width}
+      `;
+  }, [type]);
+
+  const topLine = useMemo(() => {
+    return `
               M 20 ${line.width}
               L 30 ${line.width}
               L 30 ${height / 2}
@@ -33,29 +160,26 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
               L 30 ${height - line.width}
               L 20 ${height - line.width}
               `;
-      case 2:
-        return `
-        M 20 ${line.width}
-        L 38 ${height / 2}
-        L 20 ${height - line.width}
-      `;
-      case 3:
-        return `
-          M 20 ${line.width}
-          A 10 ${height / 2} 0 0 1 20 ${height - line.width}
-          M 30 ${height / 2}
-          L 40 ${height / 2}
-        `;
-      case 4:
-        return `
-        M 20 ${line.width}
-              Q 30 ${line.width} 30 10
-              L 30 ${height / 2 - 10}
-              Q 30 ${height / 2} 40 ${height / 2}
-              Q 30 ${height / 2} 30 ${height / 2 + 10}
-              L 30 ${height - 10}
-              Q 30 ${height - line.width} 20 ${height - line.width}
-        `;
+  }, [type]);
+
+  const bottomLine = useMemo(() => {
+    return `
+              M 20 ${line.width}
+              L 30 ${line.width}
+              L 30 ${height / 2}
+              L 40 ${height / 2}
+              L 30 ${height / 2}
+              L 30 ${height - line.width}
+              L 20 ${height - line.width}
+              `;
+  }, [type]);
+
+  const path = useMemo(() => {
+    switch(linePosition) {
+      case 'bottom': return bottomLine;
+      case 'top': return topLine;
+      case 'left': return leftLine;
+      case 'right': return rightLine;
     }
   }, [type]);
 
@@ -151,7 +275,8 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
         )}
 
         <svg
-          className="absolute right--40px w-40px h-full"
+          className="absolute w-40px h-full"
+          style={lineStyle}
           viewBox={`0 0 ${40} ${height}`}
           xmlns="http://www.w3.org/2000/svg"
         >
@@ -238,8 +363,11 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
             }
           >
             <i
-              className="z-9 iconfont icon-more text-12px absolute right--35px top-25% cursor-pointer"
-              style={{ color: line.color || "#000" }}
+              className="z-9 iconfont icon-more text-12px absolute top-25% cursor-pointer"
+              style={{ 
+                color: line.color || "#000",
+                [linePosition]: '-36px'
+              }}
             />
           </Popover>
         )}
@@ -260,7 +388,7 @@ export default {
   data: {
     line: {
       width: 2,
-      color: "#939aa8",
+      color: "#63abf7",
     },
     type: 1,
   },

+ 42 - 34
apps/designer/src/components/mindMap/Topic.tsx

@@ -36,7 +36,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
     linkTopicId,
   } = node.getData();
   const { size, ref } = useSizeHook();
-  const { fillContent, strokeColor, strokeWidth, strokeDasharray } =
+  const { fillContent, strokeColor, strokeWidth } =
     useShapeProps(fill, size, stroke);
   const [selected, setSelected] = useState(false);
   const [showPopover, setShowPopover] = useState(false);
@@ -54,11 +54,20 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   const padding = useMemo(() => {
     switch (type) {
       case TopicType.main:
-        return "14px 28px";
+        return {
+          y: 14,
+          x: 28,
+        };
       case TopicType.branch:
-        return "8px 16px";
+        return {
+          y: 8,
+          x: 16,
+        };
       default:
-        return "4px 6px";
+        return {
+          y: 4,
+          x: 6,
+        };
     }
   }, [type]);
 
@@ -95,28 +104,21 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
     };
   }, []);
 
-  // useEffect(() => {
-  //   if (size.height && size.width) {
-  //     const w = Math.max(
-  //       size.width,
-  //       titleRef.current?.clientWidth || 0,
-  //       tagRef.current?.clientWidth || 0,
-  //       extraModuleRef.current?.clientWidth || 0
-  //     );
-  //     const h = Math.max(
-  //       size.height,
-  //       titleRef.current?.clientHeight || 0,
-  //       tagRef.current?.clientHeight || 0,
-  //       extraModuleRef.current?.clientHeight || 0
-  //     );
-  //     if (h !== node.size().height || w !== node.size().width) {
-  //       node.setData({
-  //         width: w,
-  //       });
-  //       node.size(w, h);
-  //     }
-  //   }
-  // }, [size, titleRef, tagRef, extraModuleRef]);
+  useEffect(() => {
+    let timer = setInterval(() => {
+      const { clientHeight = 0, clientWidth = 0 } = ref.current || {};
+      if (clientHeight && (size.width !== clientWidth || size.height !== clientHeight)) {
+        node.setData({
+          width: clientWidth,
+          height: clientHeight,
+        });
+      }
+    }, 1000);
+
+    return () => {
+      clearInterval(timer);
+    };
+  }, []);
 
   const handleResize = () => {
     node.setData({
@@ -165,11 +167,14 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   };
 
   const handleDeleteHeft = () => {
-    node.setData({
-      href: undefined,
-    }, {
-      deep: false
-    });
+    node.setData(
+      {
+        href: undefined,
+      },
+      {
+        deep: false,
+      }
+    );
   };
 
   return (
@@ -192,12 +197,13 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           ref={ref}
           style={{
             minWidth: "100%",
+            // width: "fit-content",
             // minHeight: "100%",
             opacity: opacity / 100,
             border: `solid ${strokeWidth}px ${strokeColor}`,
             background: fillContent,
             borderRadius: borderSize,
-            padding,
+            padding: `${padding.y}px ${padding.x}px`,
           }}
           onMouseOver={() => !collapsed && setShowCollapsePoint(true)}
           onMouseLeave={() => !collapsed && setShowCollapsePoint(false)}
@@ -236,7 +242,9 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
                 flexShrink: 0,
                 transform: "translateY(50%)",
                 width: "auto",
-                ...(fixedWidth ? { maxWidth: "100%" } : {}),
+                ...(fixedWidth
+                  ? { maxWidth: `calc(100% - ${2 * padding.x}px)` }
+                  : {}),
               }}
             />
             <div
@@ -269,7 +277,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
             </div>
           </div>
           {/* 标签 */}
-          <div className="flex items-center justify-center" ref={titleRef}>
+          <div className="flex items-center justify-center" ref={tagRef}>
             {tags?.map((item: { name: string; color: string }) => {
               return (
                 <CustomTag

+ 8 - 0
apps/designer/src/events/mindMapEvent.ts

@@ -227,6 +227,14 @@ export const bindMindMapEvents = (
             setMindProjectInfo
           );
       }
+      if(current?.extraModules !== previous?.extraModules) {
+        setMindProjectInfo &&
+          updateTopic(
+            args.cell.id,
+            { extraModules: current.extraModules },
+            setMindProjectInfo
+          );
+      }
       // 本地缓存更新不会重新渲染
       if(args.cell.id.includes('-border')) {
         updateTopic(args.current.origin, {border: current}, (info) => {

+ 5 - 9
apps/designer/src/models/mindMapModel.ts

@@ -49,6 +49,7 @@ export default function mindMapModel() {
     setMindProjectInfo(defaultProject);
   }
 
+  const flagRef = useRef(false);
   useEffect(() => {
     if (!graph || !mindProjectInfo) return;
     renderMindMap({
@@ -59,6 +60,10 @@ export default function mindMapModel() {
       theme: mindProjectInfo?.theme,
       topics: mindProjectInfo?.topics,
     });
+    if(!flagRef.current) {
+      flagRef.current = true;
+      graph.centerContent();
+    }
     localStorage.setItem('minMapProjectInfo', JSON.stringify(mindProjectInfo))
   }, [mindProjectInfo, graph]);
 
@@ -197,15 +202,6 @@ export default function mindMapModel() {
     }
 
     setGraph(instance);
-    mindProjectInfo && renderMindMap({
-      graph: instance,
-      setMindProjectInfo,
-      pageSetting: mindProjectInfo?.pageSetting,
-      structure: mindProjectInfo?.structure,
-      theme: mindProjectInfo?.theme,
-      topics: mindProjectInfo?.topics,
-    });
-    instance.centerContent();
   };
 
   const handleBrushClick = (args: EventArgs & { cell: Cell }) => {

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

@@ -9,10 +9,15 @@ import {
   TagOutlined,
 } from "@ant-design/icons";
 import { useModel } from "umi";
+import { insertImage } from "@/utils/mindmapHander";
 export default function index() {
   const { rightToobarActive, rightToolbarActive, selectedCell } =
     useModel("mindMapModel");
 
+  const handleAddImage = () => {
+    insertImage(selectedCell.find(node => node.isNode()));
+  }
+
   return (
     <div className="absolute top-8px right-8px bg-white shadow-md rounded-4px flex flex-col p-8px">
       <Tooltip placement="bottom" title="样式">
@@ -54,6 +59,7 @@ export default function index() {
           type="text"
           disabled={!selectedCell.length}
           icon={<FileImageOutlined />}
+          onClick={handleAddImage}
         />
       </Tooltip>
       <Tooltip placement="bottom" title="标签">

+ 4 - 0
apps/designer/src/pages/mindmap/edge.ts

@@ -529,6 +529,7 @@ const getSourceAnchor = (
           };
     }
     case StructureType.leftRight: {
+      return options?.direction === 'left' ? 'left' : 'right'
     }
     case StructureType.tree: {
       return type === TopicType.branch
@@ -609,6 +610,9 @@ const getTargetAnchor = (
         name: "left",
       };
     }
+    case StructureType.leftRight: {
+      return options?.direction === 'left' ? 'right' : 'left'
+    }
     case StructureType.tree: {
       return type === TopicType.branch
         ? {

+ 15 - 3
apps/designer/src/pages/mindmap/hierarchy.ts

@@ -7,6 +7,7 @@ import { getRightFishboneHierarchy } from "@/utils/hierarchy/rightFishbone";
 import { getLeftFishboneHierarchy } from "@/utils/hierarchy/leftFishbone";
 import { getTimeHierarchy } from "@/utils/hierarchy/time";
 import { getTreeHierarchy } from "@/utils/hierarchy/tree";
+import { traverseNode } from "@/utils/mindmapHander";
 
 // 思维导图结构实现
 export const hierarchyMethodMap: Record<
@@ -18,7 +19,7 @@ export const hierarchyMethodMap: Record<
     topic: TopicItem,
     pageSetting: MindMapProjectInfo["pageSetting"]
   ): T => {
-    return Hierarchy.mindmap(topic, {
+    const result = Hierarchy.mindmap(topic, {
       direction: "H",
       getHeight(d: TopicItem) {
         return d.height;
@@ -39,13 +40,18 @@ export const hierarchyMethodMap: Record<
         return "right";
       },
     });
+    traverseNode(result.children, (topic) => {
+      // @ts-ignore
+      topic.direction = 'right'
+    });
+    return result;
   },
   // 左侧图
   [StructureType.left]: <T>(
     topic: TopicItem,
     pageSetting: MindMapProjectInfo["pageSetting"]
   ): T => {
-    return Hierarchy.mindmap(topic, {
+    const result = Hierarchy.mindmap(topic, {
       direction: "H",
       getHeight(d: TopicItem) {
         return d.height;
@@ -66,6 +72,12 @@ export const hierarchyMethodMap: Record<
         return "left";
       },
     });
+
+    traverseNode(result.children, (topic) => {
+      // @ts-ignore
+      topic.direction = 'left'
+    });
+    return result;
   },
   // 左右分布
   [StructureType.leftRight]: <T>(
@@ -73,7 +85,7 @@ export const hierarchyMethodMap: Record<
     pageSetting: MindMapProjectInfo["pageSetting"]
   ): T => {
     if (topic.type !== TopicType.main) {
-      return hierarchyMethodMap[StructureType.left](topic, pageSetting);
+      return hierarchyMethodMap[StructureType.right](topic, pageSetting);
     } else {
       const rightChildren = (topic.children || []).filter(
         (item, index) => index % 2 === 0

+ 17 - 2
apps/designer/src/pages/mindmap/mindMap.tsx

@@ -118,6 +118,7 @@ export const renderMindMap = ({
             if (!isBracket || index === 0 || index === children.length - 1) {
               const edge = createEdge(graph, id, item, structure, theme, {
                 onlyOneChild: children.length === 1,
+                direction: item?.direction
               });
               cells.push(edge);
               node.addChild(edge);
@@ -196,12 +197,26 @@ const createSummaryCells = (
     });
     cells.push(node);
 
+    let position = {
+      x: offsetX + hierarchyItem.x + totalWidth + 40,
+      y: offsetY + hierarchyItem.y
+    }
+
+    if(
+      [StructureType.left, StructureType.leftBracket, StructureType.leftFishbone, StructureType.leftTreeShape].includes(structure)
+      || structure === StructureType.leftRight && hierarchyItem.direction === 'left'
+    ) {
+      position = {
+        x: offsetX + hierarchyItem.x - 40 - totalWidth,
+        y: offsetY + hierarchyItem.y
+      }
+    }
+
     // 概要节点
     cells.push(...renderMindMap({
       topics: [{
         ...summary.topic,
-        x: offsetX + hierarchyItem.x + totalWidth + 40,
-        y: offsetY + hierarchyItem.y
+        ...position
       }],
       pageSetting,
       structure,

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

@@ -56,6 +56,7 @@ export interface HierarchyResult {
   children: HierarchyResult[];
   totalHeight: number;
   totalWidth: number;
+  direction?: string;
 }
 
 export type TopicItem = {

+ 24 - 2
apps/designer/src/utils/mindmapHander.tsx

@@ -9,8 +9,8 @@ import { ContextMenuTool } from "./contentMenu";
 import { MutableRefObject } from "react";
 import { exportImage } from "@/components/ExportImage";
 import TopicBorder from "@/components/mindMap/Border";
-import { topicData } from "@/config/data";
 import SummaryBorder from "@/components/mindMap/SummaryBorder";
+import { openInsertImageModal } from "@/components/ImageModal";
 
 export const selectTopic = (graph: Graph, topic?: TopicItem) => {
   if (topic?.id) {
@@ -435,6 +435,7 @@ export const getBorderPositionAndSize = (hierarchyItem: HierarchyResult) => {
     totalWidth = position.maxX - position.minX;
   } else {
     totalWidth = hierarchyItem.data.width;
+    totalHeigth = hierarchyItem.data.height;
   }
   return {
     x: x - 10,
@@ -465,7 +466,7 @@ export const addSummary = (nodes: Node[]) => {
         borderSize: BorderSize.medium,
         isSummary: true,
         summarySource: node.id,
-      })
+      });
       node.setData({
         summary: {
           topic: root,
@@ -483,6 +484,27 @@ export const addSummary = (nodes: Node[]) => {
   });
 }
 
+/**
+ * 插入图片
+ */
+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
+        }
+      }
+    })
+  })
+}
+
 /**
  * 右键菜单处理方法
  */