@mx-sose-front/mx-sose-graph 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +103 -10
- package/dist/index.esm.js +6618 -5593
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +7 -7
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/{ContextMenu.vue → ContextMenu/ContextMenu.vue} +243 -70
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +138 -0
- package/src/components/Edge/Edge.vue +38 -49
- package/src/components/InteractionLayer.vue +432 -838
- package/src/components/Shape/Block.vue +8 -8
- package/src/components/ZoomSlider/ZoomSlider.vue +229 -0
- package/src/constants/index.ts +12 -0
- package/src/statics/icons/childIcons//351/241/271/347/233/256/351/241/272/345/272/217@3x.png +0 -0
- package/src/store/graphStore.ts +98 -21
- package/src/types/index.ts +14 -1
- package/src/types/interactionLayer.ts +35 -0
- package/src/utils/contextMenuUtils.ts +264 -0
- package/src/utils/diagram.ts +93 -4
- package/src/utils/edgeUtils.ts +228 -0
- package/src/utils/geom.ts +34 -0
- package/src/utils/graphDragService.ts +14 -17
- package/src/utils/keyboardUtils.ts +229 -0
- package/src/utils/license-guard.ts +50 -0
- package/src/utils/nameEditUtils.ts +132 -0
- package/src/utils/resizeUtils.ts +463 -0
- package/src/view/graph.vue +102 -134
- package/src/components/Label.vue +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- 交互层:放在图元之上,统一接管交互与鼠标样式 -->
|
|
3
|
-
<div
|
|
4
|
-
|
|
3
|
+
<div @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousemove="onMouseMove" class="interaction-layer"
|
|
4
|
+
ref="layerRef" @dragover="onCanvasDragOver" @drop="onCanvasDrop" :style="{ cursor: cursorStyle }"
|
|
5
|
+
@mousedown="onLayerMouseDown" @mouseup="onLayerMouseUp" @click="onLayerClick"
|
|
5
6
|
@contextmenu.prevent="handleContextMenu">
|
|
6
7
|
<!-- 只在"选中对象是画布(diagram)"时显示四个角手柄 -->
|
|
7
8
|
<div v-for="s in graphStore.marqueeShapes" :key="s.id" class="selection-box" :style="getSelectionBoxStyle(s)">
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
</div>
|
|
14
15
|
<div class="action-buttons"
|
|
15
16
|
v-show="!isMultiSelected && s.scenarioMenus && s.scenarioMenus.length > 0 && s.shapeType != ShapeConfig.SHAPE_TYPE"
|
|
16
|
-
:style="
|
|
17
|
+
:style="actionButtonsStyle(s)">
|
|
17
18
|
<div v-if="s.modelTypePropertyId" class="border-btn">
|
|
18
19
|
<button class="action-btn edit-btn"
|
|
19
20
|
@mousedown.stop.prevent="clickModelTypePropertyIdButton(s.modelTypePropertyId, s)" title="设置类型">
|
|
@@ -57,14 +58,16 @@
|
|
|
57
58
|
<!-- 名称编辑输入框 -->
|
|
58
59
|
<div v-if="isEditingName && graphStore.selectedShape && graphStore.selectedShape.shapeKey !== 'ConceptRole'"
|
|
59
60
|
class="name-editor-container" :style="nameEditorContainerStyle(graphStore.selectedShape)">
|
|
60
|
-
<input ref="nameInput" v-model="editingName" class="name-input" :style="nameInputStyle"
|
|
61
|
-
@
|
|
61
|
+
<input ref="nameInput" v-model="editingName" class="name-input" :style="nameInputStyle(graphStore.selectedShape)"
|
|
62
|
+
@blur="nameEditManager.handleBlur(graphStore.selectedShape)"
|
|
63
|
+
@keyup.enter="nameEditManager.handleKeyUp($event, graphStore.selectedShape)"
|
|
64
|
+
@keyup.escape="nameEditManager.cancelEdit()" />
|
|
62
65
|
</div>
|
|
63
66
|
|
|
64
67
|
<!-- 使用右键菜单组件 -->
|
|
65
68
|
<ContextMenu v-if="selectedShape && !isMultiSelected" :visible="showContextMenu" :selected-shape="selectedShape"
|
|
66
|
-
:position="contextMenuPosition" @update:visible="showContextMenu = $event"
|
|
67
|
-
@show-property-panel="onLayerDblClick(true)" />
|
|
69
|
+
:position="contextMenuPosition" @update:visible="showContextMenu = $event"
|
|
70
|
+
@delete="ContextMenuUtils.handleDelete(contextMenuTarget)" @show-property-panel="onLayerDblClick(true);" />
|
|
68
71
|
|
|
69
72
|
<!-- 连接层逻辑 - 当 connectShapeData 存在时显示 -->
|
|
70
73
|
<div v-if="connectShapeData && diagramBounds" class="connect-layer" :style="layerStyle">
|
|
@@ -87,87 +90,69 @@ import {
|
|
|
87
90
|
onMounted,
|
|
88
91
|
type CSSProperties,
|
|
89
92
|
watch,
|
|
90
|
-
onBeforeUnmount,
|
|
91
|
-
type ComponentPublicInstance,
|
|
92
93
|
} from "vue";
|
|
93
94
|
import type { Shape } from "../types";
|
|
95
|
+
import { InteractionLayerEmits } from "../types/interactionLayer";
|
|
96
|
+
import type { InteractionLayerProps, ExternalCreateDragState } from "../types/interactionLayer";
|
|
94
97
|
import { useGraphStore } from "../store/graphStore";
|
|
98
|
+
import { resizeHandles } from "../constants/index";
|
|
95
99
|
|
|
96
100
|
// 工具:几何/命中/样式/拖拽
|
|
97
101
|
import {
|
|
98
102
|
toLocalPoint,
|
|
99
103
|
getBounds,
|
|
100
104
|
getDiagramRect,
|
|
101
|
-
ghostResizeStep,
|
|
102
105
|
rectFromPoints,
|
|
103
106
|
rectContainsRect,
|
|
104
107
|
clampPointToRect,
|
|
108
|
+
clientToLocalPoint,
|
|
109
|
+
localToClientPoint,
|
|
110
|
+
isInsideCanvasClient,
|
|
105
111
|
} from "../utils/geom";
|
|
106
112
|
import { pickTarget } from "../utils/hittest";
|
|
107
113
|
import {
|
|
114
|
+
ShapeConfig,
|
|
108
115
|
selectionBoxStyle,
|
|
109
116
|
handleStyle,
|
|
110
117
|
adjustCanvasToFitAllShapes,
|
|
111
118
|
actionButtonsStyle,
|
|
112
119
|
nameTextBoxContainerStyle,
|
|
113
120
|
nameTextBoxStyle,
|
|
114
|
-
nameEditorContainerStyle
|
|
121
|
+
nameEditorContainerStyle,
|
|
122
|
+
nameInputStyle,
|
|
123
|
+
getMarqueeStyle,
|
|
124
|
+
getLayerStyle
|
|
115
125
|
} from "../utils/diagram";
|
|
116
126
|
import { withDrag } from "../utils/dom";
|
|
117
|
-
import { checkNestViaFront
|
|
127
|
+
import { checkNestViaFront } from "../utils/policy";
|
|
118
128
|
import { getShapeComponent, getShapeStyle } from "../render/shape-renderer";
|
|
119
129
|
import _ from "lodash";
|
|
120
130
|
import { eventBus } from "../store";
|
|
121
|
-
import
|
|
122
|
-
import ContextMenu from "./ContextMenu.vue";
|
|
123
|
-
import { finalizeAfterTransform } from "../utils/drag";
|
|
124
|
-
import { normalizeZOrder } from "../utils/zorder";
|
|
125
|
-
import { clampParentRectToChildrenGap } from "../utils/containers";
|
|
131
|
+
import ContextMenu from "./ContextMenu/ContextMenu.vue";
|
|
126
132
|
import { storeToRefs } from "pinia";
|
|
127
133
|
import { EdgeUtils } from "../utils/edgeUtils";
|
|
128
134
|
import ConnectionLine from "./LineStyle/ConnectionLine.vue";
|
|
129
|
-
import {
|
|
130
|
-
import { collectDescendantIds } from "../utils/containers";
|
|
135
|
+
import { isCompartment } from "../utils/compartment";
|
|
131
136
|
import { HighlightUtils } from "../utils/highlightUtils";
|
|
137
|
+
import { ContextMenuUtils } from "../utils/contextMenuUtils";
|
|
132
138
|
// 静态导入图片资源
|
|
133
139
|
import { getIcon } from "../utils/iconLoader";
|
|
134
140
|
import { getUuid } from "../utils/index";
|
|
135
141
|
import { ElMessage } from "element-plus";
|
|
136
142
|
import { snapPinToParentEdge, snapPinPointerOnMove } from '../utils/pinUtils';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"diagramDoubleClick",
|
|
144
|
-
"modelTypePropertyIdButtonClick",
|
|
145
|
-
"edge-click",
|
|
146
|
-
"property-panel",
|
|
147
|
-
"actionButtonAdd"
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
// Props
|
|
151
|
-
interface Props {
|
|
152
|
-
connectShapeData?: Shape;
|
|
153
|
-
diagramBounds?: any;
|
|
154
|
-
resShape: Shape;
|
|
155
|
-
lines?: String[];
|
|
156
|
-
packages?: String[];
|
|
157
|
-
diagram?: String[];
|
|
158
|
-
taggedValueLabels?: String[];
|
|
159
|
-
actionButtonShapeDataId?: string;
|
|
160
|
-
edgeCheck?: boolean;
|
|
161
|
-
}
|
|
162
|
-
const props = defineProps<Props>();
|
|
143
|
+
import { createKeyboardHandler } from '../utils/keyboardUtils';
|
|
144
|
+
import { NameEditManager } from "../utils/nameEditUtils";
|
|
145
|
+
import { guardOperate } from "../utils/license-guard"
|
|
146
|
+
import { createResizeUtils } from "../utils/resizeUtils";
|
|
147
|
+
|
|
148
|
+
const props = defineProps<InteractionLayerProps>();
|
|
163
149
|
|
|
164
|
-
|
|
165
|
-
const actionButtonsRefs = ref<Record<string, Element | null>>({});
|
|
150
|
+
const emit = defineEmits(InteractionLayerEmits);
|
|
166
151
|
|
|
167
152
|
// 取得 graphStore 实例
|
|
168
153
|
const graphStore = useGraphStore();
|
|
169
154
|
|
|
170
|
-
const { selectedShape } = storeToRefs(graphStore);
|
|
155
|
+
const { selectedShape, connectMode } = storeToRefs(graphStore);
|
|
171
156
|
|
|
172
157
|
// 是否正在“外部创建拖拽”(
|
|
173
158
|
const isExternalCreateDragging = ref(false)
|
|
@@ -227,38 +212,94 @@ const getGhostShapeStyle = (shape: Shape): CSSProperties => {
|
|
|
227
212
|
// 根层引用:用于本地坐标换算
|
|
228
213
|
const layerRef = ref<HTMLDivElement | null>(null);
|
|
229
214
|
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const nameInputStyle = computed(() => {
|
|
237
|
-
const s = graphStore.selectedShape as any
|
|
238
|
-
if (!s) return {}
|
|
239
|
-
const isPin = String(s?.shapeType || '').toLowerCase() === 'pin'
|
|
240
|
-
const ns = s?.nameStyle || {}
|
|
241
|
-
const nb = s?.nameBounds || {}
|
|
242
|
-
const fs = Number(ns.fontSize || nb.height || 12)
|
|
243
|
-
if (!isPin) return {}
|
|
244
|
-
return {
|
|
245
|
-
width: '100%',
|
|
246
|
-
height: '100%',
|
|
247
|
-
fontSize: `${fs}px`,
|
|
248
|
-
lineHeight: `${Math.ceil(fs + 8)}px`,
|
|
249
|
-
padding: '4px 6px',
|
|
250
|
-
boxSizing: 'border-box' as const,
|
|
215
|
+
// 名称编辑管理器
|
|
216
|
+
const nameEditManager = new NameEditManager({
|
|
217
|
+
onNameChange: (oldName, newName) => {
|
|
218
|
+
if (graphStore.selectedShape) {
|
|
219
|
+
emit("editName", graphStore.selectedShape, newName, oldName);
|
|
220
|
+
}
|
|
251
221
|
}
|
|
252
|
-
})
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 缩放工具实例
|
|
225
|
+
const resizeUtils = createResizeUtils(layerRef, {
|
|
226
|
+
packages: props.packages,
|
|
227
|
+
diagram: props.diagram,
|
|
228
|
+
taggedValueLabels: props.taggedValueLabels,
|
|
229
|
+
}, {
|
|
230
|
+
onResizeStart: (target) => {
|
|
231
|
+
eventBus.emit('resize-start', { target });
|
|
232
|
+
},
|
|
233
|
+
onResizeEnd: (target) => {
|
|
234
|
+
eventBus.emit('resize-end', { target });
|
|
235
|
+
},
|
|
236
|
+
onShapeUpdate: (id, updates) => {
|
|
237
|
+
graphStore.updateShape(id, updates);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// 解构缩放相关的变量
|
|
242
|
+
const {
|
|
243
|
+
isResizing,
|
|
244
|
+
groupGhost,
|
|
245
|
+
startResize,
|
|
246
|
+
} = resizeUtils;
|
|
247
|
+
|
|
248
|
+
// 从名称编辑管理器获取响应式状态
|
|
249
|
+
const { isEditingName, editingName } = nameEditManager.editingState;
|
|
250
|
+
// 是否在画布内
|
|
251
|
+
const isMouseInside = ref(false);
|
|
252
|
+
// 记录最近一次 mousemove(用于 rAF 合并)
|
|
253
|
+
let rafId: number | null = null;
|
|
254
|
+
let latestEvt: MouseEvent | null = null;
|
|
255
|
+
// 拖拽清理函数
|
|
256
|
+
let offDrag: (() => void) | null = null;
|
|
257
|
+
|
|
258
|
+
const emitMouse = (evt: MouseEvent, inside: boolean) => {
|
|
259
|
+
if (!layerRef.value) return;
|
|
260
|
+
// 把鼠标 client 坐标转为画布本地坐标
|
|
261
|
+
const pt = toLocalPoint(evt, layerRef.value);
|
|
262
|
+
|
|
263
|
+
const payload = {
|
|
264
|
+
x: pt.x,
|
|
265
|
+
y: pt.y,
|
|
266
|
+
clientX: evt.clientX,
|
|
267
|
+
clientY: evt.clientY,
|
|
268
|
+
inside,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// 对外抛出
|
|
272
|
+
eventBus.emit("canvas-mouse-move", payload);
|
|
273
|
+
};
|
|
274
|
+
// 鼠标进入画布
|
|
275
|
+
const onMouseEnter = () => {
|
|
276
|
+
isMouseInside.value = true;
|
|
277
|
+
eventBus.emit("canvas-mouse-enter", { inside: true });
|
|
278
|
+
};
|
|
279
|
+
// 鼠标离开画布区域
|
|
280
|
+
const onMouseLeave = () => {
|
|
281
|
+
isMouseInside.value = false;
|
|
282
|
+
// 停止 rAF
|
|
283
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
284
|
+
rafId = null;
|
|
285
|
+
latestEvt = null;
|
|
286
|
+
// 通知外部隐藏坐标
|
|
287
|
+
eventBus.emit("canvas-mouse-leave", { inside: false });
|
|
288
|
+
};
|
|
289
|
+
// 鼠标移动过程中
|
|
290
|
+
const onMouseMove = (evt: MouseEvent) => {
|
|
291
|
+
if (!isMouseInside.value) return;
|
|
292
|
+
latestEvt = evt;
|
|
293
|
+
// rAF 合并高频 mousemove
|
|
294
|
+
if (rafId) return;
|
|
295
|
+
rafId = requestAnimationFrame(() => {
|
|
296
|
+
rafId = null;
|
|
297
|
+
if (!latestEvt) return;
|
|
298
|
+
emitMouse(latestEvt, true);
|
|
299
|
+
latestEvt = null;
|
|
300
|
+
});
|
|
301
|
+
};
|
|
253
302
|
|
|
254
|
-
// 缩放状态
|
|
255
|
-
const isResizing = ref(false);
|
|
256
|
-
// 点击情景菜单状态
|
|
257
|
-
const actionButtonMode = ref(false);
|
|
258
|
-
const resizeDirection = ref<"nw" | "ne" | "sw" | "se" | "">("");
|
|
259
|
-
const startPos = ref({ x: 0, y: 0 });
|
|
260
|
-
const startBounds = ref({ x: 0, y: 0, width: 0, height: 0 });
|
|
261
|
-
let offDrag: null | (() => void) = null;
|
|
262
303
|
// 框选状态
|
|
263
304
|
const marqueeRect = ref<Rect | null>(null);
|
|
264
305
|
const marqueeAnchor = ref<{ x: number; y: number } | null>(null);
|
|
@@ -283,33 +324,10 @@ const recordClickPoint = ref({ x: 0, y: 0 })
|
|
|
283
324
|
// 高亮相关状态
|
|
284
325
|
const highlightTimeout = ref<ReturnType<typeof setTimeout> | null>(null); // 高亮定时器
|
|
285
326
|
|
|
286
|
-
// 组拖拽的预览
|
|
287
|
-
const groupBase = ref<Record<string, Rect>>({}); // 按下时各 shape 的快照
|
|
288
|
-
const groupGhost = ref<Record<string, Rect>>({}); // 拖动中的预览矩形
|
|
289
|
-
const resizingTarget = ref<Shape | null>(null);
|
|
290
|
-
// 框选矩形的样式
|
|
291
|
-
const getMarqueeStyle = (r: Rect): CSSProperties => ({
|
|
292
|
-
position: "absolute",
|
|
293
|
-
left: `${r.x}px`,
|
|
294
|
-
top: `${r.y}px`,
|
|
295
|
-
width: `${r.width}px`,
|
|
296
|
-
height: `${r.height}px`,
|
|
297
|
-
// background: 'rgba(52,152,219,0.12)',
|
|
298
|
-
border: "2px dashed #a6ddff",
|
|
299
|
-
pointerEvents: "none",
|
|
300
|
-
zIndex: 20,
|
|
301
|
-
});
|
|
302
327
|
// 正在交互:缩放中 或 元素拖动中
|
|
303
328
|
const isBusy = computed(() =>
|
|
304
329
|
isResizing.value || (graphStore.isDragging && graphStore.ghostShadow.length > 0)
|
|
305
330
|
)
|
|
306
|
-
// 四个角手柄(常量)
|
|
307
|
-
const resizeHandles: { position: "nw" | "ne" | "sw" | "se" }[] = [
|
|
308
|
-
{ position: "nw" },
|
|
309
|
-
{ position: "ne" },
|
|
310
|
-
{ position: "sw" },
|
|
311
|
-
{ position: "se" },
|
|
312
|
-
];
|
|
313
331
|
|
|
314
332
|
// 监听所有可能影响菜单显示的操作状态
|
|
315
333
|
const shouldCloseMenu = computed(() => {
|
|
@@ -319,75 +337,30 @@ const shouldCloseMenu = computed(() => {
|
|
|
319
337
|
// 预览框(ghost)的 bounds,仅在缩放时存在
|
|
320
338
|
type Rect = { x: number; y: number; width: number; height: number };
|
|
321
339
|
|
|
322
|
-
//动态绑定ref
|
|
323
|
-
const setActionButtonsRef = (el: Element | ComponentPublicInstance | null, shapeId: string) => {
|
|
324
|
-
// 只存储 DOM 元素(过滤掉组件实例)
|
|
325
|
-
// 当el为null时(元素销毁),清空对应引用
|
|
326
|
-
if (el === null) {
|
|
327
|
-
actionButtonsRefs.value[shapeId] = null;
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
// 只存储DOM元素,过滤组件实例
|
|
331
|
-
if (el instanceof Element) {
|
|
332
|
-
actionButtonsRefs.value[shapeId] = el;
|
|
333
|
-
} else {
|
|
334
|
-
actionButtonsRefs.value[shapeId] = null;
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// 计算操作按钮的位置样式
|
|
340
|
-
const getActionButtonsStyle = (shape: Shape, shapeId: string): CSSProperties => {
|
|
341
|
-
// 前置校验:shapeId不存在/元素已销毁,直接返回空样式
|
|
342
|
-
if (!shapeId || !actionButtonsRefs.value[shapeId] || !layerRef.value) {
|
|
343
|
-
return {
|
|
344
|
-
position: "absolute",
|
|
345
|
-
left: `${(shape.bounds?.width ?? 100) + 15}px`,
|
|
346
|
-
top: '0px',
|
|
347
|
-
zIndex: 1000,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const shapeBounds = shape.bounds ?? {};
|
|
352
|
-
const shapeWidth = shapeBounds.width ?? 100;
|
|
353
|
-
const shapeHeight = shapeBounds.height ?? 40;
|
|
354
|
-
if (!shape.scenarioMenus) {
|
|
355
|
-
return {}
|
|
356
|
-
}
|
|
357
|
-
// 按钮宽度:28px,间距4px,共5个按钮 (根据接口动态替换)
|
|
358
|
-
const hasTypeButton = !!shape.modelTypePropertyId;
|
|
359
|
-
const totalButtons = (shape.scenarioMenus ? shape.scenarioMenus!.length : 0) + (hasTypeButton ? 1 : 0);
|
|
360
|
-
const buttonsHeight = 28 * totalButtons + 4 * (totalButtons - (totalButtons > 0 ? 1 : 0));
|
|
361
|
-
const shapeTop = shape.bounds?.y ?? 0;
|
|
362
|
-
if (shapeTop < 10) { // 固定阈值,不受按钮DOM样式修改影响
|
|
363
|
-
return {
|
|
364
|
-
position: "absolute",
|
|
365
|
-
left: `${shapeWidth + 15}px`, // 紧贴形状右侧,间距5px
|
|
366
|
-
top: `10px`, // 顶部对齐
|
|
367
|
-
zIndex: 1000,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
return {
|
|
371
|
-
position: "absolute",
|
|
372
|
-
left: `${shapeWidth + 15}px`, // 紧贴形状右侧,间距5px
|
|
373
|
-
top: `${(shapeHeight - buttonsHeight) / 2}px`, // 垂直居中
|
|
374
|
-
zIndex: 1000,
|
|
375
|
-
};
|
|
376
|
-
};
|
|
377
340
|
|
|
378
341
|
const clickActionButton = (event: MouseEvent, value: string, shape: Shape) => {
|
|
379
342
|
// 阻止事件冒泡,避免触发 onLayerClick
|
|
380
343
|
event.stopPropagation();
|
|
381
344
|
event.preventDefault();
|
|
382
345
|
|
|
346
|
+
// 如果正在编辑名称,先触发失焦以保存当前编辑的内容
|
|
347
|
+
if (isEditingName.value) {
|
|
348
|
+
nameEditManager.handleBlur(graphStore.selectedShape);
|
|
349
|
+
}
|
|
350
|
+
|
|
383
351
|
// 清除选中状态,避免第一次点击时取消选中导致需要点击两次
|
|
384
352
|
graphStore.clearSelection();
|
|
385
353
|
|
|
386
|
-
|
|
354
|
+
graphStore.setConnectMode('action')
|
|
387
355
|
emit('actionButtonClick', value, shape);
|
|
388
356
|
}
|
|
389
357
|
|
|
390
358
|
const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
|
|
359
|
+
// 如果正在编辑名称,先触发失焦以保存当前编辑的内容
|
|
360
|
+
if (isEditingName.value) {
|
|
361
|
+
nameEditManager.handleBlur(graphStore.selectedShape);
|
|
362
|
+
}
|
|
363
|
+
|
|
391
364
|
emit('modelTypePropertyIdButtonClick', value, shape);
|
|
392
365
|
}
|
|
393
366
|
|
|
@@ -395,31 +368,9 @@ const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
|
|
|
395
368
|
const getSelectionBoxStyle = (shape: Shape) => selectionBoxStyle(shape);
|
|
396
369
|
const getHandleStyle = (h: any, shape: Shape) => handleStyle(h.position, shape);
|
|
397
370
|
|
|
398
|
-
|
|
399
|
-
|
|
400
371
|
// 名称编辑
|
|
401
372
|
const startEditName = async () => {
|
|
402
|
-
|
|
403
|
-
isEditingName.value = true;
|
|
404
|
-
editingName.value = graphStore.selectedShape.name;
|
|
405
|
-
await nextTick();
|
|
406
|
-
nameInput.value?.focus();
|
|
407
|
-
nameInput.value?.select();
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const finishEditName = () => {
|
|
411
|
-
if (!graphStore.selectedShape || !isEditingName.value) return;
|
|
412
|
-
const next = editingName.value.trim();
|
|
413
|
-
if (next && next !== graphStore.selectedShape.name) {
|
|
414
|
-
emit("editName", graphStore.selectedShape, next);
|
|
415
|
-
graphStore.updateShape(graphStore.selectedShape.id, { name: next });
|
|
416
|
-
}
|
|
417
|
-
isEditingName.value = false;
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const cancelEditName = () => {
|
|
421
|
-
isEditingName.value = false;
|
|
422
|
-
editingName.value = "";
|
|
373
|
+
await nameEditManager.startEdit(graphStore.selectedShape);
|
|
423
374
|
};
|
|
424
375
|
|
|
425
376
|
// 属性面板
|
|
@@ -438,25 +389,16 @@ const onLayerClick = (evt: MouseEvent) => {
|
|
|
438
389
|
return;
|
|
439
390
|
}
|
|
440
391
|
|
|
441
|
-
// 检查目标元素本身或其父元素是否是text元素
|
|
442
|
-
let currentElement: HTMLElement | null = target;
|
|
443
|
-
while (currentElement && currentElement !== layerRef.value) {
|
|
444
|
-
if (currentElement.tagName === "text") {
|
|
445
|
-
// 如果是text元素,不处理点击事件,让事件穿透到Capability组件
|
|
446
|
-
// 不调用stopPropagation,让事件继续冒泡
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
currentElement = currentElement.parentElement;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
392
|
// 如果点击了非菜单区域,关闭菜单
|
|
453
|
-
if (
|
|
393
|
+
if (ContextMenuUtils.isClickOutsideMenu(evt)) {
|
|
454
394
|
showContextMenu.value = false;
|
|
455
395
|
}
|
|
456
396
|
|
|
457
397
|
// 检查是否点击了name-text-box
|
|
458
398
|
if (target.classList.contains("name-text-box")) {
|
|
459
|
-
|
|
399
|
+
if (nameEditManager.canEdit(graphStore.selectedShape)) {
|
|
400
|
+
startEditName();
|
|
401
|
+
}
|
|
460
402
|
return;
|
|
461
403
|
}
|
|
462
404
|
|
|
@@ -488,11 +430,16 @@ const onLayerClick = (evt: MouseEvent) => {
|
|
|
488
430
|
// 判断点击位置是否有图形
|
|
489
431
|
const hasShapeAtPoint = EdgeUtils.isEndPointInShape(graphStore.shapes, { x: clickX, y: clickY });
|
|
490
432
|
|
|
491
|
-
// 修改:无论是否在
|
|
433
|
+
// 修改:无论是否在action模式下,只要在连接状态且点击空白处,都创建新图元
|
|
492
434
|
if (!hasShapeAtPoint) {
|
|
493
435
|
// 使用 cloneDeep 克隆 sourceShape
|
|
494
|
-
|
|
495
|
-
|
|
436
|
+
if (!!sourceShape.value?.parenShapeId) {
|
|
437
|
+
isConnecting.value = false;
|
|
438
|
+
graphStore.setConnectMode('connect')
|
|
439
|
+
highlightShape(null, false); // 取消图元高亮
|
|
440
|
+
highlightUtils.clearHighlightTimeout();
|
|
441
|
+
return;
|
|
442
|
+
};
|
|
496
443
|
const newShape = _.cloneDeep(foundSourceShape);
|
|
497
444
|
|
|
498
445
|
// 修改 id(使用 getUuid)
|
|
@@ -539,292 +486,6 @@ const onLayerClick = (evt: MouseEvent) => {
|
|
|
539
486
|
}
|
|
540
487
|
}
|
|
541
488
|
};
|
|
542
|
-
// 开始拖拽改变图元大小
|
|
543
|
-
const startResize = (
|
|
544
|
-
e: MouseEvent,
|
|
545
|
-
dir: "nw" | "ne" | "sw" | "se",
|
|
546
|
-
target: Shape
|
|
547
|
-
) => {
|
|
548
|
-
// 为conceptualRole禁用拖拽放大功能
|
|
549
|
-
if (target.shapeKey === 'ConceptRole') {
|
|
550
|
-
e.preventDefault();
|
|
551
|
-
e.stopPropagation();
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (graphStore.selectedIds.length > 1) {
|
|
556
|
-
// 阻止事件冒泡到外层,避免触发拖拽或框选
|
|
557
|
-
e.preventDefault();
|
|
558
|
-
e.stopPropagation();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
// const s = graphStore.selectedShape;
|
|
562
|
-
e.preventDefault();
|
|
563
|
-
e.stopPropagation();
|
|
564
|
-
|
|
565
|
-
isResizing.value = true;
|
|
566
|
-
resizingTarget.value = target;
|
|
567
|
-
resizeDirection.value = dir;
|
|
568
|
-
|
|
569
|
-
// 发送缩放开始事件
|
|
570
|
-
eventBus.emit('resize-start', { target });
|
|
571
|
-
// 记录 anchor(按下时的指针位置)
|
|
572
|
-
const anchor = toLocalPoint(e, layerRef.value);
|
|
573
|
-
startPos.value = { x: anchor.x, y: anchor.y };
|
|
574
|
-
// 记录基准 bounds(按下那一刻)
|
|
575
|
-
const b = target.bounds ?? {};
|
|
576
|
-
startBounds.value = {
|
|
577
|
-
x: b.x ?? 0,
|
|
578
|
-
y: b.y ?? 0,
|
|
579
|
-
width: b.width ?? 100,
|
|
580
|
-
height: b.height ?? 50,
|
|
581
|
-
};
|
|
582
|
-
// 初始 ghost = 起始尺寸
|
|
583
|
-
groupBase.value = { [target.id]: { ...startBounds.value } };
|
|
584
|
-
groupGhost.value = { [target.id]: { ...startBounds.value } };
|
|
585
|
-
offDrag = withDrag(handleResize, stopResize);
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
// 最小尺寸
|
|
589
|
-
const minW = 50,
|
|
590
|
-
minH = 30;
|
|
591
|
-
|
|
592
|
-
// 计算文本所需最小宽度 - 应用于所有图形类型
|
|
593
|
-
const calculateTextMinWidth = (shape: Shape): number => {
|
|
594
|
-
// 对于所有图形类型,使用与Block.vue中相同的宽度计算逻辑
|
|
595
|
-
// 获取字体大小(与Block.vue中逻辑一致)
|
|
596
|
-
const nameStyle = shape.nameStyle || {};
|
|
597
|
-
const nameBounds = shape.nameBounds || {};
|
|
598
|
-
const fontSize = nameStyle.fontSize || nameBounds.height || 12;
|
|
599
|
-
// 假设每个字符的平均宽度是字体大小的0.6倍
|
|
600
|
-
const charWidth = fontSize * 1;
|
|
601
|
-
|
|
602
|
-
// 计算名称文本的宽度
|
|
603
|
-
const nameTextWidth = (shape.name?.length || 0) * charWidth;
|
|
604
|
-
// 计算关键词文本的宽度
|
|
605
|
-
const keywordsTextWidth = (shape.keywords?.length || 0) * charWidth;
|
|
606
|
-
|
|
607
|
-
// 返回较大的宽度,并添加一些边距(左右各10px)
|
|
608
|
-
const maxTextWidth = Math.max(nameTextWidth, keywordsTextWidth);
|
|
609
|
-
const estimatedTextWidth = maxTextWidth + 60; // 左右各10px边距
|
|
610
|
-
|
|
611
|
-
// 确保最小宽度不小于minW
|
|
612
|
-
return Math.max(minW, estimatedTextWidth);
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* 缩放过程中:只更新 ghostBounds(预览框),不直接改实体。
|
|
617
|
-
* 松开鼠标时在方法里 里一次性把 ghost 落盘到元素上。
|
|
618
|
-
*/
|
|
619
|
-
const handleResize = (e: MouseEvent) => {
|
|
620
|
-
// 没在缩放 or 没有有效的缩放目标 => 不处理
|
|
621
|
-
if (!isResizing.value || !resizingTarget.value) return;
|
|
622
|
-
// 当前指针在交互层的本地坐标
|
|
623
|
-
const curr = toLocalPoint(e, layerRef.value);
|
|
624
|
-
// 按下那一刻的元素矩形(作为缩放基准)
|
|
625
|
-
const base = startBounds.value;
|
|
626
|
-
// 当前激活的手柄方向
|
|
627
|
-
const handle = resizeDirection.value as "nw" | "ne" | "sw" | "se";
|
|
628
|
-
// 当前被缩放的 shape
|
|
629
|
-
const shape = resizingTarget.value;
|
|
630
|
-
// 容器(画布)的约束:只限制left和top不小于0,不限制right和bottom以允许无限放大
|
|
631
|
-
const container = {
|
|
632
|
-
left: 0,
|
|
633
|
-
top: 0,
|
|
634
|
-
right: Infinity,
|
|
635
|
-
bottom: Infinity,
|
|
636
|
-
};
|
|
637
|
-
// 通过策略读取本图元需要约束的边(left/top/right/bottom)
|
|
638
|
-
const edges = getPolicy(shape).constrainToDiagram;
|
|
639
|
-
|
|
640
|
-
// 计算基于文本内容的最小宽度
|
|
641
|
-
const dynamicMinW = calculateTextMinWidth(shape);
|
|
642
|
-
|
|
643
|
-
// 关键修复:在缩放过程中,我们需要区分当前是在放大还是缩小
|
|
644
|
-
// 对于缩小操作,使用基于文本内容的最小宽度约束
|
|
645
|
-
// 对于放大操作,暂时使用更小的最小宽度约束,允许用户自由放大
|
|
646
|
-
const dx = curr.x - startPos.value.x;
|
|
647
|
-
const isEnlarging = (handle === 'ne' || handle === 'se') ? dx > 0 : dx < 0;
|
|
648
|
-
|
|
649
|
-
// 如果是放大操作,使用一个很小的值作为临时最小宽度
|
|
650
|
-
// 这样可以允许用户从任何宽度开始自由放大
|
|
651
|
-
let effectiveMinW = isEnlarging ? minW / 2 : Math.max(minW, dynamicMinW);
|
|
652
|
-
|
|
653
|
-
// 根据shapeKey确定最小高度
|
|
654
|
-
let effectiveMinH = 70; // 默认block组件的最小高度
|
|
655
|
-
if (shape.shapeKey) {
|
|
656
|
-
if (props.packages && props.packages.includes(shape.shapeKey)) {
|
|
657
|
-
effectiveMinH = 85;
|
|
658
|
-
} else if (props.diagram && props.diagram.includes(shape.shapeKey)) {
|
|
659
|
-
effectiveMinH = 80;
|
|
660
|
-
} else if (props.taggedValueLabels && props.taggedValueLabels.includes(shape.shapeKey)) {
|
|
661
|
-
effectiveMinH = 125;
|
|
662
|
-
} else if (graphStore.pinsTypes.includes(shape.shapeKey)) {
|
|
663
|
-
effectiveMinH = 22;
|
|
664
|
-
effectiveMinW = 22
|
|
665
|
-
} else if (graphStore.portsTypes.includes(shape.shapeKey)) {
|
|
666
|
-
effectiveMinH = 22;
|
|
667
|
-
effectiveMinW = 22
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// 判断是否为diagram组件
|
|
672
|
-
const isDiagramComponent = shape.shapeKey && props.diagram && props.diagram.includes(shape.shapeKey);
|
|
673
|
-
|
|
674
|
-
// 用通用几何函数计算“下一帧的ghost矩形”
|
|
675
|
-
// - anchor 用按下时的指针坐标 startPos
|
|
676
|
-
// - 使用最小可能的约束以允许自由缩放
|
|
677
|
-
let next = ghostResizeStep(
|
|
678
|
-
{ x: base.x, y: base.y, width: base.width, height: base.height },
|
|
679
|
-
handle,
|
|
680
|
-
{ x: startPos.value.x, y: startPos.value.y },
|
|
681
|
-
curr,
|
|
682
|
-
container,
|
|
683
|
-
// 只限制left和top,不限制right和bottom,允许自由放大
|
|
684
|
-
{ left: edges.left, top: edges.top, right: false, bottom: false },
|
|
685
|
-
effectiveMinW,
|
|
686
|
-
effectiveMinH
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
// 对于diagram组件,保持高度不变
|
|
690
|
-
if (isDiagramComponent && shape.shapeType === 'shape') {
|
|
691
|
-
next = { ...next, height: base.height };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 移除额外的宽度调整,因为ghostResizeStep已经处理了最小宽度约束
|
|
695
|
-
// 这样可以避免重复调整导致的预览框闪烁
|
|
696
|
-
// 兼容旧逻辑:如果是「画布」图元,缩放时不移动其 (x,y)
|
|
697
|
-
const isDiagram = (shape as any).shapeType?.toLowerCase?.() === "diagram";
|
|
698
|
-
if (isDiagram) {
|
|
699
|
-
next = { ...next, x: base.x, y: base.y };
|
|
700
|
-
next.x = Math.max(0, next.x);
|
|
701
|
-
next.y = Math.max(0, next.y);
|
|
702
|
-
}
|
|
703
|
-
// 判断是否是隔间组件
|
|
704
|
-
if (isCompartment(shape)) {
|
|
705
|
-
next = clampCompartmentResize(
|
|
706
|
-
graphStore.shapes, // 所有图元,用于找子
|
|
707
|
-
shape, // 正在缩放的父
|
|
708
|
-
next, // ghost 下一帧
|
|
709
|
-
resizeDirection.value as "nw" | "ne" | "sw" | "se"
|
|
710
|
-
)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
next = clampParentRectToChildrenGap(
|
|
714
|
-
graphStore.shapes,
|
|
715
|
-
shape, // 正在被缩放的父
|
|
716
|
-
next,
|
|
717
|
-
resizeDirection.value as any,
|
|
718
|
-
0, // gap
|
|
719
|
-
effectiveMinW, // 使用effectiveMinW而不是minW
|
|
720
|
-
minH,
|
|
721
|
-
groupGhost.value // 若有子也在 ghost,用它保证更准确
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
// 写到统一的 groupGhost(与拖动预览共用一条通路)
|
|
725
|
-
groupGhost.value = {
|
|
726
|
-
...groupGhost.value,
|
|
727
|
-
[shape.id]: next,
|
|
728
|
-
};
|
|
729
|
-
};
|
|
730
|
-
// 停止缩放:复位状态并清理监听
|
|
731
|
-
const stopResize = () => {
|
|
732
|
-
if (!isResizing.value || !resizingTarget.value) return;
|
|
733
|
-
const id = resizingTarget.value.id;
|
|
734
|
-
const shape = resizingTarget.value;
|
|
735
|
-
const final = groupGhost.value[id] ?? startBounds.value;
|
|
736
|
-
|
|
737
|
-
// 计算基于文本内容的最小宽度
|
|
738
|
-
let dynamicMinW = calculateTextMinWidth(shape);
|
|
739
|
-
|
|
740
|
-
// 根据shapeKey确定最小高度
|
|
741
|
-
// 如果shapeKey属于packages数组,最小高度为90
|
|
742
|
-
// 如果shapeKey属于diagram数组,最小高度为50
|
|
743
|
-
// 其他情况(block组件)最小高度为70
|
|
744
|
-
let minHeight = 70; // 默认block组件的最小高度
|
|
745
|
-
if (shape.shapeKey) {
|
|
746
|
-
if (props.packages && props.packages.includes(shape.shapeKey)) {
|
|
747
|
-
minHeight = 90;
|
|
748
|
-
} else if (props.diagram && props.diagram.includes(shape.shapeKey)) {
|
|
749
|
-
minHeight = 50;
|
|
750
|
-
} else if (graphStore.pinsTypes.includes(shape.shapeKey)) {
|
|
751
|
-
minHeight = 22;
|
|
752
|
-
dynamicMinW = 22
|
|
753
|
-
} else if (graphStore.portsTypes.includes(shape.shapeKey)) {
|
|
754
|
-
minHeight = 22;
|
|
755
|
-
dynamicMinW = 22
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// 判断是否为diagram组件
|
|
760
|
-
const isDiagramComponent = shape.shapeKey && props.diagram && props.diagram.includes(shape.shapeKey);
|
|
761
|
-
|
|
762
|
-
// 确保宽度不小于文本所需最小宽度,高度不小于对应组件的最小高度
|
|
763
|
-
let finalBounds = { ...final };
|
|
764
|
-
if (final.width < dynamicMinW) {
|
|
765
|
-
finalBounds.width = dynamicMinW;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// 对于非diagram组件,保持最小高度限制
|
|
769
|
-
if (!isDiagramComponent && final.height < minHeight) {
|
|
770
|
-
finalBounds.height = minHeight;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// 对于diagram组件,确保最终应用时保持原始高度
|
|
774
|
-
if (isDiagramComponent && shape.shapeType === 'shape') {
|
|
775
|
-
finalBounds = { ...finalBounds, height: startBounds.value.height };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// 移除任何可能的容器约束限制,允许图元无限放大
|
|
779
|
-
|
|
780
|
-
// 获取原始形状以检查是否真的有变化
|
|
781
|
-
const shapeBefore = graphStore.shapes.find((s) => s.id === id);
|
|
782
|
-
const changed = shapeBefore && !_.isEqual(shapeBefore.bounds, finalBounds);
|
|
783
|
-
|
|
784
|
-
if (changed) {
|
|
785
|
-
// 只有确实有变化时才更新
|
|
786
|
-
graphStore.updateShape(id, { bounds: finalBounds });
|
|
787
|
-
|
|
788
|
-
// 统一收尾并检查是否有重新父子关系
|
|
789
|
-
const containerForFinalize =
|
|
790
|
-
graphStore.hoverContainerId && graphStore.hoverNestable ? graphStore.hoverContainerId : null;
|
|
791
|
-
|
|
792
|
-
const didReparent = finalizeAfterTransform(
|
|
793
|
-
graphStore.shapes,
|
|
794
|
-
[id],
|
|
795
|
-
{ [id]: finalBounds },
|
|
796
|
-
[id],
|
|
797
|
-
graphStore.currentDiagramId,
|
|
798
|
-
containerForFinalize,
|
|
799
|
-
graphStore.updateShape
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
if (didReparent) {
|
|
803
|
-
normalizeZOrder(
|
|
804
|
-
graphStore.shapes,
|
|
805
|
-
graphStore.currentDiagramId,
|
|
806
|
-
graphStore.updateShape,
|
|
807
|
-
1,
|
|
808
|
-
1
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// 调用graphStore中的方法处理缩放结束后的事件发射
|
|
813
|
-
graphStore.endResizeShape(id);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// 发送缩放结束事件
|
|
817
|
-
eventBus.emit('resize-end', { target: resizingTarget.value });
|
|
818
|
-
|
|
819
|
-
// 清理状态
|
|
820
|
-
isResizing.value = false;
|
|
821
|
-
resizeDirection.value = "";
|
|
822
|
-
resizingTarget.value = null;
|
|
823
|
-
groupBase.value = {};
|
|
824
|
-
groupGhost.value = {};
|
|
825
|
-
offDrag?.();
|
|
826
|
-
offDrag = null;
|
|
827
|
-
};
|
|
828
489
|
|
|
829
490
|
// 处理线条点击事件
|
|
830
491
|
const handleEdgeClick = (shape: Shape, event: MouseEvent) => {
|
|
@@ -836,131 +497,133 @@ const handleEdgeClick = (shape: Shape, event: MouseEvent) => {
|
|
|
836
497
|
// 根层按下:命中测试 + 选中 + 切换光标
|
|
837
498
|
const DRAG_THRESHOLD = 4;
|
|
838
499
|
const onLayerMouseDown = (evt: MouseEvent) => {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
offDrag
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
500
|
+
return guardOperate(async () => {
|
|
501
|
+
// 若点击的是名称虚线框/容器,避免触发清选或框选(Pin 的名称可能在外部)
|
|
502
|
+
const t = evt.target as HTMLElement | null
|
|
503
|
+
if (t && (t.classList?.contains('name-text-box') || t.closest('.name-text-box-container'))) {
|
|
504
|
+
// 不改变当前选中;让后续 click 事件去触发 startEditName
|
|
505
|
+
evt.stopPropagation()
|
|
506
|
+
evt.preventDefault()
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
if (isResizing.value || isEditingName.value) return;
|
|
510
|
+
if (graphStore.isDragging) graphStore.endDragShape();
|
|
511
|
+
if (offDrag) {
|
|
512
|
+
offDrag();
|
|
513
|
+
offDrag = null;
|
|
514
|
+
}
|
|
515
|
+
const pt = toLocalPoint(evt, layerRef.value);
|
|
516
|
+
// 添加详细的点击位置日志
|
|
517
|
+
// console.log('鼠标点击位置:', pt);
|
|
518
|
+
|
|
519
|
+
// 执行命中测试
|
|
520
|
+
const hit = pickTarget(graphStore.shapes, pt);
|
|
521
|
+
// console.log('命中测试结果:', hit.kind, hit.shape?.id, hit.shape?.shapeType);
|
|
522
|
+
// 如果右键菜单已显示且点击了其他图元,关闭菜单
|
|
523
|
+
if (
|
|
524
|
+
showContextMenu.value &&
|
|
525
|
+
(hit.kind === "shape" || hit.kind === "pin") &&
|
|
526
|
+
(!contextMenuTarget.value || hit.shape.id !== contextMenuTarget.value.id)
|
|
527
|
+
) {
|
|
528
|
+
showContextMenu.value = false;
|
|
529
|
+
}
|
|
868
530
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
531
|
+
cursorStyle.value = (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") ? "pointer" : "default";
|
|
532
|
+
// 进入“框选”的条件:
|
|
533
|
+
const wantMarquee = evt.shiftKey || hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin";
|
|
534
|
+
if (wantMarquee) {
|
|
535
|
+
if ((hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin") && !evt.shiftKey) {
|
|
536
|
+
graphStore.clearSelection();
|
|
537
|
+
}
|
|
538
|
+
startMarquee(pt);
|
|
539
|
+
evt.preventDefault();
|
|
540
|
+
return;
|
|
875
541
|
}
|
|
876
|
-
|
|
877
|
-
evt.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
542
|
+
// 双击触发不同逻辑
|
|
543
|
+
if (evt.detail === 2 && graphStore.marqueeShapes.length == 1) {
|
|
544
|
+
const selectedShape = graphStore.selectedShape;
|
|
545
|
+
console.log('双击选中的图元:', selectedShape);
|
|
546
|
+
|
|
547
|
+
// 判断是否为Diagram组件
|
|
548
|
+
if (selectedShape && selectedShape.shapeType !== 'edge' && props.diagram?.includes(selectedShape.shapeKey)) {
|
|
549
|
+
// Diagram组件的特殊双击逻辑
|
|
550
|
+
// 这里可以添加你想要的其他逻辑,例如发射自定义事件
|
|
551
|
+
emit('diagramDoubleClick', selectedShape);
|
|
552
|
+
// console.log(selectedShape,'Diagram组件双击事件');
|
|
553
|
+
// 不打开属性面板
|
|
554
|
+
} else {
|
|
555
|
+
// 所有其他组件(包括连线)都打开属性面板
|
|
556
|
+
onLayerDblClick(true);
|
|
557
|
+
}
|
|
558
|
+
evt.preventDefault();
|
|
559
|
+
evt.stopPropagation();
|
|
560
|
+
return;
|
|
895
561
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
if (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") {
|
|
901
|
-
const { shape } = hit;
|
|
562
|
+
if (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") {
|
|
563
|
+
const { shape } = hit;
|
|
902
564
|
|
|
903
|
-
|
|
904
|
-
|
|
565
|
+
// 打印选中元素的数据信息 - 确保每次点击都能看到
|
|
566
|
+
console.log('点击选中的' + (hit.kind === 'edge' ? '线条' : hit.kind === 'pin' ? 'Pin' : '图元') + '数据信息:', shape);
|
|
905
567
|
|
|
906
|
-
|
|
907
|
-
|
|
568
|
+
const isMulti = graphStore.selectedIds.length > 1;
|
|
569
|
+
const clickedInSelection = graphStore.selectedIds.includes(shape.id);
|
|
908
570
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
571
|
+
if (isMulti && clickedInSelection) {
|
|
572
|
+
// 多选状态下点击已选中元素,保持现状
|
|
573
|
+
} else {
|
|
574
|
+
// 其他情况:单选当前元素
|
|
575
|
+
graphStore.clearSelection();
|
|
576
|
+
graphStore.selectShape(shape);
|
|
577
|
+
}
|
|
578
|
+
// 选区此时已是“正确”的:要么多选集合,要么当前单选
|
|
579
|
+
const ids = graphStore.selectedIds.length
|
|
580
|
+
? graphStore.selectedIds.slice()
|
|
581
|
+
: [hit.shape.id];
|
|
582
|
+
// 准备“潜在拖拽”,但先不触发 store.startDrag
|
|
583
|
+
let started = false;
|
|
584
|
+
// 绑定拖拽生命周期,把 move/up 转发给 store
|
|
585
|
+
offDrag = withDrag(
|
|
586
|
+
(e) => {
|
|
587
|
+
const curr = toLocalPoint(e, layerRef.value);
|
|
588
|
+
const dx = curr.x - pt.x;
|
|
589
|
+
const dy = curr.y - pt.y;
|
|
590
|
+
const dist2 = dx * dx + dy * dy;
|
|
591
|
+
|
|
592
|
+
if (!started && dist2 >= DRAG_THRESHOLD * DRAG_THRESHOLD) {
|
|
593
|
+
// 真正超过阈值,再启动 store 的拖拽生命周期
|
|
594
|
+
started = true;
|
|
595
|
+
graphStore.startDrag(ids, pt);
|
|
596
|
+
}
|
|
597
|
+
if (started) {
|
|
598
|
+
// 如果是 pin 类型,需要在移动过程中将“指针位置”校正为吸附后的指针坐标
|
|
599
|
+
let targetPt = curr;
|
|
600
|
+
if (ids.length === 1) {
|
|
601
|
+
const draggedShape = graphStore.shapes.find(x => x.id === ids[0]);
|
|
602
|
+
if (draggedShape && draggedShape.shapeType === 'pin' && draggedShape.parenShapeId) {
|
|
603
|
+
const parentShape = graphStore.shapes.find(x => x.id === draggedShape.parenShapeId);
|
|
604
|
+
if (parentShape) {
|
|
605
|
+
// 使用移动专用的吸附方法:根据 dragOffset 计算应传入 moveDraggedShape 的指针坐标
|
|
606
|
+
targetPt = snapPinPointerOnMove(curr, parentShape, draggedShape, graphStore.dragOffset || undefined);
|
|
607
|
+
}
|
|
945
608
|
}
|
|
946
609
|
}
|
|
610
|
+
graphStore.moveDraggedShape(targetPt);
|
|
947
611
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
612
|
+
},
|
|
613
|
+
() => {
|
|
614
|
+
// 只有在“真的开始拖拽”后,才结束拖拽
|
|
615
|
+
if (started) {
|
|
616
|
+
graphStore.endDragShape();
|
|
617
|
+
} else {
|
|
618
|
+
// 纯点击:啥也不做(已完成选中),避免误触发 reparent/zIndex
|
|
619
|
+
cursorStyle.value = "default";
|
|
620
|
+
emit("property-panel", false);
|
|
621
|
+
}
|
|
622
|
+
offDrag = null;
|
|
959
623
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
})
|
|
964
627
|
};
|
|
965
628
|
// 框选部分
|
|
966
629
|
const startMarquee = (anchor: { x: number; y: number }) => {
|
|
@@ -1038,41 +701,30 @@ const highlightShape = (shape: Shape | null, isHighlight: boolean, isValidSource
|
|
|
1038
701
|
|
|
1039
702
|
// 处理右键点击事件
|
|
1040
703
|
const handleContextMenu = (event: MouseEvent) => {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
event.preventDefault();
|
|
1047
|
-
|
|
1048
|
-
// 获取点击位置
|
|
1049
|
-
const point = toLocalPoint(event, layerRef.value);
|
|
1050
|
-
|
|
1051
|
-
// 使用命中测试找到点击的图元
|
|
1052
|
-
const hit = pickTarget(graphStore.shapes, point);
|
|
704
|
+
return guardOperate(async () => {
|
|
705
|
+
// 如果图元正在移动或缩放,则不显示菜单
|
|
706
|
+
if (graphStore.isDragging || isResizing.value) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
1053
709
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
} else {
|
|
1064
|
-
// 如果没有点击图元,隐藏菜单
|
|
1065
|
-
showContextMenu.value = false;
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
710
|
+
const hitShape = ContextMenuUtils.handleContextMenuClick(
|
|
711
|
+
event,
|
|
712
|
+
layerRef,
|
|
713
|
+
pickTarget,
|
|
714
|
+
graphStore.shapes,
|
|
715
|
+
graphStore.selectShape,
|
|
716
|
+
graphStore.isDragging,
|
|
717
|
+
isResizing.value
|
|
718
|
+
);
|
|
1068
719
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
720
|
+
if (hitShape) {
|
|
721
|
+
showContextMenu.value = true;
|
|
722
|
+
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
|
723
|
+
contextMenuTarget.value = hitShape;
|
|
724
|
+
} else {
|
|
725
|
+
showContextMenu.value = false;
|
|
726
|
+
}
|
|
727
|
+
})
|
|
1076
728
|
};
|
|
1077
729
|
|
|
1078
730
|
// 关闭右键菜单
|
|
@@ -1111,9 +763,9 @@ watch(() => props.actionButtonShapeDataId, (newVal) => {
|
|
|
1111
763
|
|
|
1112
764
|
if (connectionData) {
|
|
1113
765
|
emit("connectEnd", connectionData);
|
|
766
|
+
graphStore.setConnectMode('connect')
|
|
1114
767
|
isConnecting.value = false;
|
|
1115
768
|
highlightUtils.clearHighlightTimeout();
|
|
1116
|
-
actionButtonMode.value = false;
|
|
1117
769
|
if (highlightTimeout.value) {
|
|
1118
770
|
clearTimeout(highlightTimeout.value);
|
|
1119
771
|
highlightTimeout.value = null;
|
|
@@ -1124,47 +776,11 @@ watch(() => props.actionButtonShapeDataId, (newVal) => {
|
|
|
1124
776
|
// 处理 actionButtonShapeData 的逻辑
|
|
1125
777
|
}
|
|
1126
778
|
});
|
|
1127
|
-
// 监听delete键移除图元
|
|
1128
|
-
const onKeyDownDelete = (e: KeyboardEvent) => {
|
|
1129
|
-
const target = e.target as HTMLElement | null
|
|
1130
|
-
if (target) {
|
|
1131
|
-
const tag = target.tagName
|
|
1132
|
-
// 如果在原生输入类控件里(input / textarea)
|
|
1133
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1134
|
-
return // 让浏览器自己处理 Delete,删文字,不动图元
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
// 仅处理 Delete;
|
|
1138
|
-
if (e.key !== "Delete") return;
|
|
1139
|
-
e.preventDefault(); // 避免浏览器默认行为
|
|
1140
|
-
//选中的图元中如果有画布(diagram),则不允许删除
|
|
1141
|
-
const selectedShapes = graphStore.marqueeShapes ?? []
|
|
1142
|
-
// 判断是否包含画布
|
|
1143
|
-
const hasDiagram = selectedShapes.some(s =>
|
|
1144
|
-
s.shapeType?.toLowerCase?.() === 'diagram'
|
|
1145
|
-
)
|
|
1146
|
-
if (hasDiagram) {
|
|
1147
|
-
// 选中了画布,直接不处理删除
|
|
1148
|
-
return
|
|
1149
|
-
}
|
|
1150
|
-
const items = selectedShapes
|
|
1151
|
-
.map(s => ({
|
|
1152
|
-
modelId: s.modelId,
|
|
1153
|
-
shapeId: s.id, // 图元 id
|
|
1154
|
-
shapeType: s.shapeType, // 图元类型
|
|
1155
|
-
isRemoveModelTree: false
|
|
1156
|
-
}))
|
|
1157
|
-
|
|
1158
|
-
if (!items.length) return;
|
|
1159
|
-
eventBus.emit('shapes-remove', items);
|
|
1160
|
-
};
|
|
1161
779
|
|
|
1162
|
-
// 鼠标移动事件处理
|
|
1163
780
|
// 鼠标移动事件处理
|
|
1164
781
|
const handleMouseMove = (event: MouseEvent) => {
|
|
1165
782
|
const { clientX, clientY } = event;
|
|
1166
783
|
const localPoint = toLocalPoint(event, layerRef.value);
|
|
1167
|
-
|
|
1168
784
|
// 使用工具类检查是否在画布内
|
|
1169
785
|
if (EdgeUtils.isWithinCanvas(clientX, clientY)) {
|
|
1170
786
|
mousePosition.value = { x: localPoint.x, y: localPoint.y };
|
|
@@ -1202,7 +818,7 @@ const checkHoverTarget = (x: number, y: number) => {
|
|
|
1202
818
|
if (hoverShape) {
|
|
1203
819
|
// 检查连接有效性
|
|
1204
820
|
let targetModels = props.connectShapeData?.targetModels;
|
|
1205
|
-
if (
|
|
821
|
+
if (connectMode.value === 'action') {
|
|
1206
822
|
targetModels = props.connectShapeData?.scenarioMenus?.find(
|
|
1207
823
|
(menu) => menu.code === props.connectShapeData?.shapeKey
|
|
1208
824
|
)?.targetModels;
|
|
@@ -1212,6 +828,13 @@ const checkHoverTarget = (x: number, y: number) => {
|
|
|
1212
828
|
const hoverShapeType = hoverShape.shapeKey || '';
|
|
1213
829
|
isAllowed = targetModels.includes(hoverShapeType);
|
|
1214
830
|
}
|
|
831
|
+
// 检查parenShapeId是否匹配(无论hoverShape是否改变都要检查)
|
|
832
|
+
if (props.connectShapeData?.sourceId) {
|
|
833
|
+
const sourceShape = graphStore.shapes.find(it => it.id === props.connectShapeData?.sourceId)
|
|
834
|
+
if (sourceShape && sourceShape.parenShapeId !== hoverShape.parenShapeId && (hoverShape.shapeType !== 'pin' && sourceShape.shapeType !== 'pin')) {
|
|
835
|
+
isAllowed = false
|
|
836
|
+
}
|
|
837
|
+
}
|
|
1215
838
|
// 只有当hoverShape改变且connectShapeData存在时才发射事件
|
|
1216
839
|
if (lastHoverShapeId !== hoverShape.id && props.connectShapeData) {
|
|
1217
840
|
lastHoverShapeId = hoverShape.id;
|
|
@@ -1225,7 +848,6 @@ const checkHoverTarget = (x: number, y: number) => {
|
|
|
1225
848
|
else if (props.connectShapeData.sourceModelId && props.connectShapeData.sourceModelId.toString().trim() !== '') {
|
|
1226
849
|
sourceModelId = props.connectShapeData.sourceModelId;
|
|
1227
850
|
}
|
|
1228
|
-
|
|
1229
851
|
// 只有当sourceModelId有值时就发射事件,并将isAllowed的值一并传递
|
|
1230
852
|
if (sourceModelId) {
|
|
1231
853
|
eventBus.emit('edge-check', {
|
|
@@ -1235,10 +857,12 @@ const checkHoverTarget = (x: number, y: number) => {
|
|
|
1235
857
|
});
|
|
1236
858
|
}
|
|
1237
859
|
}
|
|
1238
|
-
// 高亮目标图元 - 根据edgeCheck
|
|
860
|
+
// 高亮目标图元 - 根据isAllowed决定高亮颜色(实时计算,不依赖异步更新的props.edgeCheck)
|
|
1239
861
|
// true为蓝色,false为红色
|
|
862
|
+
// 直接使用isAllowed(前端实时验证结果),确保快速移动时颜色能立即更新
|
|
863
|
+
// 如果后端验证结果不同,会在props.edgeCheck更新后通过watch或其他机制再次更新
|
|
1240
864
|
highlightUtils.setHighlightTimeout(() => {
|
|
1241
|
-
highlightShape(hoverShape, true,
|
|
865
|
+
highlightShape(hoverShape, true, isAllowed);
|
|
1242
866
|
isConnectAllowed.value = isAllowed;
|
|
1243
867
|
}, 10);
|
|
1244
868
|
} else {
|
|
@@ -1290,6 +914,18 @@ watch(
|
|
|
1290
914
|
}
|
|
1291
915
|
);
|
|
1292
916
|
|
|
917
|
+
// 监听edgeCheck变化,当后端验证结果返回后更新高亮颜色
|
|
918
|
+
watch(
|
|
919
|
+
() => props.edgeCheck,
|
|
920
|
+
(newEdgeCheck) => {
|
|
921
|
+
// 只有在连接状态下且有高亮图元时才更新
|
|
922
|
+
if (isConnecting.value && highlightedShape.value && newEdgeCheck !== undefined) {
|
|
923
|
+
// 使用后端验证结果更新高亮颜色
|
|
924
|
+
highlightShape(highlightedShape.value, true, newEdgeCheck);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
|
|
1293
929
|
// 处理连接层点击事件
|
|
1294
930
|
const handleConnectLayerClick = (event: MouseEvent) => {
|
|
1295
931
|
const localPoint = toLocalPoint(event, layerRef.value);
|
|
@@ -1297,10 +933,10 @@ const handleConnectLayerClick = (event: MouseEvent) => {
|
|
|
1297
933
|
const hit = pickTarget(graphStore.shapes, localPoint);
|
|
1298
934
|
|
|
1299
935
|
if (
|
|
1300
|
-
hit.kind
|
|
1301
|
-
hit.shape
|
|
936
|
+
['shape', 'pin'].includes(hit.kind) &&
|
|
937
|
+
hit.shape?.id &&
|
|
1302
938
|
props.connectShapeData?.sourceId &&
|
|
1303
|
-
hit.shape
|
|
939
|
+
hit.shape?.id !== props.connectShapeData.sourceId
|
|
1304
940
|
) {
|
|
1305
941
|
event.stopPropagation();
|
|
1306
942
|
completeConnection(hit.shape, localPoint);
|
|
@@ -1316,6 +952,23 @@ const completeConnection = (
|
|
|
1316
952
|
(shape: { id: string }) => shape.id === props.connectShapeData?.sourceId
|
|
1317
953
|
);
|
|
1318
954
|
|
|
955
|
+
// 如果高亮是红色的情况直接返回
|
|
956
|
+
if (!isConnectAllowed.value) {
|
|
957
|
+
isConnecting.value = false;
|
|
958
|
+
highlightShape(null, false); // 取消图元高亮
|
|
959
|
+
graphStore.setConnectMode('connect')
|
|
960
|
+
highlightUtils.clearHighlightTimeout();
|
|
961
|
+
ElMessage.error('当前目标图元类型不符合连接要求');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// 嵌套情况下只能连接同一个父图元
|
|
966
|
+
if (sourceShape && clickedShape && sourceShape.parenShapeId !== clickedShape.parenShapeId && (clickedShape.shapeType !== 'pin' && sourceShape.shapeType !== 'pin')) {
|
|
967
|
+
isConnecting.value = false;
|
|
968
|
+
highlightShape(null, false); // 取消图元高亮
|
|
969
|
+
highlightUtils.clearHighlightTimeout();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
1319
972
|
// 检查目标图元类型是否符合targetModels要求
|
|
1320
973
|
const targetModels = props.connectShapeData?.targetModels;
|
|
1321
974
|
if (targetModels && Array.isArray(targetModels) && targetModels.length > 0) {
|
|
@@ -1345,11 +998,11 @@ const completeConnection = (
|
|
|
1345
998
|
// alert('同类型的边已经存在,不能重复添加');
|
|
1346
999
|
ElMessage.error('同类型的边已经存在,不能重复添加');
|
|
1347
1000
|
isConnecting.value = false;
|
|
1001
|
+
graphStore.setConnectMode('connect')
|
|
1348
1002
|
highlightShape(null, false); // 取消图元高亮
|
|
1349
1003
|
highlightUtils.clearHighlightTimeout();
|
|
1350
1004
|
return;
|
|
1351
1005
|
}
|
|
1352
|
-
|
|
1353
1006
|
// 使用工具类完成连接,传递当前的shapes列表以支持差异化路由
|
|
1354
1007
|
const connectionData = EdgeUtils.completeConnection(
|
|
1355
1008
|
sourceShape,
|
|
@@ -1358,14 +1011,27 @@ const completeConnection = (
|
|
|
1358
1011
|
currentConnectPoint.value,
|
|
1359
1012
|
graphStore.shapes
|
|
1360
1013
|
);
|
|
1361
|
-
|
|
1362
1014
|
if (connectionData) {
|
|
1363
|
-
|
|
1015
|
+
// ServiceObjectFlow 特殊处理:需要在 sourcePoint 和 targetPoint 创建 pin
|
|
1016
|
+
if (props.connectShapeData?.shapeKey === 'ServiceObjectFlow' && sourceShape && connectionData.sourcePoint && connectionData.targetPoint) {
|
|
1017
|
+
const result = EdgeUtils.handleServiceObjectFlowConnection(
|
|
1018
|
+
sourceShape,
|
|
1019
|
+
clickedShape,
|
|
1020
|
+
connectionData
|
|
1021
|
+
);
|
|
1022
|
+
emit('objectFlowConnectEnd', {
|
|
1023
|
+
connectionData: result.connectionData,
|
|
1024
|
+
outputPinBounds: result.outputPinBounds,
|
|
1025
|
+
inputPinBounds: result.inputPinBounds
|
|
1026
|
+
});
|
|
1027
|
+
} else {
|
|
1028
|
+
(connectionData as any).sourceShape = sourceShape
|
|
1029
|
+
emit('connectEnd', connectionData);
|
|
1030
|
+
}
|
|
1031
|
+
graphStore.setConnectMode('connect')
|
|
1364
1032
|
isConnecting.value = false;
|
|
1365
1033
|
highlightShape(null, false); // 取消图元高亮
|
|
1366
1034
|
highlightUtils.clearHighlightTimeout();
|
|
1367
|
-
actionButtonMode.value = false;
|
|
1368
|
-
|
|
1369
1035
|
}
|
|
1370
1036
|
};
|
|
1371
1037
|
// 鼠标离开事件处理
|
|
@@ -1487,55 +1153,21 @@ const linePoints = computed(() => {
|
|
|
1487
1153
|
});
|
|
1488
1154
|
|
|
1489
1155
|
// 连接层样式 - 定位到 diagramBounds 位置,确保在最顶层
|
|
1490
|
-
const layerStyle = computed(() =>
|
|
1491
|
-
if (!props.diagramBounds) {
|
|
1492
|
-
return { display: "none" };
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
return {
|
|
1496
|
-
position: "absolute" as const,
|
|
1497
|
-
left: `${props.diagramBounds.x}px`,
|
|
1498
|
-
top: `${props.diagramBounds.y}px`,
|
|
1499
|
-
width: `${props.diagramBounds.width}px`,
|
|
1500
|
-
height: `${props.diagramBounds.height}px`,
|
|
1501
|
-
pointerEvents: "none" as const,
|
|
1502
|
-
// zIndex: 10000, // 确保在最顶层
|
|
1503
|
-
};
|
|
1504
|
-
});
|
|
1505
|
-
|
|
1506
|
-
// 处理键盘按键事件
|
|
1507
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
1508
|
-
// 按下Esc且正在连线时,取消连线(增加 event.preventDefault 避免浏览器默认行为)
|
|
1509
|
-
if (e.key === 'Escape' && isConnecting.value) {
|
|
1510
|
-
e.preventDefault(); // 阻止浏览器默认的Esc行为(如关闭弹窗)
|
|
1511
|
-
cancelConnection();
|
|
1512
|
-
}
|
|
1513
|
-
};
|
|
1514
|
-
|
|
1515
|
-
// 组件卸载时清理高亮工具
|
|
1516
|
-
onUnmounted(() => {
|
|
1517
|
-
// 清理高亮工具实例
|
|
1518
|
-
highlightUtils.dispose();
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1156
|
+
const layerStyle = computed(() => getLayerStyle(props.diagramBounds));
|
|
1521
1157
|
|
|
1522
1158
|
// 取消连线的方法(完全重置所有相关状态)
|
|
1523
1159
|
const cancelConnection = () => {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
highlightUtils.clearHighlightTimeout();
|
|
1536
|
-
|
|
1537
|
-
// 4. 最后隐藏线条(确保状态重置后再隐藏,避免延迟渲染)
|
|
1538
|
-
showLine.value = false;
|
|
1160
|
+
EdgeUtils.cancelConnection(
|
|
1161
|
+
{
|
|
1162
|
+
isConnecting,
|
|
1163
|
+
currentConnectPoint,
|
|
1164
|
+
mousePosition,
|
|
1165
|
+
targetConnectPoint,
|
|
1166
|
+
targetShape,
|
|
1167
|
+
showLine
|
|
1168
|
+
},
|
|
1169
|
+
highlightUtils
|
|
1170
|
+
);
|
|
1539
1171
|
};
|
|
1540
1172
|
|
|
1541
1173
|
// 监听 connectShapeData 变化,重新初始化连接点
|
|
@@ -1546,115 +1178,92 @@ watch(
|
|
|
1546
1178
|
},
|
|
1547
1179
|
{ deep: true }
|
|
1548
1180
|
);
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
return {
|
|
1573
|
-
clientX: rect.left + x * scale,
|
|
1574
|
-
clientY: rect.top + y * scale,
|
|
1181
|
+
|
|
1182
|
+
const externalCreateDragState: ExternalCreateDragState = {
|
|
1183
|
+
creatingId: null,
|
|
1184
|
+
pendingShape: null,
|
|
1185
|
+
isDragging: false,
|
|
1186
|
+
isCheckInFlight: false
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const resetExternalCreateDragState = () => {
|
|
1190
|
+
externalCreateDragState.creatingId = null;
|
|
1191
|
+
externalCreateDragState.pendingShape = null;
|
|
1192
|
+
externalCreateDragState.isDragging = false;
|
|
1193
|
+
externalCreateDragState.isCheckInFlight = false;
|
|
1194
|
+
isExternalCreateDragging.value = false;
|
|
1195
|
+
graphStore.canDropOnCanvas = false;
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const cleanupInertShapes = async () => {
|
|
1199
|
+
const arr = graphStore.shapes as any[];
|
|
1200
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
1201
|
+
if ('inert' in arr[i]) {
|
|
1202
|
+
arr.splice(i, 1);
|
|
1203
|
+
}
|
|
1575
1204
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const el = layerRef.value
|
|
1580
|
-
if (!el) return false
|
|
1581
|
-
const r = el.getBoundingClientRect()
|
|
1582
|
-
return (
|
|
1583
|
-
p.clientX >= r.left &&
|
|
1584
|
-
p.clientX <= r.right &&
|
|
1585
|
-
p.clientY >= r.top &&
|
|
1586
|
-
p.clientY <= r.bottom
|
|
1587
|
-
)
|
|
1588
|
-
}
|
|
1205
|
+
await nextTick();
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1589
1208
|
//拖动中添加元素并触发嵌套逻辑,
|
|
1590
1209
|
const continueExternalCreateDrag = async (payload: { clientX: number; clientY: number; shapeData?: any }) => {
|
|
1210
|
+
externalCreateDragState.isDragging = true
|
|
1591
1211
|
isExternalCreateDragging.value = true
|
|
1592
|
-
const pt = clientToLocalPoint(payload)
|
|
1212
|
+
const pt = clientToLocalPoint(payload.clientX, payload.clientY, layerRef.value)
|
|
1593
1213
|
if (payload.shapeData) {
|
|
1594
1214
|
const s = payload.shapeData
|
|
1595
1215
|
const isCmp = isCompartment(s as Shape)
|
|
1596
|
-
|
|
1216
|
+
externalCreateDragState.pendingShape = {
|
|
1597
1217
|
...s,
|
|
1598
1218
|
bounds: {
|
|
1599
|
-
// 坐标始终用当前指针位置
|
|
1600
1219
|
x: pt.x,
|
|
1601
1220
|
y: pt.y,
|
|
1602
|
-
// 宽度优先用传入的,否则给个默认
|
|
1603
1221
|
width: s.bounds?.width ?? 180,
|
|
1604
|
-
// 隔间组件固定高度 120;否则用原值或默认 80
|
|
1605
1222
|
height: isCmp ? 120 : (s.bounds?.height ?? 80),
|
|
1606
1223
|
},
|
|
1607
1224
|
inert: false,
|
|
1608
1225
|
}
|
|
1609
1226
|
}
|
|
1610
|
-
if (!
|
|
1611
|
-
|
|
1612
|
-
const draft = externalPendingShape
|
|
1227
|
+
if (!externalCreateDragState.creatingId && externalCreateDragState.pendingShape && isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)) {
|
|
1228
|
+
const draft = externalCreateDragState.pendingShape
|
|
1613
1229
|
try {
|
|
1614
1230
|
graphStore.addShape(draft)
|
|
1615
1231
|
graphStore.startDrag([draft.id], pt)
|
|
1616
|
-
|
|
1617
|
-
|
|
1232
|
+
externalCreateDragState.creatingId = draft.id
|
|
1233
|
+
externalCreateDragState.pendingShape = null
|
|
1618
1234
|
} finally {
|
|
1619
|
-
|
|
1235
|
+
externalCreateDragState.isCheckInFlight = false
|
|
1620
1236
|
}
|
|
1621
|
-
externalPendingShape = null // 草稿已用完,清空
|
|
1622
1237
|
}
|
|
1623
|
-
|
|
1238
|
+
externalCreateDragState.pendingShape = null
|
|
1624
1239
|
|
|
1625
|
-
if (!
|
|
1240
|
+
if (!externalCreateDragState.creatingId) return
|
|
1626
1241
|
|
|
1627
|
-
// 如果是pin类型,需要计算吸附位置
|
|
1628
1242
|
let targetPt = pt
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1633
|
-
if (
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(pt, parent, s)
|
|
1637
|
-
targetPt = { x: adjustedX, y: adjustedY }
|
|
1638
|
-
}
|
|
1243
|
+
const s = graphStore.shapes.find(x => x.id === externalCreateDragState.creatingId)
|
|
1244
|
+
if (s && s.shapeType === 'pin') {
|
|
1245
|
+
if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
|
|
1246
|
+
const parent = graphStore.shapes.find(x => x.id === graphStore.hoverContainerId)
|
|
1247
|
+
if (parent) {
|
|
1248
|
+
const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(pt, parent, s)
|
|
1249
|
+
targetPt = { x: adjustedX, y: adjustedY }
|
|
1639
1250
|
}
|
|
1640
1251
|
}
|
|
1641
1252
|
}
|
|
1642
1253
|
|
|
1643
|
-
graphStore.moveDraggedShape(targetPt)
|
|
1254
|
+
graphStore.moveDraggedShape(targetPt)
|
|
1644
1255
|
}
|
|
1645
1256
|
|
|
1646
1257
|
//拖拽结束后重新发送事件到front中调用接口
|
|
1647
1258
|
const finishExternalCreateDrag = async (payload: { clientX: number; clientY: number }) => {
|
|
1648
1259
|
try {
|
|
1649
|
-
if (!
|
|
1650
|
-
const pt = clientToLocalPoint(payload)
|
|
1651
|
-
if (isInsideCanvasClient(payload)) {
|
|
1652
|
-
graphStore.moveDraggedShape(pt)
|
|
1260
|
+
if (!externalCreateDragState.creatingId) return
|
|
1261
|
+
const pt = clientToLocalPoint(payload.clientX, payload.clientY, layerRef.value)
|
|
1262
|
+
if (isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)) {
|
|
1263
|
+
graphStore.moveDraggedShape(pt)
|
|
1653
1264
|
await nextTick()
|
|
1654
|
-
|
|
1655
|
-
const s: any = (graphStore.shapes || []).find((x: any) => x.id == externalCreatingId)
|
|
1265
|
+
const s: any = (graphStore.shapes || []).find((x: any) => x.id == externalCreateDragState.creatingId)
|
|
1656
1266
|
if (s && s.shapeType == 'shape' || s.shapeType == 'pin') {
|
|
1657
|
-
// s.inert = true // 直接改对象属性(Vue3 响应式)
|
|
1658
1267
|
const pure = _.omit(s, ['inert'])
|
|
1659
1268
|
pure.bounds = { // 覆盖为新的 bounds
|
|
1660
1269
|
...(pure.bounds),
|
|
@@ -1680,23 +1289,16 @@ const finishExternalCreateDrag = async (payload: { clientX: number; clientY: num
|
|
|
1680
1289
|
try {
|
|
1681
1290
|
const { ok } = await checkNestViaFront(pure as Shape, parent, graphStore.shapes[0])
|
|
1682
1291
|
if (!ok) {
|
|
1683
|
-
graphStore.setHoverState(null, false)
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
externalCreatingId = null // 清理状态
|
|
1687
|
-
isExternalCreateDragging.value = false
|
|
1688
|
-
graphStore.canDropOnCanvas = false
|
|
1292
|
+
graphStore.setHoverState(null, false)
|
|
1293
|
+
await cleanupInertShapes()
|
|
1294
|
+
resetExternalCreateDragState()
|
|
1689
1295
|
return
|
|
1690
1296
|
} else {
|
|
1691
1297
|
graphStore.canDropOnCanvas = true
|
|
1692
1298
|
}
|
|
1693
1299
|
} catch (error) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
// await graphStore.removeShape(externalCreatingId as string)
|
|
1697
|
-
// graphStore.clearSelection()
|
|
1698
|
-
externalCreatingId = null // 清理状态
|
|
1699
|
-
isExternalCreateDragging.value = false
|
|
1300
|
+
await cleanupInertShapes()
|
|
1301
|
+
resetExternalCreateDragState()
|
|
1700
1302
|
}
|
|
1701
1303
|
// 同步对外通知
|
|
1702
1304
|
// 先构造基础数据
|
|
@@ -1712,7 +1314,7 @@ const finishExternalCreateDrag = async (payload: { clientX: number; clientY: num
|
|
|
1712
1314
|
}
|
|
1713
1315
|
// 如果是 pin 类型,更新 coordinate 为吸附后的 client 坐标
|
|
1714
1316
|
if (pure.shapeType === 'pin' && parent) {
|
|
1715
|
-
const adjustedClientPt = localToClientPoint(
|
|
1317
|
+
const adjustedClientPt = localToClientPoint(pure.bounds.x, pure.bounds.y, layerRef.value)
|
|
1716
1318
|
payloadData.coordinate.clientX = adjustedClientPt.clientX
|
|
1717
1319
|
payloadData.coordinate.clientY = adjustedClientPt.clientY
|
|
1718
1320
|
}
|
|
@@ -1738,17 +1340,12 @@ const finishExternalCreateDrag = async (payload: { clientX: number; clientY: num
|
|
|
1738
1340
|
}
|
|
1739
1341
|
} catch (error) {
|
|
1740
1342
|
graphStore.setHoverState(null, false)
|
|
1741
|
-
|
|
1742
|
-
await sweepShapesWithInert()
|
|
1343
|
+
await cleanupInertShapes()
|
|
1743
1344
|
} finally {
|
|
1744
1345
|
await nextTick()
|
|
1745
|
-
|
|
1746
|
-
await sweepShapesWithInert()
|
|
1346
|
+
await cleanupInertShapes()
|
|
1747
1347
|
graphStore.setHoverState(null, false)
|
|
1748
|
-
|
|
1749
|
-
// graphStore.clearSelection()
|
|
1750
|
-
externalCreatingId = null // 清理状态
|
|
1751
|
-
isExternalCreateDragging.value = false
|
|
1348
|
+
resetExternalCreateDragState()
|
|
1752
1349
|
}
|
|
1753
1350
|
}
|
|
1754
1351
|
// 根据 graphStore.canDropOnCanvas 决定是否显示禁用放置图标
|
|
@@ -1768,21 +1365,16 @@ const onCanvasDrop = (e: DragEvent) => {
|
|
|
1768
1365
|
return
|
|
1769
1366
|
}
|
|
1770
1367
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1368
|
+
// 创建键盘事件处理器
|
|
1369
|
+
const keyboardHandler = createKeyboardHandler({
|
|
1370
|
+
onDelete: () => { },
|
|
1371
|
+
onEditProperty: () => onLayerDblClick(true),
|
|
1372
|
+
onCancelConnection: cancelConnection,
|
|
1373
|
+
isEditingName: () => isEditingName.value
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1782
1376
|
onMounted(() => {
|
|
1783
|
-
|
|
1784
|
-
document.addEventListener("keydown", onKeyDownDelete, { capture: true });
|
|
1785
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
1377
|
+
window.addEventListener('keydown', keyboardHandler);
|
|
1786
1378
|
// 添加连接层相关的事件监听
|
|
1787
1379
|
if (props.connectShapeData) {
|
|
1788
1380
|
document.addEventListener("mousemove", handleMouseMove);
|
|
@@ -1791,20 +1383,22 @@ onMounted(() => {
|
|
|
1791
1383
|
}
|
|
1792
1384
|
|
|
1793
1385
|
//拖动结束后更新情景菜单
|
|
1794
|
-
eventBus.on('shape-drag-end-updateScenarioMenu',
|
|
1386
|
+
eventBus.on('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
|
|
1795
1387
|
});
|
|
1796
1388
|
onUnmounted(() => {
|
|
1797
1389
|
offDrag?.();
|
|
1798
|
-
// 监听delete键移除图元
|
|
1799
|
-
document.removeEventListener("keydown", onKeyDownDelete, { capture: true });
|
|
1800
1390
|
// 清理连接层相关的事件监听
|
|
1801
1391
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
1802
1392
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
1803
|
-
window.removeEventListener('keydown',
|
|
1804
|
-
|
|
1393
|
+
window.removeEventListener('keydown', keyboardHandler);
|
|
1394
|
+
// 重置名称编辑状态
|
|
1395
|
+
nameEditManager.reset();
|
|
1396
|
+
eventBus.off('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
|
|
1805
1397
|
highlightUtils.clearHighlightTimeout();
|
|
1398
|
+
// 清理高亮工具实例
|
|
1399
|
+
highlightUtils.dispose();
|
|
1400
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
1806
1401
|
});
|
|
1807
|
-
|
|
1808
1402
|
defineExpose({
|
|
1809
1403
|
continueExternalCreateDrag,
|
|
1810
1404
|
finishExternalCreateDrag,
|