Просмотр исходного кода

feat: 添加图片导出部分快捷键

liaojiaxing 8 месяцев назад
Родитель
Сommit
1c1072c5aa

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

@@ -7,6 +7,9 @@ export default defineConfig({
   styles: [
     '//at.alicdn.com/t/c/font_4676747_k7eyqt2247l.css'
   ],
+  metas: [
+    { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }
+  ],
   scripts: [
     // 字体加载
     // '//ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js'

+ 21 - 0
apps/designer/src/components/Edge.tsx

@@ -0,0 +1,21 @@
+import { Edge } from "@antv/x6";
+import { LineType } from "@/enum";
+import { defaultData } from "@/components/data";
+
+export const BaseEdge: Edge.Properties = {
+  router: "manhattan",
+  attrs: {
+    line: {
+      stroke: "#323232",
+      strokeDasharray: LineType.solid,
+      strokeWidth: 2,
+      sourceMarker: {
+        name: "",
+      },
+      targetMarker: {
+        name: "block",
+      },
+    },
+    text: defaultData.text,
+  },
+};

+ 101 - 0
apps/designer/src/components/ExportImage.tsx

@@ -0,0 +1,101 @@
+import { Graph } from "@antv/x6"
+import { Button, Form, Input, Modal, Radio } from "antd";
+import { CheckCard } from "@ant-design/pro-components";
+import insertCss from 'insert-css';
+import { useState } from "react";
+
+const ExportComponent = ({graph, getModal}: { graph: Graph, getModal?: () => { destroy: () => void}}) => {
+  insertCss(`
+    .ant-pro-checkcard-content {
+      padding-block: 8px;
+    }
+  `);
+
+  const [formModel, setFormModel] = useState({
+    type: 'png',
+    quality: 1,
+    watermark: "严禁复制",
+    watermarkStyle: 1
+  });
+
+  const handleExport = () => {
+    switch(formModel.type) {
+      case 'png':
+        graph.exportPNG('', {
+          quality: formModel.quality === 1 ? 1 : 0.6
+        });
+        break;
+      case 'jpg': 
+        graph.exportJPEG('', {
+          quality: formModel.quality === 1 ? 1 : 0.6
+        });
+        break;
+      default: 
+        graph.exportSVG('', {
+
+        });
+    }
+  }
+
+  const handleSetFormValue = (key: string, value: any) => {
+    setFormModel(state => ({...state, [key]: value}))
+  }
+
+  const modal = getModal?.();
+
+  return <div className="flex">
+  <div className="h-500px flex-1 bg-#f3f5f9 rounded-4px">
+    <div className="graph-container"></div>
+  </div>
+  <div className="flex-1 p-10px">
+    <div className="flex flex-col">
+      <div className="flex-1">
+        <Form layout="vertical" size="small">
+          <Form.Item label="图片格式">
+            <CheckCard.Group className="flex" value={formModel.type} onChange={(value) => handleSetFormValue("type", value)}>
+              <CheckCard className="w-100px h-40px" title="PNG" value={'png'}></CheckCard>
+              <CheckCard className="w-100px h-40px" title="JPG" value={'jpg'}></CheckCard>
+              <CheckCard className="w-100px h-40px" title="SVG" value={'svg'}></CheckCard>
+            </CheckCard.Group>
+          </Form.Item>
+          {
+            formModel.type !== 'svg' && <Form.Item label="图片质量">
+            <Radio.Group value={formModel.quality} onChange={(e) => handleSetFormValue("quality", e.target.value)}>
+              <Radio.Button value={1}>高清</Radio.Button>
+              <Radio.Button value={2}>普通</Radio.Button>
+            </Radio.Group>
+          </Form.Item>
+          }
+          <Form.Item label="水印设置">
+            <Input placeholder="请输入水印内容" value={formModel.watermark} onChange={(e) => handleSetFormValue("watermark", e.target.value)} />
+          </Form.Item>
+          <Form.Item label="水印样式">
+            <CheckCard.Group className="flex flex-wrap" value={formModel.watermarkStyle} onChange={(value) => handleSetFormValue("watermarkStyle", value)} >
+              <CheckCard className="w-96px h-50px" title="文字水印" value={1}></CheckCard>
+              <CheckCard className="w-96px h-50px" title="图片水印" value={2}></CheckCard>
+              <CheckCard className="w-96px h-50px" title="图片水印" value={3}></CheckCard>
+              <CheckCard className="w-96px h-50px" title="图片水印" value={4}></CheckCard>
+              <CheckCard className="w-96px h-50px" title="图片水印" value={5}></CheckCard>
+              <CheckCard className="w-96px h-50px" title="图片水印" value={6}></CheckCard>
+            </CheckCard.Group>
+          </Form.Item>
+        </Form>
+      </div>
+      <div className="flex justify-end gap-8px">
+        <Button size="small" onClick={() => modal?.destroy()}>取消</Button>
+        <Button size="small" type="primary" onClick={handleExport}>开始导出</Button>
+      </div>
+    </div>
+  </div>
+</div>
+}
+export const exportImage = (graph: Graph) => {
+  const modal = Modal.info({
+    title: "下载预览",
+    icon: <></>,
+    width: 800,
+    closable: true,
+    footer: <></>,
+    content: <ExportComponent graph={graph} getModal={() => modal}/>,
+  })
+}

+ 8 - 2
apps/designer/src/components/PageContainer.tsx

@@ -100,8 +100,8 @@ const NodeComponent = ({ node }: { node: Node }) => {
         ></rect>
         <rect
           id="flow_canvas_watermark_box"
-          width="1271"
-          height="1193"
+          width="100%"
+          height="100%"
           fill="url(#flow_canvas_watermark_item)"
         ></rect>
         <path
@@ -112,6 +112,12 @@ const NodeComponent = ({ node }: { node: Node }) => {
           d=""
           stroke="rgb(215,215,215)"
         ></path>
+        <rect
+          id="flow_canvas_container"
+          width="100%"
+          height="100%"
+          fill="transparent"
+        ></rect>
       </svg>
     </div>
   );

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

@@ -1,8 +1,6 @@
-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();

+ 0 - 1
apps/designer/src/events/index.ts

@@ -1,5 +1,4 @@
 import { Graph, Node } from "@antv/x6";
-
 export const handleGraphEvent = (graph: Graph) => {
   const sourceArrowhead = {
     name: "source-arrowhead",

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

@@ -2,6 +2,13 @@ body {
   margin: 0;
 }
 
+body {
+  user-select: none; /* 禁止选择 */
+  -webkit-user-select: none; /* Safari 和 Chrome */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* Internet Explorer/Edge */
+}
+
 ::-webkit-scrollbar {
   width: 7px;
   height: 7px

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
apps/designer/src/icons/brush.svg


+ 8 - 0
apps/designer/src/models/appModel.ts

@@ -22,6 +22,12 @@ interface PageSettings {
   watermarkText: string;
 }
 export default function appModel() {
+  const [projectInfo, setProjectInfo] = useState({
+    name: "新建流程图",
+    desc: "",
+    version: "",
+    author: "",
+  });
   // 隐藏/显示右侧面板
   const [showRightPanel, setShowRightPanel] = useState(false);
   const [pageState, setPageState] = useState<PageSettings>({
@@ -104,5 +110,7 @@ export default function appModel() {
     addHistoryCoolor,
     enableFormatBrush,
     toggleFormatBrush,
+    projectInfo,
+    setProjectInfo,
   }
 }

+ 11 - 2
apps/designer/src/models/graphModel.ts

@@ -7,10 +7,12 @@ import { Clipboard } from '@antv/x6-plugin-clipboard'
 import { Selection } from '@antv/x6-plugin-selection'
 import { History } from '@antv/x6-plugin-history'
 import { Keyboard } from '@antv/x6-plugin-keyboard'
+import { Export } from '@antv/x6-plugin-export'
 import { useModel } from 'umi'
 import '@/components/PageContainer'
 import { handleGraphEvent } from '@/events'
 import { pageMenu, nodeMenu} from '@/utils/contentMenu';
+import { bindKeys } from '@/utils/fastKey'
 export default function GraphModel() {
   const [graph, setGraph] = useState<Graph>();
   const [dnd, setDnd] = useState<Dnd>();
@@ -99,7 +101,10 @@ export default function GraphModel() {
       sharp: true,
       resizing: true
     }))
-    .use(new Keyboard())
+    .use(new Keyboard({
+      enabled: true,
+      global: true,
+    }))
     .use(new Clipboard())
     .use(new History({
       enabled: true,
@@ -107,7 +112,8 @@ export default function GraphModel() {
         // @ts-ignore 排除页面节点
         return !(event === 'cell:added' && args?.cell?.getData()?.isPage)
       },
-    }));
+    }))
+    .use(new Export());
 
     setGraph(instance);
     graphRef.current = instance;
@@ -124,6 +130,9 @@ export default function GraphModel() {
 
     // 通用事件处理
     handleGraphEvent(instance);
+
+    // 绑定快捷键
+    bindKeys(instance);
   }
 
   /**初始化拖拽 */

+ 10 - 23
apps/designer/src/pages/flow/components/Content/index.tsx

@@ -6,9 +6,8 @@ import { Graph, Shape } from "@antv/x6";
 import { Scroller } from "@antv/x6-plugin-scroller";
 import Libary from "../Libary";
 import { useModel } from "umi";
-import { LineType } from '@/enum';
-import { defaultData } from '@/components/data';
-import { edgeMenu} from '@/utils/contentMenu';
+import { BaseEdge } from '@/components/Edge';
+import { edgeMenu } from "@/utils/contentMenu";
 export default function Content() {
   const stageRef = useRef<HTMLDivElement | null>(null);
   const { initGraph } = useModel("graphModel");
@@ -91,27 +90,15 @@ export default function Content() {
         },
         createEdge() {
           return new Shape.Edge({
-            router: "manhattan",
-            attrs: {
-              line: {
-                stroke: "#323232",
-                strokeDasharray: LineType.solid,
-                strokeWidth: 2,
-                sourceMarker: {
-                  name: ''
-                },
-                targetMarker: {
-                  name: 'block',
+            ...BaseEdge,
+            tools: [
+              {
+                name: "contextmenu",
+                args: {
+                  menu: edgeMenu,
                 },
               },
-              text: defaultData.text,
-            },
-            tools: [{
-              name: 'contextmenu',
-              args: {
-                menu: edgeMenu,
-              },
-            }],
+            ],
           });
         },
         validateConnection({ targetMagnet }) {
@@ -165,7 +152,7 @@ export default function Content() {
         ref={stageRef}
         style={{ width: "100%", height: "100%" }}
       >
-        <div id="graph-container"></div>
+        <div id="graph-container" style={{cursor: 'default'}}></div>
       </div>
     </Flex>
   );

+ 22 - 13
apps/designer/src/pages/flow/components/MenuBar/index.tsx

@@ -2,7 +2,7 @@ import { DownloadOutlined, LeftOutlined } from "@ant-design/icons";
 import { Button, Input, Dropdown, MenuProps, Tooltip } from "antd";
 import React, { useState } from "react";
 import logo from "@/assets/logo.png";
-import { Icon } from "umi";
+import { Icon, useModel } from "umi";
 
 const menuData: {
   key: string;
@@ -22,32 +22,32 @@ const menuData: {
           {
             key: "1-1-1",
             label: "流程图",
-            icon: <Icon icon="local:flow"/>,
+            icon: <Icon icon="local:flow" />,
           },
           {
             key: "1-1-2",
             label: "思维导图",
-            icon: <Icon icon="local:mind"/>,
+            icon: <Icon icon="local:mind" />,
           },
           {
             key: "1-1-3",
             label: "UML",
-            icon: <Icon icon="local:uml"/>,
+            icon: <Icon icon="local:uml" />,
           },
           {
             key: "1-1-4",
             label: "网络拓扑图",
-            icon: <Icon icon="local:net"/>,
+            icon: <Icon icon="local:net" />,
           },
           {
             key: "1-1-5",
             label: "组织结构图",
-            icon: <Icon icon="local:flow"/>,
+            icon: <Icon icon="local:flow" />,
           },
           {
             key: "1-1-6",
             label: "BPMN",
-            icon: <Icon icon="local:bpmn"/>,
+            icon: <Icon icon="local:bpmn" />,
           },
         ],
       },
@@ -604,7 +604,7 @@ const menuData: {
   },
 ];
 export default function MenuBar() {
-  const [name, setName] = useState("新建流程图");
+  const { projectInfo, setProjectInfo } = useModel("appModel");
   return (
     <div className="w-full px-8px flex justify-between items-center">
       <div className="menu-left flex items-center">
@@ -614,17 +614,26 @@ export default function MenuBar() {
         </div>
         <div className="flex flex-col leading-32px">
           <div>
-            <Input className="text-16px max-w-200px" variant="borderless" value={name} onChange={(e) => setName(e.target.value)}/>
+            <Input
+              className="text-16px max-w-200px"
+              variant="borderless"
+              value={projectInfo.name}
+              onChange={(e) =>
+                setProjectInfo((state) => ({ ...state, name: e.target.value }))
+              }
+            />
           </div>
           <div>
             {menuData.map((item) => {
               return (
                 <Dropdown
                   key={item.key}
-                  menu={{ items: item.children, style: { width: 200} }}
+                  menu={{ items: item.children, style: { width: 200 } }}
                   placement="bottomLeft"
                 >
-                  <Button type="text" size="small">{item.label}</Button>
+                  <Button type="text" size="small">
+                    {item.label}
+                  </Button>
                 </Dropdown>
               );
             })}
@@ -632,8 +641,8 @@ export default function MenuBar() {
         </div>
       </div>
       <div>
-        <Dropdown menu={{ items: []}}>
-          <Tooltip title='导出为'>
+        <Dropdown menu={{ items: [] }}>
+          <Tooltip title="导出为">
             <Button type="text" icon={<DownloadOutlined />}></Button>
           </Tooltip>
         </Dropdown>

+ 39 - 7
apps/designer/src/pages/flow/index.tsx

@@ -6,8 +6,40 @@ import Config from "./components/Config";
 import Content from "./components/Content";
 import styles from "./index.less";
 import { useModel } from "umi";
+import { useEffect } from "react";
 export default function HomePage() {
   const { showRightPanel } = useModel("appModel");
+
+  useEffect(() => {
+    document.addEventListener(
+      "mousewheel",
+      function (e: any) {
+        if (e.wheelDelta || e.detail) {
+          e.preventDefault();
+        }
+      },
+      { capture: false, passive: false }
+    );
+    document.addEventListener(
+      "keydown",
+      function (event) {
+        if (
+          (event.ctrlKey === true || event.metaKey === true) &&
+          (event.keyCode === 61 ||
+            event.keyCode === 107 ||
+            event.keyCode === 173 ||
+            event.keyCode === 109 ||
+            event.keyCode === 187 ||
+            event.keyCode === 189 ||
+            event.keyCode === 80)
+        ) {
+          event.preventDefault();
+        }
+      },
+      false
+    );
+  }, []);
+
   return (
     <ConfigProvider
       prefixCls="shalu"
@@ -17,13 +49,13 @@ export default function HomePage() {
         },
         components: {
           Tabs: {
-            itemActiveColor: '#000',
-            inkBarColor: '#000',
-            itemSelectedColor: '#000',
-            itemColor: '#999',
-            itemHoverColor: '#000'
-          }
-        }
+            itemActiveColor: "#000",
+            inkBarColor: "#000",
+            itemSelectedColor: "#000",
+            itemColor: "#999",
+            itemHoverColor: "#000",
+          },
+        },
       }}
     >
       <Layout className={styles.layout}>

+ 51 - 11
apps/designer/src/utils/contentMenu.tsx

@@ -3,7 +3,7 @@ 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";
+import { menuHander } from "./hander";
 
 export class ContextMenuTool extends ToolsView.ToolItem<
   EdgeView,
@@ -115,8 +115,11 @@ const commonMenuData: MenuItem[] = [
     fastKey: "Delete/Backspace",
     handler: menuHander.delete,
   },
-  { key: "setDefaultStyle", label: "设为默认样式" },
-  { key: "resetDefaultStyle", label: "恢复默认样式" },
+  {
+    key: "setDefaultStyle",
+    label: "设为默认样式",
+    handler: menuHander.defaultStyle,
+  },
   { type: "divider" },
   {
     key: "top",
@@ -147,24 +150,61 @@ const commonMenuData: MenuItem[] = [
     handler: menuHander.down,
   },
   { type: "divider" },
-  { key: "lock", label: "锁定", fastKey: "Ctrl+L", icon: "icon-lock" },
+  {
+    key: "lock",
+    label: "锁定",
+    fastKey: "Ctrl+L",
+    icon: "icon-lock",
+    handler: menuHander.lock,
+  },
   { type: "divider" },
-  { key: "selectAll", label: "全选", fastKey: "A" },
+  {
+    key: "selectAll",
+    label: "全选",
+    fastKey: "A",
+    handler: menuHander.selectAll,
+  },
   { type: "divider" },
-  { key: "export", label: "导出所选图形为PNG", icon: "icon-tupian" },
-  { key: "copyAsImage", label: "复制所选图形为图片" },
+  {
+    key: "export",
+    label: "导出所选图形为PNG",
+    icon: "icon-tupian",
+    handler: menuHander.exportImage,
+  },
+  {
+    key: "copyAsImage",
+    label: "复制所选图形为图片",
+    handler: menuHander.copyAsImage,
+  },
 ];
 
-const edgeMenuData: MenuItem[] = [...commonMenuData];
+const edgeMenuData: MenuItem[] = [
+  ...commonMenuData.toSpliced(6, 0, {
+    key: "resetDefaultStyle",
+    label: "恢复默认样式",
+    handler: menuHander.resetStyle,
+  }),
+];
 
 const nodeMenuData: MenuItem[] = [...commonMenuData];
 
 const lockMenuData: MenuItem[] = [
-  { key: "paste", label: "粘贴", fastKey: "Ctrl+V" },
+  { key: "paste", label: "粘贴", fastKey: "Ctrl+V", handler: menuHander.paste },
   { type: "divider" },
-  { key: "unlock", label: "解锁", fastKey: "Ctrl+Shift+L", icon: "icon-lock" },
+  {
+    key: "unlock",
+    label: "解锁",
+    fastKey: "Ctrl+Shift+L",
+    icon: "icon-lock",
+    handler: menuHander.unlock,
+  },
   { type: "divider" },
-  { key: "selectAll", label: "全选", fastKey: "A" },
+  {
+    key: "selectAll",
+    label: "全选",
+    fastKey: "A",
+    handler: menuHander.selectAll,
+  },
 ];
 
 const pageMenuData: MenuItem[] = [

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

@@ -1,264 +0,0 @@
-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) {
-    
-  },
-};

+ 60 - 0
apps/designer/src/utils/fastKey.tsx

@@ -0,0 +1,60 @@
+import { Graph } from "@antv/x6";
+import { printHandle } from "./index";
+import { handleInsertText } from './hander';
+
+export const bindKeys = (graph: Graph) => {
+  // Ctrl + A 全选
+  graph.bindKey('ctrl+a', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    graph.getCells().forEach((cell) => {
+      if(!cell.getData()?.isPage) {
+        graph.select(cell);
+      }
+    });
+  });
+
+  // Ctrl + F 查找替换 todo
+  graph.bindKey('ctrl+f', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+  });
+
+  // Ctrl + + 放大
+  graph.bindKey('ctrl+=', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    graph.zoomTo(graph.zoom() + 0.1);
+  });
+
+  // Ctrl + - 缩小
+  graph.bindKey('ctrl+-', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    graph.zoomTo(graph.zoom() - 0.1);
+  });
+
+  // esc 取消
+  graph.bindKey('esc', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    graph.cleanSelection();
+    graph.trigger('cancel');
+  });
+
+  // Ctrl + P 打印
+  graph.bindKey('ctrl+p', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    graph.toPNG((dataUri) => {
+      printHandle(dataUri);
+    })
+  });
+
+  // T 插入文本
+  graph.bindKey('t', (e: KeyboardEvent) => {
+    e.stopPropagation();
+
+    handleInsertText(graph);
+  });
+};

+ 458 - 0
apps/designer/src/utils/hander.tsx

@@ -0,0 +1,458 @@
+import { ContextMenuTool, edgeMenu } from "./contentMenu";
+import Text from "@/components/basic/text";
+import baseNode from "@/components/Base";
+import { Cell, Edge, Graph, Node } from "@antv/x6";
+import { nodeMenu } from "./contentMenu";
+import { setCellZIndex } from "@/utils";
+import { exportImage } from "@/components/ExportImage";
+import { BaseEdge } from "@/components/Edge";
+
+/**
+ * 执行粘贴
+ * @param graph
+ * @param position
+ */
+export 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);
+            }
+          };
+        });
+      }
+    }
+  });
+};
+
+/**
+ * 执行拷贝
+ * @param graph
+ */
+const handleCopy = (graph: Graph) => {
+  graph.copy(graph.getSelectedCells(), { useLocalStorage: true, deep: false });
+  navigator.clipboard.writeText(" ");
+};
+
+/**
+ * 执行复用
+ * @param graph
+ */
+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,
+  });
+};
+
+/**
+ * 创建边线
+ * @param graph 
+ */
+const handleCreateEdge = (graph: Graph) => {
+  let createEdgeMode = true;
+  let start: { x: number; y: number } | undefined;
+  let edge: Edge | undefined;
+
+  const graphRoot = document.querySelector(
+    "#graph-container"
+  ) as HTMLDivElement;
+  if (graphRoot) {
+    graphRoot.style.cursor = "crosshair";
+  }
+  const pageRoot = document.querySelector(
+    "#flow_canvas_container"
+  ) as HTMLDivElement;
+  if (pageRoot) {
+    pageRoot.style.cursor = "crosshair";
+  }
+  const handleCreate = ({ e, cell }: { e: MouseEvent; cell?: Cell }) => {
+    if (cell && !cell.getData()?.isPage) return;
+    start = { x: e.offsetX, y: e.offsetY };
+    graphRoot.style.cursor = "default";
+    pageRoot.style.cursor = "default";
+  };
+  const handleMove = ({
+    e,
+    x,
+    y,
+    node,
+  }: {
+    e: MouseEvent;
+    x: number;
+    y: number;
+    node?: Node;
+  }) => {
+    if (node && !node.getData()?.isPage) return;
+    if (!createEdgeMode || !start) return;
+
+    // 判断起始点是否移动了10px
+    if (Math.abs(x - start.x) > 10 || Math.abs(y - start.y) > 10) {
+      // 判断是否已创建边,创建了则修改目标位置
+      if (!edge) {
+        edge = graph.addEdge({
+          ...BaseEdge,
+          source: {
+            x: start.x,
+            y: start.y,
+          },
+          target: {
+            x: e.offsetX,
+            y: e.offsetY,
+          },
+          tools: [
+            {
+              name: "contextmenu",
+              args: {
+                menu: edgeMenu,
+              },
+            },
+          ],
+        });
+      } else {
+        edge.setTarget({
+          x: e.offsetX,
+          y: e.offsetY,
+        });
+      }
+    }
+  };
+
+  const handleCancel = () => {
+    createEdgeMode = false;
+    start = undefined;
+    graph.off("blank:mousedown", handleCreate);
+    graph.off("node:mousedown", handleCreate);
+    graph.off("blank:mousemove", handleMove);
+    graph.off("node:mousemove", handleMove);
+  };
+
+  graph.on("blank:mousedown", handleCreate);
+  graph.on("node:mousedown", handleCreate);
+  graph.on("blank:mousemove", handleMove);
+  graph.on("node:mousemove", handleMove);
+
+  graph.on("cell:click", ({ cell }) => {
+    if (cell && cell.getData()?.isPage) return;
+    handleCancel();
+  });
+  graph.on("cell:mouseup", handleCancel);
+  graph.on("blank:mouseup", handleCancel);
+  graph.on("cancel", handleCancel);
+};
+
+/**
+ * 插入文本节点
+ * @param graph 
+ */
+export const handleInsertText = (graph: Graph) => {
+  let createEdgeMode = true;
+
+  const graphRoot = document.querySelector(
+    "#graph-container"
+  ) as HTMLDivElement;
+  if (graphRoot) {
+    graphRoot.style.cursor = "crosshair";
+  }
+  const pageRoot = document.querySelector(
+    "#flow_canvas_container"
+  ) as HTMLDivElement;
+  if (pageRoot) {
+    pageRoot.style.cursor = "crosshair";
+  }
+  const handleCreate = ({ e, cell }: { e: MouseEvent; cell?: Cell }) => {
+    if (cell && !cell.getData()?.isPage) {
+      handleCancel();
+      return;
+    }
+    if(createEdgeMode) {
+      const node = {
+        ...Text.node,
+        position: {
+          x: e.offsetX,
+          y: e.offsetY
+        },
+        tools: [
+          {
+            name: "contextmenu",
+            args: {
+              menu: nodeMenu,
+            },
+          },
+        ],
+      };
+      graph.addNode(node);
+      handleCancel();
+    }
+  };
+
+  const handleCancel = () => {
+    createEdgeMode = false;
+    graph.off("blank:click", handleCreate);
+    graph.off("node:click", handleCreate);
+    graphRoot.style.cursor = "default";
+    pageRoot.style.cursor = "default";
+  };
+
+  graph.on("blank:click", handleCreate);
+  graph.on("node:click", handleCreate);
+
+  graph.on("cancel", handleCancel);
+};
+
+export const menuHander = {
+  /**复制 */
+  copy(tool: ContextMenuTool) {
+    tool.graph.select(tool.cell);
+    handleCopy(tool.graph);
+  },
+  /**剪切 */
+  cut(tool: ContextMenuTool) {
+    tool.graph.select(tool.cell);
+    tool.graph.cut(tool.graph.getSelectedCells(), {
+      useLocalStorage: true,
+      deep: false,
+    });
+    navigator.clipboard.writeText(" ");
+  },
+  /**复用 */
+  duplicate(tool: ContextMenuTool) {
+    tool.graph.select(tool.cell);
+    handleDuplicate(tool.graph);
+  },
+  /**删除 */
+  delete(tool: ContextMenuTool) {
+    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) {
+    const { sx } = tool.graph.scale();
+    tool.graph.zoomTo(sx + 0.1);
+  },
+  /**缩小 */
+  zoomOut(tool: ContextMenuTool) {
+    const { sx } = tool.graph.scale();
+    tool.graph.zoomTo(sx - 0.1);
+  },
+  /**重置视图 */
+  resetView(tool: ContextMenuTool) {
+    tool.graph.zoomToFit({});
+  },
+  // 全选
+  selectAll(tool: ContextMenuTool) {
+    tool.graph.getCells().forEach((cell) => {
+      if (!cell.getData()?.isPage) {
+        tool.graph.select(cell);
+      }
+    });
+  },
+  // 创建连线
+  createLine(tool: ContextMenuTool) {
+    handleCreateEdge(tool.graph);
+  },
+  // 插入图片
+  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) {
+    tool.cell.setData({
+      lock: true,
+    });
+  },
+  // 解锁
+  unlock(tool: ContextMenuTool, e: MouseEvent) {
+    tool.cell.setData({
+      lock: false,
+    });
+  },
+  // 导出图形
+  exportImage(tool: ContextMenuTool, e: MouseEvent) {
+    exportImage(tool.graph);
+  },
+  // 复制所选图形为图片
+  copyAsImage(tool: ContextMenuTool, e: MouseEvent) {
+    tool.graph.toPNG((dataUrl) => {
+      // base64转bolb
+      let arr = dataUrl.split(",");
+      let mime = arr[0].match(/:(.*?);/)?.[1];
+      let bstr = atob(arr[1]);
+      let n = bstr.length;
+      let u8arr = new Uint8Array(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
+      }
+      const bolb = new Blob([u8arr], { type: mime });
+      const data = [new ClipboardItem({ [bolb.type]: bolb })];
+      navigator.clipboard.write(data);
+    });
+  },
+  // 替换图形
+  replace(tool: ContextMenuTool, e: MouseEvent) {},
+};

