@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.
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
  <!-- 交互层:放在图元之上,统一接管交互与鼠标样式 -->
3
- <div class="interaction-layer" ref="layerRef" @dragover="onCanvasDragOver" @drop="onCanvasDrop"
4
- :style="{ cursor: cursorStyle }" @mousedown="onLayerMouseDown" @mouseup="onLayerMouseUp" @click="onLayerClick"
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="getActionButtonsStyle(s, s.id)" :ref="(el) => setActionButtonsRef(el, s.id)">
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" @blur="finishEditName"
61
- @keyup.enter="finishEditName" @keyup.escape="cancelEditName" />
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" @delete="handleMenuDelete"
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, getPolicy } from "../utils/policy";
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 { ShapeConfig } from "../utils/diagram";
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 { addChildShapeToCompartment, clampCompartmentResize, isCompartment } from "../utils/compartment";
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
- const emit = defineEmits([
138
- "propertyPanel",
139
- "editName",
140
- "update-shape",
141
- "connectEnd",
142
- "actionButtonClick",
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
- //情景菜单dom实例
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 isEditingName = ref(false);
232
- const editingName = ref("");
233
- const nameInput = ref<HTMLInputElement>();
234
-
235
- // Pin 名称编辑输入框样式(放大,随字体自适应),非 Pin 返回空由 CSS 控制
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
- actionButtonMode.value = true
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
- if (!graphStore.selectedShape) return;
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 (showContextMenu.value) {
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
- startEditName();
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
- // 修改:无论是否在actionButtonMode模式下,只要在连接状态且点击空白处,都创建新图元
433
+ // 修改:无论是否在action模式下,只要在连接状态且点击空白处,都创建新图元
492
434
  if (!hasShapeAtPoint) {
493
435
  // 使用 cloneDeep 克隆 sourceShape
494
- console.log(props.connectShapeData, 'props.connectShapeData');
495
- console.log(props.connectShapeData.scenarioMenus, 'scenarioMenus');
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
- // 若点击的是名称虚线框/容器,避免触发清选或框选(Pin 的名称可能在外部)
840
- const t = evt.target as HTMLElement | null
841
- if (t && (t.classList?.contains('name-text-box') || t.closest('.name-text-box-container'))) {
842
- // 不改变当前选中;让后续 click 事件去触发 startEditName
843
- evt.stopPropagation()
844
- evt.preventDefault()
845
- return
846
- }
847
- if (isResizing.value || isEditingName.value) return;
848
- if (graphStore.isDragging) graphStore.endDragShape();
849
- if (offDrag) {
850
- offDrag();
851
- offDrag = null;
852
- }
853
- const pt = toLocalPoint(evt, layerRef.value);
854
- // 添加详细的点击位置日志
855
- // console.log('鼠标点击位置:', pt);
856
-
857
- // 执行命中测试
858
- const hit = pickTarget(graphStore.shapes, pt);
859
- // console.log('命中测试结果:', hit.kind, hit.shape?.id, hit.shape?.shapeType);
860
- // 如果右键菜单已显示且点击了其他图元,关闭菜单
861
- if (
862
- showContextMenu.value &&
863
- (hit.kind === "shape" || hit.kind === "pin") &&
864
- (!contextMenuTarget.value || hit.shape.id !== contextMenuTarget.value.id)
865
- ) {
866
- showContextMenu.value = false;
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
- cursorStyle.value = (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") ? "pointer" : "default";
870
- // 进入“框选”的条件:
871
- const wantMarquee = evt.shiftKey || hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin";
872
- if (wantMarquee) {
873
- if ((hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin") && !evt.shiftKey) {
874
- graphStore.clearSelection();
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
- startMarquee(pt);
877
- evt.preventDefault();
878
- return;
879
- }
880
- // 双击触发不同逻辑
881
- if (evt.detail === 2 && graphStore.marqueeShapes.length == 1) {
882
- const selectedShape = graphStore.selectedShape;
883
- console.log('双击选中的图元:', selectedShape);
884
-
885
- // 判断是否为Diagram组件
886
- if (selectedShape && selectedShape.shapeType !== 'edge' && props.diagram?.includes(selectedShape.shapeKey)) {
887
- // Diagram组件的特殊双击逻辑
888
- // 这里可以添加你想要的其他逻辑,例如发射自定义事件
889
- emit('diagramDoubleClick', selectedShape);
890
- // console.log(selectedShape,'Diagram组件双击事件');
891
- // 不打开属性面板
892
- } else {
893
- // 所有其他组件(包括连线)都打开属性面板
894
- onLayerDblClick(true);
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
- evt.preventDefault();
897
- evt.stopPropagation();
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
- console.log('点击选中的' + (hit.kind === 'edge' ? '线条' : hit.kind === 'pin' ? 'Pin' : '图元') + '数据信息:', shape);
565
+ // 打印选中元素的数据信息 - 确保每次点击都能看到
566
+ console.log('点击选中的' + (hit.kind === 'edge' ? '线条' : hit.kind === 'pin' ? 'Pin' : '图元') + '数据信息:', shape);
905
567
 
906
- const isMulti = graphStore.selectedIds.length > 1;
907
- const clickedInSelection = graphStore.selectedIds.includes(shape.id);
568
+ const isMulti = graphStore.selectedIds.length > 1;
569
+ const clickedInSelection = graphStore.selectedIds.includes(shape.id);
908
570
 
909
- if (isMulti && clickedInSelection) {
910
- // 多选状态下点击已选中元素,保持现状
911
- } else {
912
- // 其他情况:单选当前元素
913
- graphStore.clearSelection();
914
- graphStore.selectShape(shape);
915
- }
916
- // 选区此时已是“正确”的:要么多选集合,要么当前单选
917
- const ids = graphStore.selectedIds.length
918
- ? graphStore.selectedIds.slice()
919
- : [hit.shape.id];
920
- // 准备“潜在拖拽”,但先不触发 store.startDrag
921
- let started = false;
922
- // 绑定拖拽生命周期,把 move/up 转发给 store
923
- offDrag = withDrag(
924
- (e) => {
925
- const curr = toLocalPoint(e, layerRef.value);
926
- const dx = curr.x - pt.x;
927
- const dy = curr.y - pt.y;
928
- const dist2 = dx * dx + dy * dy;
929
-
930
- if (!started && dist2 >= DRAG_THRESHOLD * DRAG_THRESHOLD) {
931
- // 真正超过阈值,再启动 store 的拖拽生命周期
932
- started = true;
933
- graphStore.startDrag(ids, pt);
934
- }
935
- if (started) {
936
- // 如果是 pin 类型,需要在移动过程中将“指针位置”校正为吸附后的指针坐标
937
- let targetPt = curr;
938
- if (ids.length === 1) {
939
- const draggedShape = graphStore.shapes.find(x => x.id === ids[0]);
940
- if (draggedShape && draggedShape.shapeType === 'pin' && draggedShape.parenShapeId) {
941
- const parentShape = graphStore.shapes.find(x => x.id === draggedShape.parenShapeId);
942
- if (parentShape) {
943
- // 使用移动专用的吸附方法:根据 dragOffset 计算应传入 moveDraggedShape 的指针坐标
944
- targetPt = snapPinPointerOnMove(curr, parentShape, draggedShape, graphStore.dragOffset || undefined);
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
- graphStore.moveDraggedShape(targetPt);
949
- }
950
- },
951
- () => {
952
- // 只有在“真的开始拖拽”后,才结束拖拽
953
- if (started) {
954
- graphStore.endDragShape();
955
- } else {
956
- // 纯点击:啥也不做(已完成选中),避免误触发 reparent/zIndex
957
- cursorStyle.value = "default";
958
- emit("property-panel", false);
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
- offDrag = null;
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
- if (graphStore.isDragging || isResizing.value) {
1043
- return;
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
- if (hit.kind === "shape") {
1055
- // 显示右键菜单
1056
- showContextMenu.value = true;
1057
- contextMenuPosition.value = {
1058
- x: event.clientX,
1059
- y: event.clientY,
1060
- };
1061
- contextMenuTarget.value = hit.shape;
1062
- graphStore.selectShape(hit.shape);
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
- const handleMenuDelete = () => {
1071
- if (contextMenuTarget.value) {
1072
- // removeSelected();
1073
- graphStore.removeSelected();
1074
- }
1075
- showContextMenu.value = false;
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 (actionButtonMode.value) {
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, props.edgeCheck);
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 === "shape" &&
1301
- hit.shape.id &&
936
+ ['shape', 'pin'].includes(hit.kind) &&
937
+ hit.shape?.id &&
1302
938
  props.connectShapeData?.sourceId &&
1303
- hit.shape.id !== props.connectShapeData.sourceId
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
- emit("connectEnd", connectionData);
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
- // 1. 先停止连线状态,避免后续计算触发
1525
- isConnecting.value = false;
1526
-
1527
- // 2. 重置关键坐标状态(核心:清空无效坐标,让 linePoints 无值可算)
1528
- currentConnectPoint.value = { x: 0, y: 0 }; // 重置为无效默认值
1529
- mousePosition.value = { x: 0, y: 0 }; // 重置鼠标位置
1530
- targetConnectPoint.value = { x: 0, y: 0 }; // 重置目标连接点
1531
- targetShape.value = null; // 清空目标图元
1532
-
1533
- // 3. 清除高亮状态和定时器
1534
- highlightShape(null, false); // 取消图元高亮,恢复原始样式
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
- let externalCreatingId: string | null // 当前正在拖拽的(已加入画布的)元素 id
1550
- let externalPendingShape: any | null // 首帧/未入画布时的草稿(等待进入画布再 add)
1551
- let entranceCheckInFlight = false // 是否正在做第一次进画布的校验
1552
- let entranceDeniedInThisDrag = false // 本次拖拽是否已经被判定为“不允许进入画布”
1553
- // 转换坐标
1554
- const clientToLocalPoint = ({ clientX, clientY }: { clientX: number; clientY: number }) => {
1555
- const el = layerRef.value
1556
- if (!el) return { x: clientX, y: clientY } // 若还没挂载,直接用屏幕坐标兜底
1557
- const rect = el.getBoundingClientRect()
1558
- // 获取当前缩放比例
1559
- const scale = graphStore.getScale() || 1
1560
- // 计算本地坐标并除以缩放比例
1561
- return {
1562
- x: (clientX - rect.left) / scale,
1563
- y: (clientY - rect.top) / scale
1564
- } // 屏幕坐标转画布本地坐标
1565
- }
1566
- // 本地坐标转 client 坐标
1567
- const localToClientPoint = ({ x, y }: { x: number; y: number }) => {
1568
- const el = layerRef.value
1569
- if (!el) return { clientX: x, clientY: y } // 兜底
1570
- const rect = el.getBoundingClientRect()
1571
- const scale = graphStore.getScale() || 1
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
- const isInsideCanvasClient = (p: { clientX: number; clientY: number }) => {
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
- externalPendingShape = {
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 (!externalCreatingId && externalPendingShape && isInsideCanvasClient(payload)) {
1611
- // entranceCheckInFlight = true
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
- externalCreatingId = draft.id
1617
- externalPendingShape = null
1232
+ externalCreateDragState.creatingId = draft.id
1233
+ externalCreateDragState.pendingShape = null
1618
1234
  } finally {
1619
- // entranceCheckInFlight = false
1235
+ externalCreateDragState.isCheckInFlight = false
1620
1236
  }
1621
- externalPendingShape = null // 草稿已用完,清空
1622
1237
  }
1623
- externalPendingShape = null
1238
+ externalCreateDragState.pendingShape = null
1624
1239
 
1625
- if (!externalCreatingId) return // 还没开始拖拽(不在画布内),直接返回
1240
+ if (!externalCreateDragState.creatingId) return
1626
1241
 
1627
- // 如果是pin类型,需要计算吸附位置
1628
1242
  let targetPt = pt
1629
- if (externalCreatingId) {
1630
- const s = graphStore.shapes.find(x => x.id === externalCreatingId)
1631
- if (s && s.shapeType === 'pin') {
1632
- // 检查是否有父容器
1633
- if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1634
- const parent = graphStore.shapes.find(x => x.id === graphStore.hoverContainerId)
1635
- if (parent) {
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 (!externalCreatingId) return // 未开始则无事可做
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
- // 结束后,把该元素标记为 inert=true(仅对 shapeType==='shape' 生效)
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
- await sweepShapesWithInert()
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
- await sweepShapesWithInert()
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({ x: pure.bounds.x, y: pure.bounds.y })
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
- // await graphStore.removeShape(externalCreatingId as string)
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
- function sweepShapesWithInert() {
1773
- const arr = graphStore.shapes as any[]
1774
- let removed = 0
1775
- for (let i = arr.length - 1; i >= 0; i--) {
1776
- if ('inert' in arr[i]) {
1777
- arr.splice(i, 1); // 直接删,避免 removeShape 的事件/副作用导致回填
1778
- removed++
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
- // 监听delete键移除图元
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', getActionButtonsStyle)
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', handleKeyDown);
1804
- eventBus.off('shape-drag-end-updateScenarioMenu', getActionButtonsStyle)
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,