Explorar el Código

feat: 搭建思维导图页面布局

liaojiaxing hace 6 meses
padre
commit
20f04f0eab

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

@@ -6,7 +6,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4676747_lyhefy3bzik.css'
+    '//at.alicdn.com/t/c/font_4676747_e2thfj9vjkt.css'
   ],
   metas: [
     { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
@@ -30,6 +30,8 @@ export default defineConfig({
   routes: [
     { path: "/", component: "home" },
     { path: "/flow", component: "flow" },
+    { path: "/mindmap", component: "mindmap" },
+    { path: "/*", component: '404' }
   ],
   npmClient: 'pnpm'
 });

+ 50 - 0
apps/designer/src/components/mindMap/Topic.tsx

@@ -0,0 +1,50 @@
+import { register } from "@antv/x6-react-shape";
+import { Node } from "@antv/x6";
+import { defaultData } from "@/components/data";
+import { useSizeHook, useShapeProps } from "@/hooks";
+import CustomInput from "../CustomInput";
+const component = ({ node }: { node: Node }) => {
+  const { fill, stroke, opacity, label, text, borderSize } = node.getData();
+  const { size, ref } = useSizeHook();
+  const {
+    fillContent,
+    strokeColor,
+    strokeWidth,
+    strokeDasharray,
+  } = useShapeProps(fill, size, stroke);
+
+  return (
+    <>
+      <div
+        className="relative text-0 w-full h-full"
+        ref={ref}
+        style={{ 
+          opacity: opacity / 100,
+          border: `solid ${strokeWidth}px ${strokeColor}`,
+          background: fillContent,
+          borderRadius: borderSize
+        }}
+      >
+       <CustomInput value={label} node={node} styles={text} />
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "mind-map-topic",
+  width: 206,
+  height: 70,
+  effect: ["data"],
+  component: component,
+});
+
+const baseNode = {
+  shape: "mind-map-topic",
+  data: {
+    label: "",
+    ...defaultData,
+  },
+};
+
+export default baseNode;

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

@@ -17,4 +17,10 @@ export enum LineType {
   dashed = "5,5",
   dotted = "1,5",
   dashdot = "5,5,1,5",
+}
+
+export enum BorderSize {
+  none = 0,
+  medium = 5,
+  large = 30,
 }

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

@@ -0,0 +1,158 @@
+import { cellStyle } from "@/types";
+import { Cell, EventArgs, Graph } from "@antv/x6";
+import { message } from "antd";
+import { useRef, useState } from "react";
+import { Selection } from "@repo/x6-plugin-selection";
+import { Keyboard } from "@antv/x6-plugin-keyboard";
+import { History } from "@antv/x6-plugin-history";
+import { Scroller } from "@antv/x6-plugin-scroller";
+import { ProjectInfo } from "@/types";
+import Topic from "@/components/mindMap/Topic";
+import { BorderSize } from "@/enum";
+
+export default function mindMapModel() {
+  const [rightToobarActive, setRgithtToolbarActive] = useState<
+    "style" | "structure" | "theme" | "icon" | "image" | "tag"
+  >();
+  // 格式刷启用
+  const [enableFormatBrush, setEnableFormatBrush] = useState(false);
+  // 格式刷样式
+  const formatBrushStyle = useRef<cellStyle>();
+  // 脑图实例
+  const graphRef = useRef<Graph>();
+  const [canRedo, setCanRedo] = useState(false);
+  const [canUndo, setCanUndo] = useState(false);
+  // 项目信息
+  const [mindProjectInfo, setMindProjectInfo] = useState({
+    name: "新建脑图",
+    desc: "",
+    version: "",
+    author: "",
+  });
+
+  // 初始化脑图
+  const initMindMap = (container: HTMLElement) => {
+    const instance = new Graph({
+      container,
+      autoResize: true,
+      connecting: {
+        connectionPoint: 'anchor',
+      },
+    });
+
+    instance.use(new Selection());
+    instance.use(new Keyboard());
+    instance.use(new History());
+    instance.use(new Scroller());
+    graphRef.current = instance;
+
+    loadProject();
+
+    instance.on('history:change', () => {
+      setCanRedo(instance.canRedo());
+      setCanUndo(instance.canUndo());
+    })
+  }
+
+  const loadProject = (projectInfo?: ProjectInfo) => {
+    if( !graphRef.current ) return;
+
+    const cells: Cell[] = [];
+    if (projectInfo) {
+      cells.push(...JSON.parse(projectInfo.data));
+    } else {
+      const { offsetHeight, offsetWidth } = document.body;
+      graphRef.current.size;
+      // 默认加载
+      cells.push(graphRef.current?.createNode({
+        ...Topic,
+        root: true,
+        data: {
+          ...Topic.data,
+          fill: {
+            ...Topic.data.fill,
+            color1: "#30304D"
+          },
+          text: {
+            ...Topic.data.text,
+            fontSize: 30,
+            color: "#fff"
+          },
+          label: "未命名文件",
+          borderSize: BorderSize.medium
+        },
+        position: {
+          x: offsetWidth / 2,
+          y: offsetHeight / 2
+        }
+      }));
+    }
+    console.log(cells)
+    graphRef.current.addCell(cells);
+    graphRef.current.centerCell(cells[0]);
+  }
+
+  const handleClick = (args: EventArgs & { cell: Cell }) => {
+    // 取消格式刷
+    if (!args?.cell || args?.cell?.data?.isPage) {
+      formatBrushStyle.current = undefined;
+      setEnableFormatBrush(false);
+      graphRef.current?.off("cell:click", handleClick);
+      graphRef.current?.off("blank:click", handleClick);
+    } else {
+      if (args.cell.data?.lock) return;
+      // 应用格式刷
+      const data = args.cell.data;
+      args.cell.setData({
+        text: formatBrushStyle.current?.text || data?.text,
+        fill: formatBrushStyle.current?.fill || data?.fill,
+        stroke: formatBrushStyle.current?.stroke || data?.stroke,
+        opacity: formatBrushStyle.current?.opacity || data?.opacity,
+      });
+    }
+  };
+
+  // 开启格式刷
+  const toggleFormatBrush = (graph: Graph) => {
+    graphRef.current = graph;
+    const cell = graph?.getSelectedCells()?.find((item) => item.isNode());
+    setEnableFormatBrush((state) => {
+      if (!state) {
+        const data = cell?.getData();
+        formatBrushStyle.current = data;
+        message.info("格式刷已开启");
+        graph.on("cell:click", handleClick);
+        graph.on("blank:click", handleClick);
+      } else {
+        formatBrushStyle.current = undefined;
+        graph.off("cell:click", handleClick);
+        graph.off("blank:click", handleClick);
+      }
+      return !state;
+    });
+  };
+
+  // 撤销
+  const onUndo = () => {
+    graphRef.current?.undo();
+  }
+
+  // 重做
+  const onRedo = () => {
+    graphRef.current?.redo();
+  }
+
+  return {
+    initMindMap,
+    rightToobarActive,
+    setRgithtToolbarActive,
+    mindProjectInfo,
+    setMindProjectInfo,
+    canUndo,
+    canRedo,
+    onUndo,
+    onRedo,
+    enableFormatBrush,
+    toggleFormatBrush,
+  };
+}

+ 19 - 0
apps/designer/src/pages/404/index.tsx

@@ -0,0 +1,19 @@
+import { Result } from 'antd'
+import React from 'react'
+
+export default function index() {
+  return (
+    <Result
+      status="404"
+      title="页面不存在"
+      subTitle="抱歉,您访问的页面不存在。"
+      extra={
+        <a href="/">
+          返回首页
+        </a>
+      }
+    >
+      
+    </Result>
+  )
+}

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

@@ -0,0 +1,846 @@
+import CustomColorPicker from "@/components/CustomColorPicker";
+import {
+  BoldOutlined,
+  ColumnHeightOutlined,
+  FontSizeOutlined,
+  ItalicOutlined,
+  PictureOutlined,
+  StrikethroughOutlined,
+  SwapOutlined,
+  UnderlineOutlined,
+  VerticalAlignBottomOutlined,
+  VerticalAlignMiddleOutlined,
+  VerticalAlignTopOutlined,
+} from "@ant-design/icons";
+import {
+  Button,
+  Col,
+  Divider,
+  Form,
+  Input,
+  InputNumber,
+  Row,
+  Select,
+  Tooltip,
+} from "antd";
+import { arrowOptions } from "@/pages/flow/data";
+import { useModel } from "umi";
+import { useEffect, useRef, useState } from "react";
+import { ImageFillType, ConnectorType, LineType } 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;
+export default function GraphStyle() {
+  const { selectedCell } = useModel("graphModel");
+  const [isMulit, setIsMulit] = useState(false);
+  const [hasEdge, setHasEdge] = useState(false);
+  const eventNodeList = useRef<Cell[]>([]);
+
+  const [formModel, setFormModel] = useState<FormModel>({
+    opacity: 100,
+    width: 20,
+    height: 20,
+    x: 0,
+    y: 0,
+    rotation: 0,
+    text: {
+      fontFamily: "normal",
+      color: "#000000",
+      fontSize: 14,
+      lineHeight: 1.25,
+      textAlign: "center",
+      textVAlign: "middle",
+      bold: false,
+      italic: false,
+      textDecoration: "none",
+    },
+    fill: {
+      fillType: "color",
+      color1: "#FFFFFF",
+      color2: "#eeeeee",
+      gradientType: "linear",
+      gradientValue: 0,
+      objectFit: ImageFillType.Fill,
+      imageUrl: "",
+    },
+    stroke: {
+      type: "solid",
+      color: "#323232",
+      width: 1,
+    },
+    connectorType: ConnectorType.Normal,
+    startArrow: "",
+    endArrow: "",
+  });
+
+  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: "",
+      });
+
+      // 监听当前选中节点的属性变化
+      if (!eventNodeList.current.find((item) => item.id === firstNode.id)) {
+        eventNodeList.current.push(firstNode);
+        firstNode.on("change:*", (args) => {
+          if (args.key === "position") {
+            setFormModel((state) => {
+              return {
+                ...state,
+                x: parseInt(args.current.x),
+                y: parseInt(args.current.y),
+              };
+            });
+          }
+          if (args.key === "size") {
+            setFormModel((state) => {
+              return {
+                ...state,
+                width: args.current.width,
+                height: args.current.height,
+              };
+            });
+          }
+          if (args.key === "data") {
+            setFormModel((state) => {
+              return {
+                ...state,
+                text: args.current.text,
+                fill: args.current.fill,
+                stroke: args.current.stroke,
+              };
+            });
+          }
+        });
+      }
+    }
+
+    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) => {
+    selectedCell?.forEach((cell) => {
+      if (cell.isNode()) {
+        cell.setPosition(model.x, model.y);
+        cell.setSize(model.width, model.height);
+        cell.rotate(model.rotation, { absolute: true });
+        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,
+              }
+            }
+          }
+        })
+      }
+    });
+  };
+
+  // 设置对齐方式
+  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);
+    set(obj, key, value);
+    setFormModel(obj);
+    handleChange(obj);
+  };
+
+  return (
+    <div>
+      <section className="px-16px">
+        <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+          {alignOptionData.map((item) => (
+            <Tooltip key={item.id} title={item.name}>
+              <Button
+                type="text"
+                icon={<i className={"iconfont " + item.icon} />}
+                disabled={!isMulit}
+                onClick={() => handleAlign(item.id)}
+              />
+            </Tooltip>
+          ))}
+        </div>
+        <Form.Item
+          label="不透明度"
+          labelCol={{ span: 14 }}
+          wrapperCol={{ span: 10 }}
+          labelAlign="left"
+          className="mb-8px"
+          colon={false}
+        >
+          <InputNumber
+            className="w-full"
+            step={5}
+            min={0}
+            max={100}
+            formatter={(val) => `${val}%`}
+            disabled={!selectedCell?.length}
+            value={formModel.opacity}
+            onChange={(val) => handleSetFormModel("opacity", val)}
+          />
+        </Form.Item>
+      </section>
+      <Divider className="my-8px" />
+      <section className="px-16px">
+        <div className="font-bold mb-8px">布局</div>
+        <Row gutter={8} className="mb-8px">
+          <Col span={12}>
+            <InputNumber
+              className="w-full"
+              step={1}
+              min={20}
+              max={10000}
+              suffix="W"
+              formatter={(val) => `${val}px`}
+              disabled={!selectedCell?.length}
+              value={formModel.width}
+              onChange={(val) => handleSetFormModel("width", val)}
+            />
+          </Col>
+          <Col span={12}>
+            <InputNumber
+              className="w-full"
+              step={1}
+              min={20}
+              max={10000}
+              suffix="H"
+              formatter={(val) => `${val}px`}
+              disabled={!selectedCell?.length}
+              value={formModel.height}
+              onChange={(val) => handleSetFormModel("height", val)}
+            />
+          </Col>
+        </Row>
+        <Row gutter={8} className="mb-8px">
+          <Col span={12}>
+            <InputNumber
+              className="w-full"
+              step={1}
+              min={0}
+              max={10000}
+              precision={0}
+              suffix="X"
+              formatter={(val) => `${val}px`}
+              disabled={!selectedCell?.length}
+              value={formModel.x}
+              onChange={(val) => handleSetFormModel("x", val)}
+            />
+          </Col>
+          <Col span={12}>
+            <InputNumber
+              className="w-full"
+              step={1}
+              min={0}
+              max={10000}
+              precision={0}
+              suffix="Y"
+              formatter={(val) => `${val}px`}
+              disabled={!selectedCell?.length}
+              value={formModel.y}
+              onChange={(val) => handleSetFormModel("y", val)}
+            />
+          </Col>
+        </Row>
+        <div className="flex justify-between items-center gap-12px mb-8px">
+          <InputNumber
+            className="flex-1"
+            step={1}
+            min={0}
+            max={360}
+            formatter={(val) => `${val}°`}
+            suffix={<i className="iconfont icon-a-ziyuan126 text-12px" />}
+            disabled={!selectedCell?.length}
+            value={formModel.rotation}
+            onChange={(val) => handleSetFormModel("rotation", val)}
+          />
+          <Button
+            icon={<i className="iconfont icon-shangxiafanzhuan" />}
+            disabled={!selectedCell?.length}
+            onClick={() => handleSetFormModel("rotation", 0)}
+          />
+          <Button
+            icon={<i className="iconfont icon-zuoyoufanzhuan" />}
+            disabled={!selectedCell?.length}
+            onClick={() => handleSetFormModel("rotation", 90)}
+          />
+        </div>
+        <Form.Item
+          label="匹配大小"
+          labelCol={{ span: 14 }}
+          wrapperCol={{ span: 10 }}
+          labelAlign="left"
+          className="mb-8px"
+          colon={false}
+        >
+          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+            <Tooltip title="适配宽度">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-kuandu" />}
+                disabled={!isMulit}
+                onClick={() => handleMatchSize("width")}
+              />
+            </Tooltip>
+            <Tooltip title="适配高度">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-gaodu" />}
+                disabled={!isMulit}
+                onClick={() => handleMatchSize("height")}
+              />
+            </Tooltip>
+            <Tooltip title="适配宽高">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-shiyingkuangao" />}
+                disabled={!isMulit}
+                onClick={() => handleMatchSize("auto")}
+              />
+            </Tooltip>
+          </div>
+        </Form.Item>
+      </section>
+      <Divider className="my-8px" />
+      <section className="px-16px">
+        <div className="font-bold mb-8px">文本</div>
+        <div className="flex items-center  gap-12px mb-8px">
+          <Select
+            className="flex-1"
+            disabled={!selectedCell?.length}
+            options={fontFamilyOptions}
+            value={formModel.text.fontFamily}
+            labelRender={(item) => item.value}
+            onChange={(val) => handleSetFormModel("text.fontFamily", val)}
+          />
+          <CustomColorPicker
+            disabled={!selectedCell?.length}
+            color={formModel.text.color}
+            onChange={(color) => handleSetFormModel("text.color", color)}
+          />
+        </div>
+        <div className="flex items-center gap-12px mb-8px">
+          <InputNumber
+            className="flex-1"
+            prefix={<FontSizeOutlined />}
+            step={1}
+            min={12}
+            max={10000}
+            formatter={(val) => `${val}px`}
+            disabled={!selectedCell?.length}
+            value={formModel.text.fontSize}
+            onChange={(val) => handleSetFormModel("text.fontSize", val)}
+          />
+          <Select
+            className="flex-1"
+            suffixIcon={<ColumnHeightOutlined />}
+            options={[
+              { label: "1.0", value: 1 },
+              { label: "1.25", value: 1.25 },
+              { label: "1.5", value: 1.5 },
+              { label: "2.0", value: 2 },
+              { label: "2.5", value: 2.5 },
+              { label: "3.0", value: 3 },
+            ]}
+            disabled={!selectedCell?.length}
+            value={formModel.text.lineHeight}
+            onChange={(val) => handleSetFormModel("text.lineHeight", val)}
+          />
+        </div>
+        <div className="flex items-center gap-12px mb-8px">
+          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+            <Tooltip title="左对齐">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-zuoduiqi" />}
+                className={formModel.text.textAlign === "left" ? "active" : ""}
+                disabled={!selectedCell?.length}
+                onClick={() => handleSetFormModel("text.textAlign", "left")}
+              />
+            </Tooltip>
+            <Tooltip title="居中">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-juzhong" />}
+                className={
+                  formModel.text.textAlign === "center" ? "active" : ""
+                }
+                disabled={!selectedCell?.length}
+                onClick={() => handleSetFormModel("text.textAlign", "center")}
+              />
+            </Tooltip>
+            <Tooltip title="右对齐">
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-youduiqi" />}
+                className={formModel.text.textAlign === "right" ? "active" : ""}
+                disabled={!selectedCell?.length}
+                onClick={() => handleSetFormModel("text.textAlign", "right")}
+              />
+            </Tooltip>
+          </div>
+          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+            <Tooltip title="顶部对齐">
+              <Button
+                type="text"
+                icon={<VerticalAlignTopOutlined />}
+                disabled={!selectedCell?.length}
+                className={formModel.text.textVAlign === "top" ? "active" : ""}
+                onClick={() => handleSetFormModel("text.textVAlign", "top")}
+              />
+            </Tooltip>
+            <Tooltip title="垂直居中">
+              <Button
+                type="text"
+                icon={<VerticalAlignMiddleOutlined />}
+                className={
+                  formModel.text.textVAlign === "middle" ? "active" : ""
+                }
+                disabled={!selectedCell?.length}
+                onClick={() => handleSetFormModel("text.textVAlign", "middle")}
+              />
+            </Tooltip>
+            <Tooltip title="底部对齐">
+              <Button
+                type="text"
+                icon={<VerticalAlignBottomOutlined />}
+                className={
+                  formModel.text.textVAlign === "bottom" ? "active" : ""
+                }
+                disabled={!selectedCell?.length}
+                onClick={() => handleSetFormModel("text.textVAlign", "bottom")}
+              />
+            </Tooltip>
+          </div>
+        </div>
+        <div className="flex items-center gap-12px mb-8px">
+          <div className="bg-white rounded-s flex justify-between items-center mb-8px">
+            <Tooltip placement="bottom" title="字体加粗">
+              <Button
+                type="text"
+                icon={<BoldOutlined />}
+                className={formModel.text.bold ? "active" : ""}
+                disabled={!selectedCell?.length}
+                onClick={() =>
+                  handleSetFormModel("text.bold", !formModel.text.bold)
+                }
+              />
+            </Tooltip>
+            <Tooltip placement="bottom" title="字体倾斜">
+              <Button
+                type="text"
+                icon={<ItalicOutlined />}
+                className={formModel.text.italic ? "active" : ""}
+                disabled={!selectedCell?.length}
+                onClick={() =>
+                  handleSetFormModel("text.italic", !formModel.text.italic)
+                }
+              />
+            </Tooltip>
+            <Tooltip placement="bottom" title="下划线">
+              <Button
+                type="text"
+                icon={<UnderlineOutlined />}
+                className={
+                  formModel.text.textDecoration === "underline" ? "active" : ""
+                }
+                disabled={!selectedCell?.length}
+                onClick={() =>
+                  handleSetFormModel(
+                    "text.textDecoration",
+                    formModel.text.textDecoration === "underline"
+                      ? "none"
+                      : "underline"
+                  )
+                }
+              />
+            </Tooltip>
+            <Tooltip placement="bottom" title="中划线">
+              <Button
+                type="text"
+                icon={<StrikethroughOutlined />}
+                className={
+                  formModel.text.textDecoration === "line-through"
+                    ? "active"
+                    : ""
+                }
+                disabled={!selectedCell?.length}
+                onClick={() =>
+                  handleSetFormModel(
+                    "text.textDecoration",
+                    formModel.text.textDecoration === "line-through"
+                      ? "none"
+                      : "line-through"
+                  )
+                }
+              />
+            </Tooltip>
+          </div>
+        </div>
+      </section>
+      <Divider className="my-8px" />
+      <section className="px-16px">
+        <div className="font-bold mb-8px">填充</div>
+        <div className="flex items-center gap-12px mb-8px">
+          <Select
+            className="flex-1"
+            options={[
+              { label: "纯色", value: "color" },
+              { label: "渐变", value: "gradient" },
+              { label: "图片", value: "image" },
+            ]}
+            disabled={!selectedCell?.length}
+            value={formModel.fill.fillType}
+            onChange={(value) => handleSetFormModel("fill.fillType", value)}
+          />
+          <div className="flex items-center gap-12px">
+            <CustomColorPicker
+              disabled={!selectedCell?.length}
+              color={formModel.fill.color1}
+              onChange={(color) => handleSetFormModel("fill.color1", color)}
+            />
+            {formModel.fill.fillType === "gradient" && (
+              <>
+                <Button
+                  icon={<SwapOutlined />}
+                  disabled={!selectedCell?.length}
+                  onClick={handleSwapColor}
+                />
+                <CustomColorPicker
+                  disabled={!selectedCell?.length}
+                  color={formModel.fill.color2}
+                  onChange={(color) => handleSetFormModel("fill.color2", color)}
+                />
+              </>
+            )}
+          </div>
+        </div>
+        {formModel.fill.fillType === "image" && (
+          <>
+            <Form.Item
+              label="图片地址"
+              labelCol={{ span: 14 }}
+              wrapperCol={{ span: 10 }}
+              labelAlign="left"
+              className="mb-8px"
+              colon={false}
+            >
+              <Input
+                placeholder="图片地址"
+                disabled={!selectedCell?.length}
+                suffix={<PictureOutlined />}
+                value={formModel.fill.imageUrl}
+                onChange={(e) =>
+                  handleSetFormModel("fill.imageUrl", e.target.value)
+                }
+              />
+            </Form.Item>
+            <Form.Item
+              label="填充方式"
+              labelCol={{ span: 14 }}
+              wrapperCol={{ span: 10 }}
+              labelAlign="left"
+              className="mb-8px"
+              colon={false}
+            >
+              <Select
+                options={[
+                  { value: ImageFillType.Fill, label: "填充" },
+                  { value: ImageFillType.Auto, label: "自适应" },
+                  { value: ImageFillType.Stretch, label: "按图形伸展" },
+                  { value: ImageFillType.Original, label: "原始尺寸" },
+                  { value: ImageFillType.Tiled, label: "平铺" },
+                ]}
+                disabled={!selectedCell?.length}
+                value={formModel.fill.objectFit}
+                onChange={(value) =>
+                  handleSetFormModel("fill.objectFit", value)
+                }
+              />
+            </Form.Item>
+          </>
+        )}
+        {formModel.fill.fillType === "gradient" && (
+          <div className="flex items-center gap-12px">
+            <Select
+              className="flex-1"
+              options={[
+                { value: "linear", label: "线性渐变" },
+                { value: "radial", label: "径向渐变" },
+              ]}
+              disabled={!selectedCell?.length}
+              value={formModel.fill.gradientType}
+              onChange={(value) => {
+                handleSetFormModel("fill.gradientType", value);
+              }}
+            />
+            {formModel.fill.fillType === "gradient" && (
+              <>
+                {formModel.fill.gradientType === "linear" && (
+                  <InputNumber
+                    className="flex-1"
+                    step={1}
+                    min={0}
+                    max={360}
+                    formatter={(val) => `${val}°`}
+                    suffix={
+                      <i className="iconfont icon-a-ziyuan126 text-12px" />
+                    }
+                    disabled={!selectedCell?.length}
+                    value={formModel.fill.gradientValue}
+                    onChange={(val) =>
+                      handleSetFormModel("fill.gradientValue", val)
+                    }
+                  />
+                )}
+                {formModel.fill.gradientType === "radial" && (
+                  <InputNumber
+                    className="flex-1"
+                    step={1}
+                    min={0}
+                    max={100}
+                    formatter={(val) => `${val}%`}
+                    suffix={
+                      <i className="iconfont icon-jingxiangjianbian text-12px" />
+                    }
+                    disabled={!selectedCell?.length}
+                    value={formModel.fill.gradientValue}
+                    onChange={(val) =>
+                      handleSetFormModel("fill.gradientValue", val)
+                    }
+                  />
+                )}
+              </>
+            )}
+          </div>
+        )}
+      </section>
+      <Divider className="my-8px" />
+      <section className="px-16px">
+        <div className="font-bold mb-8px">线条</div>
+        <div className="flex gap-12px mb-8px">
+          <Select
+            className="flex-1"
+            options={[
+              { label: "实线", value: "solid" },
+              { label: "虚线", value: "dashed" },
+              { label: "点线", value: "dotted" },
+              { label: "点划线", value: "dashdot" },
+            ]}
+            disabled={!selectedCell?.length}
+            value={formModel.stroke.type}
+            onChange={(value) => handleSetFormModel("stroke.type", value)}
+          />
+          <CustomColorPicker
+            disabled={!selectedCell?.length}
+            color={formModel.stroke.color}
+            onChange={(color) => handleSetFormModel("stroke.color", color)}
+          />
+        </div>
+        <div className="flex gap-12px mb-8px">
+          <InputNumber
+            className="flex-1"
+            step={1}
+            min={1}
+            max={10}
+            formatter={(val) => `${val}px`}
+            disabled={!selectedCell?.length}
+            value={formModel.stroke.width}
+            onChange={(val) => handleSetFormModel("stroke.width", val)}
+          />
+          <div className="bg-white rounded-s flex justify-between items-center">
+            <Button
+              type="text"
+              icon={
+                <i className="iconfont icon-a-icon16lianxianleixinghuizhilianxian" />
+              }
+              className={
+                formModel.connectorType === ConnectorType.Rounded
+                  ? "active"
+                  : ""
+              }
+              disabled={!hasEdge}
+              onClick={() =>
+                handleSetFormModel("connectorType", ConnectorType.Rounded)
+              }
+            />
+            <Button
+              type="text"
+              icon={
+                <i className="iconfont icon-a-icon16lianxianleixingbeisaierquxian" />
+              }
+              className={
+                formModel.connectorType === ConnectorType.Smooth ? "active" : ""
+              }
+              disabled={!hasEdge}
+              onClick={() =>
+                handleSetFormModel("connectorType", ConnectorType.Smooth)
+              }
+            />
+            <Button
+              type="text"
+              icon={
+                <i className="iconfont icon-a-icon16lianxianleixinghuizhizhixian" />
+              }
+              className={
+                formModel.connectorType === ConnectorType.Normal ? "active" : ""
+              }
+              disabled={!hasEdge}
+              onClick={() =>
+                handleSetFormModel("connectorType", ConnectorType.Normal)
+              }
+            />
+          </div>
+        </div>
+        <Form.Item
+          label="箭头类型"
+          labelAlign="left"
+          colon={false}
+          labelCol={{ span: 6 }}
+          wrapperCol={{ span: 18 }}
+        >
+          <div className="flex gap-12px items-center">
+            <Select
+              options={arrowOptions.map((item) => {
+                return {
+                  value: item.name,
+                  label: <img className="w-12px mx-50%" src={item.icon} />,
+                };
+              })}
+              disabled={!hasEdge}
+              value={formModel.startArrow}
+              onChange={(value) => handleSetFormModel("startArrow", value)}
+            />
+            <Select
+              options={arrowOptions.map((item) => {
+                return {
+                  value: item.name,
+                  label: <img className="w-12px mx-50%" src={item.icon} />,
+                };
+              })}
+              disabled={!hasEdge}
+              value={formModel.endArrow}
+              onChange={(value) => handleSetFormModel("endArrow", value)}
+            />
+          </div>
+        </Form.Item>
+      </section>
+    </div>
+  );
+}

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

