Explorar el Código

feat: Add the `GGEditor` demo (#3810)

* feat: Add the `GGEditor` demo

* style: Update the size of the card

* feat: Extract the `MenuItem` and `ToolbarButton` components

* feat: Add the shape type of the line

* feat: Extract the `DetailForm` components

* feat: add PageHeader

* fix: add all local laug

* fix: Fix the height of the editor
高力 hace 6 años
padre
commit
1978073929
Se han modificado 52 ficheros con 1239 adiciones y 0 borrados
  1. 23 0
      config/router.config.js
  2. 1 0
      package.json
  3. 29 0
      public/ggeditor/flow/decision.svg
  4. 29 0
      public/ggeditor/flow/model.svg
  5. 29 0
      public/ggeditor/flow/normal.svg
  6. 29 0
      public/ggeditor/flow/start.svg
  7. 29 0
      public/ggeditor/koni/bank.svg
  8. 29 0
      public/ggeditor/koni/country.svg
  9. 14 0
      public/ggeditor/koni/icon.svg
  10. 29 0
      public/ggeditor/koni/person.svg
  11. 2 0
      src/locales/en-US.js
  12. 11 0
      src/locales/en-US/editor.js
  13. 4 0
      src/locales/en-US/menu.js
  14. 2 0
      src/locales/pt-BR.js
  15. 11 0
      src/locales/pt-BR/editor.js
  16. 4 0
      src/locales/pt-BR/menu.js
  17. 2 0
      src/locales/zh-CN.js
  18. 9 0
      src/locales/zh-CN/editor.js
  19. 4 0
      src/locales/zh-CN/menu.js
  20. 2 0
      src/locales/zh-TW.js
  21. 9 0
      src/locales/zh-TW/editor.js
  22. 4 0
      src/locales/zh-TW/menu.js
  23. 43 0
      src/pages/Editor/GGEditor/Flow/index.js
  24. 41 0
      src/pages/Editor/GGEditor/Flow/index.less
  25. 45 0
      src/pages/Editor/GGEditor/Koni/index.js
  26. 33 0
      src/pages/Editor/GGEditor/Koni/shape/nodes/KoniCustomNode.js
  27. 40 0
      src/pages/Editor/GGEditor/Mind/index.js
  28. 7 0
      src/pages/Editor/GGEditor/common/IconFont/index.js
  29. 36 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/FlowContextMenu.js
  30. 3 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/KoniContextMenu.js
  31. 20 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/MenuItem.js
  32. 24 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/MindContextMenu.js
  33. 5 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/index.js
  34. 39 0
      src/pages/Editor/GGEditor/components/EditorContextMenu/index.less
  35. 129 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/DetailForm.js
  36. 29 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/FlowDetailPanel.js
  37. 3 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/KoniDetailPanel.js
  38. 20 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/MindDetailPanel.js
  39. 5 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/index.js
  40. 10 0
      src/pages/Editor/GGEditor/components/EditorDetailPanel/index.less
  41. 55 0
      src/pages/Editor/GGEditor/components/EditorItemPanel/FlowItemPanel.js
  42. 51 0
      src/pages/Editor/GGEditor/components/EditorItemPanel/KoniItemPanel.js
  43. 4 0
      src/pages/Editor/GGEditor/components/EditorItemPanel/index.js
  44. 20 0
      src/pages/Editor/GGEditor/components/EditorItemPanel/index.less
  45. 13 0
      src/pages/Editor/GGEditor/components/EditorMinimap/index.js
  46. 32 0
      src/pages/Editor/GGEditor/components/EditorToolbar/FlowToolbar.js
  47. 3 0
      src/pages/Editor/GGEditor/components/EditorToolbar/KoniToolbar.js
  48. 27 0
      src/pages/Editor/GGEditor/components/EditorToolbar/MindToolbar.js
  49. 24 0
      src/pages/Editor/GGEditor/components/EditorToolbar/ToolbarButton.js
  50. 5 0
      src/pages/Editor/GGEditor/components/EditorToolbar/index.js
  51. 39 0
      src/pages/Editor/GGEditor/components/EditorToolbar/index.less
  52. 129 0
      src/pages/Editor/GGEditor/mock/worldCup2018.json

+ 23 - 0
config/router.config.js

@@ -93,6 +93,29 @@ export default [
           },
         ],
       },
