Pārlūkot izejas kodu

feat: 添加右键菜单

liaojiaxing 7 mēneši atpakaļ
vecāks
revīzija
e4214fb331

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

@@ -5,7 +5,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4676747_7deqt42stxk.css'
+    '//at.alicdn.com/t/c/font_4676747_k7eyqt2247l.css'
   ],
   scripts: [
     // 字体加载

+ 64 - 0
apps/designer/src/components/base.tsx

@@ -0,0 +1,64 @@
+import { CompoundedComponent } from "@/types";
+import { register } from "@antv/x6-react-shape";
+import { Node } from "@antv/x6";
+import { ports, defaultData } from "./data";
+import CustomInput from "./CustomInput";
+import { useSizeHook, useShapeProps } from "@/hooks";
+const component = ({ node }: { node: Node }) => {
+  const { fill, stroke, opacity } = node.getData();
+  const { size, ref } = useSizeHook();
+  const {
+    fillContent,
+    defsContent,
+    strokeColor,
+    strokeWidth,
+    strokeDasharray,
+  } = useShapeProps(fill, size, stroke);
+
+  return (
+    <>
+      <div
+        className="relative text-0 w-full h-full"
+        ref={ref}
+        style={{ opacity: opacity / 100 }}
+      >
+        <svg
+          className="w-full h-full"
+          viewBox={`0 0 ${size?.width} ${size?.height}`}
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <defs>{defsContent}</defs>
+          <rect
+            x={strokeWidth}
+            y={strokeWidth}
+            width={size?.width - 4}
+            height={size?.height - 4}
+            fill={fillContent}
+            stroke={strokeColor}
+            strokeDasharray={strokeDasharray}
+            strokeWidth={strokeWidth}
+          />
+        </svg>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "custom-react-base",
+  width: 100,
+  height: 100,
+  effect: ["data"],
+  component: component,
+});
+
+const baseNode = {
+    shape: "custom-react-base",
+    data: {
+      label: "",
+      ...defaultData,
+    },
+    ports,
+  };
+
+export default baseNode;

+ 5 - 9
apps/designer/src/hooks/useShapeProps.tsx

@@ -40,15 +40,11 @@ export function useShapeProps(
   const fillContent =
     fillType === "color"
       ? color1
-      : fillType === "gradient"
-        ? `url(#${id})`
-        : fillType === "image"
-          ? `url(#${objectFit})`
-          : "";
+      : `url(#${id})`;
 
   const defsContent = (
     <>
-      {gradientType === "linear" && (
+      {fillType === "color" && gradientType === "linear" && (
         <linearGradient
           id={id}
           gradientTransform={`rotate(${gradientValue})`}
@@ -57,7 +53,7 @@ export function useShapeProps(
           <stop offset="100%" stopColor={color2} />
         </linearGradient>
       )}
-      {gradientType === "radial" && (
+      {fillType === "color" && gradientType === "radial" && (
         <radialGradient
           id={id}
           cx="50%"
@@ -71,8 +67,8 @@ export function useShapeProps(
         </radialGradient>
       )}
       {fillType === "image" && (
-        <pattern id={id} patternUnits="userSpaceOnUse" width="4" height="4">
-          <image href={imageUrl} x="0" y="0" width="4" height="4"></image>
+        <pattern id={id} patternUnits="userSpaceOnUse" width={width} height={height}>
+          <image href={imageUrl} x="0" y="0" width={width} height={height}></image>
         </pattern>
       )}
     </>

+ 18 - 8
apps/designer/src/models/graphModel.ts

@@ -10,6 +10,7 @@ import { Keyboard } from '@antv/x6-plugin-keyboard'
 import { useModel } from 'umi'
 import '@/components/PageContainer'
 import { handleGraphEvent } from '@/events'
+import { pageMenu, nodeMenu} from '@/utils/contentMenu';
 export default function GraphModel() {
   const [graph, setGraph] = useState<Graph>();
   const [dnd, setDnd] = useState<Dnd>();
@@ -37,11 +38,14 @@ export default function GraphModel() {
           isPage: true,
           ...pageState
         },
-        attrs: {
-          style: {
-            'pointer-events': 'none'
-          }
-        }
+        tools: [
+          {
+            name: 'contextmenu',
+            args: {
+              menu: pageMenu,
+            },
+          },
+        ]
       });
     };
   }, [pageState.width, graphRef.current])
@@ -81,7 +85,7 @@ export default function GraphModel() {
         multiple: true,
         rubberband: true,
         movable: true,
-        // showNodeSelectionBox: true,
+        showNodeSelectionBox: true,
         // showEdgeSelectionBox: true,
         pointerEvents: 'none',
         strict: true,
@@ -103,7 +107,6 @@ export default function GraphModel() {
         // @ts-ignore 排除页面节点
         return !(event === 'cell:added' && args?.cell?.getData()?.isPage)
       },
-
     }));
 
     setGraph(instance);
@@ -118,7 +121,7 @@ export default function GraphModel() {
       setCanRedo(instance.canRedo());
       setCanUndo(instance.canUndo());
     })
-    
+
     // 通用事件处理
     handleGraphEvent(instance);
   }
@@ -135,6 +138,13 @@ export default function GraphModel() {
 
     // 往画布添加节点
     const n = graphRef.current.createNode(node);
+    // 右键菜单
+    n.addTools({
+      name: 'contextmenu',
+      args: {
+        menu: nodeMenu,
+      },
+    });
     dndRef.current.start(n, e.nativeEvent as any)
   };
 

+ 1 - 1
apps/designer/src/pages/flow/components/Content/index.less

@@ -35,4 +35,4 @@
 
 .x6-edge-selected {
   filter: drop-shadow(0 0 2px #239edd);
-}
+}

+ 15 - 2
apps/designer/src/pages/flow/components/Content/index.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useMemo, useRef } from "react";
 import { Flex, Tabs } from "antd";
 import styles from "./index.less";
 import "./index.less";