@@ -0,0 +1,118 @@
+import React, { useEffect, useState } from "react";
+import CustomColorPicker from "@/components/CustomColorPicker";
+import {
+  Checkbox,
+  InputNumber,
+  Select,
+  Input,
+  Divider,
+} from "antd";
+import { useModel } from "umi";
+
+export default function PageStyle() {
+  const { pageState, onChangePageSettings } = useModel("appModel");
+
+  return (
+    <div>
+      <section className="px-16px">
+        <div className="flex-1 font-bold">背景样式</div>
+        <div className="flex items-center justify-between px-0px py-8px gap-16px">
+          <Select
+            className="flex-1"
+            options={[
+              { label: "纯色", value: 1 },
+              { label: "图片", value: 2 },
+            ]}
+          />
+          <CustomColorPicker
+            color={pageState.backgroundColor}
+            onChange={(color) => onChangePageSettings("backgroundColor", color)}
+          />
+        </div>
+        <div className="flex justify-between items-center gap-16px">
+          <span>图片地址</span>
+          <Input
+            className="flex-1"
+            placeholder="请输入图片地址"
+          />
+        </div>
+        <Divider className="my-8px" />
+        <div className="flex-1 font-bold">主题间距</div>
+        <div className="text-12px color-#6c7d8f my-4px">分支主题</div>
+        <div className="flex justify-between gap-8px">
+          <InputNumber
+            className="flex-1"
+            value={pageState.width}
+            suffix="W"
+            step={1}
+            min={10}
+            max={2000}
+            formatter={(val) => `${val}px`}
+            onChange={(val) => onChangePageSettings("width", val)}
+          />
+          <InputNumber
+            className="flex-1"
+            value={pageState.width}
+            suffix="W"
+            step={1}
+            min={10}
+            max={2000}
+            formatter={(val) => `${val}px`}
+            onChange={(val) => onChangePageSettings("width", val)}
+          />
+        </div>
+        <div className="text-12px color-#6c7d8f my-4px">子主题</div>
+        <div className="flex justify-between gap-8px">
+          <InputNumber
+            className="flex-1"
+            value={pageState.width}
+            suffix="W"
+            step={1}
+            min={10}
+            max={2000}
+            formatter={(val) => `${val}px`}
+            onChange={(val) => onChangePageSettings("width", val)}
+          />
+          <InputNumber
+            className="flex-1"
+            value={pageState.width}
+            suffix="W"
+            step={1}
+            min={10}
+            max={2000}
+            formatter={(val) => `${val}px`}
+            onChange={(val) => onChangePageSettings("width", val)}
+          />
+        </div>
+      </section>
+      <section className="px-16px my-8px">
+        <div>
+          <Checkbox
+            checked={pageState.grid}
+            onChange={(e) => onChangePageSettings("grid", e.target.checked)}
+          >
+            <span className="font-bold">同级主题对齐</span>
+          </Checkbox>
+        </div>
+      </section>
+      <Divider className="my-8px" />
+      <section className="px-16px">
+        <Checkbox
+          className="mb-8px"
+          checked={pageState.watermark}
+          onChange={(e) => onChangePageSettings("watermark", e.target.checked)}
+        >
+          <span className="font-bold">显示水印</span>
+        </Checkbox>
+        <Input
+          value={pageState.watermarkText}
+          placeholder="不超过15字,回车保存"
+          maxLength={15}
+          onChange={(e) =>
+            onChangePageSettings("watermarkText", e.target.value)
+          }
+        />
+      </section>
+    </div>
+  );
+}

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