+      //  editor
+      {
+        name: 'editor',
+        icon: 'highlight',
+        path: '/editor',
+        routes: [
+          {
+            path: '/editor/flow',
+            name: 'flow',
+            component: './Editor/GGEditor/Flow',
+          },
+          {
+            path: '/editor/mind',
+            name: 'mind',
+            component: './Editor/GGEditor/Mind',
+          },
+          {
+            path: '/editor/koni',
+            name: 'koni',
+            component: './Editor/GGEditor/Koni',
+          },
+        ],
+      },
       // list
       {
         path: '/list',

+ 1 - 0
package.json

@@ -39,6 +39,7 @@
     "classnames": "^2.2.6",
     "dva": "^2.4.1",
     "enquire-js": "^0.2.1",
+    "gg-editor": "^2.0.2",
     "hash.js": "^1.1.7",
     "lodash": "^4.17.11",
     "lodash-decorators": "^6.0.1",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 29 - 0
public/ggeditor/flow/decision.svg


+ 29 - 0
public/ggeditor/flow/model.svg

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="88px" height="56px" viewBox="0 0 88 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 49.1 (51147) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 4</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <rect id="path-1" x="0" y="0" width="80" height="48" rx="24"></rect>
+        <filter x="-8.8%" y="-10.4%" width="117.5%" height="129.2%" filterUnits="objectBoundingBox" id="filter-2">
+            <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.04 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Flow-01" transform="translate(-102.000000, -195.000000)">
+            <g id="Group-4" transform="translate(106.000000, 197.000000)">
+                <g id="Rectangle-15-Copy-35">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+                    <use fill-opacity="0.92" fill="#F9F0FF" fill-rule="evenodd" xlink:href="#path-1"></use>
+                    <rect stroke="#B37FEB" stroke-width="1" x="0.5" y="0.5" width="79" height="47" rx="23.5"></rect>
+                </g>
+                <text id="model" font-family="PingFangSC-Regular, PingFang SC" font-size="12" font-weight="normal" line-spacing="12" fill="#000000" fill-opacity="0.65">
+                    <tspan x="24" y="29">Model</tspan>
+                </text>
+            </g>
+        </g>
+    </g>
+</svg>

+ 29 - 0
public/ggeditor/flow/normal.svg

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="88px" height="56px" viewBox="0 0 88 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 49.1 (51147) - http://www.bohemiancoding.com/sketch -->
+    <title>Group</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <rect id="path-1" x="0" y="0" width="80" height="48" rx="4"></rect>
+        <filter x="-8.8%" y="-10.4%" width="117.5%" height="129.2%" filterUnits="objectBoundingBox" id="filter-2">
+            <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.04 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Flow-01" transform="translate(-6.000000, -105.000000)">
+            <g id="Group" transform="translate(10.000000, 107.000000)">
+                <g id="Rectangle-15-Copy">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+                    <use fill-opacity="0.92" fill="#E6F7FF" fill-rule="evenodd" xlink:href="#path-1"></use>
+                    <rect stroke="#1890FF" stroke-width="1" x="0.5" y="0.5" width="79" height="47" rx="4"></rect>
+                </g>
+                <text id="normal" font-family="PingFangSC-Regular, PingFang SC" font-size="12" font-weight="normal" line-spacing="12" fill="#000000" fill-opacity="0.65">
+                    <tspan x="21" y="29">Normal</tspan>
+                </text>
+            </g>
+        </g>
+    </g>
+</svg>

+ 29 - 0
public/ggeditor/flow/start.svg

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 49.1 (51147) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 2</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <circle id="path-1" cx="36" cy="36" r="36"></circle>
+        <filter x="-9.7%" y="-6.9%" width="119.4%" height="119.4%" filterUnits="objectBoundingBox" id="filter-2">
+            <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.04 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Flow-01" transform="translate(-106.000000, -93.000000)">
+            <g id="Group-2" transform="translate(110.000000, 95.000000)">
+                <g id="Oval">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+                    <use fill-opacity="0.92" fill="#FFF2E8" fill-rule="evenodd" xlink:href="#path-1"></use>
+                    <circle stroke="#FFC069" stroke-width="1" cx="36" cy="36" r="35.5"></circle>
+                </g>
+                <text id="start" font-family="PingFangSC-Regular, PingFang SC" font-size="12" font-weight="normal" line-spacing="12" fill="#000000" fill-opacity="0.65">
+                    <tspan x="23" y="41">Start</tspan>
+                </text>
+            </g>
+        </g>
+    </g>
+</svg>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 29 - 0
public/ggeditor/koni/bank.svg


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 29 - 0
public/ggeditor/koni/country.svg


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 14 - 0
public/ggeditor/koni/icon.svg


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 29 - 0
public/ggeditor/koni/person.svg


+ 2 - 0
src/locales/en-US.js

@@ -10,6 +10,7 @@ import settingDrawer from './en-US/settingDrawer';
 import settings from './en-US/settings';
 import pwa from './en-US/pwa';
 import component from './en-US/component';
+import editor from './en-US/editor';
 
 export default {
   'navBar.lang': 'Languages',
@@ -32,4 +33,5 @@ export default {
   ...settings,
   ...pwa,
   ...component,
+  ...editor,
 };

+ 11 - 0
src/locales/en-US/editor.js

@@ -0,0 +1,11 @@
+export default {
+  'app.editor.flow.title': 'Flowchart Editor',
+  'app.editor.flow.description':
+    'The flow chart is an excellent way to represent the idea of the algorithm.',
+  'app.editor.koni.title': 'Koni Editor',
+  'app.editor.koni.description':
+    'The topology diagram refers to the network structure diagram composed of network node devices and communication media.',
+  'app.editor.mind.title': 'Mind Map Editor',
+  'app.editor.mind.description':
+    'The brain map is an effective graphical thinking tool for expressing divergent thinking. It is simple but effective and is a practical thinking tool.',
+};

+ 4 - 0
src/locales/en-US/menu.js

@@ -38,4 +38,8 @@ export default {
   'menu.account.settings': 'Account Settings',
   'menu.account.trigger': 'Trigger Error',
   'menu.account.logout': 'Logout',
+  'menu.editor': 'Editor',
+  'menu.editor.flow': 'Flow Editor',
+  'menu.editor.mind': 'Mind Editor',
+  'menu.editor.koni': 'Koni Editor',
 };

+ 2 - 0
src/locales/pt-BR.js

@@ -10,6 +10,7 @@ import settingDrawer from './pt-BR/settingDrawer';
 import settings from './pt-BR/settings';
 import pwa from './pt-BR/pwa';
 import component from './pt-BR/component';
+import editor from './pt-BR/editor';
 
 export default {
   'navBar.lang': 'Idiomas',
@@ -32,4 +33,5 @@ export default {
   ...settings,
   ...pwa,
   ...component,
+  ...editor,
 };

+ 11 - 0
src/locales/pt-BR/editor.js

@@ -0,0 +1,11 @@
+export default {
+  'app.editor.flow.title': 'Editor de diagrama de flujo',
+  'app.editor.flow.description':
+    'El diagrama de flujo es una excelente manera de representar la idea del algoritmo.',
+  'app.editor.koni.title': 'Editor de topologia',
+  'app.editor.koni.description':
+    'El diagrama de topología se refiere al diagrama de estructura de red compuesto por dispositivos de nodo de red y medios de comunicación.',
+  'app.editor.mind.title': 'Editor de mapas cerebrales',
+  'app.editor.mind.description':
+    'El mapa cerebral es una herramienta de pensamiento gráfico eficaz para expresar el pensamiento divergente. Es simple pero efectivo y es una herramienta de pensamiento práctico.',
+};

+ 4 - 0
src/locales/pt-BR/menu.js

@@ -38,4 +38,8 @@ export default {
   'menu.account.settings': 'Configurar Conta',
   'menu.account.trigger': 'Disparar Erro',
   'menu.account.logout': 'Sair',
+  'menu.editor': 'Editor',
+  'menu.editor.flow': 'Flow Editor',
+  'menu.editor.mind': 'Mind Editor',
+  'menu.editor.koni': 'Koni Editor',
 };

+ 2 - 0
src/locales/zh-CN.js

@@ -10,6 +10,7 @@ import settingDrawer from './zh-CN/settingDrawer';
 import settings from './zh-CN/settings';
 import pwa from './zh-CN/pwa';
 import component from './zh-CN/component';
+import editor from './zh-CN/editor';
 
 export default {
   'navBar.lang': '语言',
@@ -32,4 +33,5 @@ export default {
   ...settings,
   ...pwa,
   ...component,
+  ...editor,
 };

+ 9 - 0
src/locales/zh-CN/editor.js

@@ -0,0 +1,9 @@
+export default {
+  'app.editor.flow.title': '流程图编辑器',
+  'app.editor.flow.description': '千言万语不如一张图,流程图是表示算法思路的好方法',
+  'app.editor.koni.title': '拓扑编辑器',
+  'app.editor.koni.description': '拓扑结构图是指由网络节点设备和通信介质构成的网络结构图',
+  'app.editor.mind.title': '脑图编辑器',
+  'app.editor.mind.description':
+    '脑图是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。',
+};

+ 4 - 0
src/locales/zh-CN/menu.js

@@ -38,4 +38,8 @@ export default {
   'menu.account.settings': '个人设置',
   'menu.account.trigger': '触发报错',
   'menu.account.logout': '退出登录',
+  'menu.editor': '编辑页',
+  'menu.editor.flow': '流程编辑器',
+  'menu.editor.mind': '脑图编辑器',
+  'menu.editor.koni': '拓扑编辑器',
 };

+ 2 - 0
src/locales/zh-TW.js

@@ -10,6 +10,7 @@ import settingDrawer from './zh-TW/settingDrawer';
 import settings from './zh-TW/settings';
 import pwa from './zh-TW/pwa';
 import component from './zh-TW/component';
+import editor from './zh-TW/editor';
 
 export default {
   'navBar.lang': '語言',
@@ -32,4 +33,5 @@ export default {
   ...settings,
   ...pwa,
   ...component,
+  ...editor,
 };

+ 9 - 0
src/locales/zh-TW/editor.js

@@ -0,0 +1,9 @@
+export default {
+  'app.editor.flow.title': '流程圖編輯器',
+  'app.editor.flow.description': '千言萬語不如一張圖,流程圖是表示算法思路的好方法',
+  'app.editor.koni.title': '拓撲編輯器',
+  'app.editor.koni.description': '拓撲結構圖是指由網絡節點設備和通信介質構成的網絡結構圖',
+  'app.editor.mind.title': '腦圖編輯器',
+  'app.editor.mind.description':
+    '腦圖是表達發散性思維的有效圖形思維工具 ,它簡單卻又很有效,是一種實用性的思維工具',
+};

+ 4 - 0
src/locales/zh-TW/menu.js

@@ -38,4 +38,8 @@ export default {
   'menu.exception.not-find': '404',
   'menu.exception.server-error': '500',
   'menu.exception.trigger': '触发错误',
+  'menu.editor': '編輯頁',
+  'menu.editor.flow': '流程編輯器',
+  'menu.editor.mind': '腦圖編輯器',
+  'menu.editor.koni': '拓撲編輯器',
 };

+ 43 - 0
src/pages/Editor/GGEditor/Flow/index.js

@@ -0,0 +1,43 @@
+import React from 'react';
+import { Row, Col } from 'antd';
+import GGEditor, { Flow } from 'gg-editor';
+import EditorMinimap from '../components/EditorMinimap';
+import { FlowContextMenu } from '../components/EditorContextMenu';
+import { FlowToolbar } from '../components/EditorToolbar';
+import { FlowItemPanel } from '../components/EditorItemPanel';
+import { FlowDetailPanel } from '../components/EditorDetailPanel';
+import styles from './index.less';
+import { FormattedMessage } from 'umi/locale';
+import PageHeaderWrapper from '@/components/PageHeaderWrapper';
+
+const FlowPage = () => {
+  return (
+    <PageHeaderWrapper
+      title={<FormattedMessage id="app.editor.flow.title" />}
+      content={<FormattedMessage id="app.editor.flow.description" />}
+    >
+      <GGEditor className={styles.editor}>
+        <Row type="flex" className={styles.editorHd}>
+          <Col span={24}>
+            <FlowToolbar />
+          </Col>
+        </Row>
+        <Row type="flex" className={styles.editorBd}>
+          <Col span={4} className={styles.editorSidebar}>
+            <FlowItemPanel />
+          </Col>
+          <Col span={16} className={styles.editorContent}>
+            <Flow className={styles.flow} />
+          </Col>
+          <Col span={4} className={styles.editorSidebar}>
+            <FlowDetailPanel />
+            <EditorMinimap />
+          </Col>
+        </Row>
+        <FlowContextMenu />
+      </GGEditor>
+    </PageHeaderWrapper>
+  );
+};
+
+export default FlowPage;

+ 41 - 0
src/pages/Editor/GGEditor/Flow/index.less

@@ -0,0 +1,41 @@
+.editor {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  width: 100%;
+  height: calc(100vh - 250px);
+  background: #fff;
+}
+
+.editorHd {
+  padding: 8px;
+  border: 1px solid #e6e9ed;
+}
+
+.editorBd {
+  flex: 1;
+}
+
+.editorSidebar,
+.editorContent {
+  display: flex;
+  flex-direction: column;
+}
+
+.editorSidebar {
+  background: #fafafa;
+
+  &:first-child {
+    border-right: 1px solid #e6e9ed;
+  }
+
+  &:last-child {
+    border-left: 1px solid #e6e9ed;
+  }
+}
+
+.flow,
+.mind,
+.koni {
+  flex: 1;
+}

+ 45 - 0
src/pages/Editor/GGEditor/Koni/index.js

@@ -0,0 +1,45 @@
+import React from 'react';
+import { Row, Col } from 'antd';
+import GGEditor, { Koni } from 'gg-editor';
+import EditorMinimap from '../components/EditorMinimap';
+import { KoniContextMenu } from '../components/EditorContextMenu';
+import { KoniToolbar } from '../components/EditorToolbar';
+import { KoniItemPanel } from '../components/EditorItemPanel';
+import { KoniDetailPanel } from '../components/EditorDetailPanel';
+import KoniCustomNode from './shape/nodes/KoniCustomNode';
+import styles from '../Flow/index.less';
+import { FormattedMessage } from 'umi/locale';
+import PageHeaderWrapper from '@/components/PageHeaderWrapper';
+
+const KoniPage = () => {
+  return (
+    <PageHeaderWrapper
+      title={<FormattedMessage id="app.editor.koni.title" />}
+      content={<FormattedMessage id="app.editor.koni.description" />}
+    >
+      <GGEditor className={styles.editor}>
+        <Row type="flex" className={styles.editorHd}>
+          <Col span={24}>
+            <KoniToolbar />
+          </Col>
+        </Row>
+        <Row type="flex" className={styles.editorBd}>
+          <Col span={4} className={styles.editorSidebar}>
+            <KoniItemPanel />
+          </Col>
+          <Col span={16} className={styles.editorContent}>
+            <Koni className={styles.koni} />
+          </Col>
+          <Col span={4} className={styles.editorSidebar}>
+            <KoniDetailPanel />
+            <EditorMinimap />
+          </Col>
+        </Row>
+        <KoniCustomNode />
+        <KoniContextMenu />
+      </GGEditor>
+    </PageHeaderWrapper>
+  );
+};
+
+export default KoniPage;

+ 33 - 0
src/pages/Editor/GGEditor/Koni/shape/nodes/KoniCustomNode.js

@@ -0,0 +1,33 @@
+import React from 'react';
+import { RegisterNode } from 'gg-editor';
+
+class KoniCustomNode extends React.Component {
+  render() {
+    const config = {
+      draw(item) {
+        const keyShape = this.drawKeyShape(item);
+
+        // draw label
+        this.drawLabel(item);
+
+        // draw image
+        const group = item.getGraphicGroup();
+        const model = item.getModel();
+
+        group.addShape('image', {
+          attrs: {
+            x: -7,
+            y: -7,
+            img: model.icon,
+          },
+        });
+
+        return keyShape;
+      },
+    };
+
+    return <RegisterNode name="koni-custom-node" config={config} />;
+  }
+}
+
+export default KoniCustomNode;

+ 40 - 0
src/pages/Editor/GGEditor/Mind/index.js

@@ -0,0 +1,40 @@
+import React from 'react';
+import { Row, Col } from 'antd';
+import GGEditor, { Mind } from 'gg-editor';
+import EditorMinimap from '../components/EditorMinimap';
+import { MindContextMenu } from '../components/EditorContextMenu';
+import { MindToolbar } from '../components/EditorToolbar';
+import { MindDetailPanel } from '../components/EditorDetailPanel';
+import data from '../mock/worldCup2018.json';
+import styles from '../Flow/index.less';
+import { FormattedMessage } from 'umi/locale';
+import PageHeaderWrapper from '@/components/PageHeaderWrapper';
+
+const MindPage = () => {
+  return (
+    <PageHeaderWrapper
+      title={<FormattedMessage id="app.editor.mind.title" />}
+      content={<FormattedMessage id="app.editor.mind.description" />}
+    >
+      <GGEditor className={styles.editor}>
+        <Row type="flex" className={styles.editorHd}>
+          <Col span={24}>
+            <MindToolbar />
+          </Col>
+        </Row>
+        <Row type="flex" className={styles.editorBd}>
+          <Col span={20} className={styles.editorContent}>
+            <Mind data={data} className={styles.mind} />
+          </Col>
+          <Col span={4} className={styles.editorSidebar}>
+            <MindDetailPanel />
+            <EditorMinimap />
+          </Col>
+        </Row>
+        <MindContextMenu />
+      </GGEditor>
+    </PageHeaderWrapper>
+  );
+};
+
+export default MindPage;

+ 7 - 0
src/pages/Editor/GGEditor/common/IconFont/index.js

@@ -0,0 +1,7 @@
+import { Icon } from 'antd';
+
+const IconFont = Icon.createFromIconfontCN({
+  scriptUrl: 'https://at.alicdn.com/t/font_1101588_01zniftxm9yp.js',
+});
+
+export default IconFont;

+ 36 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/FlowContextMenu.js

@@ -0,0 +1,36 @@
+import React from 'react';
+import { NodeMenu, EdgeMenu, GroupMenu, MultiMenu, CanvasMenu, ContextMenu } from 'gg-editor';
+import MenuItem from './MenuItem';
+import styles from './index.less';
+
+const FlowContextMenu = () => {
+  return (
+    <ContextMenu className={styles.contextMenu}>
+      <NodeMenu>
+        <MenuItem command="copy" />
+        <MenuItem command="delete" />
+      </NodeMenu>
+      <EdgeMenu>
+        <MenuItem command="delete" />
+      </EdgeMenu>
+      <GroupMenu>
+        <MenuItem command="copy" />
+        <MenuItem command="delete" />
+        <MenuItem command="unGroup" icon="ungroup" text="Ungroup" />
+      </GroupMenu>
+      <MultiMenu>
+        <MenuItem command="copy" />
+        <MenuItem command="paste" />
+        <MenuItem command="addGroup" icon="group" text="Add Group" />
+        <MenuItem command="delete" />
+      </MultiMenu>
+      <CanvasMenu>
+        <MenuItem command="undo" />
+        <MenuItem command="redo" />
+        <MenuItem command="pasteHere" icon="paste" text="Paste Here" />
+      </CanvasMenu>
+    </ContextMenu>
+  );
+};
+
+export default FlowContextMenu;

+ 3 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/KoniContextMenu.js

@@ -0,0 +1,3 @@
+import FlowContextMenu from './FlowContextMenu';
+
+export default FlowContextMenu;

+ 20 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/MenuItem.js

@@ -0,0 +1,20 @@
+import React from 'react';
+import { Command } from 'gg-editor';
+import upperFirst from 'lodash/upperFirst';
+import IconFont from '../../common/IconFont';
+import styles from './index.less';
+
+const MenuItem = props => {
+  const { command, icon, text } = props;
+
+  return (
+    <Command name={command}>
+      <div className={styles.item}>
+        <IconFont type={`icon-${icon || command}`} />
+        <span>{text || upperFirst(command)}</span>
+      </div>
+    </Command>
+  );
+};
+
+export default MenuItem;

+ 24 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/MindContextMenu.js

@@ -0,0 +1,24 @@
+import React from 'react';
+import { NodeMenu, CanvasMenu, ContextMenu } from 'gg-editor';
+import MenuItem from './MenuItem';
+import styles from './index.less';
+
+const MindContextMenu = () => {
+  return (
+    <ContextMenu className={styles.contextMenu}>
+      <NodeMenu>
+        <MenuItem command="append" text="Topic" />
+        <MenuItem command="appendChild" icon="append-child" text="Subtopic" />
+        <MenuItem command="collapse" text="Fold" />
+        <MenuItem command="expand" text="Unfold" />
+        <MenuItem command="delete" />
+      </NodeMenu>
+      <CanvasMenu>
+        <MenuItem command="undo" />
+        <MenuItem command="redo" />
+      </CanvasMenu>
+    </ContextMenu>
+  );
+};
+
+export default MindContextMenu;

+ 5 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/index.js

@@ -0,0 +1,5 @@
+import FlowContextMenu from './FlowContextMenu';
+import MindContextMenu from './MindContextMenu';
+import KoniContextMenu from './KoniContextMenu';
+
+export { FlowContextMenu, MindContextMenu, KoniContextMenu };

+ 39 - 0
src/pages/Editor/GGEditor/components/EditorContextMenu/index.less

@@ -0,0 +1,39 @@
+.contextMenu {
+  display: none;
+  overflow: hidden;
+  background: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+  .item {
+    display: flex;
+    align-items: center;
+    padding: 5px 12px;
+    cursor: pointer;
+    transition: all 0.3s;
+    user-select: none;
+
+    &:hover {
+      background: #e6f7ff;
+    }
+
+    i {
+      margin-right: 8px;
+    }
+  }
+
+  :global {
+    .disable {
+      :local {
+        .item {
+          color: rgba(0, 0, 0, 0.25);
+          cursor: auto;
+
+          &:hover {
+            background: #fff;
+          }
+        }
+      }
+    }
+  }
+}

+ 129 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/DetailForm.js

@@ -0,0 +1,129 @@
+import React, { Fragment } from 'react';
+import { Card, Form, Input, Select } from 'antd';
+import { withPropsAPI } from 'gg-editor';
+import upperFirst from 'lodash/upperFirst';
+
+const { Item } = Form;
+const { Option } = Select;
+
+const inlineFormItemLayout = {
+  labelCol: {
+    sm: { span: 8 },
+  },
+  wrapperCol: {
+    sm: { span: 16 },
+  },
+};
+
+class DetailForm extends React.Component {
+  get item() {
+    const { propsAPI } = this.props;
+
+    return propsAPI.getSelected()[0];
+  }
+
+  handleSubmit = e => {
+    if (e && e.preventDefault) {
+      e.preventDefault();
+    }
+
+    const { form, propsAPI } = this.props;
+    const { getSelected, executeCommand, update } = propsAPI;
+
+    setTimeout(() => {
+      form.validateFieldsAndScroll((err, values) => {
+        if (err) {
+          return;
+        }
+
+        const item = getSelected()[0];
+
+        if (!item) {
+          return;
+        }
+
+        executeCommand(() => {
+          update(item, {
+            ...values,
+          });
+        });
+      });
+    }, 0);
+  };
+
+  renderEdgeShapeSelect = () => {
+    return (
+      <Select onChange={this.handleSubmit}>
+        <Option value="flow-smooth">Smooth</Option>
+        <Option value="flow-polyline">Polyline</Option>
+        <Option value="flow-polyline-round">Polyline Round</Option>
+      </Select>
+    );
+  };
+
+  renderNodeDetail = () => {
+    const { form } = this.props;
+    const { label } = this.item.getModel();
+
+    return (
+      <Item label="Label" {...inlineFormItemLayout}>
+        {form.getFieldDecorator('label', {
+          initialValue: label,
+        })(<Input onBlur={this.handleSubmit} />)}
+      </Item>
+    );
+  };
+
+  renderEdgeDetail = () => {
+    const { form } = this.props;
+    const { label = '', shape = 'flow-smooth' } = this.item.getModel();
+
+    return (
+      <Fragment>
+        <Item label="Label" {...inlineFormItemLayout}>
+          {form.getFieldDecorator('label', {
+            initialValue: label,
+          })(<Input onBlur={this.handleSubmit} />)}
+        </Item>
+        <Item label="Shape" {...inlineFormItemLayout}>
+          {form.getFieldDecorator('shape', {
+            initialValue: shape,
+          })(this.renderEdgeShapeSelect())}
+        </Item>
+      </Fragment>
+    );
+  };
+
+  renderGroupDetail = () => {
+    const { form } = this.props;
+    const { label = '新建分组' } = this.item.getModel();
+
+    return (
+      <Item label="Label" {...inlineFormItemLayout}>
+        {form.getFieldDecorator('label', {
+          initialValue: label,
+        })(<Input onBlur={this.handleSubmit} />)}
+      </Item>
+    );
+  };
+
+  render() {
+    const { type } = this.props;
+
+    if (!this.item) {
+      return null;
+    }
+
+    return (
+      <Card type="inner" size="small" title={upperFirst(type)} bordered={false}>
+        <Form onSubmit={this.handleSubmit}>
+          {type === 'node' && this.renderNodeDetail()}
+          {type === 'edge' && this.renderEdgeDetail()}
+          {type === 'group' && this.renderGroupDetail()}
+        </Form>
+      </Card>
+    );
+  }
+}
+
+export default Form.create()(withPropsAPI(DetailForm));

+ 29 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/FlowDetailPanel.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import { Card } from 'antd';
+import { NodePanel, EdgePanel, GroupPanel, MultiPanel, CanvasPanel, DetailPanel } from 'gg-editor';
+import DetailForm from './DetailForm';
+import styles from './index.less';
+
+const FlowDetailPanel = () => {
+  return (
+    <DetailPanel className={styles.detailPanel}>
+      <NodePanel>
+        <DetailForm type="node" />
+      </NodePanel>
+      <EdgePanel>
+        <DetailForm type="edge" />
+      </EdgePanel>
+      <GroupPanel>
+        <DetailForm type="group" />
+      </GroupPanel>
+      <MultiPanel>
+        <Card type="inner" size="small" title="Multi Select" bordered={false} />
+      </MultiPanel>
+      <CanvasPanel>
+        <Card type="inner" size="small" title="Canvas" bordered={false} />
+      </CanvasPanel>
+    </DetailPanel>
+  );
+};
+
+export default FlowDetailPanel;

+ 3 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/KoniDetailPanel.js

@@ -0,0 +1,3 @@
+import FlowDetailPanel from './FlowDetailPanel';
+
+export default FlowDetailPanel;

+ 20 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/MindDetailPanel.js

@@ -0,0 +1,20 @@
+import React from 'react';
+import { Card } from 'antd';
+import { NodePanel, CanvasPanel, DetailPanel } from 'gg-editor';
+import DetailForm from './DetailForm';
+import styles from './index.less';
+
+const MindDetailPanel = () => {
+  return (
+    <DetailPanel className={styles.detailPanel}>
+      <NodePanel>
+        <DetailForm type="node" />
+      </NodePanel>
+      <CanvasPanel>
+        <Card type="inner" size="small" title="Canvas" bordered={false} />
+      </CanvasPanel>
+    </DetailPanel>
+  );
+};
+
+export default MindDetailPanel;

+ 5 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/index.js

@@ -0,0 +1,5 @@
+import FlowDetailPanel from './FlowDetailPanel';
+import MindDetailPanel from './MindDetailPanel';
+import KoniDetailPanel from './KoniDetailPanel';
+
+export { FlowDetailPanel, MindDetailPanel, KoniDetailPanel };

+ 10 - 0
src/pages/Editor/GGEditor/components/EditorDetailPanel/index.less

@@ -0,0 +1,10 @@
+.detailPanel {
+  flex: 1;
+  background: #fafafa;
+
+  :global {
+    .ant-card {
+      background: #fafafa;
+    }
+  }
+}

+ 55 - 0
src/pages/Editor/GGEditor/components/EditorItemPanel/FlowItemPanel.js

@@ -0,0 +1,55 @@
+import React from 'react';
+import { Card } from 'antd';
+import { ItemPanel, Item } from 'gg-editor';
+import styles from './index.less';
+
+const FlowItemPanel = () => {
+  return (
+    <ItemPanel className={styles.itemPanel}>
+      <Card bordered={false}>
+        <Item
+          type="node"
+          size="72*72"
+          shape="flow-circle"
+          model={{
+            color: '#FA8C16',
+            label: 'Start',
+          }}
+          src="/ggeditor/flow/start.svg"
+        />
+        <Item
+          type="node"
+          size="80*48"
+          shape="flow-rect"
+          model={{
+            color: '#1890FF',
+            label: 'Normal',
+          }}
+          src="/ggeditor/flow/normal.svg"
+        />
+        <Item
+          type="node"
+          size="80*72"
+          shape="flow-rhombus"
+          model={{
+            color: '#13C2C2',
+            label: 'Decision',
+          }}
+          src="/ggeditor/flow/decision.svg"
+        />
+        <Item
+          type="node"
+          size="80*48"
+          shape="flow-capsule"
+          model={{
+            color: '#722ED1',
+            label: 'Model',
+          }}
+          src="/ggeditor/flow/model.svg"
+        />
+      </Card>
+    </ItemPanel>
+  );
+};
+
+export default FlowItemPanel;

+ 51 - 0
src/pages/Editor/GGEditor/components/EditorItemPanel/KoniItemPanel.js

@@ -0,0 +1,51 @@
+import React from 'react';
+import { Card } from 'antd';
+import { ItemPanel, Item } from 'gg-editor';
+import styles from './index.less';
+
+const KoniItemPanel = () => {
+  return (
+    <ItemPanel className={styles.itemPanel}>
+      <Card bordered={false}>
+        <Item
+          type="node"
+          size="40"
+          shape="koni-custom-node"
+          model={{
+            color: '#69C0FF',
+            label: 'Bank',
+            labelOffsetY: 28,
+            icon: '/ggeditor/koni/icon.svg',
+          }}
+          src="/ggeditor/koni/bank.svg"
+        />
+        <Item
+          type="node"
+          size="40"
+          shape="koni-custom-node"
+          model={{
+            color: '#5CDBD3',
+            label: 'Person',
+            labelOffsetY: 28,
+            icon: '/ggeditor/koni/icon.svg',
+          }}
+          src="/ggeditor/koni/person.svg"
+        />
+        <Item
+          type="node"
+          size="40"
+          shape="koni-custom-node"
+          model={{
+            color: '#B37FEB',
+            label: 'Country',
+            labelOffsetY: 28,
+            icon: '/ggeditor/koni/icon.svg',
+          }}
+          src="/ggeditor/koni/country.svg"
+        />
+      </Card>
+    </ItemPanel>
+  );
+};
+
+export default KoniItemPanel;

+ 4 - 0
src/pages/Editor/GGEditor/components/EditorItemPanel/index.js

@@ -0,0 +1,4 @@
+import FlowItemPanel from './FlowItemPanel';
+import KoniItemPanel from './KoniItemPanel';
+
+export { FlowItemPanel, KoniItemPanel };

+ 20 - 0
src/pages/Editor/GGEditor/components/EditorItemPanel/index.less

@@ -0,0 +1,20 @@
+.itemPanel {
+  flex: 1;
+  background: #fafafa;
+
+  :global {
+    .ant-card {
+      background: #fafafa;
+    }
+
+    .ant-card-body {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+
+      > div {
+        margin-bottom: 16px;
+      }
+    }
+  }
+}

+ 13 - 0
src/pages/Editor/GGEditor/components/EditorMinimap/index.js

@@ -0,0 +1,13 @@
+import React from 'react';
+import { Card } from 'antd';
+import { Minimap } from 'gg-editor';
+
+const EditorMinimap = () => {
+  return (
+    <Card type="inner" size="small" title="Minimap" bordered={false}>
+      <Minimap height={200} />
+    </Card>
+  );
+};
+
+export default EditorMinimap;

+ 32 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/FlowToolbar.js

@@ -0,0 +1,32 @@
+import React from 'react';
+import { Divider } from 'antd';
+import { Toolbar } from 'gg-editor';
+import ToolbarButton from './ToolbarButton';
+import styles from './index.less';
+
+const FlowToolbar = () => {
+  return (
+    <Toolbar className={styles.toolbar}>
+      <ToolbarButton command="undo" />
+      <ToolbarButton command="redo" />
+      <Divider type="vertical" />
+      <ToolbarButton command="copy" />
+      <ToolbarButton command="paste" />
+      <ToolbarButton command="delete" />
+      <Divider type="vertical" />
+      <ToolbarButton command="zoomIn" icon="zoom-in" text="Zoom In" />
+      <ToolbarButton command="zoomOut" icon="zoom-out" text="Zoom Out" />
+      <ToolbarButton command="autoZoom" icon="fit-map" text="Fit Map" />
+      <ToolbarButton command="resetZoom" icon="actual-size" text="Actual Size" />
+      <Divider type="vertical" />
+      <ToolbarButton command="toBack" icon="to-back" text="To Back" />
+      <ToolbarButton command="toFront" icon="to-front" text="To Front" />
+      <Divider type="vertical" />
+      <ToolbarButton command="multiSelect" icon="multi-select" text="Multi Select" />
+      <ToolbarButton command="addGroup" icon="group" text="Add Group" />
+      <ToolbarButton command="unGroup" icon="ungroup" text="Ungroup" />
+    </Toolbar>
+  );
+};
+
+export default FlowToolbar;

+ 3 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/KoniToolbar.js

@@ -0,0 +1,3 @@
+import FlowToolbar from './FlowToolbar';
+
+export default FlowToolbar;

+ 27 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/MindToolbar.js

@@ -0,0 +1,27 @@
+import React from 'react';
+import { Divider } from 'antd';
+import { Toolbar } from 'gg-editor';
+import ToolbarButton from './ToolbarButton';
+import styles from './index.less';
+
+const FlowToolbar = () => {
+  return (
+    <Toolbar className={styles.toolbar}>
+      <ToolbarButton command="undo" />
+      <ToolbarButton command="redo" />
+      <Divider type="vertical" />
+      <ToolbarButton command="zoomIn" icon="zoom-in" text="Zoom In" />
+      <ToolbarButton command="zoomOut" icon="zoom-out" text="Zoom Out" />
+      <ToolbarButton command="autoZoom" icon="fit-map" text="Fit Map" />
+      <ToolbarButton command="resetZoom" icon="actual-size" text="Actual Size" />
+      <Divider type="vertical" />
+      <ToolbarButton command="append" text="Topic" />
+      <ToolbarButton command="appendChild" icon="append-child" text="Subtopic" />
+      <Divider type="vertical" />
+      <ToolbarButton command="collapse" text="Fold" />
+      <ToolbarButton command="expand" text="Unfold" />
+    </Toolbar>
+  );
+};
+
+export default FlowToolbar;

+ 24 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/ToolbarButton.js

@@ -0,0 +1,24 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+import { Command } from 'gg-editor';
+import upperFirst from 'lodash/upperFirst';
+import IconFont from '../../common/IconFont';
+import styles from './index.less';
+
+const ToolbarButton = props => {
+  const { command, icon, text } = props;
+
+  return (
+    <Command name={command}>
+      <Tooltip
+        title={text || upperFirst(command)}
+        placement="bottom"
+        overlayClassName={styles.tooltip}
+      >
+        <IconFont type={`icon-${icon || command}`} />
+      </Tooltip>
+    </Command>
+  );
+};
+
+export default ToolbarButton;

+ 5 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/index.js

@@ -0,0 +1,5 @@
+import FlowToolbar from './FlowToolbar';
+import MindToolbar from './MindToolbar';
+import KoniToolbar from './KoniToolbar';
+
+export { FlowToolbar, MindToolbar, KoniToolbar };

+ 39 - 0
src/pages/Editor/GGEditor/components/EditorToolbar/index.less

@@ -0,0 +1,39 @@
+.toolbar {
+  display: flex;
+  align-items: center;
+
+  :global {
+    .command i {
+      display: inline-block;
+      width: 27px;
+      height: 27px;
+      margin: 0 6px;
+      padding-top: 6px;
+      text-align: center;
+      border: 1px solid #fff;
+      cursor: pointer;
+
+      &:hover {
+        border: 1px solid #e6e9ed;
+      }
+    }
+
+    .disable i {
+      color: rgba(0, 0, 0, 0.25);
+      cursor: auto;
+
+      &:hover {
+        border: 1px solid #fff;
+      }
+    }
+  }
+}
+
+.tooltip {
+  :global {
+    .ant-tooltip-inner {
+      font-size: 12px;
+      border-radius: 0;
+    }
+  }
+}

+ 129 - 0
src/pages/Editor/GGEditor/mock/worldCup2018.json

@@ -0,0 +1,129 @@
+{
+  "roots": [
+    {
+      "label": "法国",
+      "children": [
+        {
+          "label": "克罗地亚",
+          "side": "left",
+          "children": [
+            {
+              "label": "克罗地亚",
+              "children": [
+                {
+                  "label": "克罗地亚",
+                  "children": [
+                    {
+                      "label": "克罗地亚"
+                    },
+                    {
+                      "label": "丹麦"
+                    }
+                  ]
+                },
+                {
+                  "label": "俄罗斯",
+                  "children": [
+                    {
+                      "label": "俄罗斯"
+                    },
+                    {
+                      "label": "西班牙"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "label": "英格兰",
+              "children": [
+                {
+                  "label": "英格兰",
+                  "children": [
+                    {
+                      "label": "英格兰"
+                    },
+                    {
+                      "label": "哥伦比亚"
+                    }
+                  ]
+                },
+                {
+                  "label": "瑞典",
+                  "children": [
+                    {
+                      "label": "瑞士"
+                    },
+                    {
+                      "label": "瑞典"
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "label": "法国",
+          "side": "right",
+          "children": [
+            {
+              "label": "法国",
+              "children": [
+                {
+                  "label": "法国",
+                  "children": [
+                    {
+                      "label": "法国"
+                    },
+                    {
+                      "label": "阿根廷"
+                    }
+                  ]
+                },
+                {
+                  "label": "乌拉圭",
+                  "children": [
+                    {
+                      "label": "乌拉圭"
+                    },
+                    {
+                      "label": "葡萄牙"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "label": "比利时",
+              "children": [
+                {
+                  "label": "比利时",
+                  "children": [
+                    {
+                      "label": "比利时"
+                    },
+                    {
+                      "label": "日本"
+                    }
+                  ]
+                },
+                {
+                  "label": "巴西",
+                  "children": [
+                    {
+                      "label": "巴西"
+                    },
+                    {
+                      "label": "墨西哥"
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}