@@ -8,6 +8,7 @@ import Libary from "../Libary";
 import { useModel } from "umi";
 import { LineType } from '@/enum';
 import { defaultData } from '@/components/data';
+import { edgeMenu} from '@/utils/contentMenu';
 export default function Content() {
   const stageRef = useRef<HTMLDivElement | null>(null);
   const { initGraph } = useModel("graphModel");
@@ -34,6 +35,12 @@ export default function Content() {
         minScale: 0.2,
         maxScale: 2,
       },
+      // panning: {
+      //   enabled: true,
+      //   modifiers: "ctrl",
+      //   // 画面平移
+      //   eventTypes: ["rightMouseDown"],
+      // },
       // 查找父节点
       embedding: {
         enabled: true,
@@ -97,8 +104,14 @@ export default function Content() {
                   name: 'block',
                 },
               },
-              text: defaultData.text
+              text: defaultData.text,
             },
+            tools: [{
+              name: 'contextmenu',
+              args: {
+                menu: edgeMenu,
+              },
+            }],
           });
         },
         validateConnection({ targetMagnet }) {

+ 6 - 1
apps/designer/src/pages/flow/components/Footer/index.tsx

@@ -59,6 +59,11 @@ export default function Footer() {
     graph?.zoomToFit({ })
   }
 
+  const handleOnChange = (value: number) => {
+    setScale(value);
+    handleZoom(value)
+  }
+
   return (
     <ConfigProvider componentSize="small">
       <div className="h-24px bg-white flex justify-between items-center px-16px relative">
@@ -74,7 +79,7 @@ export default function Footer() {
           </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={(val) => setScale(val)}/>
+          <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>

+ 1 - 4
apps/designer/src/pages/flow/components/MenuBar/index.tsx

@@ -31,19 +31,16 @@ const menuData: {
           },
           {
             key: "1-1-3",
-
             label: "UML",
             icon: <Icon icon="local:uml"/>,
           },
           {
             key: "1-1-4",
-
             label: "网络拓扑图",
             icon: <Icon icon="local:net"/>,
           },
           {
             key: "1-1-5",
-
             label: "组织结构图",
             icon: <Icon icon="local:flow"/>,
           },
@@ -617,7 +614,7 @@ export default function MenuBar() {
         </div>
         <div className="flex flex-col leading-32px">
           <div>
-            <Input className="text-16px" value={name} variant="borderless" />
+            <Input className="text-16px max-w-200px" variant="borderless" value={name} onChange={(e) => setName(e.target.value)}/>
           </div>
           <div>
             {menuData.map((item) => {

+ 2 - 22
apps/designer/src/pages/flow/components/ToolBar/index.tsx

@@ -21,7 +21,7 @@ import {
 } from "@ant-design/icons";
 import { fontFamilyOptions, alignOptionData, textAlignList } from "../../data";
 import { useModel } from "umi";
-import { alignCell, matchSize } from "@/utils";
+import { alignCell, matchSize, setCellZIndex } from "@/utils";
 import CustomColorPicker from "@/components/CustomColorPicker";
 import { ConnectorType } from "@/enum";
 import { set, cloneDeep } from 'lodash-es';
@@ -118,27 +118,7 @@ export default function ToolBar() {
 
   // 移动层级
   const handleSetIndex = (type: "top" | "bottom" | "up" | "down") => {
-    selectedCell?.forEach((cell) => {
-      switch(type) {
-        case "top":
-          cell.toFront();
-          break;
-        case "bottom":
-          cell.setZIndex(0);
-          break;
-        case "up": {
-          const index = cell.getZIndex() || 0;
-          cell.setZIndex(index + 1);
-          break;
-        }
-        case "down": {
-          const index = cell.getZIndex() || 0;
-          if (index <= 0) return;
-          cell.setZIndex(index - 1);
-          break;
-        }
-      }
-    });
+    setCellZIndex(type, selectedCell);
   };
 
   const handleChangeFormatBrush = () => {

+ 251 - 0
apps/designer/src/utils/contentMenu.tsx

@@ -0,0 +1,251 @@
+import React from "react";
+import { createRoot, Root } from "react-dom/client";
+import { Dropdown } from "antd";
+import { Graph, ToolsView, EdgeView } from "@antv/x6";
+import type { MenuProps } from "antd";
+import { menuHander } from "./contentMenuHander";
+
+export class ContextMenuTool extends ToolsView.ToolItem<
+  EdgeView,
+  ContextMenuToolOptions
+> {
+  private timer: number | null = null;
+  private root: Root | null = null;
+
+  private toggleContextMenu(visible: boolean, e?: MouseEvent) {
+    this.root?.unmount();
+    document.removeEventListener("mousedown", this.onMouseDown);
+    if (visible && e) {
+      const { sx, sy } = this.graph.scale();
+      let offsetX = e.offsetX * sx,
+        offsetY = e.offsetY * sy;
+      // 非页面节点需要获取当前节点位置 + 节点本身偏移位置
+      if (this.cell.isNode() && !this.cell.getData()?.isPage) {
+        const { x, y } = this.cell.getPosition();
+        offsetX = x * sx + e.offsetX * sx;
+        offsetY = y * sy + e.offsetY * sy;
+      }
+      this.root = createRoot(this.container);
+      const items = this.options.menu?.map((item: any) => {
+        if (!item) return item;
+        return {
+          ...item,
+          onClick: () => {
+            setTimeout(() => {
+              item?.onClick?.call(this, this, e);
+            }, 200);
+          },
+        };
+      });
+      this.root.render(
+        <Dropdown
+          open={true}
+          trigger={["contextMenu"]}
+          menu={{ items }}
+          align={{ offset: [offsetX, offsetY] }}
+        >
+          <span />
+        </Dropdown>
+      );
+      document.addEventListener("mousedown", this.onMouseDown);
+    }
+  }
+
+  private onMouseDown = () => {
+    this.timer = window.setTimeout(() => {
+      this.toggleContextMenu(false);
+    }, 200);
+  };
+
+  private onContextMenu({ e }: { e: MouseEvent }) {
+    if (this.timer) {
+      clearTimeout(this.timer);
+      this.timer = 0;
+    }
+    console.log(e, this);
+    this.toggleContextMenu(true, e);
+  }
+
+  delegateEvents() {
+    this.cellView.on("cell:contextmenu", this.onContextMenu, this);
+    this.graph.on("blank:contextmenu", this.onContextMenu, this);
+    return super.delegateEvents();
+  }
+
+  protected onRemove() {
+    this.cellView.off("cell:contextmenu", this.onContextMenu, this);
+  }
+}
+
+ContextMenuTool.config({
+  tagName: "div",
+  isSVGElement: false,
+});
+
+export interface ContextMenuToolOptions extends ToolsView.ToolItem.Options {
+  menu: MenuProps["items"];
+}
+
+Graph.registerEdgeTool("contextmenu", ContextMenuTool, true);
+Graph.registerNodeTool("contextmenu", ContextMenuTool, true);
+
+interface MenuItem {
+  key?: string;
+  label?: string;
+  type?: "divider";
+  icon?: string;
+  fastKey?: string;
+  handler?: (tool: ContextMenuTool, e: MouseEvent) => void;
+}
+
+// [复制、剪切、粘贴、复用、删除、设为默认样式],[置于顶层、置于底层、上移一层、下移一层],[锁定],[全选],[导出所选图形为PNG、复制所选图形为图片]
+const commonMenuData: MenuItem[] = [
+  { key: "copy", label: "复制", fastKey: "Ctrl+C", handler: menuHander.copy },
+  { key: "cut", label: "剪切", fastKey: "Ctrl+X", handler: menuHander.cut },
+  { key: "paste", label: "粘贴", fastKey: "Ctrl+V", handler: menuHander.paste },
+  {
+    key: "duplicate",
+    label: "复用",
+    fastKey: "Ctrl+D",
+    handler: menuHander.duplicate,
+  },
+  {
+    key: "delete",
+    label: "删除",
+    fastKey: "Delete/Backspace",
+    handler: menuHander.delete,
+  },
+  { key: "setDefaultStyle", label: "设为默认样式" },
+  { key: "resetDefaultStyle", label: "恢复默认样式" },
+  { type: "divider" },
+  {
+    key: "top",
+    label: "置于顶层",
+    fastKey: "Ctrl+]",
+    icon: "icon-zhiding1",
+    handler: menuHander.top,
+  },
+  {
+    key: "bottom",
+    label: "置于底层",
+    fastKey: "Ctrl+[",
+    icon: "icon-zhidi1",
+    handler: menuHander.bottom,
+  },
+  {
+    key: "up",
+    label: "上移一层",
+    fastKey: "Ctrl+Shift+]",
+    icon: "icon-shangyiyiceng1",
+    handler: menuHander.up,
+  },
+  {
+    key: "down",
+    label: "下移一层",
+    fastKey: "Ctrl+Shift+[",
+    icon: "icon-xiayiyiceng1",
+    handler: menuHander.down,
+  },
+  { type: "divider" },
+  { key: "lock", label: "锁定", fastKey: "Ctrl+L", icon: "icon-lock" },
+  { type: "divider" },
+  { key: "selectAll", label: "全选", fastKey: "A" },
+  { type: "divider" },
+  { key: "export", label: "导出所选图形为PNG", icon: "icon-tupian" },
+  { key: "copyAsImage", label: "复制所选图形为图片" },
+];
+
+const edgeMenuData: MenuItem[] = [...commonMenuData];
+
+const nodeMenuData: MenuItem[] = [...commonMenuData];
+
+const lockMenuData: MenuItem[] = [
+  { key: "paste", label: "粘贴", fastKey: "Ctrl+V" },
+  { type: "divider" },
+  { key: "unlock", label: "解锁", fastKey: "Ctrl+Shift+L", icon: "icon-lock" },
+  { type: "divider" },
+  { key: "selectAll", label: "全选", fastKey: "A" },
+];
+
+const pageMenuData: MenuItem[] = [
+  {
+    key: "paste",
+    label: "粘贴",
+    fastKey: "Ctrl+V",
+    handler: menuHander.paste,
+  },
+  { key: "1", type: "divider" },
+  {
+    key: "zoomIn",
+    label: "放大",
+    fastKey: "Ctrl+(+)",
+    icon: "icon-fangda",
+    handler: menuHander.zoomIn,
+  },
+  {
+    key: "zoomOut",
+    label: "缩小",
+    fastKey: "Ctrl+(-)",
+    icon: "icon-suoxiao",
+    handler: menuHander.zoomOut,
+  },
+  {
+    key: "resetView",
+    label: "重置视图缩放",
+    handler: menuHander.resetView,
+  },
+  { key: "2", type: "divider" },
+  {
+    key: "selectAll",
+    label: "全选",
+    fastKey: "A",
+    handler: menuHander.selectAll,
+  },
+  { key: "3", type: "divider" },
+  {
+    key: "createLine",
+    label: "创建连线",
+    fastKey: "L",
+    handler: menuHander.createLine,
+  },
+  {
+    key: "insertImage",
+    label: "插入图片",
+    fastKey: "I",
+    handler: menuHander.insertImage,
+  },
+];
+
+const LabelComponent = ({ item }: { item: MenuItem }) => {
+  return (
+    <div className="w-150px flex items-center justify-between">
+      <span>
+        <span className="inline-block w-20px">
+          {item.icon && <i className={`iconfont mr-8px ${item.icon}`} />}
+        </span>
+        {item.label}
+      </span>
+      <span className="text-12px color-#a6b9cd">{item.fastKey}</span>
+    </div>
+  );
+};
+
+const getMenuData = (menuData: MenuItem[]) => {
+  return menuData.map((item) => {
+    if (item.type === "divider") return item;
+    return {
+      key: item.key,
+      label: <LabelComponent item={item} />,
+      onClick: item.handler,
+    };
+  });
+};
+
+// 节点右键菜单
+export const nodeMenu = getMenuData(nodeMenuData);
+// 边线右键菜单
+export const edgeMenu = getMenuData(edgeMenuData);
+// 页面右键菜单
+export const pageMenu = getMenuData(pageMenuData);
+// 上锁节点菜单
+export const lockMenu = getMenuData(lockMenuData);

+ 264 - 0
apps/designer/src/utils/contentMenuHander.tsx

@@ -0,0 +1,264 @@
+import { ContextMenuTool, edgeMenu } from "./contentMenu";
+import Text from "@/components/basic/text";
+import baseNode from "@/components/base";
+import { Graph } from "@antv/x6";
+import { nodeMenu } from "./contentMenu";
+import { setCellZIndex } from '@/utils';
+
+const handlePaste = (graph: Graph, position?: {x: number, y: number}) => {
+  // 读取剪切板数据
+  navigator.clipboard.read().then((items) => {
+    console.log('剪切板内容:', items);
+    const item = items?.[0];
+    if (item) {
+      /**读取图片数据 */
+      if (item.types[0] === "image/png") {
+        item.getType("image/png").then((blob) => {
+          const reader = new FileReader();
+          reader.readAsDataURL(blob);
+          reader.onload = function (event) {
+            const dataUrl = event.target?.result as string;
+            // 获取图片大小
+            const img = new Image();
+            img.src = dataUrl;
+            img.onload = function () {
+              const width = img.width;
+              const height = img.height;
+              // 插入图片
+              const node = {
+                ...baseNode,
+                data: {
+                  ...baseNode.data,
+                  fill: {
+                    ...baseNode.data.fill,
+                    fillType: 'image',
+                    imageUrl: dataUrl
+                  }
+                },
+                size: {
+                  width,
+                  height
+                },
+                position
+              };
+              graph.addNode(node);
+            };
+          };
+        });
+      }
+      /**读取文本数据 */
+      if (item.types[0] === "text/plain") {
+        item.getType("text/plain").then((blob) => {
+          const reader = new FileReader();
+          reader.readAsText(blob);
+          reader.onload = function (event) {
+            const text = event.target?.result as string;
+            const exd = position ? { position } : {};
+            // 内部复制方法
+            if(text === ' ') {
+              // 执行cell复制操作
+              // todo 多选设置位置
+              graph.paste({
+                offset: {
+                  dx: 20,
+                  dy: 20
+                },
+                nodeProps: {
+                  ...exd,
+                  // @ts-ignore
+                  tools: [
+                    {
+                      name: 'contextmenu',
+                      args: {
+                        menu: nodeMenu,
+                      },
+                    }
+                  ]
+                },
+                edgeProps: {
+                  ...exd,
+                  // @ts-ignore
+                  tools: [
+                    {
+                      name: 'contextmenu',
+                      args: {
+                        menu: edgeMenu,
+                      },
+                    }
+                  ]
+                },
+                useLocalStorage: true
+              })
+            } else {
+              // 宽度最低100 最高300 高度等于行高*行数
+              const width = text.length * 15 >= 300 ? 300 : text.length * 15 < 100 ? 100 : text.length * 15;
+              const height = text.split("\n").length * 20;
+              const node = {
+                ...Text.node,
+                data: {
+                  ...Text.node.data,
+                  label: text,
+                },
+                position,
+                size: {
+                  width,
+                  height
+                },
+                tools: [
+                  {
+                    name: 'contextmenu',
+                    args: {
+                      menu: nodeMenu,
+                    },
+                  }
+                ]
+              }
+              // 插入文本
+              graph.addNode(node);
+            }
+          };
+        });
+      }
+    }
+  });
+}
+
+const handleCopy = (graph: Graph) => {
+  graph.copy(graph.getSelectedCells(), { useLocalStorage: true});
+  navigator.clipboard.writeText(" ");
+}
+
+const handleDuplicate = (graph: Graph) => {
+  handleCopy(graph);
+  graph.paste({
+    offset: {
+      dx: 20,
+      dy: 20
+    },
+    nodeProps: {
+      // @ts-ignore
+      tools: [
+        {
+          name: 'contextmenu',
+          args: {
+            menu: nodeMenu,
+          },
+        }
+      ]
+    },
+    edgeProps: {
+      // @ts-ignore
+      tools: [
+        {
+          name: 'contextmenu',
+          args: {
+            menu: edgeMenu,
+          },
+        }
+      ]
+    },
+    useLocalStorage: true
+  })
+}
+
+export const menuHander = {
+  /**复制 */
+  copy(tool: ContextMenuTool, _e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    handleCopy(tool.graph);
+  },
+  /**剪切 */
+  cut(tool: ContextMenuTool, _e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    tool.graph.cut(tool.graph.getSelectedCells(), { useLocalStorage: true });
+    navigator.clipboard.writeText(" ");
+  },
+  /**复用 */
+  duplicate(tool: ContextMenuTool, _e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    handleDuplicate(tool.graph);
+  },
+  /**删除 */
+  delete(tool: ContextMenuTool, _e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    tool.graph.removeCells(tool.graph.getSelectedCells());
+  },
+  /**粘贴 */
+  paste(tool: ContextMenuTool, e: MouseEvent) {
+    handlePaste(tool.graph, {x: e.offsetX, y: e.offsetY});
+  },
+  /**放大 */
+  zoomIn(tool: ContextMenuTool, e: MouseEvent) {
+    const { sx } = tool.graph.scale();
+    tool.graph.zoomTo(sx + 0.1);
+  },
+  /**缩小 */
+  zoomOut(tool: ContextMenuTool, e: MouseEvent) {
+    const { sx } = tool.graph.scale();
+    tool.graph.zoomTo(sx - 0.1);
+  },
+  /**重置视图 */
+  resetView(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.zoomToFit({});
+  },
+  // 全选
+  selectAll(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.getCells().forEach((cell) => {
+      if(!cell.getData()?.isPage) {
+        tool.graph.select(cell);
+      }
+    });
+  },
+  // 创建连线
+  createLine() {},
+  // 插入图片
+  insertImage() {},
+  // 上移一层
+  up(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    setCellZIndex("up", tool.graph.getSelectedCells());
+  },
+  // 下移一层
+  down(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    setCellZIndex("down", tool.graph.getSelectedCells());
+  },
+  // 置顶
+  top(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    setCellZIndex("top", tool.graph.getSelectedCells());
+  },
+  // 置底
+  bottom(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.select(tool.cell);
+    setCellZIndex("bottom", tool.graph.getSelectedCells());
+  },
+  // 设置默认样式
+  defaultStyle(tool: ContextMenuTool, e: MouseEvent) {
+   
+  },
+  // 恢复默认样式
+  resetStyle(tool: ContextMenuTool, e: MouseEvent) {
+   
+  },
+  // 锁定
+  lock(tool: ContextMenuTool, e: MouseEvent) {
+    
+  },
+  // 解锁
+  unlock(tool: ContextMenuTool, e: MouseEvent) {
+    
+  },
+  // 导出图形
+  exportImage(tool: ContextMenuTool, e: MouseEvent) {
+    
+  },
+  // 复制所选图形为图片
+  copyAsImage(tool: ContextMenuTool, e: MouseEvent) {
+    
+  },
+  // 替换图形
+  replace(tool: ContextMenuTool, e: MouseEvent) {
+    
+  },
+};

+ 30 - 0
apps/designer/src/utils/index.ts

@@ -76,3 +76,33 @@ export const matchSize = (type: "width" | "height" | "auto", cells: Cell[]) => {
     }
   });
 };
+
+/**
+ * 设置元素层级
+ * @param type 移动类型
+ * @param cells 元素列表
+ */
+export const setCellZIndex = (type: "top" | "bottom" | "up" | "down", cells: Cell[]) => {
+  // todo 重新调整全部层级
+  cells?.forEach((cell) => {
+    switch(type) {
+      case "top":
+        cell.toFront();
+        break;
+      case "bottom":
+        cell.setZIndex(0);
+        break;
+      case "up": {
+        const index = cell.getZIndex() || 0;
+        cell.setZIndex(index + 1);
+        break;
+      }
+      case "down": {
+        const index = cell.getZIndex() || 0;
+        if (index <= 0) return;
+        cell.setZIndex(index - 1);
+        break;
+      }
+    }
+  });
+};