@@ -0,0 +1,37 @@
+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"
+
+InsertCss(`
+  .shalu-tabs-nav {
+    padding: 0 16px
+  }
+`)
+export default function index() {
+  const { rightToobarActive } = useModel("mindMapModel");
+  return (
+    <div>
+      {rightToobarActive === "style" && (
+        <>
+          <Tabs
+            items={[
+              {
+                key: "1",
+                label: "页面样式",
+                children: <PageStyle />,
+              },
+              {
+                key: "2",
+                label: "主题样式",
+                children: <NodeStyle/>,
+              },
+            ]}
+          />
+        </>
+      )}
+    </div>
+  );
+}

+ 99 - 0
apps/designer/src/pages/mindmap/components/Footer/index.tsx

@@ -0,0 +1,99 @@
+import { CompressOutlined, ExpandOutlined, MinusOutlined, PlusOutlined, QuestionCircleFilled } from "@ant-design/icons";
+import { Button, ConfigProvider, Divider, Slider, Tooltip } from "antd";
+import React, { useEffect, useRef, useState } from "react";
+import { useFullscreen } from "ahooks";
+import { useModel } from "umi";
+import { MiniMap } from '@antv/x6-plugin-minimap';
+import insertCss from 'insert-css';
+
+insertCss(`
+  .navigation-view {
+    position: absolute;
+    bottom: 32px;
+    right: 32px;
+    width: 330px;
+    height: 210px;
+    background-color: #fff;
+    border: 1px solid #e9edf2;
+    border-radius: 8px 8px 0 0;
+    overflow: hidden;
+    z-index: 1;
+    box-shadow: 0 4px 10px 1px rgba(0, 0, 0, .1);
+  }`);
+export default function Footer() {
+  const [isFullscreen, { toggleFullscreen }] = useFullscreen(document.body);
+  const navigationViewRef = useRef(null);
+  const { graph, selectedCell } = useModel("graphModel");
+  const [showNavigation, setShowNavigation] = useState(false);
+  const [countCell, setCountCell] = useState(0);
+  const [scale, setScale] = useState(100);
+
+  useEffect(() => {
+    if(!graph || !navigationViewRef.current) return;
+
+    graph.use(
+      new MiniMap({
+        container: navigationViewRef.current,
+        width: 330,
+        height: 210,
+        padding: 10,
+      }),
+    )
+  }, [graph, navigationViewRef.current]);
+
+  useEffect(() => {
+    graph?.on('cell:change:*', () => {
+      const count = graph?.getCells().length || 0;
+      setCountCell(count > 0 ? count - 1 : 0)
+    });
+    graph?.on('scale', (scaleInfo) => {
+      setScale(parseInt(scaleInfo.sx * 100 + ''));
+    })
+  }, [graph]);
+
+  const handleZoom = (value: number) => {
+    graph?.zoomTo(value / 100)
+  }
+
+  const handleZoomFit = () => {
+    graph?.zoomToFit({ })
+  }
+
+  const handleOnChange = (value: number) => {
+    setScale(value);
+    handleZoom(value)
+  }
+
+  return (
+    <ConfigProvider componentSize="small">
+      <div className="absolute w-full h-24px left-0 bottom-0 bg-white flex justify-between items-center px-16px">
+        <div className="footer-left"></div>
+        <div className="footer-right flex items-center">
+          <div>字数:{selectedCell?.length}/{countCell}</div>
+          <div>主题数:{selectedCell?.length}/{countCell}</div>
+          <Divider type="vertical" />
+          <Tooltip title="模板">
+            <Button type="text" icon={<i className="iconfont icon-buju" />} />
+          </Tooltip>
+          <Tooltip title={showNavigation ? "关闭视图导航" : "显示视图导航"}>
+            <Button type="text" icon={<i className="iconfont icon-map" />} onClick={() => setShowNavigation(!showNavigation)}/>
+          </Tooltip>
+          <div className="navigation-view" style={{display: showNavigation ? 'block' : 'none'}} ref={navigationViewRef}></div>
+          <Button type="text" icon={<MinusOutlined/>} onClick={() => handleZoom( scale - 2)}/>
+          <Slider min={20} max={200} className="w-120px m-0 mx-8px" tooltip={{formatter: (val) => `${val}%`}} value={scale} onChange={handleOnChange}/>
+          <Button type="text" icon={<PlusOutlined/>} onClick={() => handleZoom( scale + 2)}/>
+          <Tooltip title="重置缩放">
+            <div className="cursor-pointer mx-8px w-40px" onClick={handleZoomFit}>{scale}%</div>
+          </Tooltip>
+          {
+            isFullscreen 
+            ? <Button type="text" icon={<CompressOutlined/>} onClick={toggleFullscreen}/>
+            : <Button type="text" icon={<ExpandOutlined/>} onClick={toggleFullscreen}/>
+          }
+          <Divider type="vertical" />
+          <Button type="text" icon={<QuestionCircleFilled/>} />
+        </div>
+      </div>
+    </ConfigProvider>
+  );
+}

