Forráskód Böngészése

feat: 添加概要功能

liaojiaxing 6 hónapja
szülő
commit
855d6b4dbc

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

@@ -6,7 +6,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4676747_2fcjmdv7h6f.css'
+    '//at.alicdn.com/t/c/font_4676747_iugzj0fa06f.css'
   ],
   metas: [
     { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }

+ 253 - 0
apps/designer/src/components/mindMap/Border.tsx

@@ -0,0 +1,253 @@
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node, EventArgs } from "@antv/x6";
+import { useEffect, useMemo, useState } from "react";
+import { TopicBorderType } from "@/enum";
+import { Button, InputNumber, Popover } from "antd";
+import { Icon } from "umi";
+
+const component = ({ node, graph }: { node: Node; graph: Graph }) => {
+  const { label, line, fill, type, origin } = node.getData();
+  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);
+    };
+    graph.on("node:selected", handleSelect);
+    graph.on("blank:click", () => setShowSetting(false));
+    return () => {
+      graph.off("node:selected", handleSelect);
+      graph.off("blank:click", () => setShowSetting(false));
+    };
+  }, []);
+
+  // 删除边框
+  const handleRemove = () => {
+    const parentNode = graph.getCellById(origin);
+    parentNode?.setData(
+      {
+        border: undefined,
+      },
+      {
+        deep: false,
+      }
+    );
+  };
+
+  const handleChange = (key: string, value: any) => {
+    node.setData({
+      [key]: value,
+    });
+  };
+
+  const colors = [
+    "#bf1e1b",
+    "#63abf7",
+    "#71cb2d",
+    "#ff9f1a",
+    "#30bfbf",
+    "#000",
+    "",
+  ];
+
+  const ColorBtn = ({
+    color,
+    value,
+    onClick,
+  }: {
+    color: string;
+    value: string;
+    onClick: () => void;
+  }) => {
+    return (
+      <div
+        className={`relative w-18px h-18px cursor-pointer rounded-4px flex items-center justify-center hover:opacity-80`}
+        style={{
+          background: color,
+          border: !color ? "1px solid #000" : "",
+        }}
+        onClick={onClick}
+      >
+        {color === value && (
+          <i className="iconfont icon-zhengque-1 color-#fff" />
+        )}
+        {!color && (
+          <i className="absolute left-0 top-0 block w-1px h-24px bg-#de0f18 origin-top-left rotate--45" />
+        )}
+      </div>
+    );
+  };
+
+  return (
+    <>
+      <div className="relative text-0 w-full h-full">
+        {showSetting && (
+          <Popover
+            // trigger={["click"]}
+            content={
+              <div className="w-240px">
+                <div className="flex justify-between items-center m-b-20px">
+                  <span className="text-14px font-bold">外框设置</span>
+                  <span
+                    className="text-12px cursor-pointer color-#666 hover:color-red"
+                    onClick={handleRemove}
+                  >
+                    删除外框
+                    <i className="iconfont icon-shanchu m-l-4px" />
+                  </span>
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">线条样式</span>
+                  <InputNumber
+                    className="w-100px"
+                    size="small"
+                    min={1}
+                    max={5}
+                    precision={0}
+                    value={line.width}
+                    onChange={(val) =>
+                      handleChange("line", { ...line, width: val })
+                    }
+                  />
+                  <Button
+                    type="text"
+                    size="small"
+                    className={line.style === "solid" ? "active" : ""}
+                    icon={<i className="iconfont icon-h-line" />}
+                    onClick={() =>
+                      handleChange("line", { ...line, style: "solid" })
+                    }
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    className={line.style === "dashed" ? "active" : ""}
+                    icon={<i className="iconfont icon-xuxian" />}
+                    onClick={() =>
+                      handleChange("line", { ...line, style: "dashed" })
+                    }
+                  ></Button>
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">填充颜色</span>
+                  {colors.map((color) => (
+                    <ColorBtn
+                      key={color}
+                      color={color}
+                      value={fill}
+                      onClick={() => handleChange("fill", color)}
+                    />
+                  ))}
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">线条颜色</span>
+                  {colors.map((color) => (
+                    <ColorBtn
+                      key={color}
+                      color={color}
+                      value={line.color}
+                      onClick={() => handleChange("line", { ...line, color })}
+                    />
+                  ))}
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">外框样式</span>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:rect" width="16px" />}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:rounded" width="16px" />}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:wavy" width="16px" />}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:trapezoid" width="16px" />}
+                  ></Button>
+                </div>
+              </div>
+            }
+          >
+            <i
+              className="iconfont icon-more text-12px absolute right--5px top--5px cursor-pointer"
+              style={{ color: line.color || "#000" }}
+            />
+          </Popover>
+        )}
+
+        <svg
+          className="w-full h-full"
+          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}
+          />
+        </svg>
+      </div>
+    </>
+  );
+};
+
+// 主题节点
+register({
+  shape: "mind-map-border",
+  effect: ["data"],
+  component: component,
+});
+
+export default {
+  shape: "mind-map-border",
+  data: {
+    line: {
+      width: 2,
+      color: "#939aa8",
+      style: "dashed",
+    },
+    fill: "#f4f4f6",
+    label: "",
+    type: TopicBorderType.normal,
+  },
+};

