Jelajahi Sumber

feat: 添加主题、图标、标签配置

liaojiaxing 6 bulan lalu
induk
melakukan
4dabb5ebae
78 mengubah file dengan 4910 tambahan dan 316 penghapusan
  1. 2 1
      apps/designer/.umirc.ts
  2. 24 0
      apps/designer/src/assets/mindmap/artistic_song.svg
  3. 27 0
      apps/designer/src/assets/mindmap/basic4.svg
  4. 26 0
      apps/designer/src/assets/mindmap/basic5.svg
  5. 27 0
      apps/designer/src/assets/mindmap/bd_caihong.svg
  6. 26 0
      apps/designer/src/assets/mindmap/bg_caihong.svg
  7. 51 0
      apps/designer/src/assets/mindmap/bg_caihong2.svg
  8. 29 0
      apps/designer/src/assets/mindmap/black.svg
  9. 21 0
      apps/designer/src/assets/mindmap/black_and_white1.svg
  10. 23 0
      apps/designer/src/assets/mindmap/black_and_white2.svg
  11. 22 0
      apps/designer/src/assets/mindmap/caihong_2.svg
  12. 23 0
      apps/designer/src/assets/mindmap/ch-dark.svg
  13. 24 0
      apps/designer/src/assets/mindmap/cool_sports1.svg
  14. 23 0
      apps/designer/src/assets/mindmap/cool_sports2.svg
  15. 24 0
      apps/designer/src/assets/mindmap/cool_sports3.svg
  16. 24 0
      apps/designer/src/assets/mindmap/cool_sports4.svg
  17. 26 0
      apps/designer/src/assets/mindmap/cool_sports5.svg
  18. 25 0
      apps/designer/src/assets/mindmap/delicate_caihong.svg
  19. 25 0
      apps/designer/src/assets/mindmap/delicate_dark.svg
  20. 25 0
      apps/designer/src/assets/mindmap/eyehelp1.svg
  21. 21 0
      apps/designer/src/assets/mindmap/eyehelp2.svg
  22. 24 0
      apps/designer/src/assets/mindmap/eyehelp4.svg
  23. 26 0
      apps/designer/src/assets/mindmap/eyehelp5.svg
  24. 25 0
      apps/designer/src/assets/mindmap/gsx_notes_color.svg
  25. 23 0
      apps/designer/src/assets/mindmap/gsx_notes_red.svg
  26. 24 0
      apps/designer/src/assets/mindmap/gsx_notes_white.svg
  27. 26 0
      apps/designer/src/assets/mindmap/gsx_notes_yellow.svg
  28. 29 0
      apps/designer/src/assets/mindmap/jbxt1.svg
  29. 37 0
      apps/designer/src/assets/mindmap/lively_caihong.svg
  30. 34 0
      apps/designer/src/assets/mindmap/mindmap1.svg
  31. 34 0
      apps/designer/src/assets/mindmap/mindmap_dark.svg
  32. 35 0
      apps/designer/src/assets/mindmap/pop_contrast1.svg
  33. 35 0
      apps/designer/src/assets/mindmap/pop_contrast2.svg
  34. 36 0
      apps/designer/src/assets/mindmap/pop_contrast4.svg
  35. 36 0
      apps/designer/src/assets/mindmap/pop_contrast6.svg
  36. 42 0
      apps/designer/src/assets/mindmap/red.svg
  37. 36 0
      apps/designer/src/assets/mindmap/shangwu2.svg
  38. 34 0
      apps/designer/src/assets/mindmap/shangwu3.svg
  39. 34 0
      apps/designer/src/assets/mindmap/shangwu4.svg
  40. 31 0
      apps/designer/src/assets/mindmap/shangwu6.svg
  41. 48 0
      apps/designer/src/assets/mindmap/shangwu_SVIP.svg
  42. 37 0
      apps/designer/src/assets/mindmap/simple1.svg
  43. 34 0
      apps/designer/src/assets/mindmap/simple2.svg
  44. 35 0
      apps/designer/src/assets/mindmap/simple_line3.svg
  45. 35 0
      apps/designer/src/assets/mindmap/simple_line5.svg
  46. 34 0
      apps/designer/src/assets/mindmap/simple_line6.svg
  47. 101 0
      apps/designer/src/assets/mindmap/theme3_2.svg
  48. 34 0
      apps/designer/src/assets/mindmap/xuanku.svg
  49. 5 2
      apps/designer/src/components/CustomColorPicker.tsx
  50. 8 1
      apps/designer/src/components/CustomInput.tsx
  51. 10 0
      apps/designer/src/components/CustomTag.tsx
  52. 97 0
      apps/designer/src/components/Editor.tsx
  53. 43 0
      apps/designer/src/components/mindMap/ExtraModule.tsx
  54. 21 0
      apps/designer/src/components/mindMap/Link.tsx
  55. 95 11
      apps/designer/src/components/mindMap/Topic.tsx
  56. 8 58
      apps/designer/src/config/data.ts
  57. 2 0
      apps/designer/src/enum/index.ts
  58. 11 6
      apps/designer/src/events/mindMapEvent.ts
  59. 7 0
      apps/designer/src/global.less
  60. 6 0
      apps/designer/src/icons/round-big.svg
  61. 6 0
      apps/designer/src/icons/round-rect.svg
  62. 6 0
      apps/designer/src/icons/round-small.svg
  63. 3 1
      apps/designer/src/models/mindMapModel.ts
  64. 383 0
      apps/designer/src/pages/mindmap/components/Config/IconConfig.tsx
  65. 93 205
      apps/designer/src/pages/mindmap/components/Config/NodeStyle.tsx
  66. 218 0
      apps/designer/src/pages/mindmap/components/Config/Structure.tsx
  67. 134 0
      apps/designer/src/pages/mindmap/components/Config/TagConfig.tsx
  68. 56 0
      apps/designer/src/pages/mindmap/components/Config/Theme.tsx
  69. 20 2
      apps/designer/src/pages/mindmap/components/Config/index.tsx
  70. 38 13
      apps/designer/src/pages/mindmap/components/HeaderToolbar/index.tsx
  71. 5 2
      apps/designer/src/pages/mindmap/components/RightToolbar/index.tsx
  72. 1737 0
      apps/designer/src/pages/mindmap/components/SvgComponent.tsx
  73. 2 0
      apps/designer/src/pages/mindmap/index.tsx
  74. 47 12
      apps/designer/src/pages/mindmap/mindMap.tsx
  75. 52 0
      apps/designer/src/pages/mindmap/theme.ts
  76. 66 2
      apps/designer/src/types.d.ts
  77. 44 0
      apps/designer/src/utils/color.ts
  78. 210 0
      apps/designer/src/utils/icons.ts

+ 2 - 1
apps/designer/.umirc.ts