+ 230 - 0
apps/designer/src/pages/mindmap/components/HeaderToolbar/index.tsx

@@ -0,0 +1,230 @@
+import React 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";
+
+export default function index() {
+  const {
+    mindProjectInfo,
+    setMindProjectInfo,
+    canRedo,
+    canUndo,
+    onRedo,
+    onUndo,
+    enableFormatBrush,
+    toggleFormatBrush,
+  } = useModel("mindMapModel");
+
+  // 创建新作品 todo
+  const createNew = (type: string) => {};
+  // 预览 todo
+  const handlePreview = () => {};
+
+  // 保存 todo
+  const handleSave = () => {};
+
+  // 克隆 todo
+  const handleClone = () => {};
+
+  // 历史记录 todo
+  const handleHistory = () => {};
+
+  // 查找替换
+  const handleReplace = () => {};
+
+  const menuData: MenuProps["items"] = [
+    {
+      key: "1",
+      label: "新建",
+      icon: <i className="w-20px iconfont icon-zengjiahuamian-1" />,
+      children: [
+        {
+          key: "1-1-1",
+          label: "流程图",
+          icon: <Icon width="20" className="mt-5px" icon="local:flow" />,
+          onClick: () => {
+            createNew("flow");
+          },
+        },
+        {
+          key: "1-1-2",
+          label: "思维导图",
+          icon: <Icon width="20" className="mt-5px" icon="local:mind" />,
+          onClick: () => {
+            createNew("mind");
+          },
+        },
+        {
+          key: "1-1-3",
+          label: "UML",
+          icon: <Icon width="20" className="mt-5px" icon="local:uml" />,
+          onClick: () => {
+            createNew("uml");
+          },
+        },
+        {
+          key: "1-1-4",
+          label: "网络拓扑图",
+          icon: <Icon width="20" className="mt-5px" icon="local:net" />,
+          onClick: () => {
+            createNew("net");
+          },
+        },
+        {
+          key: "1-1-5",
+          label: "组织结构图",
+          icon: <Icon width="20" className="mt-5px" icon="local:flow" />,
+          onClick: () => {
+            createNew("org");
+          },
+        },
+        {
+          key: "1-1-6",
+          label: "BPMN",
+          icon: <Icon width="20" className="mt-5px" icon="local:bpmn" />,
+          onClick: () => {
+            createNew("bpmn");
+          },
+        },
+      ],
+    },
+    {
+      key: "2",
+      label: "预览",
+      icon: <i className="w-20px iconfont icon-yulan" />,
+      onClick: handlePreview,
+    },
+    {
+      key: "3",
+      label: (
+        <div className="flex justify-between">
+          <span>保存</span>
+          <span>Ctrl+S</span>
+        </div>
+      ),
+      icon: <i className="w-20px iconfont icon-shangchuan" />,
+      onClick: handleSave,
+    },
+    {
+      key: "4",
+      label: "克隆",
+      icon: <i className="w-20px iconfont icon-niantie-1" />,
+      onClick: handleClone,
+    },
+    {
+      key: "5",
+      label: "查找替换",
+      icon: <i className="w-20px iconfont icon-lishijilu" />,
+      onClick: handleReplace,
+    },
+    {
+      key: "6",
+      label: "历史记录",
+      icon: <i className="w-20px iconfont icon-lishijilu" />,
+      onClick: handleHistory,
+    },
+    {
+      key: "1-8",
+      type: "divider",
+    },
+  ];
+
+  return (
+    <div className="absolute absolute top-8px left-8px bg-white shadow-md flex items-center gap-4px boder-radius-4px px-8px">
+      <Button type="text" icon={<LeftOutlined />}></Button>
+
+      <Dropdown
+        trigger={["hover"]}
+        menu={{ items: menuData, style: { width: 200 } }}
+      >
+        <div className="flex items-center gap-8px">
+          <img className="w-32px" src={logo} />
+          <MenuOutlined />
+        </div>
+      </Dropdown>
+
+      <div className="flex flex-col leading-32px">
+        <Input
+          className="text-16px max-w-100px"
+          variant="borderless"
+          value={mindProjectInfo.name}
+          onChange={(e) =>
+            setMindProjectInfo((state) => ({ ...state, name: e.target.value }))
+          }
+        />
+      </div>
+
+      <Tooltip placement="bottom" title="撤销">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-undo"></i>}
+          disabled={!canUndo}
+          onClick={onUndo}
+        />
+      </Tooltip>
+
+      <Tooltip placement="bottom" title="恢复">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-redo"></i>}
+          disabled={!canRedo}
+          onClick={onRedo}
+        />
+      </Tooltip>
+
+      <Tooltip
+        placement="bottom"
+        title={`格式刷${enableFormatBrush ? "生效中按ESC取消" : "(Ctrl+Shift+C)"}`}
+      >
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-geshishua"></i>}
+          // disabled={!selectedCell?.length}
+          // className={
+          //   enableFormatBrush && selectedCell?.length ? "active" : ""
+          // }
+          // onClick={() => graph && toggleFormatBrush(graph)}
+        />
+      </Tooltip>
+
+      <Divider type="vertical" />
+
+      <Tooltip placement="bottom" title="子主题">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-zizhuti"></i>}
+          disabled={!canRedo}
+          onClick={onRedo}
+        />
+      </Tooltip>
+
+      <Tooltip placement="bottom" title="关联线">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-guanlianxian"></i>}
+          disabled={!canRedo}
+          onClick={onRedo}
+        />
+      </Tooltip>
+
+      <Tooltip placement="bottom" title="概要">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-summary-outline"></i>}
+          disabled={!canRedo}
+          onClick={onRedo}
+        />
+      </Tooltip>
+
+      <Tooltip placement="bottom" title="外框">
+        <Button
+          type="text"
+          icon={<i className="iconfont icon-waikuang"></i>}
+          disabled={!canRedo}
+          onClick={onRedo}
+        />
+      </Tooltip>
+    </div>
+  );
+}

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

