Ver Fonte

feat: 流程图布局

Gaokun Wang há 1 semana atrás
pai
commit
8f91aa3e0e

+ 1 - 0
.prettierignore

@@ -4,3 +4,4 @@ pnpm-lock.yaml
 LICENSE.md
 tsconfig.json
 tsconfig.*.json
+public

+ 0 - 3
.prettierrc.yaml

@@ -24,6 +24,3 @@ htmlWhitespaceSensitivity: 'css'
 vueIndentScriptAndStyle: false
 #  换行符使用 lf 结尾是 可选值 "<auto|lf|crlf|cr>"
 endOfLine: 'auto'
-#  这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 (rangeStart:开始,rangeEnd:结束)
-rangeStart: 0
-rangeEnd: Infinity

+ 10 - 5
electron.vite.config.ts

@@ -7,7 +7,7 @@ import { TDesignResolver } from 'unplugin-vue-components/resolvers'
 import tailwindcss from '@tailwindcss/vite'
 
 export default defineConfig({
-main: {
+  main: {
     build: {
       rollupOptions: {
         input: {
@@ -26,9 +26,10 @@ main: {
     }
   },
   renderer: {
-     root: '.',
+    root: '.',
     build: {
       rollupOptions: {
+        external: ['mxgraph'],
         input: {
           index: resolve(__dirname, 'index.html')
         }
@@ -47,14 +48,17 @@ main: {
         dts: 'src/types/auto-imports.d.ts' // 自动生成 auto-imports.d.ts
       }),
       Components({
-        resolvers: [TDesignResolver({
-          library: 'vue-next'
-        })],
+        resolvers: [
+          TDesignResolver({
+            library: 'vue-next'
+          })
+        ],
         dirs: ['src/components', 'src/**/components', 'src/**/components/**/*.vue'],
         dts: 'src/types/auto-components.d.ts' // 自动生成 components.d.ts
       })
     ],
     optimizeDeps: {
+      exclude: ['mxgraph'],
       include: [
         'vue',
         'vue-router',
@@ -65,6 +69,7 @@ main: {
         'pinia-plugin-persistedstate',
         'js-cookie',
         'nprogress',
+        'lodash-es',
         'tdesign-vue-next'
       ]
     }

+ 4 - 4
src/App.vue

@@ -1,8 +1,8 @@
 <template>
-    <t-config-provider :global-config="globalConfig">
-      <router-view v-slot="{ Component }">
-        <component :is="Component" />
-      </router-view>
+  <t-config-provider :global-config="globalConfig">
+    <router-view v-slot="{ Component }">
+      <component :is="Component" />
+    </router-view>
   </t-config-provider>
 </template>
 <script setup lang="ts">

+ 3 - 3
src/assets/style/main.css

@@ -1,4 +1,4 @@
-@import "tailwindcss";
+@import 'tailwindcss';
 @import './base.css';
 @import 'tdesign-vue-next/es/style/index.css';
 html,
@@ -12,7 +12,8 @@ body {
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
-  font-family: Helvetica Neue,
+  font-family:
+    Helvetica Neue,
     Helvetica,
     PingFang SC,
     Hiragino Sans GB,
@@ -62,7 +63,6 @@ html {
   font-weight: normal;
 }
 
-
 div:focus {
   outline: none;
 }

+ 149 - 0
src/graph/mxGraph copy.ts

@@ -0,0 +1,149 @@
+import myMxFactory from './index'
+
+const myMx = myMxFactory({
+  mxBasePath: '/mxgraph-base',
+  mxImageBasePath: '/mxgraph-base/images'
+})
+
+// 重命名后导出
+export const myMxClient = myMx.mxClient
+export const myMxLog = myMx.mxLog
+export const myMxObjectIdentity = myMx.mxObjectIdentity
+export const myMxDictionary = myMx.mxDictionary
+export const myMxResources = myMx.mxResources
+export const myMxPoint = myMx.mxPoint
+export const myMxRectangle = myMx.mxRectangle
+export const myMxEffects = myMx.mxEffects
+export const myMxUtils = myMx.mxUtils
+export const myMxConstants = myMx.mxConstants
+export const myMxEventObject = myMx.mxEventObject
+export const myMxMouseEvent = myMx.mxMouseEvent
+export const myMxEventSource = myMx.mxEventSource
+export const myMxEvent = myMx.mxEvent
+export const myMxXmlRequest = myMx.mxXmlRequest
+export const myMxClipboard = myMx.mxClipboard
+export const myMxWindow = myMx.mxWindow
+export const myMxForm = myMx.mxForm
+export const myMxImage = myMx.mxImage
+export const myMxDivResizer = myMx.mxDivResizer
+export const myMxDragSource = myMx.mxDragSource
+export const myMxToolbar = myMx.mxToolbar
+export const myMxUndoableEdit = myMx.mxUndoableEdit
+export const myMxUndoManager = myMx.mxUndoManager
+export const myMxUrlConverter = myMx.mxUrlConverter
+export const myMxPanningManager = myMx.mxPanningManager
+export const myMxPopupMenu = myMx.mxPopupMenu
+export const myMxAutoSaveManager = myMx.mxAutoSaveManager
+export const myMxAnimation = myMx.mxAnimation
+export const myMxMorphing = myMx.mxMorphing
+export const myMxImageBundle = myMx.mxImageBundle
+export const myMxImageExport = myMx.mxImageExport
+export const myMxAbstractCanvas2D = myMx.mxAbstractCanvas2D
+export const myMxXmlCanvas2D = myMx.mxXmlCanvas2D
+export const myMxSvgCanvas2D = myMx.mxSvgCanvas2D
+export const myMxVmlCanvas2D = myMx.mxVmlCanvas2D
+export const myMxGuide = myMx.mxGuide
+export const myMxShape = myMx.mxShape
+export const myMxStencil = myMx.mxStencil
+export const myMxStencilRegistry = myMx.mxStencilRegistry
+export const myMxMarker = myMx.mxMarker
+export const myMxActor = myMx.mxActor
+export const myMxCloud = myMx.mxCloud
+export const myMxRectangleShape = myMx.mxRectangleShape
+export const myMxEllipse = myMx.mxEllipse
+export const myMxDoubleEllipse = myMx.mxDoubleEllipse
+export const myMxRhombus = myMx.mxRhombus
+export const myMxPolyline = myMx.mxPolyline
+export const myMxArrow = myMx.mxArrow
+export const myMxArrowConnector = myMx.mxArrowConnector
+export const myMxText = myMx.mxText
+export const myMxTriangle = myMx.mxTriangle
+export const myMxHexagon = myMx.mxHexagon
+export const myMxLine = myMx.mxLine
+export const myMxImageShape = myMx.mxImageShape
+export const myMxLabel = myMx.mxLabel
+export const myMxCylinder = myMx.mxCylinder
+export const myMxConnector = myMx.mxConnector
+export const myMxSwimlane = myMx.mxSwimlane
+export const myMxGraphLayout = myMx.mxGraphLayout
+export const myMxStackLayout = myMx.mxStackLayout
+export const myMxPartitionLayout = myMx.mxPartitionLayout
+export const myMxCompactTreeLayout = myMx.mxCompactTreeLayout
+export const myMxRadialTreeLayout = myMx.mxRadialTreeLayout
+export const myMxFastOrganicLayout = myMx.mxFastOrganicLayout
+export const myMxCircleLayout = myMx.mxCircleLayout
+export const myMxParallelEdgeLayout = myMx.mxParallelEdgeLayout
+export const myMxCompositeLayout = myMx.mxCompositeLayout
+export const myMxEdgeLabelLayout = myMx.mxEdgeLabelLayout
+export const myMxGraphAbstractHierarchyCell = myMx.mxGraphAbstractHierarchyCell
+export const myMxGraphHierarchyNode = myMx.mxGraphHierarchyNode
+export const myMxGraphHierarchyEdge = myMx.mxGraphHierarchyEdge
+export const myMxGraphHierarchyModel = myMx.mxGraphHierarchyModel
+export const myMxSwimlaneModel = myMx.mxSwimlaneModel
+export const myMxHierarchicalLayoutStage = myMx.mxHierarchicalLayoutStage
+export const myMxMedianHybridCrossingReduction = myMx.mxMedianHybridCrossingReduction
+export const myMxMinimumCycleRemover = myMx.mxMinimumCycleRemover
+export const myMxCoordinateAssignment = myMx.mxCoordinateAssignment
+export const myMxSwimlaneOrdering = myMx.mxSwimlaneOrdering
+export const myMxHierarchicalLayout = myMx.mxHierarchicalLayout
+export const myMxSwimlaneLayout = myMx.mxSwimlaneLayout
+export const myMxGraphModel = myMx.mxGraphModel
+export const myMxCell = myMx.mxCell
+export const myMxGeometry = myMx.mxGeometry
+export const myMxCellPath = myMx.mxCellPath
+export const myMxPerimeter = myMx.mxPerimeter
+export const myMxPrintPreview = myMx.mxPrintPreview
+export const myMxStylesheet = myMx.mxStylesheet
+export const myMxCellState = myMx.mxCellState
+export const myMxGraphSelectionModel = myMx.mxGraphSelectionModel
+export const myMxCellEditor = myMx.mxCellEditor
+export const myMxCellRenderer = myMx.mxCellRenderer
+export const myMxEdgeStyle = myMx.mxEdgeStyle
+export const myMxStyleRegistry = myMx.mxStyleRegistry
+export const myMxGraphView = myMx.mxGraphView
+export const myMxGraph = myMx.mxGraph
+export const myMxCellOverlay = myMx.mxCellOverlay
+export const myMxOutline = myMx.mxOutline
+export const myMxMultiplicity = myMx.mxMultiplicity
+export const myMxLayoutManager = myMx.mxLayoutManager
+export const myMxSwimlaneManager = myMx.mxSwimlaneManager
+export const myMxTemporaryCellStates = myMx.mxTemporaryCellStates
+export const myMxCellStatePreview = myMx.mxCellStatePreview
+export const myMxConnectionConstraint = myMx.mxConnectionConstraint
+export const myMxGraphHandler = myMx.mxGraphHandler
+export const myMxPanningHandler = myMx.mxPanningHandler
+export const myMxPopupMenuHandler = myMx.mxPopupMenuHandler
+export const myMxCellMarker = myMx.mxCellMarker
+export const myMxSelectionCellsHandler = myMx.mxSelectionCellsHandler
+export const myMxConnectionHandler = myMx.mxConnectionHandler
+export const myMxConstraintHandler = myMx.mxConstraintHandler
+export const myMxRubberband = myMx.mxRubberband
+export const myMxHandle = myMx.mxHandle
+export const myMxVertexHandler = myMx.mxVertexHandler
+export const myMxEdgeHandler = myMx.mxEdgeHandler
+export const myMxElbowEdgeHandler = myMx.mxElbowEdgeHandler
+export const myMxEdgeSegmentHandler = myMx.mxEdgeSegmentHandler
+export const myMxKeyHandler = myMx.mxKeyHandler
+export const myMxTooltipHandler = myMx.mxTooltipHandler
+export const myMxCellTracker = myMx.mxCellTracker
+export const myMxCellHighlight = myMx.mxCellHighlight
+export const myMxDefaultKeyHandler = myMx.mxDefaultKeyHandler
+export const myMxDefaultPopupMenu = myMx.mxDefaultPopupMenu
+export const myMxDefaultToolbar = myMx.mxDefaultToolbar
+export const myMxEditor = myMx.mxEditor
+export const myMxCodecRegistry = myMx.mxCodecRegistry
+export const myMxCodec = myMx.mxCodec
+export const myMxObjectCodec = myMx.mxObjectCodec
+export const myMxCellCodec = myMx.mxObjectCodec
+export const myMxModelCodec = myMx.mxObjectCodec
+export const myMxRootChangeCodec = myMx.mxObjectCodec
+export const myMxChildChangeCodec = myMx.mxObjectCodec
+export const myMxTerminalChangeCodec = myMx.mxObjectCodec
+export const myMxGenericChangeCodec = myMx.mxGenericChangeCodec
+export const myMxGraphCodec = myMx.mxObjectCodec
+export const myMxGraphViewCodec = myMx.mxObjectCodec
+export const myMxStylesheetCodec = myMx.mxObjectCodec
+export const myMxDefaultKeyHandlerCodec = myMx.mxObjectCodec
+export const myMxDefaultToolbarCodec = myMx.mxObjectCodec
+export const myMxDefaultPopupMenuCodec = myMx.mxObjectCodec
+export const myMxEditorCodec = myMx.mxObjectCodec

+ 1 - 3
src/layouts/index.vue

@@ -4,6 +4,7 @@
       <t-head-menu :default-value="activeMenu">
         <t-menu-item value="/case" :to="'/case'"> 测试用例 </t-menu-item>
         <t-menu-item value="/case-list" :to="'/case-list'"> 测试用例列表 </t-menu-item>
+        <t-menu-item value="/mx-graph" :to="'/mx-graph'"> 流程图画布 </t-menu-item>
       </t-head-menu>
     </div>
     <div class="main-container">
@@ -20,8 +21,6 @@
 </template>
 
 <script setup lang="tsx" name="Layout">
-import { HeadMenuProps } from 'tdesign-vue-next';
-
 const version = ref('v1.0.0')
 const currentRoute = useRoute()
 const activeMenu = computed(() => {
@@ -63,7 +62,6 @@ const activeMenu = computed(() => {
 .t-menu {
   font-size: 16px;
   font-weight: 700 !important;
-
 }
 
 .t-menu__item {

+ 1 - 1
src/main.ts

@@ -1,5 +1,5 @@
 import '@/assets/style/main.css'
-import router from "./router";
+import router from './router'
 import { createApp } from 'vue'
 import App from './App.vue'
 import { setupAuthRoutes } from '@/router/before'

+ 5 - 5
src/router/before.ts

@@ -1,12 +1,12 @@
 import router from '@/router'
-import  { done, start }  from '@/utils/nprogress'
-const WHITE_LIST = ['/case','/case-list']
+import { done, start } from '@/utils/nprogress'
+const WHITE_LIST = ['/case', '/case-list', '/mx-graph']
 export const setupAuthRoutes = () => {
   router.beforeEach(async (to, from, next) => {
     start()
-      if (WHITE_LIST.includes(to.path)) return next()
-      // 重定向登录页面
-      next(`/case`)
+    if (WHITE_LIST.includes(to.path)) return next()
+    // 重定向登录页面
+    next(`/case`)
   })
   router.afterEach(() => {
     done() // 结束Progress

+ 10 - 5
src/router/constant-router-map.ts

@@ -2,7 +2,7 @@ import { RouteRecordRaw } from 'vue-router'
 export const Layout = () => import('@/layouts/index.vue')
 
 const routes: Array<RouteRecordRaw> = [
-  { path: '/:pathMatch(.*)*', component: () => import("@/views/error/404.vue") },
+  { path: '/:pathMatch(.*)*', component: () => import('@/views/error/404.vue') },
   { path: '/home', name: '总览', component: () => import('@/views/home/index.vue') },
   {
     path: '',
@@ -12,23 +12,28 @@ const routes: Array<RouteRecordRaw> = [
       {
         path: '/demo',
         name: 'Demo',
-        meta: {  hidden: false, title: '测试用例'},
+        meta: { hidden: false, title: '测试用例' },
         component: () => import('@/views/demo/index.vue')
       },
       {
         path: '/case',
         name: 'TestCase',
-        meta: {  hidden: false, title: '测试用例'},
+        meta: { hidden: false, title: '测试用例' },
         component: () => import('@/views/test/index.vue')
       },
       {
         path: '/case-list',
         name: 'TestCaseList',
-        meta: {  hidden: false, title: '测试用例'},
+        meta: { hidden: false, title: '测试用例' },
         component: () => import('@/views/test/list.vue')
+      },
+      {
+        path: '/mx-graph',
+        name: 'MxGraph',
+        meta: { hidden: false, title: '流程图画布' },
+        component: () => import('@/views/test/mxgraph.vue')
       }
     ]
-
   }
 ]
 

+ 4 - 4
src/router/index.ts

@@ -1,7 +1,7 @@
-import { createRouter, createWebHashHistory } from "vue-router";
+import { createRouter, createWebHashHistory } from 'vue-router'
 import routerMap from './constant-router-map'
 
 export default createRouter({
-    history: createWebHashHistory(),
-    routes: routerMap
-})
+  history: createWebHashHistory(),
+  routes: routerMap
+})

+ 4 - 4
src/types/auto-components.d.ts

@@ -8,14 +8,14 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    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']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
-    TButton: typeof import('tdesign-vue-next')['Button']
-    TCollapse: typeof import('tdesign-vue-next')['Collapse']
-    TCollapsePanel: typeof import('tdesign-vue-next')['CollapsePanel']
     TConfigProvider: typeof import('tdesign-vue-next')['ConfigProvider']
     THeadMenu: typeof import('tdesign-vue-next')['HeadMenu']
-    TIcon: typeof import('tdesign-vue-next')['Icon']
     TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
     Toolbar: typeof import('./../views/components/Toolbar.vue')['default']
   }

+ 13 - 14
src/utils/nprogress.ts

@@ -1,24 +1,23 @@
 // /src/utils/nprogress.ts
-import NProgress from 'nprogress';
-import 'nprogress/nprogress.css';
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
 
 //全局进度条的配置
 NProgress.configure({
-	easing: 'ease', // 动画方式
-	speed: 300, // 递增进度条的速度
-	showSpinner: false, // 是否显示加载ico
-	trickleSpeed: 200, // 自动递增间隔
-	minimum: 0.3, // 更改启动时使用的最小百分比
-	parent: 'body' //指定进度条的父容器
-});
+  easing: 'ease', // 动画方式
+  speed: 300, // 递增进度条的速度
+  showSpinner: false, // 是否显示加载ico
+  trickleSpeed: 200, // 自动递增间隔
+  minimum: 0.3, // 更改启动时使用的最小百分比
+  parent: 'body' //指定进度条的父容器
+})
 
 // 打开进度条
 export const start = () => {
-	NProgress.start();
-};
+  NProgress.start()
+}
 
 // 关闭进度条
 export const done = () => {
-	NProgress.done();
-};
-
+  NProgress.done()
+}

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

@@ -0,0 +1,118 @@
+<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>

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

@@ -0,0 +1,152 @@
+<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>

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

@@ -0,0 +1,172 @@
+<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>

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

@@ -0,0 +1,161 @@
+<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>

+ 2 - 4
src/views/components/Toolbar.vue

@@ -17,14 +17,12 @@
   </div>
 </template>
 <script setup lang="ts">
-import {
-  AddIcon
-} from 'tdesign-icons-vue-next';
+import { AddIcon } from 'tdesign-icons-vue-next'
 
 const activeNames = ref(['1', '2'])
 </script>
 <style lang="scss" scoped>
-@import "@/assets/graph/style/general-shap.css";
+@import '@/assets/graph/style/general-shap.css';
 
 .toolbar-container {
   width: 100%;

+ 45 - 8
src/views/test/index.vue

@@ -2,24 +2,61 @@
   <div class="example-editor" ref="editorRef"></div>
 </template>
 <script setup lang="ts">
-import { type mxGraph } from "mxgraph"
-import { MxGraph as myMxGraph } from "@/graph/MxGraph"
+import { type mxGraph, type mxCell } from 'mxgraph'
+import { myMxGraph, myMxPoint } from '@/graph/mxGraph'
 const editorRef: Ref<HTMLDivElement | undefined> = ref()
 const currentGraph: Ref<mxGraph | undefined> = ref()
 const init = (container: HTMLElement) => {
-  const graph = new myMxGraph(container);
-  currentGraph.value = graph;
+  const graph = new myMxGraph(container)
+  graph.setEnabled(false)
+
+  currentGraph.value = graph
+  const parent = graph.getDefaultParent()
+
+  const vertexStyle =
+    'shape=cylinder;strokeWidth=2;fillColor=#ffffff;strokeColor=black;' +
+    'gradientColor=#a0a0a0;fontColor=black;fontStyle=1;spacingTop=14;'
+  const edgeStyle = 'strokeWidth=3;endArrow=block;endSize=2;endFill=1;strokeColor=black;rounded=1;'
+
+  graph.getModel().beginUpdate()
+
+  let edge1: mxCell
+  try {
+    const v1 = graph.insertVertex(parent, null, 'Pump', 20, 20, 60, 60, vertexStyle)
+    const v2 = graph.insertVertex(parent, null, 'Tank', 200, 150, 60, 60, vertexStyle)
+    const e1 = graph.insertEdge(parent, null, '', v1, v2, edgeStyle)
+    e1.geometry.points = [new myMxPoint(230, 50)]
+    graph.orderCells(true, [e1])
+
+    edge1 = e1
+  } finally {
+    // 更新显示
+    graph.getModel().endUpdate()
+  }
+
+  // 添加动画
+  const state = graph.view.getState(edge1)
+  state.shape.node.getElementsByTagName('path')[0].removeAttribute('visibility')
+  state.shape.node.getElementsByTagName('path')[0].setAttribute('stroke-width', '6')
+  state.shape.node.getElementsByTagName('path')[0].setAttribute('stroke', 'lightGray')
+  state.shape.node.getElementsByTagName('path')[1].setAttribute('class', 'flow')
 }
 onMounted(() => {
   if (editorRef.value) {
-    const container = editorRef.value;
-    init(container);
+    const container = editorRef.value
+    init(container)
   }
-});
+})
 </script>
 <style lang="scss" scoped>
 .example-editor {
-  background: url("@/assets/graph/images/grid.gif");
+  background: url('@/assets/graph/images/grid.gif');
   height: calc(100vh - 90px);
 }
+
+.flow {
+  stroke-dasharray: 8;
+  animation: dash 0.5s linear;
+  animation-iteration-count: infinite;
+}
 </style>

+ 1 - 3
src/views/test/list.vue

@@ -1,6 +1,4 @@
 <template>
   <Toolbar></Toolbar>
 </template>
-<script lang="ts" setup>
-
-</script>
+<script lang="ts" setup></script>

+ 36 - 0
src/views/test/mxgraph.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="app-container">
+    <h1>mxGraph 图形编辑器</h1>
+    <MxToolbar />
+    <div class="editor-layout">
+      <MxPalette />
+      <MxGraphContainer />
+      <MxPropertiesPanel />
+    </div>
+  </div>
+</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>