|
@@ -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>
|