@@ -0,0 +1,69 @@
+import React from "react";
+import { Button, Divider, Tooltip } from "antd";
+import {
+  ApartmentOutlined,
+  FileImageOutlined,
+  SmileOutlined,
+  StarOutlined,
+  SwapOutlined,
+  TagOutlined,
+} from "@ant-design/icons";
+import { useModel } from "umi";
+export default function index() {
+  const { rightToobarActive, setRgithtToolbarActive } =
+    useModel("mindMapModel");
+
+  return (
+    <div className="absolute top-8px right-8px bg-white shadow-md boder-radius-4px flex flex-col p-8px">
+      <Tooltip placement="bottom" title="样式">
+        <Button
+          type="text"
+          icon={<SwapOutlined />}
+          className={rightToobarActive === "style" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("style")}
+        />
+      </Tooltip>
+      <Tooltip placement="bottom" title="结构">
+        <Button
+          type="text"
+          icon={<ApartmentOutlined />}
+          className={rightToobarActive === "structure" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("structure")}
+        />
+      </Tooltip>
+      <Tooltip placement="bottom" title="风格">
+        <Button
+          type="text"
+          icon={<StarOutlined />}
+          className={rightToobarActive === "theme" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("theme")}
+        />
+      </Tooltip>
+      <Divider className="my-8px" />
+      <Tooltip placement="bottom" title="图标">
+        <Button
+          type="text"
+          icon={<SmileOutlined />}
+          className={rightToobarActive === "icon" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("icon")}
+        />
+      </Tooltip>
+      <Tooltip placement="bottom" title="图片">
+        <Button
+          type="text"
+          icon={<FileImageOutlined />}
+          className={rightToobarActive === "image" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("image")}
+        />
+      </Tooltip>
+      <Tooltip placement="bottom" title="标签">
+        <Button
+          type="text"
+          icon={<TagOutlined />}
+          className={rightToobarActive === "tag" ? "active" : ""}
+          onClick={() => setRgithtToolbarActive("tag")}
+        />
+      </Tooltip>
+    </div>
+  );
+}