+ 267 - 0
apps/designer/src/components/mindMap/SummaryBorder.tsx

@@ -0,0 +1,267 @@
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node, EventArgs } from "@antv/x6";
+import { useEffect, useMemo, useState } from "react";
+import { Button, InputNumber, Popover } from "antd";
+import { Icon } from "umi";
+
+const component = ({ node, graph }: { node: Node; graph: Graph }) => {
+  const { line, origin, summarySource, type } = node.getData();
+  const { width, height } = node.size();
+  const [showSetting, setShowSetting] = useState(false);
+
+  useEffect(() => {
+    const handleSelect = (args: EventArgs["node:selected"]) => {
+      setShowSetting(args.node.id === origin);
+    };
+    graph.on("node:selected", handleSelect);
+    graph.on("blank:click", () => setShowSetting(false));
+    return () => {
+      graph.off("node:selected", handleSelect);
+      graph.off("blank:click", () => setShowSetting(false));
+    };
+  }, []);
+
+  const path = useMemo(() => {
+    switch (type) {
+      case 1:
+        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}
+              `;
+      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 handleRemove = () => {
+    const parentNode = graph.getCellById(summarySource);
+    parentNode?.setData(
+      {
+        summary: undefined,
+      },
+      {
+        deep: false,
+      }
+    );
+  };
+
+  const handleChange = (key: string, value: any) => {
+    node.setData({
+      [key]: value,
+    });
+    const parentNode = graph.getCellById(summarySource);
+    if (parentNode) {
+      parentNode.setData({
+        summary: {
+          ...parentNode.data.summary,
+          border: {
+            ...parentNode.data.summary.border,
+            [key]: value,
+          },
+        },
+      });
+    }
+  };
+
+  const colors = [
+    "#bf1e1b",
+    "#63abf7",
+    "#71cb2d",
+    "#ff9f1a",
+    "#30bfbf",
+    "#000",
+  ];
+
+  const ColorBtn = ({
+    color,
+    value,
+    onClick,
+  }: {
+    color: string;
+    value: string;
+    onClick: () => void;
+  }) => {
+    return (
+      <div
+        className={`relative w-18px h-18px cursor-pointer rounded-4px flex items-center justify-center hover:opacity-80`}
+        style={{
+          background: color,
+          border: !color ? "1px solid #000" : "",
+        }}
+        onClick={onClick}
+      >
+        {color === value && (
+          <i className="iconfont icon-zhengque-1 color-#fff" />
+        )}
+        {!color && (
+          <i className="absolute left-0 top-0 block w-1px h-24px bg-#de0f18 origin-top-left rotate--45" />
+        )}
+      </div>
+    );
+  };
+
+  return (
+    <>
+      <div className="relative text-0 w-full h-full">
+        {showSetting && (
+          <svg
+            className="w-full h-full"
+            viewBox={`0 0 ${width} ${height}`}
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <path
+              fill="none"
+              d={`
+              M ${0},${0}
+              L ${width},${0}
+              L ${width},${height}
+              L ${0},${height} Z
+            `}
+              stroke={line.color}
+              strokeWidth={2}
+            />
+          </svg>
+        )}
+
+        <svg
+          className="absolute right--40px w-40px h-full"
+          viewBox={`0 0 ${40} ${height}`}
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <path
+            d={path}
+            fill="none"
+            stroke={line.color}
+            strokeWidth={line.width}
+          />
+        </svg>
+
+        {showSetting && (
+          <Popover
+            trigger={["hover"]}
+            content={
+              <div className="w-240px">
+                <div className="flex justify-between items-center m-b-20px">
+                  <span className="text-14px font-bold">概要设置</span>
+                  <span
+                    className="text-12px cursor-pointer color-#666 hover:color-red"
+                    onClick={handleRemove}
+                  >
+                    删除概要
+                    <i className="iconfont icon-shanchu m-l-4px" />
+                  </span>
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">概要宽度</span>
+                  <InputNumber
+                    className="w-100px"
+                    size="small"
+                    min={1}
+                    max={4}
+                    precision={0}
+                    value={line.width}
+                    onChange={(val) =>
+                      handleChange("line", { ...line, width: val })
+                    }
+                  />
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">概要颜色</span>
+                  {colors.map((color) => (
+                    <ColorBtn
+                      key={color}
+                      color={color}
+                      value={line.color}
+                      onClick={() => handleChange("line", { ...line, color })}
+                    />
+                  ))}
+                </div>
+                <div className="flex items-center gap-8px m-b-8px">
+                  <span className="text-12px color-#333">概要样式</span>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:1" height="16px" />}
+                    className={type === 1 ? "active" : ""}
+                    onClick={() => handleChange("type", 1)}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:2" height="16px" />}
+                    className={type === 1 ? "active" : ""}
+                    onClick={() => handleChange("type", 2)}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:3" height="16px" />}
+                    className={type === 1 ? "active" : ""}
+                    onClick={() => handleChange("type", 3)}
+                  ></Button>
+                  <Button
+                    type="text"
+                    size="small"
+                    icon={<Icon icon="local:4" height="16px" />}
+                    className={type === 1 ? "active" : ""}
+                    onClick={() => handleChange("type", 4)}
+                  ></Button>
+                </div>
+              </div>
+            }
+          >
+            <i
+              className="z-9 iconfont icon-more text-12px absolute right--35px top-25% cursor-pointer"
+              style={{ color: line.color || "#000" }}
+            />
+          </Popover>
+        )}
+      </div>
+    </>
+  );
+};
+
+// 主题节点
+register({
+  shape: "mind-map-summary-border",
+  effect: ["data"],
+  component: component,
+});
+
+export default {
+  shape: "mind-map-summary-border",
+  data: {
+    line: {
+      width: 2,
+      color: "#939aa8",
+    },
+    type: 1,
+  },
+};

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

@@ -140,7 +140,9 @@ export const topicData = {
   },
   children: [],
   extraModules: undefined,
-  links: []
+  links: [],
+  summary: undefined,
+  isSummary: false,
 }
 
 // 初始化项目

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

@@ -62,4 +62,12 @@ export enum MindmapConnectorType {
   straight = 'straight',
   rounded = 'rounded',
   poly = 'poly',
+}
+
+// 主题外框样式
+export enum TopicBorderType {
+  normal = 'normal',
+  rounded = 'rounded',
+  wavy = 'wavy',
+  trapezoid = 'trapezoid',
 }

+ 27 - 3
apps/designer/src/events/mindMapEvent.ts

@@ -190,6 +190,7 @@ export const bindMindMapEvents = (
    */
   graph.on("node:change:*", (args) => {
     const { current, previous } = args;
+    console.log(args.key, args)
     if (args.key === "data") {
       // 收折子项 setMindProjectInfo更新会重新渲染
       if (current.collapsed !== previous.collapsed) {
@@ -209,10 +210,33 @@ export const bindMindMapEvents = (
             setMindProjectInfo
           );
       }
+      if (current?.border !== previous?.border) {
+        setMindProjectInfo &&
+          updateTopic(
+            args.cell.id,
+            { border: current.border },
+            setMindProjectInfo
+          );
+      }
+      console.log(args)
+      if (current?.summary !== previous?.summary) {
+        setMindProjectInfo &&
+          updateTopic(
+            args.cell.id,
+            { summary: current.summary },
+            setMindProjectInfo
+          );
+      }
       // 本地缓存更新不会重新渲染
-      updateTopic(args.cell.id, current, (info) => {
-        localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
-      });
+      if(args.cell.id.includes('-border')) {
+        updateTopic(args.current.origin, {border: current}, (info) => {
+          localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
+        });
+      } else {
+         updateTopic(args.cell.id, current, (info) => {
+          localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
+        });
+      }
     }
     if (args.key === "position") {
       if (args.cell.isNode() && !args.cell.data.parentId) {

+ 6 - 0
apps/designer/src/icons/1.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="25.5" height="25" viewBox="0 0 25.5 25">
+  <g id="_1" data-name="1" transform="translate(-832 -532)">
+    <path id="路径_1" data-name="路径 1" d="M858.27,531.333h-17v24h17" transform="translate(-1.27 1.167)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1"/>
+    <path id="路径_2" data-name="路径 2" d="M841.27,542.948h-8.582" transform="translate(-0.689 1.552)" fill="none" stroke="#000" stroke-width="1"/>
+  </g>
+</svg>

+ 6 - 0
apps/designer/src/icons/2.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="25.697" height="25.394" viewBox="0 0 25.697 25.394">
+  <g id="_2" data-name="2" transform="translate(-875 -531.803)">
+    <path id="路径_3" data-name="路径 3" d="M858.27,531.333l-17,12,17,12" transform="translate(41.73 1.167)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1"/>
+    <path id="路径_4" data-name="路径 4" d="M841.27,542.948h-8.582" transform="translate(42.311 1.552)" fill="none" stroke="#000" stroke-width="1"/>
+  </g>
+</svg>

+ 8 - 0
apps/designer/src/icons/3.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="25.515" height="25.02" viewBox="0 0 25.515 25.02">
+  <g id="_3" data-name="3" transform="translate(-927 -531.99)">
+    <g id="组_1" data-name="组 1">
+      <path id="路径_6" data-name="路径 6" d="M858.27,531.333s-17-.5-17,12,17,12,17,12" transform="translate(93.73 1.167)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1"/>
+      <path id="路径_5" data-name="路径 5" d="M841.27,542.948h-8.582" transform="translate(94.311 1.552)" fill="none" stroke="#000" stroke-width="1"/>
+    </g>
+  </g>
+</svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
apps/designer/src/icons/4.svg


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

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30" viewBox="0 0 40 30">
+  <g id="_1" data-name="1" fill="none" stroke="#707070" stroke-width="1">
+    <rect width="40" height="30" stroke="none"/>
+    <rect x="0.5" y="0.5" width="39" height="29" fill="none"/>
+  </g>
+</svg>

+ 6 - 0
apps/designer/src/icons/rounded.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30" viewBox="0 0 40 30">
+  <g id="_2" data-name="2" fill="none" stroke="#707070" stroke-width="1">
+    <rect width="40" height="30" rx="5" stroke="none"/>
+    <rect x="0.5" y="0.5" width="39" height="29" rx="4.5" fill="none"/>
+  </g>
+</svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 6 - 0
apps/designer/src/icons/trapezoid.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
apps/designer/src/icons/wavy.svg


+ 19 - 2
apps/designer/src/models/mindMapModel.ts

@@ -51,7 +51,14 @@ export default function mindMapModel() {
 
   useEffect(() => {
     if (!graph || !mindProjectInfo) return;
-    renderMindMap(graph, setMindProjectInfo);
+    renderMindMap({
+      graph,
+      setMindProjectInfo,
+      pageSetting: mindProjectInfo?.pageSetting,
+      structure: mindProjectInfo?.structure,
+      theme: mindProjectInfo?.theme,
+      topics: mindProjectInfo?.topics,
+    });
     localStorage.setItem('minMapProjectInfo', JSON.stringify(mindProjectInfo))
   }, [mindProjectInfo, graph]);
 
@@ -120,11 +127,14 @@ export default function mindMapModel() {
             type: TopicType;
             parentId: string;
             shadow: boolean;
+            isSummary: boolean;
           }>();
           // 禁止拖拽或锁节点
           if (data?.ignoreDrag || data?.lock) return false;
           // 影子节点
           if (data?.shadow) return true;
+          // 概要
+          if (data?.isSummary) return false;
           // 自由节点
           return data?.type === TopicType.branch && !data?.parentId;
         },
@@ -187,7 +197,14 @@ export default function mindMapModel() {
     }
 
     setGraph(instance);
-    renderMindMap(instance, setMindProjectInfo);
+    mindProjectInfo && renderMindMap({
+      graph: instance,
+      setMindProjectInfo,
+      pageSetting: mindProjectInfo?.pageSetting,
+      structure: mindProjectInfo?.structure,
+      theme: mindProjectInfo?.theme,
+      topics: mindProjectInfo?.topics,
+    });
     instance.centerContent();
   };
 

+ 21 - 5
apps/designer/src/pages/mindmap/components/HeaderToolbar/index.tsx

@@ -5,7 +5,7 @@ import logo from "@/assets/logo.png";
 import { useModel, Icon } from "umi";
 import { addTopic } from "../../mindMap";
 import { TopicType } from "@/enum";
-import { selectTopic } from "@/utils/mindmapHander";
+import { selectTopic, addBorder, addSummary } from "@/utils/mindmapHander";
 
 export default function index() {
   const {
@@ -56,6 +56,16 @@ export default function index() {
     graph && selectTopic(graph, topic);
   };
 
+  // 添加边框
+  const handleAddBorder = () => {
+    addBorder(currentNode.filter(item => item.data.parentId));
+  }
+
+  // 添加概要
+  const handleAddSummary = () => {
+    addSummary(currentNode.filter(item => item.data.parentId))
+  }
+
   // 添加关联线
   const handleAddCorrelation = () => {
     if(!currentNode.length) return;
@@ -160,6 +170,12 @@ export default function index() {
     // },
   ];
 
+  const noParent = useMemo(() => {
+    const nodes = selectedCell?.filter(cell => cell.isNode());
+    return !!(nodes.length && !nodes.find(node => !node.data?.parentId));
+
+  }, [selectedCell]);
+
   return (
     <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>
@@ -244,8 +260,8 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-summary-outline"></i>}
-          disabled={currentNode.length < 2}
-          onClick={onRedo}
+          disabled={!noParent}
+          onClick={handleAddSummary}
         />
       </Tooltip>
 
@@ -253,8 +269,8 @@ export default function index() {
         <Button
           type="text"
           icon={<i className="iconfont icon-waikuang"></i>}
-          disabled={currentNode.length < 2}
-          onClick={onRedo}
+          disabled={!noParent}
+          onClick={handleAddBorder}
         />
       </Tooltip>
     </div>

+ 163 - 36
apps/designer/src/pages/mindmap/mindMap.tsx

@@ -2,25 +2,44 @@ import { StructureType, TopicType } from "@/enum";
 import { MindMapProjectInfo, TopicItem, HierarchyResult } from "@/types";
 import { Graph, Cell, Node } from "@antv/x6";
 import TopicComponent from "@/components/mindMap/Topic";
+import TopicBorder from "@/components/mindMap/Border";
+import SummaryBorder from "@/components/mindMap/SummaryBorder";
 import { topicData } from "@/config/data";
 import { uuid } from "@/utils";
 import { hierarchyMethodMap } from "@/pages/mindmap/hierarchy";
 import { createEdge } from "./edge";
 import { getTheme } from "./theme";
 import { topicMenu } from "@/utils/contentMenu";
+import {
+  cacluculateExtremeValue,
+  getBorderPositionAndSize,
+} from "@/utils/mindmapHander";
 
 /**
  * 渲染思维导图项目
  * @param graph
  */
-export const renderMindMap = (graph: Graph, setMindProjectInfo: () => void) => {
-  const projectInfo = getMindMapProjectByLocal();
-  if (!projectInfo) return;
-  const { topics, pageSetting } = projectInfo;
+export const renderMindMap = ({
+  topics,
+  pageSetting,
+  structure,
+  theme,
+  graph,
+  setMindProjectInfo,
+  returnCells = false
+}: {
+  topics: TopicItem[];
+  pageSetting: MindMapProjectInfo["pageSetting"];
+  structure: StructureType;
+  theme: string;
+  graph: Graph;
+  setMindProjectInfo: (info: MindMapProjectInfo) => void;
+  returnCells?: boolean;
+}) => {
   const cells: Cell[] = [];
   topics.forEach((topic) => {
     // 遍历出层次结构
-    const result: HierarchyResult = hierarchyMethodMap[projectInfo.structure]?.(
+    const result: HierarchyResult = hierarchyMethodMap[structure]?.(
       topic,
       pageSetting
     );
@@ -53,34 +72,53 @@ export const renderMindMap = (graph: Graph, setMindProjectInfo: () => void) => {
           id,
           x: offsetX + x,
           y: offsetY + y,
-          tools: [{
-            name: 'contextmenu',
-            args: {
-              menu: topicMenu,
+          tools: [
+            {
+              name: "contextmenu",
+              args: {
+                menu: topicMenu,
+              },
             },
-          },]
+          ],
         });
+        // 渲染边框
+        if (data.border) {
+          cells.push(
+            createBorderComponent(hierarchyItem, offsetX, offsetY, graph)
+          );
+        }
+        // 渲染概要
+        if (data.summary) {
+          const summaryCells = createSummaryCells(
+            hierarchyItem,
+            data.summary,
+            structure,
+            pageSetting,
+            theme,
+            graph,
+            setMindProjectInfo,
+            offsetX,
+            offsetY
+          );
+          cells.push(...(summaryCells || []));
+        }
         cells.push(node);
         parent && parent.addChild(node);
-        if(data?.links) {
-          cells.push(...data.links.map(item => graph.createEdge(item)));
+        if (data?.links) {
+          cells.push(...data.links.map((item) => graph.createEdge(item)));
         }
 
         if (children) {
           children.forEach((item: HierarchyResult, index) => {
-            const isBracket = [StructureType.leftBracket, StructureType.rightBracket].includes(projectInfo.structure)
+            const isBracket = [
+              StructureType.leftBracket,
+              StructureType.rightBracket,
+            ].includes(structure);
             // 括号图不绘制中间连线
-            if(!isBracket || (index === 0 || index === children.length - 1)) {
-              const edge = createEdge(
-                graph,
-                id,
-                item,
-                projectInfo.structure,
-                projectInfo.theme,
-                {
-                  onlyOneChild: children.length === 1
-                }
-              );
+            if (!isBracket || index === 0 || index === children.length - 1) {
+              const edge = createEdge(graph, id, item, structure, theme, {
+                onlyOneChild: children.length === 1,
+              });
               cells.push(edge);
               node.addChild(edge);
             }
@@ -93,7 +131,7 @@ export const renderMindMap = (graph: Graph, setMindProjectInfo: () => void) => {
 
     traverse(result);
   });
-
+  if(returnCells) return cells;
   const oldCells = graph.getCells();
   // 移除不要的节点及对应的边
   oldCells.forEach((cell) => {
@@ -115,23 +153,105 @@ export const renderMindMap = (graph: Graph, setMindProjectInfo: () => void) => {
       cell.isNode() && updateNode(cell, graph);
     });
   // 添加所需的节点
-  cells
-    .filter((cell) => cell.isEdge())
-    .forEach((cell) => {
-      graph.addCell(cell);
+  const edgeCells = cells.filter((cell) => cell.isEdge());
+  graph.removeCells(edgeCells);
+  graph.addCell(edgeCells);
+};
+
+// 渲染概要
+const createSummaryCells = (
+  hierarchyItem: HierarchyResult, 
+  summary: TopicItem['summary'],
+  structure: StructureType,
+  pageSetting: MindMapProjectInfo['pageSetting'],
+  theme: string,
+  graph: Graph,
+  setMindProjectInfo: (info: MindMapProjectInfo) => void,
+  offsetX: number,
+  offsetY: number
+): Cell[] => {
+  let cells: Cell[] = [];
+  if (summary) {
+    const positionAndSize = cacluculateExtremeValue(
+      hierarchyItem,
+      hierarchyItem.children
+    );
+    const totalHeight = positionAndSize.maxY - positionAndSize.minY;
+    const totalWidth = positionAndSize.maxX - positionAndSize.minX;
+
+    // 概要边框
+    const node = graph.createNode({
+      ...SummaryBorder,
+      data: summary.border,
+      id: summary.topic.id + "-border",
+      zIndex: 0,
+      position: {
+        x: offsetX + positionAndSize.minX - 2,
+        y: offsetY + positionAndSize.minY - 2,
+      },
+      size: {
+        width: totalWidth + 4,
+        height: totalHeight + 4,
+      },
     });
+    cells.push(node);
+
+    // 概要节点
+    cells.push(...renderMindMap({
+      topics: [{
+        ...summary.topic,
+        x: offsetX + hierarchyItem.x + totalWidth + 40,
+        y: offsetY + hierarchyItem.y
+      }],
+      pageSetting,
+      structure,
+      theme,
+      graph,
+      setMindProjectInfo,
+      returnCells: true
+    }) || []);
+  }
+  return cells;
+}
+
+// 创建外框组件
+const createBorderComponent = (
+  hierarchyItem: HierarchyResult,
+  offsetX: number,
+  offsetY: number,
+  graph: Graph
+) => {
+  const positionAndSize = getBorderPositionAndSize(hierarchyItem);
+  return graph.createNode({
+    ...TopicBorder,
+    id: hierarchyItem.id + "-border",
+    data: {
+      ...hierarchyItem.data.border,
+      origin: hierarchyItem.id,
+    },
+    zIndex: 0,
+    position: {
+      x: offsetX + positionAndSize.x,
+      y: offsetY + positionAndSize.y,
+    },
+    size: {
+      width: positionAndSize.width,
+      height: positionAndSize.height,
+    },
+  });
 };
 
 const updateNode = (node: Node, graph: Graph) => {
   const oldCell = graph.getCellById(node.id);
-  if(oldCell.isNode()) {
+  if (oldCell.isNode()) {
     oldCell.setData(node.data);
     oldCell.position(node.position().x, node.position().y);
+    oldCell.setSize(node.size().width, node.size().height);
     // oldCell.setAttrs(node.attrs);
     // const cells = node.children?.map(item => graph.getCellById(item.id));
     // oldCell.setChildren(cells ?? null);
   }
-}
+};
 
 /**
  * 添加分支主题
@@ -150,6 +270,8 @@ export const addTopic = (
     {
       ...(otherData || {}),
       parentId: node?.id,
+      isSummary: node?.data?.isSummary,
+      summarySource: node?.data?.summarySource
     },
     node
   );
@@ -168,6 +290,9 @@ export const addTopic = (
         if (item.children) {
           traverse(item.children);
         }
+        if (item.summary) {
+          traverse([item.summary.topic])
+        }
       });
     };
     traverse(projectInfo?.topics || []);
@@ -239,12 +364,14 @@ export const buildTopic = (
       color: theme[type]?.edge.color,
     },
     ...options,
-    children: (options?.children || topicData.children || []).map((item: TopicItem) => {
-      return {
-        ...item,
-        parentId: id
+    children: (options?.children || topicData.children || []).map(
+      (item: TopicItem) => {
+        return {
+          ...item,
+          parentId: id,
+        };
       }
-    })
+    ),
   };
 };
 

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

@@ -1,5 +1,5 @@
 import { Node } from "@antv/x6";
-import { StructureType, TopicType } from "@/enum";
+import { StructureType, TopicBorderType, TopicType } from "@/enum";
 import { topicData } from "@/config/data";
 export interface CompoundedComponent {
   name: string;
@@ -144,6 +144,35 @@ export type TopicItem = {
    * 主题链接
    */
   linkTopicId?: string;
+  /**
+   * 外框
+   */
+  border?: {
+    line: {
+      width: number;
+      style: 'solid' | 'dashed',
+      color: string;
+    };
+    fill: string;
+    type: TopicBorderType;
+    label: string;
+  }
+  /**
+   * 概要
+   */
+  isSummary?: boolean;
+  summarySource?: string;
+  summary?: {
+    topic: TopicItem;
+    range: [];
+    border: {
+      line: {
+        width: number;
+        color: string;
+      };
+      type: TopicBorderType;
+    }
+  }
 } & cellStyle;
 export interface MindMapProjectInfo{
   name: string;

+ 131 - 15
apps/designer/src/utils/mindmapHander.tsx

@@ -1,6 +1,6 @@
-import { TopicType } from "@/enum";
-import { addTopic, getMindMapProjectByLocal } from "@/pages/mindmap/mindMap";
-import { MindMapProjectInfo, TopicItem } from "@/types";
+import { BorderSize, TopicType } from "@/enum";
+import { addTopic, buildTopic, getMindMapProjectByLocal } from "@/pages/mindmap/mindMap";
+import { HierarchyResult, MindMapProjectInfo, TopicItem } from "@/types";
 import { Cell, Graph, Node, Edge } from "@antv/x6";
 import { message } from "antd";
 import { cloneDeep } from "lodash-es";
@@ -8,6 +8,9 @@ import { uuid } from "@/utils";
 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";
 
 export const selectTopic = (graph: Graph, topic?: TopicItem) => {
   if (topic?.id) {
@@ -138,10 +141,7 @@ export const deleteTopics = (
  * @param graph
  * @param nodes
  */
-export const handleDeleteCurrentTopic = (
-  graph: Graph, 
-  nodes: Node[],
-) => {
+export const handleDeleteCurrentTopic = (graph: Graph, nodes: Node[]) => {
   const mindProjectInfo = getMindMapProjectByLocal();
   if (!mindProjectInfo) return;
 
@@ -157,21 +157,20 @@ export const handleDeleteCurrentTopic = (
               (childNode: TopicItem) => {
                 return {
                   ...childNode,
-                  type: topic.type === TopicType.main ? TopicType.branch : TopicType.sub,
+                  type:
+                    topic.type === TopicType.main
+                      ? TopicType.branch
+                      : TopicType.sub,
                   parentId: topic.id,
                 };
               }
             );
-            (topic.children || []).splice(
-              index,
-              1,
-              ...newChildren
-            );
+            (topic.children || []).splice(index, 1, ...newChildren);
           }
         }
       });
     }
-    console.log(mindProjectInfo)
+    console.log(mindProjectInfo);
     // @ts-ignore
     graph?.extendAttr?.setMindProjectInfo?.(mindProjectInfo);
     localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo));
@@ -367,6 +366,123 @@ export const handleCreateCorrelationEdge = (
   }
 };
 
+export const addBorder = (nodes: Node[]) => {
+  // 判断节点是否在当前存在父级以上节点
+  nodes.forEach((node) => {
+    let hasParent = false;
+    traverseNode(node.data.children || [], (child) => {
+      if (child.id === node.id) {
+        hasParent = true;
+      }
+    });
+
+    // 添加边框数据
+    if (!hasParent && !node.data.border) {
+      node.setData({
+        border: {
+          ...TopicBorder.data,
+        },
+      });
+    }
+  });
+};
+
+/**
+ * 计算当前节点总大小
+ * @param topItem 
+ * @param children 
+ * @returns 
+ */
+export const cacluculateExtremeValue = (topItem: HierarchyResult, children: HierarchyResult[]) => {
+  let minX = topItem.x;
+  let minY = topItem.y;
+  let maxX = minX + topItem.data.width;
+  let maxY = minY + topItem.data.height;
+  children.forEach((child) => {
+    const childXY = cacluculateExtremeValue(child, child.children);
+    minX = Math.min(minX, child.x, childXY.minX);
+    minY = Math.min(minY, child.y, childXY.minY);
+    maxX = Math.max(maxX, child.x + child.data.width, childXY.maxX);
+    maxY = Math.max(maxY, child.y + child.data.height, childXY.maxY);
+  });
+
+  return {
+    minX,
+    minY,
+    maxX,
+    maxY
+  }
+};
+
+/**
+ * 获取边框位置及大小
+ * @param hierarchyItem
+ * @returns
+ */
+export const getBorderPositionAndSize = (hierarchyItem: HierarchyResult) => {
+  const firstChild = hierarchyItem?.children?.[0];
+  let totalHeigth = hierarchyItem?.totalHeight || 0;
+  let totalWidth = hierarchyItem?.totalWidth || 0;
+  let x = hierarchyItem?.x || 0;
+  let y = hierarchyItem?.y || 0;
+
+  // 是否存在子节点
+  if (firstChild) {
+    const position = cacluculateExtremeValue(hierarchyItem, hierarchyItem.children || []);
+    x = position.minX;
+    y = position.minY;
+    totalHeigth = position.maxY - position.minY;
+    totalWidth = position.maxX - position.minX;
+  } else {
+    totalWidth = hierarchyItem.data.width;
+  }
+  return {
+    x: x - 10,
+    y: y - 10,
+    width: totalWidth + 20,
+    height: totalHeigth + 20,
+  };
+};
+
+/**
+ * 添加概要
+ */
+export const addSummary = (nodes: Node[]) => {
+  // 判断节点是否在当前存在父级以上节点
+  nodes.forEach((node) => {
+    let hasParent = false;
+    traverseNode(node.data.children || [], (child) => {
+      if (child.id === node.id) {
+        hasParent = true;
+      }
+    });
+    // 添加边框数据
+    if (!hasParent && !node.data.summary) {
+      const root = buildTopic(node.data.type, {
+        setMindProjectInfo: node.data.setMindProjectInfo,
+        type: TopicType.branch,
+        label: "概要",
+        borderSize: BorderSize.medium,
+        isSummary: true,
+        summarySource: node.id,
+      })
+      node.setData({
+        summary: {
+          topic: root,
+          range: [node.id],
+          border: {
+            ...SummaryBorder.data,
+            origin: root.id,
+            summarySource: node.id,
+          }
+        }
+      }, {
+        deep: false
+      });
+    }
+  });
+}
+
 /**
  * 右键菜单处理方法
  */
@@ -399,7 +515,7 @@ export const mindmapMenuHander = {
     selectTopic(tool.graph, tool.cell.data);
   },
   addHref(tool: ContextMenuTool) {
-    console.log(tool.cell)
+    console.log(tool.cell);
     // @ts-ignore
     tool.cell?.extendAttr?.showHrefConfig?.();
   },