Selaa lähdekoodia

feat: 左侧添加基础图形, 渲染区域可以显示

Gaokun Wang 4 päivää sitten
vanhempi
sitoutus
261e92be59
45 muutettua tiedostoa jossa 961 lisäystä ja 622 poistoa
  1. 11 0
      package-lock.json
  2. 1 0
      package.json
  3. BIN
      public/icon/2.png
  4. BIN
      public/icon/KAFKA.png
  5. BIN
      public/icon/authorizate.png
  6. BIN
      public/icon/beats.png
  7. BIN
      public/icon/boxDomain.png
  8. BIN
      public/icon/center.png
  9. BIN
      public/icon/cluster.png
  10. BIN
      public/icon/connectPoint.png
  11. BIN
      public/icon/connectionpoint.png
  12. BIN
      public/icon/database.png
  13. BIN
      public/icon/delete.png
  14. BIN
      public/icon/dot.gif
  15. 1 0
      public/icon/dot.svg
  16. BIN
      public/icon/editor.png
  17. BIN
      public/icon/elasticsearch.png
  18. BIN
      public/icon/exchange.png
  19. BIN
      public/icon/group.png
  20. BIN
      public/icon/handler.png
  21. BIN
      public/icon/home.png
  22. BIN
      public/icon/input2.png
  23. 13 0
      public/icon/kibana.svg
  24. BIN
      public/icon/label.png
  25. BIN
      public/icon/line.png
  26. BIN
      public/icon/logstash.png
  27. BIN
      public/icon/output2.png
  28. BIN
      public/icon/redis.png
  29. 1 0
      public/icon/rotate.svg
  30. BIN
      public/icon/stencil.png
  31. BIN
      public/icon/toolbar.png
  32. BIN
      public/icon/toolbar2.png
  33. BIN
      public/icon/warning.gif
  34. 1 0
      public/icon/旋转.png
  35. 1 0
      public/icon/旋转.svg
  36. BIN
      public/icon/无标题文档.pdf
  37. 3 1
      src/types/auto-components.d.ts
  38. 17 0
      src/types/mxgraph.d.ts
  39. 0 120
      src/views/components/MxGraphContainer.vue
  40. 0 147
      src/views/components/MxPalette.vue
  41. 0 164
      src/views/components/MxPropertiesPanel.vue
  42. 0 156
      src/views/components/MxToolbar.vue
  43. 49 0
      src/views/components/MyMxGraph/graph_shapes.ts
  44. 862 0
      src/views/components/MyMxGraph/index.vue
  45. 1 34
      src/views/test/mxgraph.vue

+ 11 - 0
package-lock.json

@@ -18,6 +18,7 @@
         "elkjs": "^0.10.0",
         "mxgraph": "^4.2.2",
         "nprogress": "^0.2.0",
+        "ramda": "^0.31.3",
         "tailwindcss": "^4.1.10",
         "tdesign-icons-vue-next": "^0.3.6",
         "tdesign-vue-next": "^1.13.2"