+ 9 - 0
apps/designer/src/pages/mindmap/index.less

@@ -0,0 +1,9 @@
+@import url("@/global.less");
+
+.container {
+  background: #fff;
+}
+
+.sider {
+  background: @background-color !important;
+}

+ 37 - 5
apps/designer/src/pages/mindmap/index.tsx

@@ -1,9 +1,41 @@
-import React from 'react'
+import React, { useEffect, useRef } from "react";
+import { Layout, ConfigProvider } from "antd";
+import styles from "./index.less";
+import RightToolbar from "./components/RightToolbar";
+import HeaderToolbar from "./components/HeaderToolbar"
+import { useModel } from "umi";
+import Footer from "./components/Footer";
+import Config from "./components/Config";
 
 export default function MindMap() {
+  const { rightToobarActive, initMindMap } = useModel("mindMapModel");
+  const graphRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    graphRef.current && initMindMap(graphRef.current);
+  }, []);
+
   return (
-    <div>
-      思维导图
-    </div>
-  )
+    <ConfigProvider
+      // componentSize="small"
+      prefixCls="shalu"
+      theme={{
+        token: {
+          colorPrimary: "#1890ff",
+        },
+      }}
+    >
+      <Layout className="w-100vw h-100vh">
+        <Layout.Content className={styles.container + " relative"}>
+          <div ref={graphRef} className="w-full h-full"></div>
+          <HeaderToolbar/>
+          <RightToolbar />
+          <Footer />
+        </Layout.Content>
+        <Layout.Sider className={styles.sider} width={rightToobarActive ? 280 : 0}>
+          <Config />
+        </Layout.Sider>
+      </Layout>
+    </ConfigProvider>
+  );
 }

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

@@ -43,4 +43,18 @@ export interface cellStyle {
     color: string;
     width: number;
   };
+}
+
+export interface ProjectInfo{
+  name: string;
+  desc: string;
+  version: string;
+  author: string;
+  data: string;
+}
+
+export interface TopicItem {
+  id: string;
+  label: string;
+  children: TopicItem[];
 }