+ 33 - 2
apps/designer/src/utils/index.ts

@@ -82,10 +82,13 @@ export const matchSize = (type: "width" | "height" | "auto", cells: Cell[]) => {
  * @param type 移动类型
  * @param cells 元素列表
  */
-export const setCellZIndex = (type: "top" | "bottom" | "up" | "down", cells: Cell[]) => {
+export const setCellZIndex = (
+  type: "top" | "bottom" | "up" | "down",
+  cells: Cell[]
+) => {
   // todo 重新调整全部层级
   cells?.forEach((cell) => {
-    switch(type) {
+    switch (type) {
       case "top":
         cell.toFront();
         break;
@@ -106,3 +109,31 @@ export const setCellZIndex = (type: "top" | "bottom" | "up" | "down", cells: Cel
     }
   });
 };
+
+/**
+ * 打印图片
+ * @param imgUri 图片地址
+ */
+export const printHandle = (imgUri: string) => {
+  const iframe = document.createElement("iframe");
+  iframe.style.height = "0";
+  iframe.style.visibility = "hidden";
+  iframe.style.width = "0";
+  const str = `<html>
+            <style media='print'>
+                 @page{size:A4 landscape};margin:0mm;padding:0}
+            </style>
+            <body>
+                 <img src="${imgUri}"/>
+            </body>
+	</html>
+	`;
+  iframe.setAttribute("srcdoc", str);
+  document.body.appendChild(iframe);
+  iframe.addEventListener("load", () => {
+    iframe?.contentWindow?.print();
+  });
+  iframe?.contentWindow?.addEventListener("afterprint", function () {
+    iframe?.parentNode?.removeChild(iframe);
+  });
+};