@@ -8028,6 +8029,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/ramda": {
+      "version": "0.31.3",
+      "resolved": "https://registry.npmmirror.com/ramda/-/ramda-0.31.3.tgz",
+      "integrity": "sha512-xKADKRNnqmDdX59PPKLm3gGmk1ZgNnj3k7DryqWwkamp4TJ6B36DdpyKEQ0EoEYmH2R62bV4Q+S0ym2z8N2f3Q==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ramda"
+      }
+    },
     "node_modules/read-binary-file-arch": {
       "version": "1.0.6",
       "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "elkjs": "^0.10.0",
     "mxgraph": "^4.2.2",
     "nprogress": "^0.2.0",
+    "ramda": "^0.31.3",
     "tailwindcss": "^4.1.10",
     "tdesign-icons-vue-next": "^0.3.6",
     "tdesign-vue-next": "^1.13.2"

BIN
public/icon/2.png


BIN
public/icon/KAFKA.png


BIN
public/icon/authorizate.png


BIN
public/icon/beats.png


BIN
public/icon/boxDomain.png


BIN
public/icon/center.png


BIN
public/icon/cluster.png


BIN
public/icon/connectPoint.png


BIN
public/icon/connectionpoint.png


BIN
public/icon/database.png


BIN
public/icon/delete.png


BIN
public/icon/dot.gif


+ 1 - 0
public/icon/dot.svg

@@ -0,0 +1 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" version="1.1"><circle cx="9" cy="9" r="5" stroke="#fff" fill="#29b6f2" stroke-width="1"/></svg>

BIN
public/icon/editor.png


BIN
public/icon/elasticsearch.png


BIN
public/icon/exchange.png


BIN
public/icon/group.png


BIN
public/icon/handler.png


BIN
public/icon/home.png


BIN
public/icon/input2.png


+ 13 - 0
public/icon/kibana.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="64px" height="64px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
+    <title>icon / product-logo / 64x64px / stack / color</title>
+    <desc>Created with Sketch.</desc>
+    <g id="icon-/-product-logo-/-64x64px-/-stack-/-color" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="logo-kibana-64x64-color-reverse" transform="translate(11.000000, 4.999900)">
+            <path d="M-2.84217094e-14,20.25 L-2.84217094e-14,48.581 L20.137,25.392 C14.149,22.13 7.299,20.25 -2.84217094e-14,20.25" id="Fill-1" fill="#343741"></path>
+            <path d="M-7.10542736e-15,-3.01980663e-14 L-7.10542736e-15,20.25 C7.299,20.25 14.149,22.13 20.137,25.392 L42.188,-3.01980663e-14 L-7.10542736e-15,-3.01980663e-14 Z" id="Fill-3" fill="#F04E98"></path>
+            <path d="M24.4434,28.0591 L3.7754,51.8601 L1.9164,54.0001 L41.3414,54.0001 C39.1784,43.3421 32.9924,34.1491 24.4434,28.0591" id="Fill-6" fill="#00BFB3"></path>
+        </g>
+    </g>
+</svg>

BIN
public/icon/label.png


BIN
public/icon/line.png


BIN
public/icon/logstash.png


BIN
public/icon/output2.png


BIN
public/icon/redis.png


+ 1 - 0
public/icon/rotate.svg

@@ -0,0 +1 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 24 24" version="1.1"><path stroke="#29b6f2" fill="#29b6f2" d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/></svg>

BIN
public/icon/stencil.png


BIN
public/icon/toolbar.png


BIN
public/icon/toolbar2.png


BIN
public/icon/warning.gif


+ 1 - 0
public/icon/旋转.png

@@ -0,0 +1 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 24 24" version="1.1"><path stroke="#29b6f2" fill="#29b6f2" d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/></svg>

+ 1 - 0
public/icon/旋转.svg

@@ -0,0 +1 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 24 24" version="1.1"><path stroke="#29b6f2" fill="#29b6f2" d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/></svg>

BIN
public/icon/无标题文档.pdf


+ 3 - 1
src/types/auto-components.d.ts

@@ -8,13 +8,15 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    Mxgraph: typeof import('./../views/components/mxgraph/index.vue')['default']
     MxGraphContainer: typeof import('./../views/components/MxGraphContainer.vue')['default']
     MxPalette: typeof import('./../views/components/MxPalette.vue')['default']
     MxPropertiesPanel: typeof import('./../views/components/MxPropertiesPanel.vue')['default']
     MxToolbar: typeof import('./../views/components/MxToolbar.vue')['default']
+    MyMxgraph: typeof import('./../views/components/MyMxgraph/index.vue')['default']
+    MyMxGraph: typeof import('./../views/components/MyMxGraph/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
-    TButton: typeof import('tdesign-vue-next')['Button']
     TConfigProvider: typeof import('tdesign-vue-next')['ConfigProvider']
     THeadMenu: typeof import('tdesign-vue-next')['HeadMenu']
     TMenuItem: typeof import('tdesign-vue-next')['MenuItem']

+ 17 - 0
src/types/mxgraph.d.ts

@@ -0,0 +1,17 @@
+export interface PaletteShape {
+  index: number
+  idSeed: number
+  type: string
+  id: string
+  title: string
+  class: string
+  width: number
+  height: number
+  style: Record<string, any>
+  icon?: string
+  iconColor?: string
+}
+export interface CellGeometry {
+  width: number
+  height: number
+}

+ 0 - 120
src/views/components/MxGraphContainer.vue

@@ -1,120 +0,0 @@
-<template>
-  <div ref="graphContainerRef" class="mxgraph-container"></div>
-</template>
-
-<script setup lang="ts">
-import { myMxGraph, myMxRubberband, myMxConstants, myMxEvent, myMxPopupMenu } from '@/graph/mxGraph'
-const graphContainerRef = ref<HTMLElement | null>(null)
-const graphInstance = ref<InstanceType<typeof myMxGraph> | null>(null)
-provide('mxgraph-instance', graphInstance)
-
-function initGraph(container: HTMLElement): InstanceType<typeof myMxGraph> {
-  const graph = new myMxGraph(container)
-
-  // 基本配置
-  graph.setPanning(true)
-  graph.setConnectable(true)
-  new myMxRubberband(graph)
-
-  // 配置样式
-  configureStyles(graph)
-
-  // 设置事件监听
-  setupGraphListeners(graph)
-
-  // 添加上下文菜单
-  setupContextMenu(graph)
-
-  // 初始示例图形
-  createSampleGraph(graph)
-
-  return graph
-}
-
-function configureStyles(graph: InstanceType<typeof myMxGraph>) {
-  const style = graph.getStylesheet().getDefaultVertexStyle()
-  Object.assign(style, {
-    [myMxConstants.STYLE_SHAPE]: myMxConstants.SHAPE_RECTANGLE,
-    [myMxConstants.STYLE_STROKECOLOR]: '#2d8cf0',
-    [myMxConstants.STYLE_FILLCOLOR]: '#ffffff',
-    [myMxConstants.STYLE_FONTCOLOR]: '#333333',
-    [myMxConstants.STYLE_STROKEWIDTH]: 2
-  })
-
-  const edgeStyle = graph.getStylesheet().getDefaultEdgeStyle()
-  Object.assign(edgeStyle, {
-    [myMxConstants.STYLE_ENDARROW]: myMxConstants.ARROW_CLASSIC,
-    [myMxConstants.STYLE_STROKECOLOR]: '#666666',
-    [myMxConstants.STYLE_STROKEWIDTH]: 2
-  })
-}
-
-function setupGraphListeners(graph: InstanceType<typeof myMxGraph>) {
-  graph
-    .getSelectionModel()
-    .addListener(myMxConstants.EVENT_CHANGE, (_sender: unknown, evt: any) => {
-      const cells = evt.getProperty('added')
-      console.log('Selected cells:', cells)
-    })
-
-  graph.addListener(myMxConstants.EVENT_DOUBLE_CLICK, (_sender: unknown, evt: any) => {
-    const cell = evt.getProperty('cell')
-    if (cell) {
-      const newValue = prompt('输入新值:', graph.convertValueToString(cell) || '')
-      if (newValue !== null) {
-        graph.getModel().setValue(cell, newValue)
-      }
-    }
-  })
-}
-
-function setupContextMenu(graph: InstanceType<typeof myMxGraph>) {
-  myMxEvent.disableContextMenu(graph.container)
-
-  graph.addListener(myMxConstants.EVENT_CONTEXT_MENU, (_sender: unknown, evt: any) => {
-    const cell = evt.getProperty('cell')
-    evt.preventDefault()
-
-    if (!cell) return
-
-    const menu = new myMxPopupMenu((menu: any) => {
-      menu.addItem('删除', null, () => {
-        graph.removeCells([cell])
-      })
-
-      menu.addItem('复制', null, () => {
-        graph.setSelectionCell(graph.cloneCell(cell))
-      })
-    })
-
-    const pt = myMxEvent.getClientXY(evt.getEvent())
-    menu.popup(pt.x, pt.y, null, evt.getEvent())
-  })
-}
-
-function createSampleGraph(graph: InstanceType<typeof myMxGraph>) {
-  const parent = graph.getDefaultParent()
-  graph.getModel().beginUpdate()
-  try {
-    const v1 = graph.insertVertex(parent, null, '开始', 20, 20, 80, 40)
-    const v2 = graph.insertVertex(parent, null, '步骤1', 20, 80, 80, 40)
-    const v3 = graph.insertVertex(parent, null, '结束', 20, 140, 80, 40)
-    graph.insertEdge(parent, null, '', v1, v2)
-    graph.insertEdge(parent, null, '', v2, v3)
-  } finally {
-    graph.getModel().endUpdate()
-  }
-}
-onMounted(async () => {
-  if (graphContainerRef.value) {
-    graphInstance.value = initGraph(graphContainerRef.value)
-  }
-})
-</script>
-<style lang="scss" scoped>
-.mxgraph-container {
-  flex: 1;
-  height: calc(100vh - 90px);
-  background: url('@/assets/graph/images/grid.gif');
-}
-</style>

+ 0 - 147
src/views/components/MxPalette.vue

@@ -1,147 +0,0 @@
-<template>
-  <div class="mx-palette">
-    <div class="palette-title">基本图形</div>
-    <div v-for="(shape, index) in shapes" :key="index" class="palette-shape" draggable="true"
-      @dragstart="handleDragStart($event, shape)" @dblclick="handleDoubleClick(shape)">
-      <div class="shape-icon" :style="getShapeStyle(shape)">
-        {{ shape.icon }}
-      </div>
-      <div class="shape-label">{{ shape.label }}</div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { inject } from 'vue'
-import { myMxConstants, myMxGraph } from '@/graph/mxGraph'
-
-export interface PaletteShape {
-  type: string
-  label: string
-  width: number
-  height: number
-  style: Record<string, any>
-  icon?: string
-  iconColor?: string
-}
-
-const graph = inject<Ref<InstanceType<typeof myMxGraph> | null>>('mxgraph-instance')
-
-const shapes: PaletteShape[] = [
-  {
-    type: myMxConstants.SHAPE_RECTANGLE,
-    label: '矩形',
-    width: 80,
-    height: 60,
-    style: {
-      [myMxConstants.STYLE_FILLCOLOR]: '#ffffff',
-      [myMxConstants.STYLE_STROKECOLOR]: '#2d8cf0'
-    },
-    icon: '□',
-    iconColor: '#2d8cf0'
-  }
-  // 其他形状定义...
-]
-
-function getShapeStyle(shape: PaletteShape) {
-  return {
-    backgroundColor: shape.style[myMxConstants.STYLE_FILLCOLOR] || '#ffffff',
-    borderColor: shape.style[myMxConstants.STYLE_STROKECOLOR] || '#000000',
-    color: shape.iconColor
-  }
-}
-
-function handleDragStart(event: DragEvent, shape: PaletteShape) {
-  if (!graph?.value) return
-
-  event.dataTransfer?.setData('text/plain', JSON.stringify(shape))
-
-  const dragPreview = document.createElement('div')
-  dragPreview.style.width = `${shape.width}px`
-  dragPreview.style.height = `${shape.height}px`
-  dragPreview.style.border = `2px dashed ${shape.style[myMxConstants.STYLE_STROKECOLOR]}`
-  dragPreview.style.backgroundColor = shape.style[myMxConstants.STYLE_FILLCOLOR] || 'white'
-  document.body.appendChild(dragPreview)
-  event.dataTransfer?.setDragImage(dragPreview, shape.width / 2, shape.height / 2)
-
-  setTimeout(() => document.body.removeChild(dragPreview), 0)
-}
-
-function handleDoubleClick(shape: PaletteShape) {
-  if (!graph?.value) return
-
-  const parent = graph.value.getDefaultParent()
-  const pt = graph.value.getFreeInsertPoint()
-
-  graph.value.getModel().beginUpdate()
-  try {
-    graph.value.insertVertex(
-      parent,
-      null,
-      shape.label,
-      pt.x,
-      pt.y,
-      shape.width,
-      shape.height,
-      shape.type
-    )
-  } finally {
-    graph.value.getModel().endUpdate()
-  }
-}
-</script>
-<style lang="scss" scoped>
-.mx-palette {
-  width: 160px;
-  height: 100%;
-  border-right: 1px solid #e0e0e0;
-  background-color: #f5f5f5;
-  overflow-y: auto;
-  user-select: none;
-}
-
-.palette-title {
-  padding: 10px;
-  font-weight: bold;
-  border-bottom: 1px solid #ddd;
-  background-color: #eee;
-  position: sticky;
-  top: 0;
-  z-index: 1;
-}
-
-.palette-shape {
-  padding: 10px;
-  margin: 8px;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  background-color: white;
-  cursor: move;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  transition: all 0.2s;
-}
-
-.palette-shape:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  transform: translateY(-2px);
-}
-
-.shape-icon {
-  width: 40px;
-  height: 40px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: 2px solid;
-  margin-bottom: 5px;
-  font-size: 20px;
-  border-radius: 3px;
-}
-
-.shape-label {
-  font-size: 12px;
-  text-align: center;
-}
-</style>

+ 0 - 164
src/views/components/MxPropertiesPanel.vue

@@ -1,164 +0,0 @@
-<template>
-  <div class="mx-properties">
-    <div class="properties-title">属性</div>
-    <div v-if="selectedCell" class="properties-content">
-      <div class="property-item">
-        <label>文本:</label>
-        <input v-model="cellText" @change="updateCellText" type="text" />
-      </div>
-      <div class="property-item">
-        <label>宽度:</label>
-        <input v-model.number="cellGeometry.width" @change="updateCellGeometry" type="number" min="10" />
-      </div>
-      <div class="property-item">
-        <label>高度:</label>
-        <input v-model.number="cellGeometry.height" @change="updateCellGeometry" type="number" min="10" />
-      </div>
-      <div class="property-item">
-        <label>填充色:</label>
-        <input v-model="cellStyle.fillColor" @change="updateCellStyle" type="color" />
-      </div>
-      <div class="property-item">
-        <label>边框色:</label>
-        <input v-model="cellStyle.strokeColor" @change="updateCellStyle" type="color" />
-      </div>
-    </div>
-    <div v-else class="properties-empty">未选中任何元素</div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { inject, ref, watch } from 'vue'
-import { myMxConstants, myMxGraph, myMxUtils } from '@/graph/mxGraph'
-export interface CellGeometry {
-  width: number
-  height: number
-}
-
-const graph = inject<Ref<InstanceType<typeof myMxGraph> | null>>('mxgraph-instance')
-
-const selectedCell = ref<any>(null)
-const cellText = ref('')
-const cellGeometry = ref<CellGeometry>({ width: 0, height: 0 })
-const cellStyle = ref({
-  fillColor: '#ffffff',
-  strokeColor: '#000000'
-})
-
-watch(
-  () => graph?.value,
-  newGraph => {
-    if (!newGraph) return
-
-    newGraph.getSelectionModel().addListener(myMxConstants.EVENT_CHANGE, () => {
-      const cells = newGraph.getSelectionCells()
-      selectedCell.value = cells.length === 1 ? cells[0] : null
-
-      if (selectedCell.value) {
-        cellText.value = newGraph.convertValueToString(selectedCell.value) || ''
-
-        const geo = selectedCell.value.getGeometry()
-        if (geo) {
-          cellGeometry.value = {
-            width: geo.width,
-            height: geo.height
-          }
-        }
-
-        const style = newGraph.getCellStyle(selectedCell.value)
-        cellStyle.value = {
-          fillColor: style[myMxConstants.STYLE_FILLCOLOR] || '#ffffff',
-          strokeColor: style[myMxConstants.STYLE_STROKECOLOR] || '#000000'
-        }
-      }
-    })
-  },
-  { immediate: true }
-)
-
-function updateCellText() {
-  if (!graph?.value || !selectedCell.value) return
-  graph.value.getModel().setValue(selectedCell.value, cellText.value)
-}
-
-function updateCellGeometry() {
-  if (!graph?.value || !selectedCell.value) return
-  const geo = selectedCell.value.getGeometry()
-  if (geo) {
-    geo.width = cellGeometry.value.width
-    geo.height = cellGeometry.value.height
-    graph.value.getModel().setGeometry(selectedCell.value, geo)
-  }
-}
-
-function updateCellStyle() {
-  if (!graph?.value || !selectedCell.value) return
-  const newStyle = {
-    [myMxConstants.STYLE_FILLCOLOR]: cellStyle.value.fillColor,
-    [myMxConstants.STYLE_STROKECOLOR]: cellStyle.value.strokeColor
-  }
-  graph.value
-    .getModel()
-    .setStyle(
-      selectedCell.value,
-      myMxUtils.setStyle(graph.value.getModel().getStyle(selectedCell.value) || '', newStyle)
-    )
-}
-</script>
-<style scoped>
-.mx-properties {
-  width: 240px;
-  height: 100%;
-  border-left: 1px solid #e0e0e0;
-  background-color: #f5f5f5;
-  overflow-y: auto;
-  user-select: none;
-}
-
-.properties-title {
-  padding: 10px;
-  font-weight: bold;
-  border-bottom: 1px solid #ddd;
-  background-color: #eee;
-  position: sticky;
-  top: 0;
-  z-index: 1;
-}
-
-.properties-content {
-  padding: 10px;
-}
-
-.properties-empty {
-  padding: 20px;
-  text-align: center;
-  color: #999;
-}
-
-.property-item {
-  margin-bottom: 15px;
-}
-
-.property-item label {
-  display: block;
-  margin-bottom: 5px;
-  font-size: 13px;
-  color: #666;
-}
-
-.property-item input[type='text'],
-.property-item input[type='number'] {
-  width: 100%;
-  padding: 6px;
-  border: 1px solid #ddd;
-  border-radius: 3px;
-}
-
-.property-item input[type='color'] {
-  width: 100%;
-  height: 30px;
-  padding: 2px;
-  border: 1px solid #ddd;
-  border-radius: 3px;
-}
-</style>

+ 0 - 156
src/views/components/MxToolbar.vue

@@ -1,156 +0,0 @@
-<template>
-  <div class="mx-toolbar">
-    <button @click="zoomIn"><span class="icon">+</span> 放大</button>
-    <button @click="zoomOut"><span class="icon">-</span> 缩小</button>
-    <button @click="zoomActual"><span class="icon">↻</span> 实际大小</button>
-    <button @click="deleteSelected"><span class="icon">×</span> 删除</button>
-    <button @click="saveGraph"><span class="icon">💾</span> 保存</button>
-    <button @click="loadGraph"><span class="icon">📂</span> 加载</button>
-    <button @click="exportXML"><span class="icon">📤</span> 导出XML</button>
-    <button @click="importXML"><span class="icon">📥</span> 导入XML</button>
-    <input ref="fileInput" type="file" accept=".xml" @change="handleFileUpload" style="display: none" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { inject } from 'vue'
-import { myMxCodec, myMxGraph, myMxUtils } from '@/graph/mxGraph'
-
-const graph = inject<Ref<InstanceType<typeof myMxGraph> | null>>('mxgraph-instance')
-const fileInput = ref<HTMLInputElement | null>(null)
-// 导出XML
-function exportXML() {
-  if (!graph?.value) return
-
-  const encoder = new myMxCodec()
-  const node = encoder.encode(graph.value.getModel())
-  const xml = myMxUtils.getXml(node)
-
-  // 创建下载链接
-  const blob = new Blob([xml], { type: 'text/xml' })
-  const url = URL.createObjectURL(blob)
-  const a = document.createElement('a')
-  a.href = url
-  a.download = `graph-${new Date().toISOString().slice(0, 10)}.xml`
-  document.body.appendChild(a)
-  a.click()
-  document.body.removeChild(a)
-  URL.revokeObjectURL(url)
-}
-
-// 触发文件选择
-function importXML() {
-  fileInput.value?.click()
-}
-
-// 处理文件上传
-function handleFileUpload(event: Event) {
-  const input = event.target as HTMLInputElement
-  if (!input.files?.length || !graph?.value) return
-
-  const file = input.files[0]
-  const reader = new FileReader()
-
-  reader.onload = e => {
-    try {
-      const xml = e.target?.result as string
-      loadXML(xml)
-    } catch (error) {
-      alert('导入失败: ' + (error as Error).message)
-    }
-    // 重置input值,允许重复选择同一文件
-    input.value = ''
-  }
-
-  reader.readAsText(file)
-}
-
-// 加载XML到图形
-function loadXML(xml: string) {
-  if (!graph?.value) return
-
-  try {
-    const doc = myMxUtils.parseXml(xml)
-    const codec = new myMxCodec(doc)
-
-    // 先清空现有图形
-    graph.value.getModel().clear()
-
-    // 导入新图形
-    codec.decode(doc.documentElement, graph.value.getModel())
-    alert('图形导入成功')
-  } catch (error) {
-    alert('导入失败: ' + (error as Error).message)
-  }
-}
-function zoomIn() {
-  graph?.value?.zoomIn()
-}
-
-function zoomOut() {
-  graph?.value?.zoomOut()
-}
-
-function zoomActual() {
-  graph?.value?.zoomActual()
-}
-
-function deleteSelected() {
-  const cells = graph?.value?.getSelectionCells()
-  if (cells && cells.length > 0) {
-    graph?.value?.removeCells(cells)
-  }
-}
-
-function saveGraph() {
-  if (!graph?.value) return
-  const encoder = new myMxCodec()
-  const node = encoder.encode(graph.value.getModel())
-  const xml = myMxUtils.getXml(node)
-  console.log('Graph XML:', xml)
-  alert('图形已保存到控制台(查看日志)')
-}
-
-function loadGraph() {
-  if (!graph?.value) return
-  const xml = prompt('请输入图形XML:')
-  if (xml) {
-    try {
-      const doc = myMxUtils.parseXml(xml)
-      const codec = new myMxCodec(doc)
-      codec.decode(doc.documentElement, graph.value.getModel())
-    } catch (e) {
-      alert('加载失败: ' + (e as Error).message)
-    }
-  }
-}
-</script>
-<style lang="scss" scoped>
-.mx-toolbar {
-  padding: 8px 12px;
-  background: #f5f5f5;
-  border-bottom: 1px solid #e0e0e0;
-  display: flex;
-  gap: 8px;
-}
-
-.mx-toolbar button {
-  padding: 6px 12px;
-  background: white;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 13px;
-}
-
-.mx-toolbar button:hover {
-  background: #f0f0f0;
-}
-
-.mx-toolbar .icon {
-  font-size: 14px;
-}
-</style>

+ 49 - 0
src/views/components/MyMxGraph/graph_shapes.ts

@@ -0,0 +1,49 @@
+import { myMxConstants } from '@/graph/mxGraph'
+import { PaletteShape } from '@/types/mxgraph'
+
+const shapes: PaletteShape[] = [
+  {
+    index: 0,
+    idSeed: 1,
+    type: myMxConstants.SHAPE_RECTANGLE,
+    id: myMxConstants.SHAPE_RECTANGLE,
+    title: '矩形',
+    class: 'common rectangle',
+    width: 80,
+    height: 60,
+    style: {
+      [myMxConstants.STYLE_SHAPE]: 'rectangle',
+      [myMxConstants.STYLE_ARCSIZE]: 20
+    }
+  },
+  {
+    index: 1,
+    idSeed: 1,
+    type: myMxConstants.SHAPE_RHOMBUS,
+    id: myMxConstants.SHAPE_RHOMBUS,
+    title: '菱形',
+    class: 'common rhombus',
+    width: 80,
+    height: 80,
+    style: {
+      [myMxConstants.STYLE_SHAPE]: 'rhombus'
+    }
+  },
+  {
+    index: 1,
+    idSeed: 1,
+    type: myMxConstants.SHAPE_TRIANGLE,
+    id: myMxConstants.SHAPE_TRIANGLE,
+    title: '三角形',
+    class: 'common triangle',
+    width: 120,
+    height: 60,
+    style: {
+      [myMxConstants.STYLE_SHAPE]: 'triangle',
+      [myMxConstants.STYLE_DIRECTION]: 'north'
+    }
+  }
+  // 其他形状定义...
+]
+
+export { shapes }

+ 862 - 0
src/views/components/MyMxGraph/index.vue

@@ -0,0 +1,862 @@
+<template>
+  <div class="app-container">
+    <h1>mxGraph 图形编辑器</h1>
+    <div class="mx-toolbar">
+      <button><span class="icon">+</span> 放大</button>
+      <button><span class="icon">-</span> 缩小</button>
+      <button><span class="icon">↻</span> 实际大小</button>
+      <button><span class="icon">×</span> 删除</button>
+      <button><span class="icon">💾</span> 保存</button>
+      <button><span class="icon">📂</span> 加载</button>
+      <button><span class="icon">📤</span> 导出XML</button>
+      <button><span class="icon">📥</span> 导入XML</button>
+      <input ref="fileInput" type="file" accept=".xml" style="display: none" />
+    </div>
+    <div class="editor-layout">
+      <div class="mx-palette">
+        <div class="palette-title">基本图形</div>
+        <div class="palette-shape">
+          <span
+            v-for="(shape, index) in shapes"
+            :key="index"
+            :style="shape.style"
+            :class="shape.class"
+            ref="baseShapeRef">
+            <!-- <span class="shape-label">{{ shape.label }}</span> -->
+          </span>
+        </div>
+      </div>
+      <div ref="graphContainerRef" id="mxgraph-container" class="mxgraph-container"></div>
+      <div class="mx-properties">
+        <div class="properties-title">属性</div>
+        <div v-if="selectedCell" class="properties-content">
+          <div class="property-item">
+            <label>文本:</label>
+            <input v-model="cellText" @change="updateCellText" type="text" />
+          </div>
+          <div class="property-item">
+            <label>宽度:</label>
+            <input
+              v-model.number="cellGeometry.width"
+              @change="updateCellGeometry"
+              type="number"
+              min="10" />
+          </div>
+          <div class="property-item">
+            <label>高度:</label>
+            <input
+              v-model.number="cellGeometry.height"
+              @change="updateCellGeometry"
+              type="number"
+              min="10" />
+          </div>
+          <div class="property-item">
+            <label>填充色:</label>
+            <input v-model="cellStyle.fillColor" @change="updateCellStyle" type="color" />
+          </div>
+          <div class="property-item">
+            <label>边框色:</label>
+            <input v-model="cellStyle.strokeColor" @change="updateCellStyle" type="color" />
+          </div>
+        </div>
+        <div v-else class="properties-empty">未选中任何元素</div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {
+  myMxConstants,
+  myMxGraph,
+  myMxEvent,
+  myMxPopupMenu,
+  myMxEditor,
+  myMxPerimeter,
+  myMxClient,
+  myMxRectangleShape,
+  myMxCellState,
+  myMxVertexHandler,
+  myMxUtils,
+  myMxPoint,
+  myMxRectangle,
+  myMxImageShape,
+  myMxCell,
+  myMxGeometry,
+  myMxRubberband,
+  myMxUndoManager,
+  myMxOutline,
+  myMxEventObject,
+  myMxGraphHandler,
+  myMxEdgeHandler,
+  myMxConnectionHandler,
+  myMxImage,
+  myMxConstraintHandler,
+  myMxEllipse,
+  myMxShape,
+  myMxConnectionConstraint,
+  myMxPolyline
+} from '@/graph/mxGraph'
+import { CellGeometry } from '@/types/mxgraph'
+import { shapes } from '@/views/components/MyMxGraph/graph_shapes'
+import _ from 'lodash'
+import * as R from 'ramda'
+const graphContainerRef = ref<HTMLElement | null>(null)
+const baseShapeRef = ref<HTMLElement[]>([])
+const graph = ref<typeof myMxGraph | null>(null)
+const editor = ref<typeof myMxEditor | null>(null)
+const undoMng = ref<typeof myMxUndoManager | null>(null)
+const outline = ref<typeof myMxOutline | null>(null)
+const currentNormalType = ref<typeof myMxCell | null>(null)
+const textValue = ref('')
+const uploadDataVisible = ref(false)
+const isNode = ref(false)
+const cellStyle = ref<Record<string, any>>({})
+const edgeStyle = ref('orthogonalEdgeStyle')
+
+const graphX = ref(100)
+const graphY = ref(10)
+const jsonData = ref({
+  cells: {
+    nodes: [],
+    groups: []
+  },
+  edges: []
+})
+const selectedCell = ref<any>(null)
+const cellText = ref('')
+const cellGeometry = ref<CellGeometry>({ width: 0, height: 0 })
+
+watch(
+  () => graph?.value,
+  newGraph => {
+    if (!newGraph) return
+
+    newGraph.getSelectionModel().addListener(myMxConstants.EVENT_CHANGE, () => {
+      const cells = newGraph.getSelectionCells()
+      selectedCell.value = cells.length === 1 ? cells[0] : null
+
+      if (selectedCell.value) {
+        cellText.value = newGraph.convertValueToString(selectedCell.value) || ''
+
+        const geo = selectedCell.value.getGeometry()
+        if (geo) {
+          cellGeometry.value = {
+            width: geo.width,
+            height: geo.height
+          }
+        }
+
+        const style = newGraph.getCellStyle(selectedCell.value)
+        cellStyle.value = {
+          fillColor: style[myMxConstants.STYLE_FILLCOLOR] || '#ffffff',
+          strokeColor: style[myMxConstants.STYLE_STROKECOLOR] || '#000000'
+        }
+      }
+    })
+  },
+  { immediate: true }
+)
+
+function updateCellText() {
+  if (!graph?.value || !selectedCell.value) return
+  graph.value.getModel().setValue(selectedCell.value, cellText.value)
+}
+
+function updateCellGeometry() {
+  if (!graph?.value || !selectedCell.value) return
+  const geo = selectedCell.value.getGeometry()
+  if (geo) {
+    geo.width = cellGeometry.value.width
+    geo.height = cellGeometry.value.height
+    graph.value.getModel().setGeometry(selectedCell.value, geo)
+  }
+}
+
+function updateCellStyle() {
+  if (!graph?.value || !selectedCell.value) return
+  const newStyle = {
+    [myMxConstants.STYLE_FILLCOLOR]: cellStyle.value.fillColor,
+    [myMxConstants.STYLE_STROKECOLOR]: cellStyle.value.strokeColor
+  }
+  graph.value
+    .getModel()
+    .setStyle(
+      selectedCell.value,
+      myMxUtils.setStyle(graph.value.getModel().getStyle(selectedCell.value) || '', newStyle)
+    )
+}
+const initGraph = (container: HTMLElement) => {
+  editor.value = new myMxEditor()
+  graph.value = unref(editor).graph
+  editor.value.setGraphContainer(container)
+
+  // 配置样式
+  configureStyles(unref(graph))
+  // 去锯齿效果
+  myMxRectangleShape.prototype.crisp = true
+  // 定义全局变量,如。用于触发建立新的连接的活动区域的最小尺寸(以像素为单位),该部分(100%)的小区区域被用于触发新的连接,以及一些窗口和“下拉菜菜单选择
+  myMxConstants.MIN_HOTSPOT_SIZE = 16
+  myMxConstants.DEFAULT_HOTSPOT = 1
+
+  graph.value.setHtmlLabels(true)
+  graph.value.setDropEnabled(true)
+  graph.value.setSplitEnabled(false)
+
+  // 有效的拖放操作,则返回true
+  graph.value.isValidDropTarget = (target: any, cells: any, evt: any) => {
+    if (graph.value.isSplitEnabled() && graph.value.isSplitTarget(target, cells, evt)) {
+      console.log('拖放')
+      return true
+    }
+  }
+
+  // 禁用分组的收缩功能 方法2:
+  graph.value.foldingEnabled = false
+  // 组内的子元素是否随父元素变化而变化
+  graph.value.recursiveResize = true
+
+  // 设置连线时的预览路径及样式
+  graph.value.connectionHandler.createEdgeState = () => {
+    // 设置预览的连接线,第三个参数为连接成功后连接线上的label
+    var edge = graph.value.createEdge(null, null, null, null, null)
+    // edge.style += `;edgeStyle=orthogonalEdgeStyle `
+    return new myMxCellState(graph.value.view, edge, graph.value.getCellStyle(edge))
+  }
+
+  // 是否开启旋转
+  myMxVertexHandler.prototype.livePreview = true
+  myMxVertexHandler.prototype.rotationEnabled = true
+
+  // 设置旋转按钮
+  // myMxVertexHandler.prototype.createSizerShape = function (bounds, index, fillColor) {
+  //   if (this.handleImage != null) {
+  //     bounds = new myMxRectangle(
+  //       bounds.x,
+  //       bounds.y,
+  //       this.handleImage.width,
+  //       this.handleImage.height
+  //     )
+  //     const shape = new myMxImageShape(bounds, this.handleImage.src)
+  //     shape.preserveImageAspect = true
+  //     return shape
+  //   } else if (index == myMxEvent.ROTATION_HANDLE) {
+  //     bounds = new myMxRectangle(bounds.x, bounds.y, 15, 15)
+  //     const rotationShape = new myMxImageShape(bounds, 'icon/rotate.svg')
+  //     rotationShape.preserveImageAspect = true
+  //     return rotationShape
+  //   } else {
+  //     return new myMxRectangleShape(
+  //       bounds,
+  //       fillColor || myMxConstants.HANDLE_FILLCOLOR,
+  //       myMxConstants.HANDLE_STROKECOLOR
+  //     )
+  //   }
+  // }
+
+  // 设置旋转角度(解决默认旋转180度的bug)
+  // myMxVertexHandler.prototype.getRotationHandlePosition = function () {
+  //   const padding = this.getHandlePadding()
+  //   return new myMxPoint(
+  //     this.bounds.x + this.bounds.width - this.rotationHandleVSpacing + padding.x / 2,
+  //     this.bounds.y + this.rotationHandleVSpacing - padding.y / 2
+  //   )
+  // }
+
+  // 设置默认组
+  const group = new myMxCell('Group', new myMxGeometry(), 'group;fontColor=white;')
+  group.setVertex(true)
+  group.setConnectable(true)
+  editor.value.defaultGroup = group
+  editor.value.groupBorderSize = 80
+
+  // 是否根元素
+  // graph.value.isValidRoot = function (cell) {
+  //   return this.isValidDropTarget(cell)
+  // }
+
+  // 允许重复连接
+  graph.value.setMultigraph(true)
+  // 禁止连接线晃动(即连线两端必须在节点上)
+  graph.value.setAllowDanglingEdges(false)
+  // 允许连线的目标和源是同一元素
+  graph.value.setAllowLoops(false)
+  // 边被拖动时始终保持连接
+  graph.value.setDisconnectOnMove(false)
+  // 选择基本元素开启
+  graph.value.setEnabled(true)
+  // 动态改变样式
+  graph.value.getView().updateStyle = true
+  // 鼠标框选
+  new myMxRubberband(graph.value)
+  graph.value.setResizeContainer(true)
+
+  // 开启画布平滑移动
+  graph.value.setPanning(true)
+  // 开启提示
+  graph.value.setTooltips(false)
+  // 允许连线
+  graph.value.setConnectable(true)
+  // 移动元素的步长
+  graph.value.gridSize = 3
+  graph.value.setBorder(160)
+
+  // 开启方块上的文字编辑功能
+  graph.value.setCellsEditable(true)
+
+  // 准备撤销还原功能
+  undoMng.value = new myMxUndoManager()
+  const listener = (_sender: any, evt: any) => {
+    undoMng.value?.undoableEditHappened(evt.getProperty('edit'))
+  }
+  graph.value.getModel().addListener(myMxEvent.UNDO, listener)
+  graph.value.getView().addListener(myMxEvent.UNDO, listener)
+
+  // 创建缩略图
+  outline.value = new myMxOutline(graph.value, document.createElement('div'))
+
+  // 从value中获取显示的内容(如果节点的value为空则显示节点的title)
+  graph.value.convertValueToString = (cell: { [x: string]: any }) => {
+    return cell['value'] ? cell['value'] : cell['title']
+  }
+}
+
+const configureStyles = (graph: typeof myMxGraph) => {
+  // 设置节点的文字可被移动
+  graph.vertexLabelsMovable = false
+
+  const vertexStyle = graph.getStylesheet().getDefaultVertexStyle()
+  vertexStyle[myMxConstants.STYLE_SHAPE] = myMxConstants.SHAPE_LABEL
+  vertexStyle[myMxConstants.STYLE_PERIMETER] = myMxPerimeter.RectanglePerimeter
+  vertexStyle[myMxConstants.STYLE_VERTICAL_ALIGN] = myMxConstants.ALIGN_MIDDLE
+  vertexStyle[myMxConstants.STYLE_ALIGN] = myMxConstants.ALIGN_CENTER
+  vertexStyle[myMxConstants.STYLE_IMAGE_ALIGN] = myMxConstants.ALIGN_CENTER
+  vertexStyle[myMxConstants.STYLE_IMAGE_VERTICAL_ALIGN] = myMxConstants.ALIGN_CENTER
+  // style[myMxConstants.STYLE_SPACING_TOP] = 6;
+  vertexStyle[myMxConstants.STYLE_SPACING_LEFT] = 5
+  // style[myMxConstants.STYLE_GRADIENTCOLOR] = 'skyblue'; // 渐变颜色
+  vertexStyle[myMxConstants.STYLE_STROKECOLOR] = '#5d65df' // 线条颜色
+  vertexStyle[myMxConstants.STYLE_FILLCOLOR] = '#FFFFFF'
+  vertexStyle[myMxConstants.STYLE_FONTCOLOR] = '#1d258f' // 字体颜色
+  vertexStyle[myMxConstants.STYLE_FONTFAMILY] = 'Verdana' // 字体风格
+  vertexStyle[myMxConstants.STYLE_FONTSIZE] = '12' // 字体大小
+  vertexStyle[myMxConstants.STYLE_FONTSTYLE] = '0' // 斜体字
+  vertexStyle[myMxConstants.WORD_WRAP] = 'normal' // 文字换行    word-break: break-all;
+  vertexStyle[myMxConstants['word-break']] = 'break-all' // 文字换行
+  vertexStyle[myMxConstants.STYLE_WHITE_SPACE] = 'wrap' // 文字换行
+  vertexStyle[myMxConstants.STYLE_ROUNDED] = false // 圆角
+  vertexStyle[myMxConstants.STYLE_IMAGE_WIDTH] = '28' // 图片宽度
+  vertexStyle[myMxConstants.STYLE_IMAGE_HEIGHT] = '28' // 图片高度
+  vertexStyle[myMxConstants.STYLE_OPACITY] = '100' // 节点透明度(不包含字体)
+
+  const cellStyle = graph.getStylesheet().getCellStyle('group')
+  cellStyle[myMxConstants.STYLE_SHAPE] = myMxConstants.SHAPE_IMAGE
+  cellStyle[myMxConstants.STYLE_FONTCOLOR] = '#774400'
+  cellStyle[myMxConstants.STYLE_PERIMETER] = myMxPerimeter.RectanglePerimeter
+  cellStyle[myMxConstants.STYLE_PERIMETER_SPACING] = '6'
+  cellStyle[myMxConstants.STYLE_ALIGN] = myMxConstants.ALIGN_LEFT
+  cellStyle[myMxConstants.STYLE_VERTICAL_ALIGN] = myMxConstants.ALIGN_MIDDLE
+  cellStyle[myMxConstants.STYLE_FONTSIZE] = '10'
+  cellStyle[myMxConstants.STYLE_FONTSTYLE] = 2
+  cellStyle[myMxConstants.STYLE_IMAGE_WIDTH] = '16'
+  cellStyle[myMxConstants.STYLE_IMAGE_HEIGHT] = '16'
+  cellStyle[myMxConstants.STYLE_BACKGROUNDCOLOR] = 'transparent'
+  // graph.getStylesheet().putCellStyle('group', cellStyle)
+
+  const portCellStyle = graph.getStylesheet().getCellStyle('port')
+  portCellStyle[myMxConstants.STYLE_SHAPE] = myMxConstants.SHAPE_IMAGE
+  portCellStyle[myMxConstants.STYLE_FONTCOLOR] = '#774400'
+  portCellStyle[myMxConstants.STYLE_PERIMETER] = myMxPerimeter.RectanglePerimeter
+  portCellStyle[myMxConstants.STYLE_PERIMETER_SPACING] = '6'
+  portCellStyle[myMxConstants.STYLE_ALIGN] = myMxConstants.ALIGN_LEFT
+  portCellStyle[myMxConstants.STYLE_VERTICAL_ALIGN] = myMxConstants.ALIGN_MIDDLE
+  portCellStyle[myMxConstants.STYLE_FONTSIZE] = '10'
+  portCellStyle[myMxConstants.STYLE_FONTSTYLE] = 2
+  portCellStyle[myMxConstants.STYLE_IMAGE_WIDTH] = '16'
+  portCellStyle[myMxConstants.STYLE_IMAGE_HEIGHT] = '16'
+  portCellStyle[myMxConstants.STYLE_BACKGROUNDCOLOR] = 'transparent'
+  // graph.getStylesheet().putCellStyle('port', portCellStyle)
+
+  const edgeStyle = graph.getStylesheet().getDefaultEdgeStyle()
+
+  Object.assign(edgeStyle, {
+    [myMxConstants.STYLE_LABEL_BACKGROUNDCOLOR]: '#FFFFFF',
+    [myMxConstants.STYLE_STROKEWIDTH]: '2',
+    [myMxConstants.STYLE_ROUNDED]: false
+  })
+
+  var edgeStyles = graph.getStylesheet().getDefaultEdgeStyle()
+  let labelStyle = graph.getStylesheet().getDefaultVertexStyle()
+
+  // 设置连线风格(设置为正交折线)
+  edgeStyles['edgeStyle'] = 'orthogonalEdgeStyle'
+
+  // 选中 cell/edge 后的伸缩大小的点/拖动连线位置的点的颜色
+  // style[myMxConstants.STYLE_WHITE_SPACE] = 'wrap'
+
+  myMxConstants.HANDLE_FILLCOLOR = '#409eff'
+  myMxConstants.HANDLE_STROKECOLOR = 'transparent'
+  myMxConstants.STYLE_ANCHOR_POINT_DIRECTION = 'anchorPointDirection'
+  myMxConstants.STYLE_STYLE_ROTATION = 'rotation'
+  // 是否缩放网格
+  myMxGraphHandler.prototype.scaleGrid = true
+  myMxGraph.prototype.pageBreakDashed = false
+
+  // 指定是否应使用其他单元格对齐当前所选内容的右侧,中间或左侧。默认为false。
+  myMxGraphHandler.prototype.guidesEnabled = true
+  myMxGraphHandler.prototype.htmlPreview = false
+  myMxGraphHandler.prototype.allowLivePreview = true
+  // 指定预览形状的颜色。默认为黑色。
+  myMxGraphHandler.prototype.previewColor = 'red'
+  // 应该使用实时预览的最大单元数。默认值为0,表示没有实时预览。
+  myMxGraphHandler.prototype.maxLivePreview = 100
+  // Alt 按下禁用导航线
+  myMxGraphHandler.prototype.useGuidesForEvent = me => {
+    return !myMxEvent.isAltDown(me.getEvent())
+  }
+  // 导航线颜色
+  myMxConstants.GUIDE_COLOR = '#1a73e8'
+  // 导航线宽度
+  myMxConstants.GUIDE_STROKEWIDTH = 2
+  // 导航线自动连接到目标
+  myMxEdgeHandler.prototype.snapToTerminals = true
+  // 选中线条时的虚线颜色
+  myMxConstants.EDGE_SELECTION_COLOR = '#99ccff'
+  // myMxConstants.DEFAULT_INVALID_COLOR = 'yellow';
+  // myMxConstants.INVALID_CONNECT_TARGET_COLOR = 'yellow';
+  // 连线(未满足连线要求)时预览的颜色
+  myMxConstants.INVALID_COLOR = '#99ccff'
+  // 连线(满足连线要求)时预览的颜色
+  myMxConstants.VALID_COLOR = 'blue'
+  // myMxConstants.GUIDE_COLOR = 'yellow';
+  // myMxConstants.LOCKED_HANDLE_FILLCOLOR = '#24bcab';
+  // 选中节点时选中框的颜色
+  myMxConstants.VERTEX_SELECTION_COLOR = '#99ccff'
+
+  //折叠-/展开+图标大小
+  // mxGraph.prototype.collapsedImage = new mxImage('images/collapsed.gif', 15, 15);
+  // mxGraph.prototype.expandedImage = new mxImage('images/expanded.gif', 15, 15);
+
+  // 配置节点中心的连接图标(注釋掉即可指定錨點連接到另一個節點的錨點上)
+  myMxConnectionHandler.prototype.connectImage = new myMxImage('./icon/connectionpoint.png', 14, 14)
+  // 显示中心端口图标
+  graph.connectionHandler.targetConnectImage = false
+
+  // 是否开启浮动自动连接
+  graph.connectionHandler.isConnectableCell = () => {
+    return true
+  }
+
+  // 设定锚点的位置、可编辑状态和图标
+  myMxConstraintHandler.prototype.pointImage = new myMxImage('icon/dot.svg', 10, 10)
+  // 设置锚点上的高亮颜色
+  myMxConstraintHandler.prototype.createHighlightShape = function () {
+    return new myMxEllipse(null, '#409eff99', '#409eff99', 15)
+  }
+  myMxShape.prototype.constraints = [
+    new myMxConnectionConstraint(new myMxPoint(0, 0), true),
+    new myMxConnectionConstraint(new myMxPoint(0.25, 0), true),
+    new myMxConnectionConstraint(new myMxPoint(0.5, 0), true),
+    new myMxConnectionConstraint(new myMxPoint(0.75, 0), true),
+    new myMxConnectionConstraint(new myMxPoint(0, 0.25), true),
+    new myMxConnectionConstraint(new myMxPoint(0, 0.5), true),
+    new myMxConnectionConstraint(new myMxPoint(0, 0.75), true),
+    new myMxConnectionConstraint(new myMxPoint(1, 0), true),
+    new myMxConnectionConstraint(new myMxPoint(1, 0.25), true),
+    new myMxConnectionConstraint(new myMxPoint(1, 0.5), true),
+    new myMxConnectionConstraint(new myMxPoint(1, 0.75), true),
+    new myMxConnectionConstraint(new myMxPoint(0, 1), true),
+    new myMxConnectionConstraint(new myMxPoint(0.25, 1), true),
+    new myMxConnectionConstraint(new myMxPoint(0.5, 1), true),
+    new myMxConnectionConstraint(new myMxPoint(0.75, 1), true),
+    new myMxConnectionConstraint(new myMxPoint(1, 1), true)
+  ]
+  myMxPolyline.prototype.constraints = null
+}
+// 事件中心
+const eventCenter = () => {
+  if (!graph.value) return
+  // 监听自定义事件
+  // graph.value.addListener(myMxEvent.NORMAL_TYPE_CLICKED, (_sender: any, evt: any) => {
+  //   const cell = evt.properties.cell.state.cell
+  //   console.log('cell', cell)
+  //   currentNormalType.value = cell
+  // })
+
+  // 画布平移事件
+  graph.value.addListener(myMxEvent.PAN, (sender: any, evt: any) => {
+    console.log('画布平移了', sender, evt)
+  })
+
+  // 新增节点事件
+  graph.value.addListener(myMxEvent.ADD_CELLS, (_sender: any, evt: any) => {
+    nextTick(() => {
+      console.log('添加节点')
+
+      const addCell = evt.properties.cells[0]
+      if (addCell.vertex) {
+        // 判断是否为组节点
+        if (addCell.isGroup) {
+          const groupObj = _.pick(addCell, ['id', 'title', 'parent', 'geometry'])
+          jsonData.value.cells.groups.push(groupObj)
+        } else {
+          const nodeObj = _.pick(addCell, ['id', 'title', 'parent', 'geometry'])
+          jsonData.value.cells.nodes.push(nodeObj)
+        }
+      } else if (addCell.edge) {
+        console.log(addCell)
+        const lineObj = _.pick(addCell, ['id', 'edge', 'source', 'parent', 'geometry', 'value'])
+        jsonData.value.edges.push(lineObj)
+      }
+    })
+  })
+}
+
+// 配置鼠标事件
+const configMouseEvent = () => {
+  if (!graph.value) return
+
+  graph.value.addMouseListener({
+    currentState: null,
+    previousStyle: null,
+
+    mouseDown: (_sender, evt) => {
+      if (!evt.state) {
+        console.log('点击了画布')
+        return
+      } else if (evt.state.cell.edge) {
+        console.log('点击了连线')
+        return
+      }
+
+      const cell = evt.state.cell
+      let clickNormalType = false
+      if (cell.style !== undefined) {
+        clickNormalType = cell.style.includes('normalType')
+      }
+      if (clickNormalType) {
+        // 使用 mxGraph 事件中心,注册自定义事件
+        graph.value?.fireEvent(new myMxEventObject(myMxEvent.NORMAL_TYPE_CLICKED, 'cell', evt))
+      } else {
+        return
+      }
+    },
+
+    mouseMove: (sender, me) => {
+      graphX.value = Math.ceil(me.graphX)
+      graphY.value = Math.ceil(me.graphY)
+    },
+
+    mouseUp: (sender, evt) => {
+      console.log('mouseUp')
+      if (evt.sourceState === undefined) {
+        return false
+      } else {
+        const cell = evt.sourceState.cell
+        if (cell) {
+          textValue.value = cell['value'] ? cell['value'] : cell['title']
+          console.log(textValue.value, 'cellValue')
+          console.log('cellValuie', cell)
+          cell.vertex ? (isNode.value = true) : (isNode.value = false)
+          const getcellStyle = cell.getStyle() ? cell.getStyle() : null
+          if (!isNode.value) {
+            // 点击的不是节点
+            getcellStyle ? (edgeStyle.value = getcellStyle) : 'orthogonalEdgeStyle'
+          } else {
+            // 点击的是节点
+            if (getcellStyle) {
+              const arr = getcellStyle.split(';')
+              const styleObject: Record<string, any> = {}
+              arr.forEach(item => {
+                const [key, value] = item.split('=')
+                if (key && value) {
+                  styleObject[key] = value
+                }
+              })
+              cellStyle.value = styleObject
+            }
+          }
+        } else {
+          alert('请选择节点或者连线')
+        }
+      }
+    }
+  })
+}
+
+const handleScroll = (e: WheelEvent) => {
+  if (!graph.value) return
+
+  if (e.deltaY < 0) {
+    graph.value.zoomIn()
+  } else {
+    graph.value.zoomOut()
+  }
+}
+// 初始化基础节点
+const initGeneralTool = () => {
+  if (!graph.value) return
+
+  const generalToolbarDomArray = unref(baseShapeRef)
+  // 判断是否为数组且数组是否为空
+  if (!(generalToolbarDomArray instanceof Array || generalToolbarDomArray)) {
+    return
+  }
+  generalToolbarDomArray.forEach((dom, domIndex) => {
+    const toolItem = shapes[domIndex]
+    const { width, height } = toolItem
+    const itemClass = toolItem.class
+    // 新增基础节点
+    const generalDropHandler = (
+      graph: typeof myMxGraph,
+      _evt: any,
+      dropCell: any,
+      x: number,
+      y: number
+    ) => {
+      const drop = !R.isNil(dropCell)
+      const realX = drop ? x - dropCell.geometry.x : x
+      const realY = drop ? y - dropCell.geometry.y : y
+      const { width, height } = toolItem
+      const styleObj = toolItem.style
+      const style = Object.keys(styleObj)
+        .map(attr => `${attr}=${styleObj[attr]}`)
+        .join(';')
+      const parent = drop ? dropCell : graph.getDefaultParent()
+      graph.getModel().beginUpdate()
+      try {
+        const vertex = graph.insertVertex(
+          parent,
+          null,
+          null,
+          realX - width / 2,
+          realY - height / 2,
+          width,
+          height,
+          style + ';whiteSpace=wrap;word-break=break-all'
+        )
+        vertex.title = `<div style='word-break:break-all'>` + toolItem['title'] + '</div>'
+        vertex.dropAble = toolItem['dropAble']
+        vertex.id = toolItem['id'] + '-' + toolItem['idSeed']
+        toolItem['idSeed']++
+        vertex['isGroup'] = toolItem['id'].includes('group') ? true : false
+      } finally {
+        graph.getModel().endUpdate()
+      }
+    }
+
+    // 设置节点被拖拽时的样式(预览)
+    const generalcreateDragPreview = () => {
+      const elt = document.createElement('div')
+      elt.style.width = `${width}px`
+      elt.style.height = `${height}px`
+      elt.style.transform = 'translate(-50%,-50%)'
+      elt.className = itemClass
+      return elt
+    }
+
+    // 允许拖拽
+    const ds = myMxUtils.makeDraggable(
+      dom as HTMLElement,
+      graph.value,
+      generalDropHandler,
+      generalcreateDragPreview(),
+      0,
+      0,
+      true,
+      true
+    )
+    ds.setGuidesEnabled(true)
+  })
+}
+onMounted(async () => {
+  if (!myMxClient.isBrowserSupported()) {
+    alert('当前浏览器不支持拓扑图功能,请更换浏览器访问,建议使用Chrome浏览器访问!')
+  } else {
+    myMxGraph.prototype.getAllConnectionConstraints = (
+      terminal: {
+        shape: { stencil: { constraints: null } | null; constraints: null } | null
+      } | null
+    ) => {
+      if (terminal != null && terminal.shape != null) {
+        if (terminal.shape.stencil != null) {
+          if (terminal.shape.stencil.constraints != null) {
+            return terminal.shape.stencil.constraints
+          }
+        } else if (terminal.shape.constraints != null) {
+          return terminal.shape.constraints
+        }
+      }
+      return null
+    }
+    if (graphContainerRef.value) {
+      initGraph(graphContainerRef.value)
+      eventCenter()
+      configMouseEvent()
+      nextTick(() => {
+        initGeneralTool()
+      })
+    }
+  }
+
+  const graphContainer = document.getElementById('mxgraph-container')
+  if (graphContainer) {
+    graphContainer.addEventListener('wheel', handleScroll, true)
+  }
+})
+</script>
+<style lang="scss" scoped>
+@import '@/assets/graph/style/general-shap.css';
+.app-container {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  font-family: Arial, sans-serif;
+  overflow: hidden;
+}
+
+h1 {
+  padding: 12px 20px;
+  font-size: 18px;
+  color: #333;
+  border-bottom: 1px solid #e0e0e0;
+  background: #f5f5f5;
+}
+
+.editor-layout {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+
+.mx-palette {
+  width: 160px;
+  height: 100%;
+  border-right: 1px solid #e0e0e0;
+  background-color: #f5f5f5;
+  overflow-y: auto;
+  user-select: none;
+}
+
+.palette-title {
+  padding: 10px;
+  font-weight: bold;
+  border-bottom: 1px solid #ddd;
+  background-color: #eee;
+  position: sticky;
+  top: 0;
+  z-index: 1;
+}
+
+.palette-shape {
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-around;
+  align-content: space-around;
+  .common {
+    width: 30%;
+    cursor: pointer;
+    height: 50px;
+    white-space: wrap;
+    text-align: center;
+    position: relative;
+    .shape-label {
+      position: absolute;
+      bottom: -30px;
+      left: 0;
+      width: 100%;
+    }
+  }
+}
+
+.mxgraph-container {
+  flex: 1;
+  height: calc(100vh - 90px);
+  background: url('@/assets/graph/images/grid.gif');
+}
+
+.mx-properties {
+  width: 240px;
+  height: 100%;
+  border-left: 1px solid #e0e0e0;
+  background-color: #f5f5f5;
+  overflow-y: auto;
+  user-select: none;
+}
+
+.properties-title {
+  padding: 10px;
+  font-weight: bold;
+  border-bottom: 1px solid #ddd;
+  background-color: #eee;
+  position: sticky;
+  top: 0;
+  z-index: 1;
+}
+
+.properties-content {
+  padding: 10px;
+}
+
+.properties-empty {
+  padding: 20px;
+  text-align: center;
+  color: #999;
+}
+
+.property-item {
+  margin-bottom: 15px;
+}
+
+.property-item label {
+  display: block;
+  margin-bottom: 5px;
+  font-size: 13px;
+  color: #666;
+}
+
+.property-item input[type='text'],
+.property-item input[type='number'] {
+  width: 100%;
+  padding: 6px;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+}
+
+.property-item input[type='color'] {
+  width: 100%;
+  height: 30px;
+  padding: 2px;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+}
+
+.mx-toolbar {
+  padding: 8px 12px;
+  background: #f5f5f5;
+  border-bottom: 1px solid #e0e0e0;
+  display: flex;
+  gap: 8px;
+}
+
+.mx-toolbar button {
+  padding: 6px 12px;
+  background: white;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+}
+
+.mx-toolbar button:hover {
+  background: #f0f0f0;
+}
+
+.mx-toolbar .icon {
+  font-size: 14px;
+}
+</style>

+ 1 - 34
src/views/test/mxgraph.vue

@@ -1,36 +1,3 @@
 <template>
-  <div class="app-container">
-    <h1>mxGraph 图形编辑器</h1>
-    <MxToolbar />
-    <div class="editor-layout">
-      <MxPalette />
-      <MxGraphContainer />
-      <MxPropertiesPanel />
-    </div>
-  </div>
+  <MyMxGraph />
 </template>
-<script setup lang="ts"></script>
-<style lang="scss" scoped>
-.app-container {
-  width: 100vw;
-  height: 100vh;
-  display: flex;
-  flex-direction: column;
-  font-family: Arial, sans-serif;
-  overflow: hidden;
-}
-
-h1 {
-  padding: 12px 20px;
-  font-size: 18px;
-  color: #333;
-  border-bottom: 1px solid #e0e0e0;
-  background: #f5f5f5;
-}
-
-.editor-layout {
-  flex: 1;
-  display: flex;
-  overflow: hidden;
-}
-</style>