@@ -6,7 +6,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4676747_e2thfj9vjkt.css'
+    '//at.alicdn.com/t/c/font_4676747_tigpugzo8xc.css'
   ],
   metas: [
     { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
@@ -14,6 +14,7 @@ export default defineConfig({
   scripts: [
     // 字体加载
     // '//ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js'
+    '//at.alicdn.com/t/c/font_4676747_tigpugzo8xc.js'
   ],
   plugins: [
     require.resolve('@umijs/plugins/dist/unocss'),

File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/artistic_song.svg


File diff ditekan karena terlalu besar
+ 27 - 0
apps/designer/src/assets/mindmap/basic4.svg


File diff ditekan karena terlalu besar
+ 26 - 0
apps/designer/src/assets/mindmap/basic5.svg


File diff ditekan karena terlalu besar
+ 27 - 0
apps/designer/src/assets/mindmap/bd_caihong.svg


File diff ditekan karena terlalu besar
+ 26 - 0
apps/designer/src/assets/mindmap/bg_caihong.svg


File diff ditekan karena terlalu besar
+ 51 - 0
apps/designer/src/assets/mindmap/bg_caihong2.svg


File diff ditekan karena terlalu besar
+ 29 - 0
apps/designer/src/assets/mindmap/black.svg


File diff ditekan karena terlalu besar
+ 21 - 0
apps/designer/src/assets/mindmap/black_and_white1.svg


File diff ditekan karena terlalu besar
+ 23 - 0
apps/designer/src/assets/mindmap/black_and_white2.svg


File diff ditekan karena terlalu besar
+ 22 - 0
apps/designer/src/assets/mindmap/caihong_2.svg


File diff ditekan karena terlalu besar
+ 23 - 0
apps/designer/src/assets/mindmap/ch-dark.svg


File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/cool_sports1.svg


File diff ditekan karena terlalu besar
+ 23 - 0
apps/designer/src/assets/mindmap/cool_sports2.svg


File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/cool_sports3.svg


File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/cool_sports4.svg


File diff ditekan karena terlalu besar
+ 26 - 0
apps/designer/src/assets/mindmap/cool_sports5.svg


File diff ditekan karena terlalu besar
+ 25 - 0
apps/designer/src/assets/mindmap/delicate_caihong.svg


File diff ditekan karena terlalu besar
+ 25 - 0
apps/designer/src/assets/mindmap/delicate_dark.svg


File diff ditekan karena terlalu besar
+ 25 - 0
apps/designer/src/assets/mindmap/eyehelp1.svg


File diff ditekan karena terlalu besar
+ 21 - 0
apps/designer/src/assets/mindmap/eyehelp2.svg


File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/eyehelp4.svg


File diff ditekan karena terlalu besar
+ 26 - 0
apps/designer/src/assets/mindmap/eyehelp5.svg


File diff ditekan karena terlalu besar
+ 25 - 0
apps/designer/src/assets/mindmap/gsx_notes_color.svg


File diff ditekan karena terlalu besar
+ 23 - 0
apps/designer/src/assets/mindmap/gsx_notes_red.svg


File diff ditekan karena terlalu besar
+ 24 - 0
apps/designer/src/assets/mindmap/gsx_notes_white.svg


File diff ditekan karena terlalu besar
+ 26 - 0
apps/designer/src/assets/mindmap/gsx_notes_yellow.svg


File diff ditekan karena terlalu besar
+ 29 - 0
apps/designer/src/assets/mindmap/jbxt1.svg


File diff ditekan karena terlalu besar
+ 37 - 0
apps/designer/src/assets/mindmap/lively_caihong.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/mindmap1.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/mindmap_dark.svg


File diff ditekan karena terlalu besar
+ 35 - 0
apps/designer/src/assets/mindmap/pop_contrast1.svg


File diff ditekan karena terlalu besar
+ 35 - 0
apps/designer/src/assets/mindmap/pop_contrast2.svg


File diff ditekan karena terlalu besar
+ 36 - 0
apps/designer/src/assets/mindmap/pop_contrast4.svg


File diff ditekan karena terlalu besar
+ 36 - 0
apps/designer/src/assets/mindmap/pop_contrast6.svg


File diff ditekan karena terlalu besar
+ 42 - 0
apps/designer/src/assets/mindmap/red.svg


File diff ditekan karena terlalu besar
+ 36 - 0
apps/designer/src/assets/mindmap/shangwu2.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/shangwu3.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/shangwu4.svg


File diff ditekan karena terlalu besar
+ 31 - 0
apps/designer/src/assets/mindmap/shangwu6.svg


File diff ditekan karena terlalu besar
+ 48 - 0
apps/designer/src/assets/mindmap/shangwu_SVIP.svg


File diff ditekan karena terlalu besar
+ 37 - 0
apps/designer/src/assets/mindmap/simple1.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/simple2.svg


File diff ditekan karena terlalu besar
+ 35 - 0
apps/designer/src/assets/mindmap/simple_line3.svg


File diff ditekan karena terlalu besar
+ 35 - 0
apps/designer/src/assets/mindmap/simple_line5.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/simple_line6.svg


File diff ditekan karena terlalu besar
+ 101 - 0
apps/designer/src/assets/mindmap/theme3_2.svg


File diff ditekan karena terlalu besar
+ 34 - 0
apps/designer/src/assets/mindmap/xuanku.svg


+ 5 - 2
apps/designer/src/components/CustomColorPicker.tsx

@@ -312,7 +312,7 @@ export default function CustomColorPicker(props: {
   };
 
   const handleOpenChange = (newOpen: boolean) => {
-    setOpen(newOpen);
+    setOpen(newOpen && !props.disabled);
   };
 
   const PopoverContent = () => {
@@ -475,7 +475,10 @@ export default function CustomColorPicker(props: {
         ) : (
           <div
             className="w-30px h-24px border border-solid border-#435f7329 border-radius-2px"
-            style={backgroundStyle}
+            style={{
+              ...backgroundStyle,
+              opacity: props.disabled ? 0.5 : 1
+            }}
           />
         )}
       </Popover>

+ 8 - 1
apps/designer/src/components/CustomInput.tsx

@@ -41,6 +41,14 @@ export default function CustomInput(props: {
     if(node.data?.lock) {
       return
     }
+    node.setData({
+      ignoreDrag: edit
+    })
+    if(edit) {
+      setTimeout(() => {
+        inputRef.current?.focus({ cursor: 'all' });
+      }, 100)
+    }
     setIsEditing(edit);
   }
 
@@ -66,7 +74,6 @@ export default function CustomInput(props: {
           style={style}
           onChange={(e) => handleChange(e.target.value)}
           onBlur={() => handleSetEditing(false)}
-          autoFocus
           autoSize
         />
      ) : (

+ 10 - 0
apps/designer/src/components/CustomTag.tsx

@@ -0,0 +1,10 @@
+import { Tag, TagProps } from 'antd'
+import React from 'react'
+
+export default function CustomTag(props: TagProps) {
+  return (
+    <div className='inline'>
+      <Tag {...props}/>
+    </div>
+  )
+}

+ 97 - 0
apps/designer/src/components/Editor.tsx

@@ -0,0 +1,97 @@
+import { CompoundedComponent } from "@/types";
+import { rosePineDawn } from "thememirror";
+import CodeMirror from "@uiw/react-codemirror";
+
+import { javascript } from "@codemirror/lang-javascript";
+import { css } from "@codemirror/lang-css";
+import { go } from "@codemirror/lang-go";
+import { html } from "@codemirror/lang-html";
+import { java } from "@codemirror/lang-java";
+import { php } from "@codemirror/lang-php";
+import { python } from "@codemirror/lang-python";
+import { rust } from "@codemirror/lang-rust";
+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";
+
+const langMap = {
+  javascript: javascript(),
+  css: css(),
+  go: go(),
+  html: html(),
+  java: java(),
+  php: php(),
+  python: python(),
+  rust: rust(),
+  sql: sql(),
+  vue: vue(),
+  xml: xml(),
+  yaml: yaml(),
+};
+export default function Editor({ 
+  code, 
+  language,
+  onChange,
+  onLanguageChange,
+  width,
+  height
+}: {
+  code: string;
+  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;
+}) {
+  const [showSetting, setShowSetting] = useState(false);
+
+  const languageOptions = [
+    { value: "javascript", label: "JavaScript" },
+    { value: "css", label: "CSS" },
+    { value: "go", label: "Go" },
+    { value: "html", label: "HTML" },
+    { value: "java", label: "Java" },
+    { value: "php", label: "PHP" },
+    { value: "python", label: "Python" },
+    { value: "rust", label: "Rust" },
+    { value: "sql", label: "SQL" },
+    { value: "typescript", label: "TypeScript" },
+    { value: "xml", label: "XML" },
+    { value: "yaml", label: "YAML" },
+  ]
+
+  const handleCopy = () => {
+    navigator.clipboard.writeText(code).then(() => {
+      message.success("复制成功");
+    })
+  }
+
+  return (
+    <>
+      <div className="relative w-full h-full" autoFocus onFocus={() => setShowSetting(true)}>
+        {
+          showSetting && (
+            <div className="absolute" style={{ left: 0, top: -35, zIndex: 1 }}>
+              <Tooltip title="切换代码语言">
+                <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>
+              </Tooltip>
+            </div>
+          )
+        }
+        <CodeMirror
+          value={code}
+          height={height + "px"}
+          width={width + "px"}
+          extensions={[rosePineDawn, langMap[language]]}
+          onChange={onChange}
+        />
+      </div>
+    </>
+  );
+}

+ 43 - 0
apps/designer/src/components/mindMap/ExtraModule.tsx

@@ -0,0 +1,43 @@
+import React from "react";
+import Editor from "@/components/Editor";
+import { Node } from "@antv/x6";
+export default function ExtraModule({
+  node,
+  extraModules,
+}: {
+  node: Node;
+  extraModules: { type: "image" | "code"; data: Record<string, any> };
+}) {
+  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,
+            },
+          },
+        });
+      }}
+      onLanguageChange={(language: string) => {
+        node.setData({
+          extraModules: {
+            type: "code",
+            data: {
+              code: extraModules.data.code,
+              language,
+            },
+          },
+        });
+      }}
+    />
+  ) : (
+    <img src={extraModules.data.imageUrl} alt="" />
+  );
+}

+ 21 - 0
apps/designer/src/components/mindMap/Link.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+import { Popover } from "antd";
+export default function Link({
+  link,
+}: {
+  link: { title: string; value: string };
+}) {
+  const [isEdit, setIsEdit] = React.useState(false);
+  
+  return (
+    <Popover
+      content={
+        <div>
+          链接:{link.title} <a href={link.value}>打开链接</a>
+        </div>
+      }
+    >
+      <i className="iconfont icon-link" />
+    </Popover>
+  );
+}

+ 95 - 11
apps/designer/src/components/mindMap/Topic.tsx

@@ -3,12 +3,30 @@ import { EventArgs, Graph, Node, Path } from "@antv/x6";
 import { topicData } from "@/config/data";
 import { useSizeHook, useShapeProps } from "@/hooks";
 import CustomInput from "../CustomInput";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
 import { PlusOutlined } from "@ant-design/icons";
 import { TopicType } from "@/enum";
 import { addTopic } from "@/pages/mindmap/mindMap";
+
+import Link from "./Link";
+import ExtraModule from "./ExtraModule";
+import CustomTag from "@/components/CustomTag";
 const component = ({ node, graph }: { node: Node; graph: Graph }) => {
-  const { fill, stroke, opacity, label, text, borderSize, setMindProjectInfo } = node.getData();
+  const {
+    fill,
+    stroke,
+    opacity,
+    label,
+    text,
+    borderSize,
+    setMindProjectInfo,
+    icons,
+    tags,
+    extraModules,
+    remark,
+    link,
+    children
+  } = node.getData();
   const { size, ref } = useSizeHook();
   const { fillContent, strokeColor, strokeWidth, strokeDasharray } =
     useShapeProps(fill, size, stroke);
@@ -29,19 +47,33 @@ 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]);
+
   const handleAddBranch = () => {
     const data = node.getData();
-    if( data.type === TopicType.main) {
-      addTopic(TopicType.branch, setMindProjectInfo, node);
+    let topic;
+    if (data.type === TopicType.main) {
+      topic = addTopic(TopicType.branch, setMindProjectInfo, node);
     } else {
-      addTopic(TopicType.sub, setMindProjectInfo, node);
+      topic = addTopic(TopicType.sub, setMindProjectInfo, node);
     }
-  }
-  
+    graph.resetSelection(topic?.id)
+  };
+
   return (
     <>
       <div
-        className="relative text-0 w-full h-full"
+        className="relative text-0 w-full h-full px-4px"
         ref={ref}
         style={{
           opacity: opacity / 100,
@@ -50,8 +82,60 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           borderRadius: borderSize,
         }}
       >
-        <CustomInput value={label} node={node} styles={text} />
-        {selected && (
+        <div className="w-full h-full flex flex-col">
+          {extraModules && (
+            <ExtraModule node={node} extraModules={extraModules} />
+          )}
+
+          <div className="flex-1">
+
+            <div className="flex-1 flex flex-col justify-center" style={{ height: getSize.contentHeight }}>
+              <div className="flex justify-start items-center text-20px">
+                {icons?.map((icon: string) => {
+                  return (
+                    <svg key={icon} className="icon mr-6px" aria-hidden="true">
+                      <use xlinkHref={`#${icon}`}></use>
+                    </svg>
+                  );
+                })}
+                <CustomInput
+                  value={label}
+                  node={node}
+                  styles={{
+                    ...text,
+                    position: "relative"
+                  }}
+                  txtStyle={{ 
+                    position: "relative", 
+                    flex: 1,
+                  }}
+                />
+              </div>
+
+              <div>
+                {tags?.map((item: { name: string; color: string }) => {
+                  return (
+                    <CustomTag
+                      className="text-14px"
+                      key={item.name}
+                      title={item.name}
+                      color={item.color}
+                    >
+                      {item.name}
+                    </CustomTag>
+                  );
+                })}
+              </div>
+            </div>
+
+            <div>
+              {link && <Link link={link} />}
+              {remark && <i className="iconfont icon-pinglun1" />}
+            </div>
+          </div>
+        </div>
+
+        {selected && !children?.length && (
           <div
             className={`
               absolute 
@@ -70,7 +154,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
               color-#9aa5b8 
               hover:bg-#067bef 
               hover:color-white`}
-              onClick={handleAddBranch}
+            onClick={handleAddBranch}
           >
             <PlusOutlined />
           </div>

+ 8 - 58
apps/designer/src/config/data.ts

@@ -131,6 +131,11 @@ export const topicData = {
   ...defaultData,
   type: TopicType.main,
   borderSize: BorderSize.medium,
+  fixedWidth: false,
+  edge: {
+    color: "#323232",
+    width: 3,
+  },
   children: [],
 }
 
@@ -140,13 +145,13 @@ export const defaultProject: MindMapProjectInfo = {
   desc: "",
   version: "",
   author: "",
-  mapType: StructureType.right,
+  structure: StructureType.right,
   pageSetting: {
     fillType: "color",
     fill: "#ffffff",
     fillImageUrl: "",
     branchY: 30,
-    branchX: 64,
+    branchX: 44,
     subTopicY: 16,
     subTopicX: 20,
     alignSameTopic: false,
@@ -154,62 +159,7 @@ export const defaultProject: MindMapProjectInfo = {
     watermark: "",
   },
   // 主题
-  theme: {
-    main: {
-      width: 206,
-      height: 70,
-      fill: {
-        fillType: "color",
-        color1: "#30304D",
-      },
-      text: {
-        color: "#fff",
-        fontSize: 30,
-      },
-      stroke: {
-        width: 0,
-      }
-    },
-    branch: {
-      width: 104,
-      height: 40,
-      fill: {
-        fillType: "color",
-        color1: "#A244EE",
-      },
-      text: {
-        color: "#fff",
-        fontSize: 18,
-      },
-      stroke: {
-        width: 0,
-      },
-      edge: {
-        color: '#A244EE',
-        width: 3
-      }
-    },
-    sub: {
-      width: 65,
-      height: 27,
-      fill: {
-        fillType: "color",
-        color1: "#F4E8FD",
-      },
-      text: {
-        color: "#712FA6",
-        fontSize: 14,
-      },
-      stroke: {
-        width: 0,
-      },
-      edge: {
-        color: '#712FA6',
-        width: 2
-      }
-    },
-    edge: {}
-  },
+  theme: "default",
   topics: [
     buildTopic(TopicType.main, {
       label: "中心主题",

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

@@ -52,4 +52,6 @@ export enum StructureType {
   treeShape = 'treeShape',
   rightTreeShape = 'rightTreeShape',
   leftTreeShape = 'leftTreeShape',
+  rightBracket = 'rightBracket',
+  leftBracket = 'leftBracket',
 }

+ 11 - 6
apps/designer/src/events/mindMapEvent.ts

@@ -1,20 +1,23 @@
-import { Graph, Node } from "@antv/x6";
+import { Graph, Cell } from "@antv/x6";
 import { BorderSize, TopicType } from "@/enum";
-import { addTopic, updateTopic } from "@/pages/mindmap/mindMap";
+import { addTopic } from "@/pages/mindmap/mindMap";
 import { MindMapProjectInfo } from "@/types";
 
 export const bindMindMapEvents = (
   graph: Graph,
   mindProjectInfo?: MindMapProjectInfo,
-  setMindProjectInfo?: (info: MindMapProjectInfo) => void
+  setMindProjectInfo?: (info: MindMapProjectInfo) => void,
+  setSelectedCell?: (cell: Cell[]) => void
 ) => {
-  graph.on("node:click", ({ cell }) => {
-  });
+  // 选中的节点/边发生改变(增删)时触发
+  graph.on('selection:changed', ({selected}: {selected: Cell[];}) => {
+    setSelectedCell?.(selected);
+  })
 
   // 双击画布空白-新增自由主题
   graph.on("blank:dblclick", (args) => {
     if(setMindProjectInfo) {
-      addTopic(TopicType.branch, setMindProjectInfo, undefined, {
+      const topic = addTopic(TopicType.branch, setMindProjectInfo, undefined, {
         x: args.x,
         y: args.y,
         setMindProjectInfo,
@@ -22,6 +25,8 @@ export const bindMindMapEvents = (
         label: "自由主题",
         borderSize: BorderSize.medium,
       });
+
+      graph.resetSelection(topic?.id)
     }
   });
 };

+ 7 - 0
apps/designer/src/global.less

@@ -38,4 +38,11 @@ body {
 .shalu-btn.active:disabled {
   color: rgba(0, 0, 0, 0.25);
   background: #fff;
+}
+
+.icon {
+  width: 1em; height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
 }

+ 6 - 0
apps/designer/src/icons/round-big.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="140" height="80" viewBox="0 0 140 80">
+  <g id="_3" data-name="3" fill="none" stroke="#000" stroke-width="2">
+    <rect width="140" height="80" rx="40" stroke="none"/>
+    <rect x="1" y="1" width="138" height="78" rx="39" fill="none"/>
+  </g>
+</svg>

+ 6 - 0
apps/designer/src/icons/round-rect.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="140" height="80" viewBox="0 0 140 80">
+  <g id="_1" data-name="1" fill="none" stroke="#000" stroke-width="2">
+    <rect width="140" height="80" stroke="none"/>
+    <rect x="1" y="1" width="138" height="78" fill="none"/>
+  </g>
+</svg>

+ 6 - 0
apps/designer/src/icons/round-small.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="140" height="80" viewBox="0 0 140 80">
+  <g id="_2" data-name="2" fill="none" stroke="#000" stroke-width="2">
+    <rect width="140" height="80" rx="10" stroke="none"/>
+    <rect x="1" y="1" width="138" height="78" rx="9" fill="none"/>
+  </g>
+</svg>

+ 3 - 1
apps/designer/src/models/mindMapModel.ts

@@ -32,6 +32,7 @@ export default function mindMapModel() {
   const [graph, setGraph] = useState<Graph>();
   const [canRedo, setCanRedo] = useState(false);
   const [canUndo, setCanUndo] = useState(false);
+  const [selectedCell, setSelectedCell] = useState<Cell[]>([]);
 
   const [mindProjectInfo, setMindProjectInfo] =
     useLocalStorageState<MindMapProjectInfo>("minMapProjectInfo", {
@@ -112,7 +113,7 @@ export default function mindMapModel() {
       setCanUndo(instance.canUndo());
     });
 
-    bindMindMapEvents(instance, mindProjectInfo, setMindProjectInfo);
+    bindMindMapEvents(instance, mindProjectInfo, setMindProjectInfo, setSelectedCell);
     setGraph(instance);
     renderMindMap(instance, setMindProjectInfo);
     instance.centerContent();
@@ -175,6 +176,7 @@ export default function mindMapModel() {
 
   return {
     graph,
+    selectedCell,
     initMindMap,
     rightToobarActive,
     rightToolbarActive,

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

@@ -0,0 +1,383 @@
+import { Tabs } from "antd";
+import { useModel } from "umi";
+import { cloneDeep } from "lodash-es"
+
+const icons = [
+  {
+    name: "优先级",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64_huaban1",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-02",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-03",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-04",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-05",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-06",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-07",
+      },
+      {
+        name: 8,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-08",
+      },
+      {
+        name: 9,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-09",
+      },
+    ],
+  },
+  {
+    name: "进度",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-11",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-12",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-13",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-14",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-15",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-15",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-16",
+      },
+      {
+        name: 8,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-17",
+      },
+      {
+        name: 9,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-18",
+      },
+    ],
+  },
+  {
+    name: "旗帜",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-19",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-20",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-21",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-22",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-23",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-24",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-25",
+      },
+    ],
+  },
+  {
+    name: "星星",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-30",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-28",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-34",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-36",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-29",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-33",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-32",
+      },
+      {
+        name: 8,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-31",
+      },
+      {
+        name: 9,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-35",
+      },
+    ],
+  },
+  {
+    name: "人物",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-37",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-43",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-41",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-40",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-38",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-42",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-45",
+      },
+      {
+        name: 8,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-44",
+      },
+      {
+        name: 9,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-39",
+      },
+    ],
+  },
+  {
+    name: "其他",
+    children: [
+      {
+        name: 1,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-46",
+      },
+      {
+        name: 2,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-47",
+      },
+      {
+        name: 3,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-48",
+      },
+      {
+        name: 4,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-49",
+      },
+      {
+        name: 5,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-50",
+      },
+      {
+        name: 6,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-55",
+      },
+      {
+        name: 7,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-51",
+      },
+      {
+        name: 8,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-60",
+      },
+      {
+        name: 9,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-63",
+      },
+      {
+        name: 10,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-54",
+      },
+      {
+        name: 11,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-62",
+      },
+      {
+        name: 12,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-67",
+      },
+      {
+        name: 13,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-65",
+      },
+      {
+        name: 14,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-56",
+      },
+      {
+        name: 15,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-10",
+      },
+      {
+        name: 16,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-57",
+      },
+      {
+        name: 17,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-52",
+      },
+      {
+        name: 18,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-53",
+      },
+      {
+        name: 19,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-59",
+      },
+      {
+        name: 20,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-61",
+      },
+      {
+        name: 21,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-64",
+      },
+      {
+        name: 22,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-66",
+      },
+      {
+        name: 23,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-58",
+      },
+      {
+        name: 24,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-71",
+      },
+      {
+        name: 25,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-69",
+      },
+      {
+        name: 26,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-72",
+      },
+      {
+        name: 27,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-68",
+      },
+      {
+        name: 28,
+        icon: "icon-siweidaotu-tubiaojiyihebing64-70",
+      },
+    ],
+  },
+];
+
+export default function IconConfig() {
+  const { selectedCell } = useModel("mindMapModel");
+
+  const handleClick = (icon: string) => {
+    selectedCell.forEach(cell => {
+      if(cell.isNode()) {
+        const icons = cloneDeep(cell.getData()?.icons || []);
+        if(!icons.includes(icon)) {
+          icons.push(icon);
+        } else {
+          icons.splice(icons.indexOf(icon), 1);
+        };
+
+        cell.setData({
+          icons
+        }, {
+          deep: false,
+        });
+      }
+    })
+  };
+
+  return (
+    <div className={`px-16px ${!selectedCell.length ? 'opacity-50' : ''}`}>
+      <Tabs
+        items={[
+          {
+            key: "1",
+            label: "图标",
+            children: (
+              <div>
+                {icons.map((item, index) => {
+                  return (
+                    <div key={item.name}>
+                      <div className={`text-14px color-#000 font-bold my-16px ${index === 0 ? 'mt-0' : ''}`}>
+                        {item.name}
+                      </div>
+                      <div className="flex gap-6px flex-wrap text-20px">
+                        {item.children.map((iconItem) => {
+                          return (
+                            <svg
+                              key={iconItem.name}
+                              className={`icon ${ !selectedCell.length ? 'cursor-default' : 'cursor-pointer'}`}
+                              aria-hidden="true"
+                              onClick={() => handleClick(iconItem.icon)}
+                            >
+                              <use xlinkHref={`#${iconItem.icon}`}></use>
+                            </svg>
+                          );
+                        })}
+                      </div>
+                    </div>
+                  );
+                })}
+              </div>
+            ),
+          },
+        ]}
+      />
+    </div>
+  );
+}

+ 93 - 205
apps/designer/src/pages/mindmap/components/Config/NodeStyle.tsx

@@ -1,63 +1,30 @@
 import CustomColorPicker from "@/components/CustomColorPicker";
 import {
   BoldOutlined,
-  ColumnHeightOutlined,
   FontSizeOutlined,
   ItalicOutlined,
-  PictureOutlined,
   StrikethroughOutlined,
-  SwapOutlined,
   UnderlineOutlined,
   VerticalAlignBottomOutlined,
   VerticalAlignMiddleOutlined,
   VerticalAlignTopOutlined,
 } from "@ant-design/icons";
-import {
-  Button,
-  Checkbox,
-  Col,
-  Divider,
-  Form,
-  Input,
-  InputNumber,
-  Row,
-  Select,
-  Tooltip,
-} from "antd";
-import { arrowOptions } from "@/pages/flow/data";
-import { useModel } from "umi";
+import { Button, Checkbox, Divider, InputNumber, Select, Tooltip } from "antd";
+import { Icon, useModel } from "umi";
 import { useEffect, useRef, useState } from "react";
-import { ImageFillType, ConnectorType, LineType } from "@/enum";
+import { ImageFillType, BorderSize, TopicType } from "@/enum";
 import { set, cloneDeep } from "lodash-es";
 import { Cell } from "@antv/x6";
-import { fontFamilyOptions, alignOptionData } from "@/pages/flow/data";
-import { alignCell, matchSize } from "@/utils";
-import { cellStyle } from "@/types";
-
-type FormModel = {
-  opacity: number;
-  width: number;
-  height: number;
-  x: number;
-  y: number;
-  rotation: number;
-  connectorType: ConnectorType;
-  startArrow: string;
-  endArrow: string;
-} & cellStyle;
+import { fontFamilyOptions } from "@/pages/flow/data";
 export default function GraphStyle() {
-  const { selectedCell } = useModel("graphModel");
-  const [isMulit, setIsMulit] = useState(false);
-  const [hasEdge, setHasEdge] = useState(false);
+  const { selectedCell } = useModel("mindMapModel");
   const eventNodeList = useRef<Cell[]>([]);
 
-  const [formModel, setFormModel] = useState<FormModel>({
-    opacity: 100,
-    width: 20,
-    height: 20,
-    x: 0,
-    y: 0,
-    rotation: 0,
+  const [formModel, setFormModel] = useState({
+    width: 100,
+    fixedWidth: false,
+    borderSize: BorderSize.medium,
+    type: TopicType.main,
     text: {
       fontFamily: "normal",
       color: "#000000",
@@ -69,6 +36,7 @@ export default function GraphStyle() {
       italic: false,
       textDecoration: "none",
     },
+    isFill: true,
     fill: {
       fillType: "color",
       color1: "#FFFFFF",
@@ -78,37 +46,39 @@ export default function GraphStyle() {
       objectFit: ImageFillType.Fill,
       imageUrl: "",
     },
+    isStroke: false,
     stroke: {
       type: "solid",
       color: "#323232",
       width: 1,
     },
-    connectorType: ConnectorType.Normal,
-    startArrow: "",
-    endArrow: "",
+    edge: {
+      color: "#000000",
+      width: 2,
+    },
   });
 
   useEffect(() => {
     const firstNode = selectedCell?.find((item) => item.isNode());
-    const firstEdge = selectedCell?.find((item) => item.isEdge());
     eventNodeList.current = [];
     if (firstNode) {
-      const position = firstNode.position();
       const size = firstNode.size();
       const data = firstNode.getData();
       setFormModel({
         ...formModel,
-        x: position.x,
-        y: position.y,
         width: size.width,
-        height: size.height,
-        rotation: firstNode.angle(),
         text: data.text,
         fill: data.fill,
-        stroke: data.stroke,
-        connectorType: ConnectorType.Normal,
-        startArrow: "",
-        endArrow: "",
+        edge: data.edge,
+        fixedWidth: data.fixedWidth,
+        borderSize: data.borderSize,
+        type: data.type,
+        isFill: data.fill.color1 !== "transparent",
+        isStroke: data.stroke.width > 0,
+        stroke: {
+          ...data.stroke,
+          width: data.stroke.width || 1
+        }
       });
 
       // 监听当前选中节点的属性变化
@@ -137,135 +107,41 @@ export default function GraphStyle() {
             setFormModel((state) => {
               return {
                 ...state,
+                type: args.current.type,
                 text: args.current.text,
                 fill: args.current.fill,
                 stroke: args.current.stroke,
+                edge: args.current.edge,
               };
             });
           }
         });
       }
     }
-
-    if (firstEdge) {
-      const data = firstEdge.getData();
-      const attrs = firstEdge.attrs || {};
-      const sourceMarker = attrs.line?.sourceMarker as Record<string, any>;
-      const targetMarker = attrs.line?.targetMarker as Record<string, any>;
-      const lineType =
-        attrs.line?.strokeDasharray === LineType.solid
-          ? "solid"
-          : attrs.line?.strokeDasharray === LineType.dashed
-            ? "dashed"
-            : attrs.line?.strokeDasharray === LineType.dotted
-              ? "dotted"
-              : "dashdot";
-      let obj = {};
-      if (!firstNode) {
-        obj = {
-          stroke: {
-            type: lineType,
-            color: attrs.line?.stroke || "#000000",
-            width: attrs.line?.strokeWidth || 1,
-          },
-        };
-      }
-      setFormModel((state) => {
-        return {
-          ...state,
-          ...obj,
-          startArrow: sourceMarker?.name,
-          endArrow: targetMarker?.name,
-        };
-      });
-    }
-
-    let nodeCount = 0;
-    selectedCell?.forEach((cell) => {
-      if (cell.isEdge()) {
-        // 存在边线
-        setHasEdge(true);
-      }
-      if (cell.isNode()) nodeCount++;
-    });
-    // 多个节点
-    setIsMulit(nodeCount > 1);
   }, [selectedCell]);
 
   // 表单值改变,修改元素属性
-  const handleChange = (model: FormModel) => {
+  const handleChange = (model: typeof formModel) => {
     selectedCell?.forEach((cell) => {
       if (cell.isNode()) {
-        cell.setPosition(model.x, model.y);
-        cell.setSize(model.width, model.height);
-        cell.rotate(model.rotation, { absolute: true });
+        model.fixedWidth && cell.setSize(model.width, cell.size().height);
         cell.setData({
           text: model.text,
-          fill: model.fill,
-          stroke: model.stroke,
-          opacity: model.opacity,
-        });
-      }
-      if (cell.isEdge()) {
-        const attr = cell.attrs;
-        const sourceMarker = attr?.line?.sourceMarker as Record<string, any>;
-        const targetMarker = attr?.line?.targetMarker as Record<string, any>;
-        cell.setAttrs({
-          line: {
-            ...(attr?.line || {}),
-            stroke: model.stroke.color,
-            strokeWidth: model.stroke.width,
-            strokeDasharray: LineType[model.stroke.type],
-            targetMarker: {
-              ...(targetMarker || {}),
-              name: model.endArrow,
-              args: {
-                size: model.stroke.width + 8,
-              },
-            },
-            sourceMarker: {
-              ...(sourceMarker || {}),
-              name: model.startArrow,
-              args: {
-                size: model.stroke.width + 8,
-              },
-            },
+          edge: model.edge,
+          BorderSize: model.borderSize,
+          fill: {
+            ...model.fill,
+            color1: model.isFill ? model.fill.color1 : "transparent"
+          },
+          stroke: {
+            ...model.stroke,
+            width: model.isStroke ? model.stroke.width : 0
           },
         });
       }
     });
   };
 
-  // 设置对齐方式
-  const handleAlign = (
-    type:
-      | "left"
-      | "hcenter"
-      | "right"
-      | "top"
-      | "vcenter"
-      | "bottom"
-      | "h"
-      | "v"
-  ) => {
-    selectedCell && alignCell(type, selectedCell);
-  };
-
-  // 匹配宽高
-  const handleMatchSize = (type: "width" | "height" | "auto") => {
-    selectedCell && matchSize(type, selectedCell);
-  };
-
-  // 调换渐变色颜色
-  const handleSwapColor = () => {
-    const { color1, color2 } = formModel.fill;
-    handleSetFormModel("fill", {
-      ...formModel.fill,
-      color1: color2,
-      color2: color1,
-    });
-  };
-
   // 设置表单数据
   const handleSetFormModel = (key: string, value: any) => {
     const obj = cloneDeep(formModel);
@@ -284,53 +160,53 @@ export default function GraphStyle() {
               <Button
                 type="text"
                 icon={
-                  <i className="iconfont icon-a-icon16lianxianleixinghuizhilianxian" />
+                  <Icon
+                    icon="local:round-rect"
+                    className="text-14px"
+                    width="20"
+                  />
                 }
                 className={
-                  formModel.connectorType === ConnectorType.Rounded
-                    ? "active"
-                    : ""
+                  formModel.borderSize === BorderSize.none ? "active" : ""
                 }
-                disabled={!hasEdge}
+                disabled={!selectedCell.length}
                 onClick={() =>
-                  handleSetFormModel("connectorType", ConnectorType.Rounded)
+                  handleSetFormModel("borderSize", BorderSize.none)
                 }
               />
               <Button
                 type="text"
-                icon={
-                  <i className="iconfont icon-a-icon16lianxianleixingbeisaierquxian" />
-                }
+                icon={<Icon icon="local:round-small" width="20" />}
                 className={
-                  formModel.connectorType === ConnectorType.Smooth
-                    ? "active"
-                    : ""
+                  formModel.borderSize === BorderSize.medium ? "active" : ""
                 }
-                disabled={!hasEdge}
+                disabled={!selectedCell.length}
                 onClick={() =>
-                  handleSetFormModel("connectorType", ConnectorType.Smooth)
+                  handleSetFormModel("borderSize", BorderSize.medium)
                 }
               />
               <Button
                 type="text"
-                icon={
-                  <i className="iconfont icon-a-icon16lianxianleixinghuizhizhixian" />
-                }
+                icon={<Icon icon="local:round-big" width="20" />}
                 className={
-                  formModel.connectorType === ConnectorType.Normal
-                    ? "active"
-                    : ""
+                  formModel.borderSize === BorderSize.large ? "active" : ""
                 }
-                disabled={!hasEdge}
+                disabled={!selectedCell.length}
                 onClick={() =>
-                  handleSetFormModel("connectorType", ConnectorType.Normal)
+                  handleSetFormModel("borderSize", BorderSize.large)
                 }
               />
             </div>
           </div>
         </div>
         <div className="flex items-center justify-between">
-          <Checkbox>固定宽度</Checkbox>
+          <Checkbox
+            disabled={!selectedCell?.length}
+            checked={formModel.fixedWidth}
+            onChange={(e) => handleSetFormModel("fixedWidth", e.target.checked)}
+          >
+            固定宽度
+          </Checkbox>
           <InputNumber
             className="w-96px"
             suffix={<FontSizeOutlined />}
@@ -338,9 +214,9 @@ export default function GraphStyle() {
             min={12}
             max={10000}
             formatter={(val) => `${val}px`}
-            disabled={!selectedCell?.length}
-            value={formModel.text.fontSize}
-            onChange={(val) => handleSetFormModel("text.fontSize", val)}
+            disabled={!formModel.fixedWidth}
+            value={formModel.width}
+            onChange={(val) => handleSetFormModel("width", val)}
           />
         </div>
       </section>
@@ -507,11 +383,17 @@ export default function GraphStyle() {
       <Divider className="my-8px" />
       <section className="px-16px">
         <div className="flex items-center justify-between mb-8px">
-          <Checkbox>边框</Checkbox>
-          <CustomColorPicker
+          <Checkbox
             disabled={!selectedCell?.length}
-            color={formModel.fill.color1}
-            onChange={(color) => handleSetFormModel("fill.color1", color)}
+            checked={formModel.isStroke}
+            onChange={(e) => handleSetFormModel("isStroke", e.target.checked)}
+          >
+            边框
+          </Checkbox>
+          <CustomColorPicker
+            disabled={!formModel.isStroke}
+            color={formModel.stroke.color}
+            onChange={(color) => handleSetFormModel("stroke.color", color)}
           />
         </div>
         <div className="flex gap-12px mb-8px">
@@ -521,7 +403,7 @@ export default function GraphStyle() {
             min={1}
             max={10}
             formatter={(val) => `${val}px`}
-            disabled={!selectedCell?.length}
+            disabled={!formModel.isStroke}
             value={formModel.stroke.width}
             onChange={(val) => handleSetFormModel("stroke.width", val)}
           />
@@ -533,7 +415,7 @@ export default function GraphStyle() {
               { label: "点线", value: "dotted" },
               { label: "点划线", value: "dashdot" },
             ]}
-            disabled={!selectedCell?.length}
+            disabled={!formModel.isStroke}
             value={formModel.stroke.type}
             onChange={(value) => handleSetFormModel("stroke.type", value)}
           />
@@ -542,9 +424,15 @@ export default function GraphStyle() {
       <Divider className="my-8px" />
       <section className="px-16px">
         <div className="flex items-center justify-between mb-8px">
-          <Checkbox>填充</Checkbox>
-          <CustomColorPicker
+          <Checkbox
             disabled={!selectedCell?.length}
+            checked={formModel.isFill}
+            onChange={(e) => handleSetFormModel("isFill", e.target.checked)}
+          >
+            填充
+          </Checkbox>
+          <CustomColorPicker
+            disabled={!formModel.isFill}
             color={formModel.fill.color1}
             onChange={(color) => handleSetFormModel("fill.color1", color)}
           />
@@ -553,11 +441,11 @@ export default function GraphStyle() {
       <Divider className="my-8px" />
       <section className="px-16px">
         <div className="flex items-center justify-between mb-8px">
-          <Checkbox>连线</Checkbox>
+          <div className="font-bold mb-8px">连线</div>
           <CustomColorPicker
-            disabled={!selectedCell?.length}
-            color={formModel.fill.color1}
-            onChange={(color) => handleSetFormModel("fill.color1", color)}
+            disabled={!selectedCell?.length || formModel.type === TopicType.main}
+            color={formModel.edge.color}
+            onChange={(color) => handleSetFormModel("edge.color", color)}
           />
         </div>
         <div className="flex gap-12px mb-8px">
@@ -567,11 +455,11 @@ export default function GraphStyle() {
             min={1}
             max={10}
             formatter={(val) => `${val}px`}
-            disabled={!selectedCell?.length}
-            value={formModel.stroke.width}
-            onChange={(val) => handleSetFormModel("stroke.width", val)}
+            disabled={!selectedCell?.length || formModel.type === TopicType.main}
+            value={formModel.edge.width}
+            onChange={(val) => handleSetFormModel("edge.width", val)}
           />
-         </div>
+        </div>
       </section>
     </div>
   );

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

@@ -0,0 +1,218 @@
+import { StructureType } from "@/enum";
+import { Tabs } from "antd";
+import React from "react";
+
+const structures = [
+  {
+    name: "左右分布",
+    icon: "icon-Frame1",
+    type: StructureType.leftRight,
+  },
+  {
+    name: "自由分布",
+    icon: "icon-ziyoubuju",
+    type: StructureType.free,
+  },
+  {
+    name: "右侧分布",
+    icon: "icon-siweidaotubianjiqi",
+    type: StructureType.right,
+  },
+  {
+    name: "左侧分布",
+    icon: "icon-siweidaotubianjiqi-1",
+    type: StructureType.left,
+  },
+  {
+    name: "树状结构图",
+    icon: "icon-siweidaotubianjiqi-2",
+    type: StructureType.tree,
+  },
+  {
+    name: "组织结构图",
+    icon: "icon-xinzuzhijiegoutu",
+    type: StructureType.organization,
+  },
+  {
+    name: "左侧鱼骨图",
+    icon: "icon-siweidaotubianjiqi-3",
+    type: StructureType.leftFishbone,
+  },
+  {
+    name: "右侧鱼骨图",
+    icon: "icon-siweidaotubianjiqi-4",
+    type: StructureType.rightFishbone,
+  },
+  {
+    name: "横向时间轴",
+    icon: "icon-shijianzhou",
+    type: StructureType.horizontalTime,
+  },
+  {
+    name: "向上时间轴",
+    icon: (
+      <svg
+        width="28"
+        height="28"
+        viewBox="0 0 28 28"
+        fill="none"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <g clip-path="url(#clip0_31667_57555)">
+          <path
+            d="M26.4241 15.0547L8.93872 15.0547"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <path
+            d="M7.40805 15.0547L13.708 15.0547L13.708 9.00107L15.8855 9.00107"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <path
+            d="M16.2302 15.0547L22.5302 15.0547L22.5302 9.00107L24.499 9.00107"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <rect
+            width="8.4"
+            height="6.3"
+            rx="0.7"
+            transform="matrix(-1 0 0 1 10.3999 11.9062)"
+            fill="#6C7D8F"
+          ></rect>
+        </g>
+        <defs>
+          <clipPath id="clip0_31667_57555">
+            <rect width="28" height="28" fill="white"></rect>
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+    type: StructureType.upwardTime,
+  },
+  {
+    name: "向下时间轴",
+    icon: (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="28"
+        height="28"
+        viewBox="0 0 28 28"
+        fill="none"
+      >
+        <g clip-path="url(#clip0_31757_133105)">
+          <path
+            d="M26.2122 12.5469L8.72681 12.5469"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <path
+            d="M8.72689 12.5469L15.0269 12.5469L15.0269 18.6005L17.2043 18.6005"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <path
+            d="M17.5491 12.5469L23.8491 12.5469L23.8491 18.6005L25.8179 18.6005"
+            stroke="#9AA5B8"
+            stroke-width="1.4"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          ></path>
+          <rect
+            width="8.4"
+            height="6.3"
+            rx="0.7"
+            transform="matrix(-1 0 0 1 10.188 9.39844)"
+            fill="#6C7D8F"
+          ></rect>
+        </g>
+        <defs>
+          <clipPath id="clip0_31757_133105">
+            <rect width="28" height="28" fill="white"></rect>
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+    type: StructureType.downwardTime,
+  },
+  {
+    name: "树形图",
+    icon: "icon-zuoyoujiegou",
+    type: StructureType.treeShape,
+  },
+  {
+    name: "右侧树形图",
+    icon: "icon-youjiegou",
+    type: StructureType.rightTreeShape,
+  },
+  {
+    name: "左侧树形图",
+    icon: "icon-zuojiegou",
+    type: StructureType.leftTreeShape,
+  },
+  {
+    name: "左侧括号图",
+    icon: "icon-zuokuohaotu",
+    type: StructureType.leftBracket,
+  },
+  {
+    name: "右侧括号图",
+    icon: "icon-youkuohaotu",
+    type: StructureType.rightBracket,
+  },
+];
+
+export default function Structure() {
+  return (
+    <>
+      <Tabs>
+        <Tabs.TabPane tab="结构类型" key="1">
+          <div className="flex gap-4 flex-wrap justify-between px-8px">
+            {structures.map((item) => (
+              <div className="w-70px flex flex-col items-center">
+                <span
+                  className={`
+              w-50px
+              h-30px
+              bg-white
+              inline-flex
+              rounded-4px
+              flex
+              items-center
+              justify-center
+              border-1px
+              border-solid
+              border-#e6ebf5
+              cursor-pointer
+              `}
+                >
+                  {typeof item.icon === "string" ? (
+                    <svg className="w-1em h-2em fill-current text-24px">
+                      <use xlinkHref={`#${item.icon}`} />
+                    </svg>
+                  ) : (
+                    item.icon
+                  )}
+                </span>
+                <div className="mt-8px text-12px color-#6c7d8f">
+                  {item.name}
+                </div>
+              </div>
+            ))}
+          </div>
+        </Tabs.TabPane>
+      </Tabs>
+    </>
+  );
+}

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

@@ -0,0 +1,134 @@
+import CustomColorPicker from "@/components/CustomColorPicker";
+import { useLocalStorageState } from "ahooks";
+import { Tabs, Input } from "antd";
+import React, { useEffect, useState } from "react";
+import { useModel } from "umi";
+import CustomTag from "@/components/CustomTag";
+
+interface TagItem {
+  name: string;
+  color: string;
+}
+const defaultTags: TagItem[] = [
+  { name: "待定", color: "#6e4dbe" },
+  { name: "已确认", color: "rgb(80, 194, 139)" },
+  { name: "已完成", color: "rgb(109, 109, 109)" },
+  { name: "已关闭", color: "rgb(196, 196, 196)" },
+  { name: "进行中", color: "rgb(39, 96, 242)" },
+];
+export default function TagConfig() {
+  const { selectedCell } = useModel("mindMapModel");
+  const [newTag, setNewTag] = useState<TagItem>({
+    name: "",
+    color: "#EC7270",
+  });
+  const [recentTags, setRecentTags] = useLocalStorageState<TagItem[]>(
+    "recentTags",
+    {
+      defaultValue: [],
+      listenStorageChange: true,
+    }
+  );
+  const [currentTags, setCurrentTags] = useState<TagItem[]>([]);
+
+  useEffect(() => {
+    if (selectedCell) {
+      const firstNode = selectedCell.find((item) => item.isNode());
+      const tags = firstNode?.getData()?.tags || [];
+      setCurrentTags(tags);
+    }
+  }, [selectedCell]);
+  const handleAddTag = (tag: TagItem, ignore = false) => {
+    if (!tag) return;
+    if(currentTags.find(item => item.name === tag.name)) return;
+
+    selectedCell.forEach((cell) => {
+      if (cell.isNode()) {
+        const tags = cell.getData()?.tags || [];
+        cell.setData({
+          tags: [...tags, tag],
+        });
+      }
+    });
+    
+    setCurrentTags((state) => [...state, tag]);
+    setNewTag((state) => ({ ...state, name: "" }));
+    if (!recentTags?.find((item) => item.name === tag.name) && !ignore) {
+      setRecentTags([tag, ...(recentTags || [])]);
+    }
+  };
+
+  return (
+    <Tabs>
+      <Tabs.TabPane key="1" tab="标签">
+        <div className={`px-8px ${!selectedCell.length ? "opacity-50 pointer-events-none" : ""}`}>
+          <div className="text-14px color-#000 font-bold my-8px mt-0">
+            当前标签
+          </div>
+          <div className="flex flex-wrap gap-4px">
+            {currentTags.length ? (
+              currentTags.map((item) => (
+                <CustomTag key={item.name} color={item.color}>
+                  {item.name}
+                </CustomTag>
+              ))
+            ) : (
+              <span className="text-12px color-#999">
+                暂无标签,请在下方输入创建
+              </span>
+            )}
+          </div>
+          <div className="text-14px color-#000 font-bold my-8px mt-16px">
+            创建标签
+          </div>
+          <div className="flex items-center gap-8px">
+            <CustomColorPicker
+              color={newTag.color}
+              onChange={(color) => setNewTag((state) => ({ ...state, color }))}
+            />
+            <Input
+              className="flex-1"
+              maxLength={20}
+              placeholder="不超过20个字,回车添加"
+              value={newTag.name}
+              onChange={(e) =>
+                setNewTag((state) => ({ ...state, name: e.target.value }))
+              }
+              onPressEnter={() => newTag.name && handleAddTag(newTag)}
+              onBlur={() => newTag.name && handleAddTag(newTag)}
+            />
+          </div>
+          <div className="text-12px color-#999 my-8px mt-16px">最近标签</div>
+          <div className="flex flex-wrap gap-4px">
+            {recentTags?.length ? (
+              recentTags.map((item) => (
+                <CustomTag
+                  key={item.name}
+                  color={item.color}
+                  onClick={() => handleAddTag(item)}
+                >
+                  {item.name}
+                </CustomTag>
+              ))
+            ) : (
+              <span className="text-12px color-#999">暂无最近标签</span>
+            )}
+          </div>
+
+          <div className="text-12px color-#999 my-8px mt-16px">我的标签</div>
+          <div className="flex flex-wrap gap-4px">
+            {defaultTags.map((item) => (
+              <CustomTag
+                key={item.name}
+                color={item.color}
+                onClick={() => handleAddTag(item, true)}
+              >
+                {item.name}
+              </CustomTag>
+            ))}
+          </div>
+        </div>
+      </Tabs.TabPane>
+    </Tabs>
+  );
+}

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

@@ -0,0 +1,56 @@
+import { Tabs } from "antd";
+import React from "react";
+
+const themes = [
+  {
+    key: "default",
+    image: require("@/assets/mindmap/bd_caihong.svg").default,
+  },
+  {
+    key: "theme3",
+    image: require("@/assets/mindmap/theme3_2.svg").default,
+  },
+  {
+    key: "default1",
+    image: require("@/assets/mindmap/bd_caihong.svg").default,
+  },
+  {
+    key: "theme4",
+    image: require("@/assets/mindmap/theme3_2.svg").default,
+  },
+];
+export default function Theme() {
+  return (
+    <Tabs>
+      <Tabs.TabPane key="1" tab="风格">
+        <div
+          className={`
+          flex
+          flex-wrap
+          px-8px
+          gap-5
+          justify-between
+          `}
+        >
+          {themes.map((item) => (
+            <div className={`
+              h-60px
+              bg-white
+              inline-flex
+              rounded-4px
+              flex
+              items-center
+              justify-center
+              border-1px
+              border-solid
+              border-#e6ebf5
+              cursor-pointer
+            `} key={item.key}>
+              <img src={item.image} className="h-full" />
+            </div>
+          ))}
+        </div>
+      </Tabs.TabPane>
+    </Tabs>
+  );
+}

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

@@ -1,10 +1,15 @@
 import React from "react";
-import PageStyle from "./PageStyle";
-import NodeStyle from "./NodeStyle";
 import { useModel } from "umi";
 import { Tabs } from "antd";
 import InsertCss from "insert-css"
 
+import PageStyle from "./PageStyle";
+import NodeStyle from "./NodeStyle";
+import Structure from "./Structure";
+import Theme from "./Theme";
+import TagConfig from "./TagConfig";
+import IconConfig from "./IconConfig";
+
 InsertCss(`
   .shalu-tabs-nav {
     padding: 0 16px
@@ -14,6 +19,7 @@ export default function index() {
   const { rightToobarActive } = useModel("mindMapModel");
   return (
     <div>
+      {/* 样式 */}
       {rightToobarActive === "style" && (
         <>
           <Tabs
@@ -32,6 +38,18 @@ export default function index() {
           />
         </>
       )}
+      {/* 结构 */}
+      {
+        rightToobarActive === "structure" && (
+          <Structure />
+        )
+      }
+      {/* 风格 */}
+      {rightToobarActive === "theme" && <Theme />}
+      {/* 图标 */}
+      {rightToobarActive === "icon" && <IconConfig />}
+      {/* 标签 */}
+      {rightToobarActive === "tag" && <TagConfig />}
     </div>
   );
 }

+ 38 - 13
apps/designer/src/pages/mindmap/components/HeaderToolbar/index.tsx

@@ -1,8 +1,10 @@
-import React from "react";
+import React, { useMemo } from "react";
 import { Button, Input, Dropdown, Tooltip, MenuProps, Divider } from "antd";
 import { LeftOutlined, MenuOutlined } from "@ant-design/icons";
 import logo from "@/assets/logo.png";
 import { useModel, Icon } from "umi";
+import { addTopic } from "../../mindMap";
+import { TopicType } from "@/enum";
 
 export default function index() {
   const {
@@ -14,8 +16,14 @@ export default function index() {
     onUndo,
     enableFormatBrush,
     toggleFormatBrush,
+    graph,
+    selectedCell
   } = useModel("mindMapModel");
 
+  const currentNode = useMemo(() => {
+    return selectedCell.filter(cell => cell.isNode());
+  }, [selectedCell]);
+
   // 创建新作品 todo
   const createNew = (type: string) => {};
   // 预览 todo
@@ -33,6 +41,23 @@ export default function index() {
   // 查找替换
   const handleReplace = () => {};
 
+  // 增加子主题
+  const handleAddSubTopic = () => {
+    if(!currentNode.length) return;
+
+    const topic = addTopic(
+      currentNode[0].data.type === TopicType.main ? TopicType.branch : TopicType.sub,
+      setMindProjectInfo,
+      currentNode[0]
+    );
+    graph?.resetSelection(topic?.id);
+  };
+
+  // 添加关联线
+  const handleAddLink = () => {
+
+  };
+
   const menuData: MenuProps["items"] = [
     {
       key: "1",
@@ -131,7 +156,7 @@ export default function index() {
   ];
 
   return (
-    <div className="absolute absolute top-8px left-8px bg-white shadow-md flex items-center gap-4px boder-radius-4px px-8px">
+    <div className="absolute absolute top-8px left-8px bg-white shadow-md flex items-center gap-4px rounded-4px px-8px">
       <Button type="text" icon={<LeftOutlined />}></Button>
 
       <Dropdown
@@ -182,11 +207,11 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-geshishua"></i>}
-          // disabled={!selectedCell?.length}
-          // className={
-          //   enableFormatBrush && selectedCell?.length ? "active" : ""
-          // }
-          // onClick={() => graph && toggleFormatBrush(graph)}
+          disabled={!currentNode?.length}
+          className={
+            enableFormatBrush && currentNode?.length ? "active" : ""
+          }
+          onClick={() => graph && toggleFormatBrush(graph)}
         />
       </Tooltip>
 
@@ -196,8 +221,8 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-zizhuti"></i>}
-          disabled={!canRedo}
-          onClick={onRedo}
+          disabled={!currentNode.length}
+          onClick={handleAddSubTopic}
         />
       </Tooltip>
 
@@ -205,8 +230,8 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-guanlianxian"></i>}
-          disabled={!canRedo}
-          onClick={onRedo}
+          disabled={!currentNode.length}
+          onClick={handleAddLink}
         />
       </Tooltip>
 
@@ -214,7 +239,7 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-summary-outline"></i>}
-          disabled={!canRedo}
+          disabled={currentNode.length < 2}
           onClick={onRedo}
         />
       </Tooltip>
@@ -223,7 +248,7 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-waikuang"></i>}
-          disabled={!canRedo}
+          disabled={currentNode.length < 2}
           onClick={onRedo}
         />
       </Tooltip>

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

@@ -10,11 +10,11 @@ import {
 } from "@ant-design/icons";
 import { useModel } from "umi";
 export default function index() {
-  const { rightToobarActive, rightToolbarActive } =
+  const { rightToobarActive, rightToolbarActive, selectedCell } =
     useModel("mindMapModel");
 
   return (
-    <div className="absolute top-8px right-8px bg-white shadow-md boder-radius-4px flex flex-col p-8px">
+    <div className="absolute top-8px right-8px bg-white shadow-md rounded-4px flex flex-col p-8px">
       <Tooltip placement="bottom" title="样式">
         <Button
           type="text"
@@ -43,6 +43,7 @@ export default function index() {
       <Tooltip placement="bottom" title="图标">
         <Button
           type="text"
+          disabled={!selectedCell.length}
           icon={<SmileOutlined />}
           className={rightToobarActive === "icon" ? "active" : ""}
           onClick={() => rightToolbarActive("icon")}
@@ -51,6 +52,7 @@ export default function index() {
       <Tooltip placement="bottom" title="图片">
         <Button
           type="text"
+          disabled={!selectedCell.length}
           icon={<FileImageOutlined />}
           className={rightToobarActive === "image" ? "active" : ""}
           onClick={() => rightToolbarActive("image")}
@@ -59,6 +61,7 @@ export default function index() {
       <Tooltip placement="bottom" title="标签">
         <Button
           type="text"
+          disabled={!selectedCell.length}
           icon={<TagOutlined />}
           className={rightToobarActive === "tag" ? "active" : ""}
           onClick={() => rightToolbarActive("tag")}

File diff ditekan karena terlalu besar
+ 1737 - 0
apps/designer/src/pages/mindmap/components/SvgComponent.tsx


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

@@ -7,6 +7,7 @@ import HeaderToolbar from "./components/HeaderToolbar"
 import { useModel } from "umi";
 import Footer from "./components/Footer";
 import Config from "./components/Config";
+import SvgComponent from "./components/SvgComponent";
 
 export default function MindMap() {
   const { rightToobarActive, initMindMap } = useModel("mindMapModel");
@@ -26,6 +27,7 @@ export default function MindMap() {
         },
       }}
     >
+      <SvgComponent/>
       <Layout className="w-100vw h-100vh">
         <Layout.Content className={styles.container + " relative"}>
           <div ref={graphRef} className="w-full h-full"></div>

+ 47 - 12
apps/designer/src/pages/mindmap/mindMap.tsx

@@ -5,6 +5,7 @@ import TopicComponent from "@/components/mindMap/Topic";
 import { topicData } from "@/config/data";
 import { uuid } from "@/utils";
 import { hierarchyMethodMap } from "@/pages/mindmap/hierarchy";
+import { getTheme } from "./theme";
 
 interface HierarchyResult {
   id: string;
@@ -27,8 +28,14 @@ export const renderMindMap = (
   const cells: Cell[] = [];
   topics.forEach((topic) => {
     // 遍历出层次结构
-    const result: HierarchyResult = hierarchyMethodMap[projectInfo.mapType]?.(topic, pageSetting);
-    const originPosition = { x: topic?.x ?? -10, y: topic?.y ?? -10 };
+    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);
+      if(node.isNode()) {
+        originPosition = node.position();
+      }
+    }
     const offsetX = originPosition.x - result.x;
     const offsetY = originPosition.y - result.y;
     const traverse = (
@@ -57,10 +64,12 @@ export const renderMindMap = (
 
         if (children) {
           children.forEach((item: HierarchyResult) => {
+            console.log(item)
             cells.push(
               // 创建连线
               graph.createEdge({
                 shape: "mindmap-edge",
+                id: item.id + '-edge',
                 source: {
                   cell: id,
                   anchor: {
@@ -76,6 +85,13 @@ export const renderMindMap = (
                     name: "left",
                   },
                 },
+                attrs: {
+                  line: {
+                    targetMarker: "",
+                    stroke: item.data.edge?.color || "#A2B1C3",
+                    strokeWidth: item.data?.edge?.width || 3,
+                  },
+                },
               })
             );
             // 递归遍历
@@ -90,7 +106,7 @@ export const renderMindMap = (
   // 处理节点
   cells.filter(cell => cell.isNode()).forEach((cell) => {
     // 存在更新位置,否则添加
-    if (graph.hasCell(cell.id) && cell.isNode()) {
+    if (graph.hasCell(cell.id)) {
       const oldCell = graph.getCellById(cell.id);
       oldCell.isNode() && oldCell.position(cell.position().x, cell.position().y);
     } else {
@@ -125,7 +141,7 @@ export const addTopic = (
   const topic = buildTopic(type, {
     ...(otherData || {}),
     parentId: node?.id
-  });
+  }, node);
 
   if( node) {
     const parentData = node.getData();
@@ -149,12 +165,26 @@ export const addTopic = (
   }
 
   setMindProjectInfo(projectInfo);
+
+  return topic;
 };
 
 const topicMap = {
-  [TopicType.main]: "中心主题",
-  [TopicType.branch]: "分支主题",
-  [TopicType.sub]: "子主题"
+  [TopicType.main]: {
+    label: "中心主题",
+    width: 206,
+    height: 70
+  },
+  [TopicType.branch]: {
+    label: "分支主题",
+    width: 104,
+    height: 40
+  },
+  [TopicType.sub]: {
+    label: "子主题",
+    width: 76,
+    height: 27
+  }
 };
 
 /**
@@ -165,18 +195,19 @@ const topicMap = {
  */
 export const buildTopic = (
   type: TopicType,
-  options: Record<string, any> = {}
+  options: Record<string, any> = {},
+  parentNode?: Node
 ) => {
   const projectInfo = getMindMapProjectByLocal();
 
-  const theme = projectInfo?.theme || {};
+  const theme = getTheme(projectInfo?.theme, type === TopicType.sub ? parentNode : undefined);
   return {
     ...topicData,
     id: uuid(),
     type,
-    label: topicMap[type] || "自由主题",
-    width: theme[type]?.width || 206,
-    height: theme[type]?.height || 70,
+    label: topicMap[type].label || "自由主题",
+    width: topicMap[type].width || 206,
+    height: topicMap[type].height || 70,
     fill: {
       ...topicData.fill,
       ...theme[type]?.fill,
@@ -189,6 +220,10 @@ export const buildTopic = (
       ...topicData.stroke,
       ...theme[type]?.stroke,
     },
+    edge: {
+      ...topicData.edge,
+      color: theme[type]?.fill.color1,
+    },
     ...options,
   };
 };

+ 52 - 0
apps/designer/src/pages/mindmap/theme.ts

@@ -0,0 +1,52 @@
+import { getRandomColor, lightenColor } from "@/utils/color";
+import { Node } from "@antv/x6";
+
+export const getTheme = (key?: string, parentNode?: Node) => {
+
+  const map = {
+    // 默认主题
+    default: {
+      main: {
+        fill: {
+          fillType: "color",
+          color1: "#30304D",
+        },
+        text: {
+          color: "#fff",
+          fontSize: 30,
+        },
+        stroke: {
+          width: 0,
+        }
+      },
+      branch: {
+        fill: {
+          fillType: "color",
+          color1: getRandomColor(["#3D4BCF", "#9C2CB8"]),
+        },
+        text: {
+          color: "#fff",
+          fontSize: 18,
+        },
+        stroke: {
+          width: 0,
+        },
+      },
+      sub: {
+        fill: {
+          fillType: "color",
+          color1: parentNode?.data?.fill?.color1 ? lightenColor(parentNode.data.fill.color1, 150) : getRandomColor(["#3D4BCF", "#9C2CB8"]),
+        },
+        text: {
+          color: parentNode?.data?.fill?.color1 ? lightenColor(parentNode.data.fill.color1, -10) : "#fff",
+          fontSize: 14,
+        },
+        stroke: {
+          width: 0,
+        },
+      }
+    }
+  }
+
+  return map[key as keyof typeof map] || {};
+};

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

@@ -47,23 +47,87 @@ export interface cellStyle {
 }
 
 export interface TopicItem {
+  /**
+   * 主键
+   */
   id: string;
+  /**
+   * 主题内容
+   */
   label: string;
+  /**
+   * 宽
+   */
   width: number;
+  /**
+   * 高
+   */
   height: number;
+  /**
+   * 主题类型 main:中心主题 branch:分支主题 sub:子主题
+   */
   type: TopicType;
+  /**
+   * 是否固定宽度
+   */
+  fixedWidth: boolean;
+  /**
+   * 扩展模块
+   */
+  extraModules?: {
+    type: "image" | "code";
+    data: Record<string, any>;
+  };
+  /**
+   * 图标
+   */
+  icons?: string[];
+  /**
+   * 标签
+   */
+  tags?: string[];
+  /**
+   * 备注
+   */
+  remark?: string;
+  /**
+   * 链接
+   */
+  link?: {
+    title: string;
+    value: string;
+  };
+  /**
+   * 父级节点
+   */
   parentId?: string;
+  /**
+   * 位置x
+   */
   x?: number;
+  /**
+   * 位置y
+   */
   y?: number;
+  /**
+   * 子节点
+   */
   children?: TopicItem[];
+  /**
+   * 连线
+   */
+  edge?: {
+    color: string;
+    width: number;
+  }
 }
 export interface MindMapProjectInfo{
   name: string;
   desc: string;
   version: string;
   author: string;
-  theme: Record<string, any>;
-  mapType: StructureType;
+  theme: string;
+  structure: StructureType;
   pageSetting: {
     fillType: 'color' | 'image',
     fill: string,

+ 44 - 0
apps/designer/src/utils/color.ts

@@ -0,0 +1,44 @@
+/**
+ * 获取随机颜色
+ * @param baseColor 基础颜色
+ * @param alpha 透明度 
+ */
+export const getRandomColor = (baseColors: string[], alpha?: number) => {
+  const randomIndex = Math.floor(Math.random() * baseColors.length);
+  const randomAlpha = Math.random();
+  alpha = alpha || randomAlpha < 0.3 ? 0.3 : randomAlpha;
+  const r = parseInt(baseColors[randomIndex].slice(1, 3), 16);
+  const g = parseInt(baseColors[randomIndex].slice(3, 5), 16);
+  const b = parseInt(baseColors[randomIndex].slice(5, 7), 16) * alpha;
+  // 返回hex颜色
+  return `#${r.toString(16)}${g.toString(16)}${Number(b.toFixed(0)).toString(16)}`
+}
+
+/**
+ * 改变颜色亮度
+ * @param color 颜色
+ * @param amount 改变值
+ * @returns 
+ */
+export function lightenColor(color: string, amount: number): string {
+  let usePound = false;
+  if (color[0] === '#') {
+    color = color.slice(1);
+    usePound = true;
+  }
+
+  const num = parseInt(color, 16);
+  let r = (num >> 16) + amount;
+  if (r > 255) r = 255;
+  else if (r < 0) r = 0;
+
+  let b = ((num >> 8) & 0x00ff) + amount;
+  if (b > 255) b = 255;
+  else if (b < 0) b = 0;
+
+  let g = (num & 0x0000ff) + amount;
+  if (g > 255) g = 255;
+  else if (g < 0) g = 0;
+
+  return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0');
+}

File diff ditekan karena terlalu besar
+ 210 - 0
apps/designer/src/utils/icons.ts