@mx-sose-front/mx-sose-graph 1.1.2 → 1.1.4

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.
Files changed (48) hide show
  1. package/dist/index.d.ts +178 -10
  2. package/dist/index.esm.js +4944 -63227
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.umd.js +1 -38
  5. package/dist/index.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +10 -1
  8. package/src/components/ContextMenu/ContextMenu.vue +27 -13
  9. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
  10. package/src/components/InteractionLayer.vue +656 -496
  11. package/src/components/LineStyle/LineStyleMarker.vue +1 -1
  12. package/src/components/NameEditor/NameEditor.vue +212 -0
  13. package/src/components/SelectionBox/SelectionBox.vue +189 -0
  14. package/src/components/Shape/Block.vue +1 -1
  15. package/src/constants/edgeShapeKeys.ts +43 -3
  16. package/src/constants/index.ts +21 -4
  17. package/src/hooks/index.ts +3 -0
  18. package/src/hooks/useHighlight.ts +223 -0
  19. package/src/hooks/useNameEdit.ts +234 -0
  20. package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
  21. package/src/index.ts +4 -1
  22. package/src/render/shape-renderer.ts +59 -46
  23. package/src/statics/icons/createMenu/show.png +0 -0
  24. package/src/statics/icons/createMenu/tree.png +0 -0
  25. package/src/statics/icons/createMenu//345/261/225/347/244/272/347/253/257/345/217/243/345/261/236/346/200/247@3x.png +0 -0
  26. package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
  27. package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
  28. package/src/store/graphStore.ts +185 -65
  29. package/src/types/index.ts +4 -2
  30. package/src/types/interactionLayer.ts +1 -0
  31. package/src/utils/batchAutoExpand.ts +65 -0
  32. package/src/utils/compartment.ts +78 -4
  33. package/src/utils/containers.ts +24 -10
  34. package/src/utils/contextMenuUtils.ts +126 -110
  35. package/src/utils/diagram.ts +19 -15
  36. package/src/utils/drag.ts +10 -5
  37. package/src/utils/edgeUtils.ts +3 -4
  38. package/src/utils/graphDragService.ts +27 -23
  39. package/src/utils/iconLoader.ts +7 -7
  40. package/src/utils/keyboardUtils.ts +221 -30
  41. package/src/utils/pinUtils.ts +1 -2
  42. package/src/utils/shapeOps/shapeOps.ts +168 -0
  43. package/src/utils/viewportCulling.ts +193 -0
  44. package/src/view/graph.vue +115 -60
  45. package/src/utils/highlightUtils.ts +0 -162
  46. package/src/utils/nameEditUtils.ts +0 -132
  47. package/src/utils/packgeMap.ts +0 -1
  48. /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
@@ -5,28 +5,11 @@
5
5
  @mousedown="onLayerMouseDown" @mouseup="onLayerMouseUp" @click="onLayerClick"
6
6
  @contextmenu.prevent="handleContextMenu">
7
7
  <!-- 只在"选中对象是画布(diagram)"时显示四个角手柄 -->
8
- <div v-for="s in graphStore.marqueeShapes" :key="s.id" class="selection-box" :style="getSelectionBoxStyle(s)">
9
- <!-- 只有当shapeType不是edge且不是conceptualRole时才渲染四个角手柄 -->
10
- <div class="resize-handles" v-show="!isBusy && s.shapeType !== 'edge'">
11
- <div v-for="h in resizeHandles" :key="h.position" class="resize-handle"
12
- :class="[`resize-${h.position}`, { 'is-disabled': isMultiSelected }]" :style="getHandleStyle(h, s)"
13
- @mousedown.stop.prevent="startResize($event, h.position, s)" />
14
- </div>
15
- <div class="action-buttons"
16
- v-show="!isMultiSelected && s.scenarioMenus && s.scenarioMenus.length > 0 && s.shapeType != ShapeConfig.SHAPE_TYPE"
17
- :style="actionButtonsStyle(s)">
18
- <div v-if="s.modelTypePropertyId" class="border-btn">
19
- <button class="action-btn edit-btn"
20
- @mousedown.stop.prevent="clickModelTypePropertyIdButton(s.modelTypePropertyId, s)" title="设置类型">
21
- <img src="../statics/icons/childIcons/设置类型.png" alt="设置类型">
22
- </button>
23
- </div>
24
- <button v-for="value in s.scenarioMenus" class="action-btn edit-btn"
25
- @mousedown.stop.prevent="clickActionButton($event, value.code, s)" @click.stop.prevent :title="value.name">
26
- <img :src="getIcon('childIcons', value.icon || '')" />
27
- </button>
28
- </div>
29
- </div>
8
+ <template v-if="!marqueeRect">
9
+ <SelectionBox v-for="s in graphStore.marqueeShapes" :key="s.id" :shape="s" :is-busy="isBusy"
10
+ :is-multi-selected="isMultiSelected" @resize-start="startResize" @action-button-click="clickActionButton"
11
+ @model-type-property-id-click="clickModelTypePropertyIdButton" />
12
+ </template>
30
13
  <!-- 命中容器的高亮矩形(虚线框) -->
31
14
  <div v-if="hoverRect" :class="[
32
15
  'hover-container-outline',
@@ -41,43 +24,39 @@
41
24
  height: hoverRect.height + 10 + 'px',
42
25
  }" />
43
26
  <!-- 框选预览矩形 -->
44
- <div v-if="marqueeRect" class="marquee-rect" :style="getMarqueeStyle(marqueeRect)" />
27
+ <div v-show="marqueeRect" class="marquee-rect"
28
+ :style="getMarqueeStyle(marqueeRect || { x: 0, y: 0, width: 0, height: 0 })" />
45
29
  <!-- 拖动和缩放的预览框 -->
46
30
  <component v-for="g in allGhosts" :key="g.id" class="ghost-shape" :is="getShapeComponent(g)" :shape="g"
47
31
  :style="getGhostShapeStyle(g)" />
48
- <!-- 名称虚线框(选中时显示) -->
49
- <div v-if="
50
- graphStore.selectedShape &&
51
- graphStore.selectedShape.nameBounds &&
52
- !isEditingName && graphStore.selectedShape.shapeKey !== 'ConceptRole'
53
- && !graphStore.pendingNestedIds.includes(graphStore.selectedShape.id)
54
- " class="name-text-box-container" :style="nameTextBoxContainerStyle(graphStore.selectedShape.id)">
55
- <div class="name-text-box" :style="nameTextBoxStyle(graphStore.selectedShape)" title="点击编辑名称"></div>
56
- </div>
57
-
58
- <!-- 名称编辑输入框 -->
59
- <div v-if="isEditingName && graphStore.selectedShape && graphStore.selectedShape.shapeKey !== 'ConceptRole'"
60
- class="name-editor-container" :style="nameEditorContainerStyle(graphStore.selectedShape)">
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()" />
65
- </div>
32
+ <!-- 名称编辑组件 -->
33
+ <NameEditor :selected-shape="graphStore.selectedShape" :can-edit="!graphStore.pendingNestedIds.includes(
34
+ graphStore.selectedShape?.id || ''
35
+ )
36
+ " :is-editing-name="isEditingName" :editing-name="editingName" :name-edit-manager="nameEditManager" />
66
37
 
67
38
  <!-- 使用右键菜单组件 -->
68
39
  <ContextMenu v-if="selectedShape && !isMultiSelected" :visible="showContextMenu" :selected-shape="selectedShape"
69
40
  :position="contextMenuPosition" @update:visible="showContextMenu = $event"
70
- @delete="ContextMenuUtils.handleDelete(contextMenuTarget)" @show-property-panel="onLayerDblClick(true);" />
41
+ @show-property-panel="onLayerDblClick(true)" />
71
42
 
72
43
  <!-- 连接层逻辑 - 当 connectShapeData 存在时显示 -->
73
44
  <div v-if="connectShapeData && diagramBounds" class="connect-layer" :style="layerStyle">
74
45
  <!-- 连线起点的黑点 - 只在连接时显示 -->
75
- <div v-if="isConnecting" class="connect-dot-direct" :style="dotStyle"></div>
46
+ <div v-show="isConnecting" class="connect-dot-direct" :style="dotStyle"></div>
76
47
 
77
48
  <!-- 连线 -->
78
- <ConnectionLine v-if="showLine" :show-line="showLine" :points="linePoints" :shape-key="connectShapeData?.shapeKey"
79
- :style="svgStyle" />
49
+ <ConnectionLine v-show="showLine" :show-line="showLine" :points="linePoints"
50
+ :shape-key="connectShapeData?.shapeKey" :style="svgStyle" />
80
51
  </div>
52
+
53
+ <!-- 高亮覆盖层 - 独立于 connect-layer,支持连线模式和拖拽模式 -->
54
+ <div v-if="highlightOverlayBounds" class="highlight-overlay" :class="highlightOverlayColor" :style="{
55
+ left: highlightOverlayBounds.x + 'px',
56
+ top: highlightOverlayBounds.y + 'px',
57
+ width: highlightOverlayBounds.width + 'px',
58
+ height: highlightOverlayBounds.height + 'px',
59
+ }" />
81
60
  </div>
82
61
  </template>
83
62
 
@@ -90,12 +69,17 @@ import {
90
69
  onMounted,
91
70
  type CSSProperties,
92
71
  watch,
72
+ watchEffect,
93
73
  } from "vue";
94
74
  import type { Shape } from "../types";
95
75
  import { InteractionLayerEmits } from "../types/interactionLayer";
96
- import type { InteractionLayerProps, ExternalCreateDragState } from "../types/interactionLayer";
76
+ import type {
77
+ InteractionLayerProps,
78
+ ExternalCreateDragState,
79
+ } from "../types/interactionLayer";
97
80
  import { useGraphStore } from "../store/graphStore";
98
- import { resizeHandles } from "../constants/index";
81
+ import SelectionBox from "./SelectionBox/SelectionBox.vue";
82
+ import NameEditor from "./NameEditor/NameEditor.vue";
99
83
 
100
84
  // 工具:几何/命中/样式/拖拽
101
85
  import {
@@ -111,17 +95,10 @@ import {
111
95
  } from "../utils/geom";
112
96
  import { pickTarget } from "../utils/hittest";
113
97
  import {
114
- ShapeConfig,
115
- selectionBoxStyle,
116
- handleStyle,
117
98
  adjustCanvasToFitAllShapes,
118
99
  actionButtonsStyle,
119
- nameTextBoxContainerStyle,
120
- nameTextBoxStyle,
121
- nameEditorContainerStyle,
122
- nameInputStyle,
123
100
  getMarqueeStyle,
124
- getLayerStyle
101
+ getLayerStyle,
125
102
  } from "../utils/diagram";
126
103
  import { withDrag } from "../utils/dom";
127
104
  import { checkNestViaFront } from "../utils/policy";
@@ -133,17 +110,16 @@ import { storeToRefs } from "pinia";
133
110
  import { EdgeUtils } from "../utils/edgeUtils";
134
111
  import ConnectionLine from "./LineStyle/ConnectionLine.vue";
135
112
  import { isCompartment } from "../utils/compartment";
136
- import { HighlightUtils } from "../utils/highlightUtils";
137
113
  import { ContextMenuUtils } from "../utils/contextMenuUtils";
138
114
  // 静态导入图片资源
139
- import { getIcon } from "../utils/iconLoader";
140
115
  import { getUuid } from "../utils/index";
141
116
  import { ElMessage } from "element-plus";
142
- import { snapPinToParentEdge, snapPinPointerOnMove } from '../utils/pinUtils';
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";
117
+ import { snapPinToParentEdge, snapPinPointerOnMove } from "../utils/pinUtils";
118
+ import { createKeyboardHandler } from "../utils/keyboardUtils";
119
+ import { useNameEdit } from "../hooks/useNameEdit";
120
+ import { guardOperate } from "../utils/license-guard";
121
+ import { useResize } from "../hooks/useResize";
122
+ import { useHighlight, type IHighlightUtils } from "../hooks/useHighlight";
147
123
 
148
124
  const props = defineProps<InteractionLayerProps>();
149
125
 
@@ -155,7 +131,7 @@ const graphStore = useGraphStore();
155
131
  const { selectedShape, connectMode } = storeToRefs(graphStore);
156
132
 
157
133
  // 是否正在“外部创建拖拽”(
158
- const isExternalCreateDragging = ref(false)
134
+ const isExternalCreateDragging = ref(false);
159
135
  // 光标样式(仅在按下时切换)
160
136
  const cursorStyle = ref<"default" | "pointer">("default");
161
137
  // 缩放时使用的预览框
@@ -186,7 +162,7 @@ const allGhosts = computed<Shape[]>(() => {
186
162
  const byId = new Map<string, Shape>();
187
163
  // 收集所有Ghost形状
188
164
  if (!isExternalCreateDragging.value) {
189
- graphStore.ghostShadow.forEach(g => byId.set(g.id, g))
165
+ graphStore.ghostShadow.forEach((g) => byId.set(g.id, g));
190
166
  }
191
167
  // 缩放时的预览
192
168
  resizeGhostShadow.value.forEach((g) => byId.set(g.id, g));
@@ -212,41 +188,38 @@ const getGhostShapeStyle = (shape: Shape): CSSProperties => {
212
188
  // 根层引用:用于本地坐标换算
213
189
  const layerRef = ref<HTMLDivElement | null>(null);
214
190
 
215
- // 名称编辑管理器
216
- const nameEditManager = new NameEditManager({
217
- onNameChange: (oldName, newName) => {
218
- if (graphStore.selectedShape) {
219
- emit("editName", graphStore.selectedShape, newName, oldName);
220
- }
221
- }
191
+ // 名称编辑 - 使用 Composable
192
+ const nameEditManager = useNameEdit({
193
+ getSelectedShape: () => graphStore.selectedShape,
194
+ onNameChange: (shape, oldName, newName) => {
195
+ emit("editName", shape, newName, oldName);
196
+ },
222
197
  });
223
198
 
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 });
199
+ // 解构名称编辑状态(方便在组件内使用)
200
+ const { isEditingName, editingName } = nameEditManager;
201
+
202
+ // 缩放 - 使用 Composable
203
+ const { isResizing, groupGhost, startResize } = useResize(
204
+ layerRef,
205
+ {
206
+ packages: props.packages,
207
+ diagram: props.diagram,
208
+ taggedValueLabels: props.taggedValueLabels,
235
209
  },
236
- onShapeUpdate: (id, updates) => {
237
- graphStore.updateShape(id, updates);
210
+ {
211
+ onResizeStart: (target) => {
212
+ eventBus.emit("resize-start", { target });
213
+ },
214
+ onResizeEnd: (target) => {
215
+ eventBus.emit("resize-end", { target });
216
+ },
217
+ onShapeUpdate: (id, updates) => {
218
+ graphStore.updateShape(id, updates);
219
+ },
238
220
  }
239
- });
240
-
241
- // 解构缩放相关的变量
242
- const {
243
- isResizing,
244
- groupGhost,
245
- startResize,
246
- } = resizeUtils;
221
+ );
247
222
 
248
- // 从名称编辑管理器获取响应式状态
249
- const { isEditingName, editingName } = nameEditManager.editingState;
250
223
  // 是否在画布内
251
224
  const isMouseInside = ref(false);
252
225
  // 记录最近一次 mousemove(用于 rAF 合并)
@@ -314,20 +287,36 @@ const isConnecting = ref(false); // 是否正在连接状态
314
287
  const targetConnectPoint = ref({ x: 0, y: 0 }); // 目标连接点
315
288
  const targetShape = ref<Shape | null>(null); // 目标图形
316
289
 
317
- // 高亮工具实例
318
- const highlightUtils = new HighlightUtils(graphStore);
290
+ // 高亮相关 - 使用 Composable
291
+ const {
292
+ overlayBounds: highlightOverlayBounds,
293
+ overlayColor: highlightOverlayColor,
294
+ highlightedShapeId,
295
+ highlightShape,
296
+ setHighlightTimeout,
297
+ clearHighlightTimeout,
298
+ } = useHighlight();
299
+
300
+ // 创建 highlightUtils 对象供 EdgeUtils.cancelConnection 使用
301
+ const highlightUtils: IHighlightUtils = {
302
+ highlightShape,
303
+ clearHighlightTimeout,
304
+ };
319
305
 
320
- // 高亮相关状态 - 使用计算属性从工具类获取
321
- const highlightedShape = computed(() => highlightUtils.getHighlightedShape());
322
- const sourceShape = ref<Shape | null>(null)
323
- const recordClickPoint = ref({ x: 0, y: 0 })
324
- // 高亮相关状态
325
- const highlightTimeout = ref<ReturnType<typeof setTimeout> | null>(null); // 高亮定时器
306
+ // 保持兼容性:highlightedShape 计算属性
307
+ const highlightedShape = computed(() => {
308
+ if (!highlightedShapeId.value) return null;
309
+ return graphStore.shapes.find(s => s.id === highlightedShapeId.value) || null;
310
+ });
311
+ const sourceShape = ref<Shape | null>(null);
312
+ const recordClickPoint = ref({ x: 0, y: 0 });
326
313
 
327
314
  // 正在交互:缩放中 或 元素拖动中
328
- const isBusy = computed(() =>
329
- isResizing.value || (graphStore.isDragging && graphStore.ghostShadow.length > 0)
330
- )
315
+ const isBusy = computed(
316
+ () =>
317
+ isResizing.value ||
318
+ (graphStore.isDragging && graphStore.ghostShadow.length > 0)
319
+ );
331
320
 
332
321
  // 监听所有可能影响菜单显示的操作状态
333
322
  const shouldCloseMenu = computed(() => {
@@ -337,12 +326,7 @@ const shouldCloseMenu = computed(() => {
337
326
  // 预览框(ghost)的 bounds,仅在缩放时存在
338
327
  type Rect = { x: number; y: number; width: number; height: number };
339
328
 
340
-
341
- const clickActionButton = (event: MouseEvent, value: string, shape: Shape) => {
342
- // 阻止事件冒泡,避免触发 onLayerClick
343
- event.stopPropagation();
344
- event.preventDefault();
345
-
329
+ const clickActionButton = (value: string, shape: Shape) => {
346
330
  // 如果正在编辑名称,先触发失焦以保存当前编辑的内容
347
331
  if (isEditingName.value) {
348
332
  nameEditManager.handleBlur(graphStore.selectedShape);
@@ -351,9 +335,9 @@ const clickActionButton = (event: MouseEvent, value: string, shape: Shape) => {
351
335
  // 清除选中状态,避免第一次点击时取消选中导致需要点击两次
352
336
  graphStore.clearSelection();
353
337
 
354
- graphStore.setConnectMode('action')
355
- emit('actionButtonClick', value, shape);
356
- }
338
+ graphStore.setConnectMode("action");
339
+ emit("actionButtonClick", value, shape);
340
+ };
357
341
 
358
342
  const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
359
343
  // 如果正在编辑名称,先触发失焦以保存当前编辑的内容
@@ -361,16 +345,12 @@ const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
361
345
  nameEditManager.handleBlur(graphStore.selectedShape);
362
346
  }
363
347
 
364
- emit('modelTypePropertyIdButtonClick', value, shape);
365
- }
366
-
367
- // 计算样式:调用 utils(保持单一职责)
368
- const getSelectionBoxStyle = (shape: Shape) => selectionBoxStyle(shape);
369
- const getHandleStyle = (h: any, shape: Shape) => handleStyle(h.position, shape);
348
+ emit("modelTypePropertyIdButtonClick", value, shape);
349
+ };
370
350
 
371
- // 名称编辑
372
- const startEditName = async () => {
373
- await nameEditManager.startEdit(graphStore.selectedShape);
351
+ // 名称编辑处理
352
+ const handleEditName = (shape: Shape, newName: string, oldName: string) => {
353
+ emit("editName", shape, newName, oldName);
374
354
  };
375
355
 
376
356
  // 属性面板
@@ -394,11 +374,8 @@ const onLayerClick = (evt: MouseEvent) => {
394
374
  showContextMenu.value = false;
395
375
  }
396
376
 
397
- // 检查是否点击了name-text-box
377
+ // 检查是否点击了name-text-box(由NameEditor组件内部处理)
398
378
  if (target.classList.contains("name-text-box")) {
399
- if (nameEditManager.canEdit(graphStore.selectedShape)) {
400
- startEditName();
401
- }
402
379
  return;
403
380
  }
404
381
 
@@ -428,18 +405,21 @@ const onLayerClick = (evt: MouseEvent) => {
428
405
  const clickY = localPoint.y;
429
406
 
430
407
  // 判断点击位置是否有图形
431
- const hasShapeAtPoint = EdgeUtils.isEndPointInShape(graphStore.shapes, { x: clickX, y: clickY });
408
+ const hasShapeAtPoint = EdgeUtils.isEndPointInShape(graphStore.shapes, {
409
+ x: clickX,
410
+ y: clickY,
411
+ });
432
412
 
433
413
  // 修改:无论是否在action模式下,只要在连接状态且点击空白处,都创建新图元
434
414
  if (!hasShapeAtPoint) {
435
415
  // 使用 cloneDeep 克隆 sourceShape
436
416
  if (!!sourceShape.value?.parenShapeId) {
437
417
  isConnecting.value = false;
438
- graphStore.setConnectMode('connect')
418
+ graphStore.setConnectMode("connect");
439
419
  highlightShape(null, false); // 取消图元高亮
440
- highlightUtils.clearHighlightTimeout();
420
+ clearHighlightTimeout();
441
421
  return;
442
- };
422
+ }
443
423
  const newShape = _.cloneDeep(foundSourceShape);
444
424
 
445
425
  // 修改 id(使用 getUuid)
@@ -451,8 +431,9 @@ const onLayerClick = (evt: MouseEvent) => {
451
431
  (menu) => menu.code === props.connectShapeData?.shapeKey
452
432
  );
453
433
  // 获取 targetModels,优先使用 currentMenu 中的值
454
- const targetModels = currentMenu?.targetCreateModel?.split(',')
455
- || props.connectShapeData?.targetCreateModel?.split(',');
434
+ const targetModels =
435
+ currentMenu?.targetCreateModel?.split(",") ||
436
+ props.connectShapeData?.targetCreateModel?.split(",");
456
437
 
457
438
  // 如果只有一个 targetModel,使用它作为 shapeKey
458
439
  if (targetModels?.length === 1) {
@@ -474,11 +455,16 @@ const onLayerClick = (evt: MouseEvent) => {
474
455
  y: newShapeY,
475
456
  width: defaultWidth,
476
457
  height: defaultHeight,
477
- modelId: '',
458
+ modelId: "",
478
459
  };
479
460
 
480
461
  // 添加新 shape 到画布,只传递 shapeKey 和 x, y 坐标
481
- emit('actionButtonAdd', { shapeKey: newShape.shapeKey, x: newShapeX, y: newShapeY, diagramId: diagramId });
462
+ emit("actionButtonAdd", {
463
+ shapeKey: newShape.shapeKey,
464
+ x: newShapeX,
465
+ y: newShapeY,
466
+ diagramId: diagramId,
467
+ });
482
468
  return;
483
469
  } else {
484
470
  handleConnectLayerClick(evt);
@@ -489,7 +475,7 @@ const onLayerClick = (evt: MouseEvent) => {
489
475
 
490
476
  // 处理线条点击事件
491
477
  const handleEdgeClick = (shape: Shape, event: MouseEvent) => {
492
- console.log('通过edge-click事件选中的线条数据:', shape);
478
+ console.log("通过edge-click事件选中的线条数据:", shape);
493
479
  graphStore.selectShape(shape);
494
480
  event.stopPropagation();
495
481
  };
@@ -499,12 +485,16 @@ const DRAG_THRESHOLD = 4;
499
485
  const onLayerMouseDown = (evt: MouseEvent) => {
500
486
  return guardOperate(async () => {
501
487
  // 若点击的是名称虚线框/容器,避免触发清选或框选(Pin 的名称可能在外部)
502
- const t = evt.target as HTMLElement | null
503
- if (t && (t.classList?.contains('name-text-box') || t.closest('.name-text-box-container'))) {
488
+ const t = evt.target as HTMLElement | null;
489
+ if (
490
+ t &&
491
+ (t.classList?.contains("name-text-box") ||
492
+ t.closest(".name-text-box-container"))
493
+ ) {
504
494
  // 不改变当前选中;让后续 click 事件去触发 startEditName
505
- evt.stopPropagation()
506
- evt.preventDefault()
507
- return
495
+ evt.stopPropagation();
496
+ evt.preventDefault();
497
+ return;
508
498
  }
509
499
  if (isResizing.value || isEditingName.value) return;
510
500
  if (graphStore.isDragging) graphStore.endDragShape();
@@ -528,12 +518,24 @@ const onLayerMouseDown = (evt: MouseEvent) => {
528
518
  showContextMenu.value = false;
529
519
  }
530
520
 
531
- cursorStyle.value = (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") ? "pointer" : "default";
521
+ cursorStyle.value =
522
+ hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin"
523
+ ? "pointer"
524
+ : "default";
532
525
  // 进入“框选”的条件:
533
- const wantMarquee = evt.shiftKey || hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin";
526
+ const wantMarquee =
527
+ evt.shiftKey ||
528
+ (hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin");
534
529
  if (wantMarquee) {
535
- if ((hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin") && !evt.shiftKey) {
530
+ if (
531
+ hit.kind !== "shape" &&
532
+ hit.kind !== "edge" &&
533
+ hit.kind !== "pin" &&
534
+ !evt.shiftKey
535
+ ) {
536
536
  graphStore.clearSelection();
537
+ // 点击空白处时,清除剪切状态
538
+ ContextMenuUtils.clearCutState();
537
539
  }
538
540
  startMarquee(pt);
539
541
  evt.preventDefault();
@@ -542,13 +544,17 @@ const onLayerMouseDown = (evt: MouseEvent) => {
542
544
  // 双击触发不同逻辑
543
545
  if (evt.detail === 2 && graphStore.marqueeShapes.length == 1) {
544
546
  const selectedShape = graphStore.selectedShape;
545
- console.log('双击选中的图元:', selectedShape);
547
+ console.log("双击选中的图元:", selectedShape);
546
548
 
547
549
  // 判断是否为Diagram组件
548
- if (selectedShape && selectedShape.shapeType !== 'edge' && props.diagram?.includes(selectedShape.shapeKey)) {
550
+ if (
551
+ selectedShape &&
552
+ selectedShape.shapeType !== "edge" &&
553
+ props.diagram?.includes(selectedShape.shapeKey)
554
+ ) {
549
555
  // Diagram组件的特殊双击逻辑
550
556
  // 这里可以添加你想要的其他逻辑,例如发射自定义事件
551
- emit('diagramDoubleClick', selectedShape);
557
+ emit("diagramDoubleClick", selectedShape);
552
558
  // console.log(selectedShape,'Diagram组件双击事件');
553
559
  // 不打开属性面板
554
560
  } else {
@@ -563,7 +569,12 @@ const onLayerMouseDown = (evt: MouseEvent) => {
563
569
  const { shape } = hit;
564
570
 
565
571
  // 打印选中元素的数据信息 - 确保每次点击都能看到
566
- console.log('点击选中的' + (hit.kind === 'edge' ? '线条' : hit.kind === 'pin' ? 'Pin' : '图元') + '数据信息:', shape);
572
+ console.log(
573
+ "点击选中的" +
574
+ (hit.kind === "edge" ? "线条" : hit.kind === "pin" ? "Pin" : "图元") +
575
+ "数据信息:",
576
+ shape
577
+ );
567
578
 
568
579
  const isMulti = graphStore.selectedIds.length > 1;
569
580
  const clickedInSelection = graphStore.selectedIds.includes(shape.id);
@@ -574,6 +585,8 @@ const onLayerMouseDown = (evt: MouseEvent) => {
574
585
  // 其他情况:单选当前元素
575
586
  graphStore.clearSelection();
576
587
  graphStore.selectShape(shape);
588
+ // 选中其他图元时,清除剪切状态
589
+ ContextMenuUtils.clearCutState();
577
590
  }
578
591
  // 选区此时已是“正确”的:要么多选集合,要么当前单选
579
592
  const ids = graphStore.selectedIds.length
@@ -598,21 +611,37 @@ const onLayerMouseDown = (evt: MouseEvent) => {
598
611
  // 如果是 pin 类型,需要在移动过程中将“指针位置”校正为吸附后的指针坐标
599
612
  let targetPt = curr;
600
613
  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);
614
+ const draggedShape = graphStore.shapes.find(
615
+ (x) => x.id === ids[0]
616
+ );
617
+ if (
618
+ draggedShape &&
619
+ draggedShape.shapeType === "pin" &&
620
+ draggedShape.parenShapeId
621
+ ) {
622
+ const parentShape = graphStore.shapes.find(
623
+ (x) => x.id === draggedShape.parenShapeId
624
+ );
604
625
  if (parentShape) {
605
626
  // 使用移动专用的吸附方法:根据 dragOffset 计算应传入 moveDraggedShape 的指针坐标
606
- targetPt = snapPinPointerOnMove(curr, parentShape, draggedShape, graphStore.dragOffset || undefined);
627
+ targetPt = snapPinPointerOnMove(
628
+ curr,
629
+ parentShape,
630
+ draggedShape,
631
+ graphStore.dragOffset || undefined
632
+ );
607
633
  }
608
634
  }
609
635
  }
610
- graphStore.moveDraggedShape(targetPt);
636
+ // pin 拖动:targetPt 用于落位/ghost,hover 命中用真实鼠标点,避免 hoverContainerId 来回切换闪动
637
+ // graphStore.moveDraggedShape(targetPt, { hitPointer: curr });
638
+ scheduleMoveDraggedShape(targetPt, { hitPointer: curr });
611
639
  }
612
640
  },
613
641
  () => {
614
642
  // 只有在“真的开始拖拽”后,才结束拖拽
615
643
  if (started) {
644
+ flushMoveDraggedShape();
616
645
  graphStore.endDragShape();
617
646
  } else {
618
647
  // 纯点击:啥也不做(已完成选中),避免误触发 reparent/zIndex
@@ -623,7 +652,7 @@ const onLayerMouseDown = (evt: MouseEvent) => {
623
652
  }
624
653
  );
625
654
  }
626
- })
655
+ });
627
656
  };
628
657
  // 框选部分
629
658
  const startMarquee = (anchor: { x: number; y: number }) => {
@@ -646,6 +675,22 @@ const startMarquee = (anchor: { x: number; y: number }) => {
646
675
  width: 0,
647
676
  height: 0,
648
677
  };
678
+ // 预先缓存“可被框选的图元”及其 bounds(避免 move 时重复 getBounds)
679
+ const candidates = graphStore.shapes
680
+ .filter((s) => s.shapeType?.toLowerCase?.() !== "diagram")
681
+ .map((s) => ({
682
+ id: s.id,
683
+ //缓存 bounds,避免 move 中每次 getBounds(s)
684
+ b: getBounds(s),
685
+ }));
686
+ // rAF 合并高频 move(只更新 marqueeRect,避免过多响应式刷新)
687
+ let raf = 0;
688
+ let latestPoint = anchorClamped;
689
+ const updateRect = () => {
690
+ raf = 0;
691
+ // 根据“锚点 & 当前点”得到框选矩形
692
+ marqueeRect.value = rectFromPoints(marqueeAnchor.value!, latestPoint);
693
+ };
649
694
  // 拖拽生命周期
650
695
  offDrag?.();
651
696
  offDrag = null;
@@ -655,28 +700,49 @@ const startMarquee = (anchor: { x: number; y: number }) => {
655
700
  // 当前指针的本地坐标
656
701
  const currRaw = toLocalPoint(e, layerRef.value);
657
702
  // 只对左/上做夹取(右/下不限制)
658
- const currClamped = clampPointToRect(
703
+ latestPoint = clampPointToRect(
659
704
  currRaw,
660
705
  { x: 0, y: 0, width: 0, height: 0 },
661
706
  container,
662
707
  edges
663
708
  );
709
+
664
710
  // 根据“锚点 & 当前点”得到框选矩形(会自动处理反向拖拽)
665
- const rect = rectFromPoints(marqueeAnchor.value!, currClamped);
666
- marqueeRect.value = rect;
667
- // 只选“完全位于框内”的图元(排除 diagram)
668
- const ids = graphStore.shapes
669
- .filter((s) => s.shapeType?.toLowerCase?.() !== "diagram")
670
- .filter((s) => rectContainsRect(rect, getBounds(s)))
671
- .map((s) => s.id);
672
- // 多选:所有被框中的图元都进入选中态
673
- graphStore.selectMany(ids);
711
+ // rAF 合并更新,避免 mousemove 触发过多响应式刷新
712
+ if (raf) return;
713
+ raf = requestAnimationFrame(updateRect);
674
714
  },
675
715
  // 结束框选,清理预览
676
716
  () => {
717
+ // 结束时确保最后一次 rect 已更新
718
+ if (raf) cancelAnimationFrame(raf);
719
+ updateRect();
720
+
721
+ const rect = marqueeRect.value!;
722
+ const right = rect.x + rect.width;
723
+ const bottom = rect.y + rect.height;
724
+ // 松手时一次性计算选中 ids(只做一次 O(N))
725
+ const ids: string[] = [];
726
+ for (let i = 0; i < candidates.length; i++) {
727
+ const c = candidates[i];
728
+ const b = c.b;
729
+ // 避免函数调用/对象创建
730
+ if (
731
+ b.x >= rect.x &&
732
+ b.y >= rect.y &&
733
+ b.x + b.width <= right &&
734
+ b.y + b.height <= bottom
735
+ ) {
736
+ ids.push(c.id);
737
+ }
738
+ }
739
+ // 一次性更新选中态(不会在拖动中疯狂更新 store)
740
+ graphStore.selectMany(ids);
741
+ // 清理预览
677
742
  marqueeAnchor.value = null;
678
743
  marqueeRect.value = null;
679
744
  cursorStyle.value = "default";
745
+ offDrag = null;
680
746
  }
681
747
  );
682
748
  };
@@ -694,11 +760,6 @@ const contextMenuTarget = ref<Shape | null>(null);
694
760
  // 是否允许连接当前高亮的图元
695
761
  const isConnectAllowed = ref(false);
696
762
 
697
- // 高亮图元的边框样式 - 使用工具类实现
698
- const highlightShape = (shape: Shape | null, isHighlight: boolean, isValidSource: boolean = true) => {
699
- highlightUtils.highlightShape(shape, isHighlight, isValidSource);
700
- };
701
-
702
763
  // 处理右键点击事件
703
764
  const handleContextMenu = (event: MouseEvent) => {
704
765
  return guardOperate(async () => {
@@ -714,7 +775,8 @@ const handleContextMenu = (event: MouseEvent) => {
714
775
  graphStore.shapes,
715
776
  graphStore.selectShape,
716
777
  graphStore.isDragging,
717
- isResizing.value
778
+ isResizing.value,
779
+ graphStore.currentScale // 传入当前缩放比例
718
780
  );
719
781
 
720
782
  if (hitShape) {
@@ -724,7 +786,7 @@ const handleContextMenu = (event: MouseEvent) => {
724
786
  } else {
725
787
  showContextMenu.value = false;
726
788
  }
727
- })
789
+ });
728
790
  };
729
791
 
730
792
  // 关闭右键菜单
@@ -747,35 +809,56 @@ watch(shouldCloseMenu, (shouldClose) => {
747
809
  closeMenu();
748
810
  }
749
811
  });
750
- watch(() => props.actionButtonShapeDataId, (newVal) => {
751
- if (newVal) {
752
- const foundShape = graphStore.shapes.find((x) => x.id === newVal);
753
- if (foundShape) {
754
- // 连接 sourceShape 和新 shape
755
- if (sourceShape.value) {
756
- const connectionData = EdgeUtils.completeConnection(
757
- sourceShape.value,
758
- foundShape,
759
- { x: recordClickPoint.value.x, y: recordClickPoint.value.y },
760
- currentConnectPoint.value,
761
- graphStore.shapes
762
- );
763
-
764
- if (connectionData) {
765
- emit("connectEnd", connectionData);
766
- graphStore.setConnectMode('connect')
767
- isConnecting.value = false;
768
- highlightUtils.clearHighlightTimeout();
769
- if (highlightTimeout.value) {
770
- clearTimeout(highlightTimeout.value);
771
- highlightTimeout.value = null;
812
+ watch(
813
+ () => props.actionButtonShapeDataId,
814
+ (newVal) => {
815
+ if (newVal) {
816
+ const foundShape = graphStore.shapes.find((x) => x.id === newVal);
817
+ if (foundShape) {
818
+ // 连接 sourceShape 和新 shape
819
+ if (sourceShape.value) {
820
+ const connectionData = EdgeUtils.completeConnection(
821
+ sourceShape.value,
822
+ foundShape,
823
+ { x: recordClickPoint.value.x, y: recordClickPoint.value.y },
824
+ currentConnectPoint.value,
825
+ graphStore.shapes
826
+ );
827
+
828
+ if (connectionData) {
829
+ // ServiceObjectFlow 特殊处理:需要在 sourcePoint 和 targetPoint 创建 pin
830
+ if (
831
+ props.connectShapeData?.shapeKey
832
+ ?.toLowerCase()
833
+ .includes("objectflow") &&
834
+ sourceShape.value &&
835
+ connectionData.sourcePoint &&
836
+ connectionData.targetPoint
837
+ ) {
838
+ const result = EdgeUtils.handleServiceObjectFlowConnection(
839
+ sourceShape.value,
840
+ foundShape,
841
+ connectionData
842
+ );
843
+ emit("objectFlowConnectEnd", {
844
+ connectionData: result.connectionData,
845
+ outputPinBounds: result.outputPinBounds,
846
+ inputPinBounds: result.inputPinBounds,
847
+ });
848
+ } else {
849
+ (connectionData as any).sourceShape = sourceShape.value;
850
+ emit("connectEnd", connectionData);
851
+ }
852
+ graphStore.setConnectMode("connect");
853
+ isConnecting.value = false;
854
+ clearHighlightTimeout();
772
855
  }
773
856
  }
774
857
  }
858
+ // 处理 actionButtonShapeData 的逻辑
775
859
  }
776
- // 处理 actionButtonShapeData 的逻辑
777
860
  }
778
- });
861
+ );
779
862
 
780
863
  // 鼠标移动事件处理
781
864
  const handleMouseMove = (event: MouseEvent) => {
@@ -802,6 +885,10 @@ const handleMouseMove = (event: MouseEvent) => {
802
885
  // 用于跟踪上一次的hoverShape,避免重复发射事件
803
886
  let lastHoverShapeId: string | null = null;
804
887
 
888
+ // 用于防抖 edge-check 事件和高亮,只有鼠标停留一段时间后才触发
889
+ let edgeCheckDebounceTimer: ReturnType<typeof setTimeout> | null = null;
890
+ const EDGE_CHECK_DEBOUNCE_DELAY = 150; // 150ms 防抖延迟
891
+
805
892
  const checkHoverTarget = (x: number, y: number) => {
806
893
  if (!props.diagramBounds) return;
807
894
 
@@ -813,66 +900,102 @@ const checkHoverTarget = (x: number, y: number) => {
813
900
  props.diagramBounds,
814
901
  props.connectShapeData?.sourceId
815
902
  );
816
- highlightUtils.clearHighlightTimeout();
903
+ clearHighlightTimeout();
817
904
 
818
905
  if (hoverShape) {
819
906
  // 检查连接有效性
820
907
  let targetModels = props.connectShapeData?.targetModels;
821
- if (connectMode.value === 'action') {
908
+ if (connectMode.value === "action") {
822
909
  targetModels = props.connectShapeData?.scenarioMenus?.find(
823
910
  (menu) => menu.code === props.connectShapeData?.shapeKey
824
911
  )?.targetModels;
825
912
  }
826
913
  let isAllowed = true;
827
- if (targetModels && Array.isArray(targetModels) && targetModels.length > 0) {
828
- const hoverShapeType = hoverShape.shapeKey || '';
914
+ if (
915
+ targetModels &&
916
+ Array.isArray(targetModels) &&
917
+ targetModels.length > 0
918
+ ) {
919
+ const hoverShapeType = hoverShape.shapeKey || "";
829
920
  isAllowed = targetModels.includes(hoverShapeType);
830
921
  }
831
922
  // 检查parenShapeId是否匹配(无论hoverShape是否改变都要检查)
832
923
  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
924
+ const sourceShape = graphStore.shapes.find(
925
+ (it) => it.id === props.connectShapeData?.sourceId
926
+ );
927
+ if (
928
+ sourceShape &&
929
+ sourceShape.parenShapeId !== hoverShape.parenShapeId &&
930
+ hoverShape.shapeType !== "pin" &&
931
+ sourceShape.shapeType !== "pin"
932
+ ) {
933
+ isAllowed = false;
836
934
  }
837
935
  }
838
- // 只有当hoverShape改变且connectShapeData存在时才发射事件
839
- if (lastHoverShapeId !== hoverShape.id && props.connectShapeData) {
840
- lastHoverShapeId = hoverShape.id;
841
- // 使用更严格的优先级判断逻辑,确保只要有一个属性有实际值就使用它
842
- let sourceModelId;
843
- // 如果modelId是有效字符串或数字,使用modelId
844
- if (props.connectShapeData.modelId && props.connectShapeData.modelId.toString().trim() !== '') {
845
- sourceModelId = props.connectShapeData.modelId;
846
- }
847
- // 否则,如果sourceModelId是有效字符串或数字,使用sourceModelId
848
- else if (props.connectShapeData.sourceModelId && props.connectShapeData.sourceModelId.toString().trim() !== '') {
849
- sourceModelId = props.connectShapeData.sourceModelId;
936
+
937
+ // hoverShape 改变时,清除之前的防抖定时器
938
+ if (lastHoverShapeId !== hoverShape.id) {
939
+ // 清除旧的防抖定时器
940
+ if (edgeCheckDebounceTimer) {
941
+ clearTimeout(edgeCheckDebounceTimer);
942
+ edgeCheckDebounceTimer = null;
850
943
  }
851
- // 只有当sourceModelId有值时就发射事件,并将isAllowed的值一并传递
852
- if (sourceModelId) {
853
- eventBus.emit('edge-check', {
854
- sourceModelId: sourceModelId, // 显式指定键值对,避免属性简写可能带来的混淆
855
- targetModelId: hoverShape.modelId,
856
- isAllowed: isAllowed // 将验证结果一并发射
857
- });
944
+
945
+ // 立即取消之前图元的高亮(如果有)
946
+ highlightShape(null, false);
947
+ isConnectAllowed.value = false;
948
+
949
+ lastHoverShapeId = hoverShape.id;
950
+
951
+ // 只有当 connectShapeData 存在时才设置防抖定时器
952
+ if (props.connectShapeData) {
953
+ // 使用防抖:只有鼠标停留一段时间后才发射 edge-check 事件和高亮
954
+ edgeCheckDebounceTimer = setTimeout(() => {
955
+ // 使用更严格的优先级判断逻辑,确保只要有一个属性有实际值就使用它
956
+ let sourceModelId;
957
+ // 如果modelId是有效字符串或数字,使用modelId
958
+ if (
959
+ props.connectShapeData?.modelId &&
960
+ props.connectShapeData.modelId.toString().trim() !== ""
961
+ ) {
962
+ sourceModelId = props.connectShapeData.modelId;
963
+ }
964
+ // 否则,如果sourceModelId是有效字符串或数字,使用sourceModelId
965
+ else if (
966
+ props.connectShapeData?.sourceModelId &&
967
+ props.connectShapeData.sourceModelId.toString().trim() !== ""
968
+ ) {
969
+ sourceModelId = props.connectShapeData.sourceModelId;
970
+ }
971
+ // 只有当sourceModelId有值时就发射事件,并将isAllowed的值一并传递
972
+ if (sourceModelId) {
973
+ eventBus.emit("edge-check", {
974
+ sourceModelId: sourceModelId, // 显式指定键值对,避免属性简写可能带来的混淆
975
+ targetModelId: hoverShape.modelId,
976
+ isAllowed: isAllowed, // 将验证结果一并发射
977
+ });
978
+ }
979
+
980
+ // 高亮目标图元 - 根据isAllowed决定高亮颜色
981
+ highlightShape(hoverShape, true, isAllowed);
982
+ isConnectAllowed.value = isAllowed;
983
+ }, EDGE_CHECK_DEBOUNCE_DELAY);
858
984
  }
859
985
  }
860
- // 高亮目标图元 - 根据isAllowed决定高亮颜色(实时计算,不依赖异步更新的props.edgeCheck)
861
- // true为蓝色,false为红色
862
- // 直接使用isAllowed(前端实时验证结果),确保快速移动时颜色能立即更新
863
- // 如果后端验证结果不同,会在props.edgeCheck更新后通过watch或其他机制再次更新
864
- highlightUtils.setHighlightTimeout(() => {
865
- highlightShape(hoverShape, true, isAllowed);
866
- isConnectAllowed.value = isAllowed;
867
- }, 10);
868
986
  } else {
869
- // 如果没有悬停在目标上,重置lastHoverShapeId
987
+ // 如果没有悬停在目标上
988
+ // 清除防抖定时器
989
+ if (edgeCheckDebounceTimer) {
990
+ clearTimeout(edgeCheckDebounceTimer);
991
+ edgeCheckDebounceTimer = null;
992
+ }
993
+ // 重置lastHoverShapeId
870
994
  lastHoverShapeId = null;
871
- // 延迟取消高亮
872
- highlightUtils.setHighlightTimeout(() => {
873
- highlightShape(null, false);
874
- isConnectAllowed.value = false;
875
- }, 60);
995
+ // 立即取消高亮(无需延迟,因为高亮本身已有防抖)
996
+ clearHighlightTimeout();
997
+ highlightShape(null, false);
998
+ isConnectAllowed.value = false;
876
999
  }
877
1000
  };
878
1001
 
@@ -908,6 +1031,13 @@ watch(
908
1031
  () => isConnecting.value,
909
1032
  (newVal) => {
910
1033
  if (!newVal) {
1034
+ // 清除防抖定时器
1035
+ if (edgeCheckDebounceTimer) {
1036
+ clearTimeout(edgeCheckDebounceTimer);
1037
+ edgeCheckDebounceTimer = null;
1038
+ }
1039
+ // 重置 lastHoverShapeId
1040
+ lastHoverShapeId = null;
911
1041
  isConnectAllowed.value = false;
912
1042
  highlightShape(null, false); // 取消图元高亮
913
1043
  }
@@ -919,7 +1049,11 @@ watch(
919
1049
  () => props.edgeCheck,
920
1050
  (newEdgeCheck) => {
921
1051
  // 只有在连接状态下且有高亮图元时才更新
922
- if (isConnecting.value && highlightedShape.value && newEdgeCheck !== undefined) {
1052
+ if (
1053
+ isConnecting.value &&
1054
+ highlightedShape.value &&
1055
+ newEdgeCheck !== undefined
1056
+ ) {
923
1057
  // 使用后端验证结果更新高亮颜色
924
1058
  highlightShape(highlightedShape.value, true, newEdgeCheck);
925
1059
  }
@@ -933,7 +1067,7 @@ const handleConnectLayerClick = (event: MouseEvent) => {
933
1067
  const hit = pickTarget(graphStore.shapes, localPoint);
934
1068
 
935
1069
  if (
936
- ['shape', 'pin'].includes(hit.kind) &&
1070
+ ["shape", "pin"].includes(hit.kind) &&
937
1071
  hit.shape?.id &&
938
1072
  props.connectShapeData?.sourceId &&
939
1073
  hit.shape?.id !== props.connectShapeData.sourceId
@@ -956,51 +1090,56 @@ const completeConnection = (
956
1090
  if (!isConnectAllowed.value) {
957
1091
  isConnecting.value = false;
958
1092
  highlightShape(null, false); // 取消图元高亮
959
- graphStore.setConnectMode('connect')
960
- highlightUtils.clearHighlightTimeout();
961
- ElMessage.error('当前目标图元类型不符合连接要求');
1093
+ graphStore.setConnectMode("connect");
1094
+ clearHighlightTimeout();
1095
+ ElMessage.error("当前目标图元类型不符合连接要求");
962
1096
  return;
963
1097
  }
964
1098
 
965
1099
  // 嵌套情况下只能连接同一个父图元
966
- if (sourceShape && clickedShape && sourceShape.parenShapeId !== clickedShape.parenShapeId && (clickedShape.shapeType !== 'pin' && sourceShape.shapeType !== 'pin')) {
1100
+ if (
1101
+ sourceShape &&
1102
+ clickedShape &&
1103
+ sourceShape.parenShapeId !== clickedShape.parenShapeId &&
1104
+ clickedShape.shapeType !== "pin" &&
1105
+ sourceShape.shapeType !== "pin"
1106
+ ) {
967
1107
  isConnecting.value = false;
968
1108
  highlightShape(null, false); // 取消图元高亮
969
- highlightUtils.clearHighlightTimeout();
1109
+ clearHighlightTimeout();
970
1110
  return;
971
1111
  }
972
1112
  // 检查目标图元类型是否符合targetModels要求
973
1113
  const targetModels = props.connectShapeData?.targetModels;
974
1114
  if (targetModels && Array.isArray(targetModels) && targetModels.length > 0) {
975
-
976
- const clickedShapeType = clickedShape.shapeKey || '';
1115
+ const clickedShapeType = clickedShape.shapeKey || "";
977
1116
  if (!targetModels.includes(clickedShapeType)) {
978
1117
  isConnecting.value = false;
979
1118
  highlightShape(null, false); // 取消图元高亮
980
- highlightUtils.clearHighlightTimeout();
1119
+ clearHighlightTimeout();
981
1120
  // alert('当前目标图元类型不符合连接要求');
982
- ElMessage.error('当前目标图元类型不符合连接要求');
1121
+ ElMessage.error("当前目标图元类型不符合连接要求");
983
1122
  return;
984
1123
  }
985
1124
  }
986
1125
 
987
1126
  // 检查是否已存在相同类型的边
988
- const existingEdge = graphStore.shapes.find((shape: Shape) =>
989
- shape.shapeType === 'edge' &&
990
- shape.sourceId === props.connectShapeData?.sourceId &&
991
- shape.targetId === clickedShape.id &&
992
- shape.shapeKey === props.connectShapeData?.shapeKey
1127
+ const existingEdge = graphStore.shapes.find(
1128
+ (shape: Shape) =>
1129
+ shape.shapeType === "edge" &&
1130
+ shape.sourceId === props.connectShapeData?.sourceId &&
1131
+ shape.targetId === clickedShape.id &&
1132
+ shape.shapeKey === props.connectShapeData?.shapeKey
993
1133
  );
994
1134
 
995
-
996
1135
  // 如果边已存在,错误提示并返回
997
1136
  if (existingEdge) {
998
1137
  // alert('同类型的边已经存在,不能重复添加');
999
- ElMessage.error('同类型的边已经存在,不能重复添加');
1138
+ ElMessage.error("同类型的边已经存在,不能重复添加");
1000
1139
  isConnecting.value = false;
1001
- graphStore.setConnectMode('connect')
1140
+ graphStore.setConnectMode("connect");
1002
1141
  highlightShape(null, false); // 取消图元高亮
1003
- highlightUtils.clearHighlightTimeout();
1142
+ clearHighlightTimeout();
1004
1143
  return;
1005
1144
  }
1006
1145
  // 使用工具类完成连接,传递当前的shapes列表以支持差异化路由
@@ -1013,25 +1152,30 @@ const completeConnection = (
1013
1152
  );
1014
1153
  if (connectionData) {
1015
1154
  // ServiceObjectFlow 特殊处理:需要在 sourcePoint 和 targetPoint 创建 pin
1016
- if (props.connectShapeData?.shapeKey === 'ServiceObjectFlow' && sourceShape && connectionData.sourcePoint && connectionData.targetPoint) {
1155
+ if (
1156
+ props.connectShapeData?.shapeKey?.toLowerCase().includes("objectflow") &&
1157
+ sourceShape &&
1158
+ connectionData.sourcePoint &&
1159
+ connectionData.targetPoint
1160
+ ) {
1017
1161
  const result = EdgeUtils.handleServiceObjectFlowConnection(
1018
1162
  sourceShape,
1019
1163
  clickedShape,
1020
1164
  connectionData
1021
1165
  );
1022
- emit('objectFlowConnectEnd', {
1166
+ emit("objectFlowConnectEnd", {
1023
1167
  connectionData: result.connectionData,
1024
1168
  outputPinBounds: result.outputPinBounds,
1025
- inputPinBounds: result.inputPinBounds
1169
+ inputPinBounds: result.inputPinBounds,
1026
1170
  });
1027
1171
  } else {
1028
- (connectionData as any).sourceShape = sourceShape
1029
- emit('connectEnd', connectionData);
1172
+ (connectionData as any).sourceShape = sourceShape;
1173
+ emit("connectEnd", connectionData);
1030
1174
  }
1031
- graphStore.setConnectMode('connect')
1175
+ graphStore.setConnectMode("connect");
1032
1176
  isConnecting.value = false;
1033
1177
  highlightShape(null, false); // 取消图元高亮
1034
- highlightUtils.clearHighlightTimeout();
1178
+ clearHighlightTimeout();
1035
1179
  }
1036
1180
  };
1037
1181
  // 鼠标离开事件处理
@@ -1040,7 +1184,7 @@ const handleMouseLeave = () => {
1040
1184
  showLine.value = false;
1041
1185
  }
1042
1186
  highlightShape(null, false); // 取消图元高亮
1043
- highlightUtils.clearHighlightTimeout();
1187
+ clearHighlightTimeout();
1044
1188
  };
1045
1189
 
1046
1190
  // 初始化连接点位置
@@ -1058,7 +1202,8 @@ const initializeConnectPoint = () => {
1058
1202
  props.diagramBounds // 传递图表边界
1059
1203
  );
1060
1204
 
1061
- if (initialPoint && sourceShape) { // 确保sourceShape存在,防止从工具栏拖拽时错误初始化
1205
+ if (initialPoint && sourceShape) {
1206
+ // 确保sourceShape存在,防止从工具栏拖拽时错误初始化
1062
1207
  currentConnectPoint.value = initialPoint;
1063
1208
 
1064
1209
  if (!isConnecting.value) {
@@ -1164,7 +1309,7 @@ const cancelConnection = () => {
1164
1309
  mousePosition,
1165
1310
  targetConnectPoint,
1166
1311
  targetShape,
1167
- showLine
1312
+ showLine,
1168
1313
  },
1169
1314
  highlightUtils
1170
1315
  );
@@ -1173,7 +1318,10 @@ const cancelConnection = () => {
1173
1318
  // 监听 connectShapeData 变化,重新初始化连接点
1174
1319
  watch(
1175
1320
  () => props.connectShapeData,
1176
- () => {
1321
+ (newVal) => {
1322
+ if (newVal && !newVal.scenarioMenus) {
1323
+ graphStore.setConnectMode('connect');
1324
+ }
1177
1325
  initializeConnectPoint();
1178
1326
  },
1179
1327
  { deep: true }
@@ -1183,7 +1331,7 @@ const externalCreateDragState: ExternalCreateDragState = {
1183
1331
  creatingId: null,
1184
1332
  pendingShape: null,
1185
1333
  isDragging: false,
1186
- isCheckInFlight: false
1334
+ isCheckInFlight: false,
1187
1335
  };
1188
1336
 
1189
1337
  const resetExternalCreateDragState = () => {
@@ -1198,7 +1346,7 @@ const resetExternalCreateDragState = () => {
1198
1346
  const cleanupInertShapes = async () => {
1199
1347
  const arr = graphStore.shapes as any[];
1200
1348
  for (let i = arr.length - 1; i >= 0; i--) {
1201
- if ('inert' in arr[i]) {
1349
+ if ("inert" in arr[i]) {
1202
1350
  arr.splice(i, 1);
1203
1351
  }
1204
1352
  }
@@ -1206,99 +1354,151 @@ const cleanupInertShapes = async () => {
1206
1354
  };
1207
1355
 
1208
1356
  //拖动中添加元素并触发嵌套逻辑,
1209
- const continueExternalCreateDrag = async (payload: { clientX: number; clientY: number; shapeData?: any }) => {
1210
- externalCreateDragState.isDragging = true
1211
- isExternalCreateDragging.value = true
1212
- const pt = clientToLocalPoint(payload.clientX, payload.clientY, layerRef.value)
1357
+ const continueExternalCreateDrag = async (payload: {
1358
+ clientX: number;
1359
+ clientY: number;
1360
+ shapeData?: any;
1361
+ }) => {
1362
+ externalCreateDragState.isDragging = true;
1363
+ isExternalCreateDragging.value = true;
1364
+ const pt = clientToLocalPoint(
1365
+ payload.clientX,
1366
+ payload.clientY,
1367
+ layerRef.value
1368
+ );
1213
1369
  if (payload.shapeData) {
1214
- const s = payload.shapeData
1215
- const isCmp = isCompartment(s as Shape)
1370
+ const s = payload.shapeData;
1371
+ const isCmp = isCompartment(s as Shape);
1216
1372
  externalCreateDragState.pendingShape = {
1217
1373
  ...s,
1218
1374
  bounds: {
1219
1375
  x: pt.x,
1220
1376
  y: pt.y,
1221
1377
  width: s.bounds?.width ?? 180,
1222
- height: isCmp ? 120 : (s.bounds?.height ?? 80),
1378
+ height: isCmp ? 120 : s.bounds?.height ?? 80,
1223
1379
  },
1224
1380
  inert: false,
1225
- }
1381
+ };
1226
1382
  }
1227
- if (!externalCreateDragState.creatingId && externalCreateDragState.pendingShape && isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)) {
1228
- const draft = externalCreateDragState.pendingShape
1383
+ if (
1384
+ !externalCreateDragState.creatingId &&
1385
+ externalCreateDragState.pendingShape &&
1386
+ isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)
1387
+ ) {
1388
+ const draft = externalCreateDragState.pendingShape;
1229
1389
  try {
1230
- graphStore.addShape(draft)
1231
- graphStore.startDrag([draft.id], pt)
1232
- externalCreateDragState.creatingId = draft.id
1233
- externalCreateDragState.pendingShape = null
1390
+ graphStore.addShape(draft);
1391
+ graphStore.startDrag([draft.id], pt);
1392
+ externalCreateDragState.creatingId = draft.id;
1393
+ externalCreateDragState.pendingShape = null;
1234
1394
  } finally {
1235
- externalCreateDragState.isCheckInFlight = false
1395
+ externalCreateDragState.isCheckInFlight = false;
1236
1396
  }
1237
1397
  }
1238
- externalCreateDragState.pendingShape = null
1398
+ externalCreateDragState.pendingShape = null;
1239
1399
 
1240
- if (!externalCreateDragState.creatingId) return
1400
+ if (!externalCreateDragState.creatingId) return;
1241
1401
 
1242
- let targetPt = pt
1243
- const s = graphStore.shapes.find(x => x.id === externalCreateDragState.creatingId)
1244
- if (s && s.shapeType === 'pin') {
1402
+ let targetPt = pt;
1403
+ const s = graphStore.shapes.find(
1404
+ (x) => x.id === externalCreateDragState.creatingId
1405
+ );
1406
+ if (s && s.shapeType === "pin") {
1245
1407
  if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1246
- const parent = graphStore.shapes.find(x => x.id === graphStore.hoverContainerId)
1408
+ const parent = graphStore.shapes.find(
1409
+ (x) => x.id === graphStore.hoverContainerId
1410
+ );
1247
1411
  if (parent) {
1248
- const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(pt, parent, s)
1249
- targetPt = { x: adjustedX, y: adjustedY }
1412
+ const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(
1413
+ pt,
1414
+ parent,
1415
+ s
1416
+ );
1417
+ targetPt = { x: adjustedX, y: adjustedY };
1250
1418
  }
1251
1419
  }
1252
1420
  }
1253
-
1254
- graphStore.moveDraggedShape(targetPt)
1255
- }
1421
+ // 外部创建 pin:targetPt 是期望的最终落位,但 hover 命中仍用真实鼠标 pt,避免闪动
1422
+ // graphStore.moveDraggedShape(targetPt, { hitPointer: pt });
1423
+ scheduleMoveDraggedShape(targetPt, { hitPointer: pt });
1424
+ };
1256
1425
 
1257
1426
  //拖拽结束后重新发送事件到front中调用接口
1258
- const finishExternalCreateDrag = async (payload: { clientX: number; clientY: number }) => {
1427
+ const finishExternalCreateDrag = async (payload: {
1428
+ clientX: number;
1429
+ clientY: number;
1430
+ }) => {
1259
1431
  try {
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)
1264
- await nextTick()
1265
- const s: any = (graphStore.shapes || []).find((x: any) => x.id == externalCreateDragState.creatingId)
1266
- if (s && s.shapeType == 'shape' || s.shapeType == 'pin') {
1267
- const pure = _.omit(s, ['inert'])
1268
- pure.bounds = { // 覆盖为新的 bounds
1269
- ...(pure.bounds),
1270
- x: pt.x,
1271
- y: pt.y,
1272
- }
1273
- // 先推断这次 drop 的“候选父节点”
1274
- let parent: Shape | null = null
1275
- if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1276
- parent = (graphStore.shapes as any[]).find(
1432
+ if (!externalCreateDragState.creatingId) return;
1433
+ const pt = clientToLocalPoint(
1434
+ payload.clientX,
1435
+ payload.clientY,
1436
+ layerRef.value
1437
+ );
1438
+ if (
1439
+ isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)
1440
+ ) {
1441
+ let targetPt = pt;
1442
+ const s0 = (graphStore.shapes || []).find(
1443
+ (x: any) => x.id == externalCreateDragState.creatingId
1444
+ ) as any;
1445
+ // 推断这次 drop 的“候选父节点”
1446
+ let parent: Shape | null = null;
1447
+ if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1448
+ parent =
1449
+ (graphStore.shapes as any[]).find(
1277
1450
  (x: any) => x.id === graphStore.hoverContainerId
1278
- ) || null
1279
- }
1451
+ ) || null;
1452
+ }
1453
+ // pin:松手瞬间就先算好最终吸附坐标
1454
+ if (s0 && s0.shapeType === "pin" && parent) {
1455
+ const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(
1456
+ pt,
1457
+ parent,
1458
+ s0
1459
+ );
1460
+ targetPt = { x: adjustedX, y: adjustedY };
1461
+ }
1462
+
1463
+ graphStore.moveDraggedShape(targetPt, { hitPointer: pt });
1464
+ await nextTick();
1465
+ const s: any = (graphStore.shapes || []).find(
1466
+ (x: any) => x.id == externalCreateDragState.creatingId
1467
+ );
1468
+ if ((s && s.shapeType == "shape") || s.shapeType == "pin") {
1469
+ const pure = _.omit(s, ["inert"]);
1470
+ pure.bounds = {
1471
+ // 覆盖为新的 bounds
1472
+ ...pure.bounds,
1473
+ x: targetPt.x,
1474
+ y: targetPt.y,
1475
+ };
1280
1476
  // 如果是 pin 类型,调整位置吸附到父图元最近的边
1281
- if (pure.shapeType === 'pin' && parent) {
1282
- const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(pt, parent, pure)
1283
- pure.bounds.x = adjustedX
1284
- pure.bounds.y = adjustedY
1477
+ if (pure.shapeType === "pin" && parent) {
1478
+ // 这里直接沿用上面计算出的 targetPt,避免再次出现“先 pt 后吸附”的一帧回跳
1479
+ pure.bounds.x = targetPt.x;
1480
+ pure.bounds.y = targetPt.y;
1285
1481
  pure.parenShapeId = parent.id;
1286
- // 将吸附后的坐标同步回 ghost
1287
- graphStore.moveDraggedShape({ x: adjustedX, y: adjustedY })
1482
+ // 将吸附后的坐标同步回 ghost(hover 命中仍用真实鼠标点)
1483
+ graphStore.moveDraggedShape(targetPt, { hitPointer: pt });
1288
1484
  }
1289
1485
  try {
1290
- const { ok } = await checkNestViaFront(pure as Shape, parent, graphStore.shapes[0])
1486
+ const { ok } = await checkNestViaFront(
1487
+ pure as Shape,
1488
+ parent,
1489
+ graphStore.shapes[0]
1490
+ );
1291
1491
  if (!ok) {
1292
- graphStore.setHoverState(null, false)
1293
- await cleanupInertShapes()
1294
- resetExternalCreateDragState()
1295
- return
1492
+ graphStore.setHoverState(null, false);
1493
+ await cleanupInertShapes();
1494
+ resetExternalCreateDragState();
1495
+ return;
1296
1496
  } else {
1297
- graphStore.canDropOnCanvas = true
1497
+ graphStore.canDropOnCanvas = true;
1298
1498
  }
1299
1499
  } catch (error) {
1300
- await cleanupInertShapes()
1301
- resetExternalCreateDragState()
1500
+ await cleanupInertShapes();
1501
+ resetExternalCreateDragState();
1302
1502
  }
1303
1503
  // 同步对外通知
1304
1504
  // 先构造基础数据
@@ -1311,70 +1511,140 @@ const finishExternalCreateDrag = async (payload: { clientX: number; clientY: num
1311
1511
  type: s.type,
1312
1512
  nodeType: s.shapeKey,
1313
1513
  icon: s.icon,
1314
- }
1514
+ };
1315
1515
  // 如果是 pin 类型,更新 coordinate 为吸附后的 client 坐标
1316
- if (pure.shapeType === 'pin' && parent) {
1317
- const adjustedClientPt = localToClientPoint(pure.bounds.x, pure.bounds.y, layerRef.value)
1318
- payloadData.coordinate.clientX = adjustedClientPt.clientX
1319
- payloadData.coordinate.clientY = adjustedClientPt.clientY
1516
+ if (pure.shapeType === "pin" && parent) {
1517
+ const adjustedClientPt = localToClientPoint(
1518
+ pure.bounds.x,
1519
+ pure.bounds.y,
1520
+ layerRef.value
1521
+ );
1522
+ payloadData.coordinate.clientX = adjustedClientPt.clientX;
1523
+ payloadData.coordinate.clientY = adjustedClientPt.clientY;
1320
1524
  }
1321
1525
  // 如果当前图元的shapeKey存在于ownerRequiredShapeKeys当中,再补 ownerId 字段
1322
1526
  // @todo OperationalPort合并到ownerRequiredShapeKeys
1323
1527
  if (graphStore.ownerRequiredShapeKeys.includes(pure.shapeKey)) {
1324
- payloadData.ownerId = parent?.modelId
1528
+ payloadData.ownerId = parent?.modelId;
1325
1529
  }
1326
1530
  // 发送事件
1327
1531
  await new Promise<void>((resolve, reject) => {
1328
- eventBus.emit('addShape', {
1532
+ eventBus.emit("addShape", {
1329
1533
  ...payloadData,
1330
1534
  resolve,
1331
1535
  reject,
1332
- })
1333
- })
1334
- if (pure.shapeType === 'pin') {
1335
- graphStore.endDragShape('pinDrop')
1536
+ });
1537
+ });
1538
+ if (pure.shapeType === "pin") {
1539
+ graphStore.endDragShape("pinDrop");
1336
1540
  } else {
1337
- graphStore.endDragShape('addEntity') // 提交拖拽(触发嵌套/吸附等 finalize)
1541
+ graphStore.endDragShape("addEntity"); // 提交拖拽(触发嵌套/吸附等 finalize)
1338
1542
  }
1339
1543
  }
1340
1544
  }
1341
1545
  } catch (error) {
1342
- graphStore.setHoverState(null, false)
1343
- await cleanupInertShapes()
1546
+ graphStore.setHoverState(null, false);
1547
+ await cleanupInertShapes();
1344
1548
  } finally {
1345
- await nextTick()
1346
- await cleanupInertShapes()
1347
- graphStore.setHoverState(null, false)
1348
- resetExternalCreateDragState()
1549
+ await nextTick();
1550
+ await cleanupInertShapes();
1551
+ graphStore.setHoverState(null, false);
1552
+ resetExternalCreateDragState();
1349
1553
  }
1350
- }
1554
+ };
1351
1555
  // 根据 graphStore.canDropOnCanvas 决定是否显示禁用放置图标
1352
1556
  const onCanvasDragOver = (e: DragEvent) => {
1353
- e.preventDefault() // 必须,否则不会触发 drop
1354
- if (!e.dataTransfer) return
1557
+ e.preventDefault(); // 必须,否则不会触发 drop
1558
+ if (!e.dataTransfer) return;
1355
1559
  if (graphStore.canDropOnCanvas) {
1356
- e.dataTransfer.dropEffect = 'copy' // 显示带 + 的复制光标
1560
+ e.dataTransfer.dropEffect = "copy"; // 显示带 + 的复制光标
1357
1561
  } else {
1358
- e.dataTransfer.dropEffect = 'none' //显示禁止的圈圈光标
1562
+ e.dataTransfer.dropEffect = "none"; //显示禁止的圈圈光标
1359
1563
  }
1360
- }
1564
+ };
1361
1565
 
1362
1566
  const onCanvasDrop = (e: DragEvent) => {
1363
1567
  if (!graphStore.canDropOnCanvas) {
1364
1568
  // 不允许丢,直接 return
1365
- return
1569
+ return;
1366
1570
  }
1367
- }
1571
+ };
1572
+
1368
1573
  // 创建键盘事件处理器
1369
1574
  const keyboardHandler = createKeyboardHandler({
1370
1575
  onDelete: () => { },
1371
1576
  onEditProperty: () => onLayerDblClick(true),
1372
1577
  onCancelConnection: cancelConnection,
1373
- isEditingName: () => isEditingName.value
1578
+ isEditingName: () => isEditingName.value,
1579
+ isTextareaDialogOpen: () => props.isTextareaDialogOpen || false, // 传递对话框状态
1580
+ onCopy: () => {
1581
+ // 获取当前选中的图元
1582
+ const selectedShapes = graphStore.selectedIds
1583
+ .map((id) => graphStore.shapes.find((s) => s.id === id))
1584
+ .filter(Boolean) as Shape[];
1585
+
1586
+ if (selectedShapes.length === 0) {
1587
+ return;
1588
+ }
1589
+
1590
+ // 使用ContextMenuUtils处理复制
1591
+ ContextMenuUtils.handleCopy(selectedShapes);
1592
+ },
1593
+ onCut: () => {
1594
+ // 获取当前选中的图元
1595
+ const selectedShapes = graphStore.selectedIds
1596
+ .map((id) => graphStore.shapes.find((s) => s.id === id))
1597
+ .filter(Boolean) as Shape[];
1598
+
1599
+ if (selectedShapes.length === 0) {
1600
+ return;
1601
+ }
1602
+
1603
+ // 使用ContextMenuUtils处理剪切
1604
+ ContextMenuUtils.handleCut(selectedShapes);
1605
+ },
1606
+ onPaste: () => {
1607
+ // 使用ContextMenuUtils处理粘贴
1608
+ ContextMenuUtils.handlePaste(graphStore.selectedShape);
1609
+ },
1374
1610
  });
1611
+ // 拖动 move 的 rAF 节流器(只合并 move,不影响 mouseup 的最终落位)
1612
+ let dragMoveRafId: number | null = null;
1613
+ let latestDragMove:
1614
+ | { pointer: { x: number; y: number }; options?: { hitPointer?: { x: number; y: number } } }
1615
+ | null = null;
1616
+
1617
+ /** 安排一次 move(同一帧内多次调用会合并为最后一次) */
1618
+ const scheduleMoveDraggedShape = (
1619
+ pointer: { x: number; y: number },
1620
+ options?: { hitPointer?: { x: number; y: number } }
1621
+ ) => {
1622
+ latestDragMove = { pointer, options };
1623
+ if (dragMoveRafId) return;
1624
+
1625
+ dragMoveRafId = requestAnimationFrame(() => {
1626
+ dragMoveRafId = null;
1627
+ if (!latestDragMove) return;
1628
+
1629
+ graphStore.moveDraggedShape(latestDragMove.pointer, latestDragMove.options);
1630
+ latestDragMove = null;
1631
+ });
1632
+ };
1633
+
1634
+ /** 在 mouseup/drag end 前强制把最后一次 move 刷进去,避免“最后一帧没跟上” */
1635
+ const flushMoveDraggedShape = () => {
1636
+ if (dragMoveRafId) {
1637
+ cancelAnimationFrame(dragMoveRafId);
1638
+ dragMoveRafId = null;
1639
+ }
1640
+ if (latestDragMove) {
1641
+ graphStore.moveDraggedShape(latestDragMove.pointer, latestDragMove.options);
1642
+ latestDragMove = null;
1643
+ }
1644
+ };
1375
1645
 
1376
1646
  onMounted(() => {
1377
- window.addEventListener('keydown', keyboardHandler);
1647
+ window.addEventListener("keydown", keyboardHandler);
1378
1648
  // 添加连接层相关的事件监听
1379
1649
  if (props.connectShapeData) {
1380
1650
  document.addEventListener("mousemove", handleMouseMove);
@@ -1383,28 +1653,32 @@ onMounted(() => {
1383
1653
  }
1384
1654
 
1385
1655
  //拖动结束后更新情景菜单
1386
- eventBus.on('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
1656
+ eventBus.on("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1657
+
1658
+ // 监听选择图元事件
1659
+ eventBus.on("select-shapes", (ids: string[]) => {
1660
+ graphStore.clearSelection();
1661
+ graphStore.selectMany(ids);
1662
+ });
1387
1663
  });
1388
1664
  onUnmounted(() => {
1389
1665
  offDrag?.();
1390
1666
  // 清理连接层相关的事件监听
1391
1667
  document.removeEventListener("mousemove", handleMouseMove);
1392
1668
  document.removeEventListener("mouseleave", handleMouseLeave);
1393
- window.removeEventListener('keydown', keyboardHandler);
1394
- // 重置名称编辑状态
1395
- nameEditManager.reset();
1396
- eventBus.off('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
1397
- highlightUtils.clearHighlightTimeout();
1398
- // 清理高亮工具实例
1399
- highlightUtils.dispose();
1669
+ window.removeEventListener("keydown", keyboardHandler);
1670
+ eventBus.off("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1400
1671
  if (rafId) cancelAnimationFrame(rafId);
1672
+ if (dragMoveRafId) cancelAnimationFrame(dragMoveRafId);
1673
+ dragMoveRafId = null;
1674
+ latestDragMove = null;
1401
1675
  });
1402
1676
  defineExpose({
1403
1677
  continueExternalCreateDrag,
1404
1678
  finishExternalCreateDrag,
1405
1679
  handleEdgeClick,
1406
1680
  getBoundingClientRect: () => layerRef.value?.getBoundingClientRect(),
1407
- })
1681
+ });
1408
1682
  </script>
1409
1683
 
1410
1684
  <style scoped>
@@ -1416,22 +1690,11 @@ defineExpose({
1416
1690
  height: 100%;
1417
1691
  /* 应用与画布相同的缩放变换 */
1418
1692
  transform-origin: 0 0;
1419
- transform: scale(v-bind('graphStore.currentScale'));
1693
+ transform: scale(v-bind("graphStore.currentScale"));
1420
1694
  pointer-events: all;
1421
1695
  z-index: 999;
1422
1696
  }
1423
1697
 
1424
- .selection-box {
1425
- pointer-events: none;
1426
- background: transparent;
1427
- }
1428
-
1429
- .resize-handles {
1430
- position: relative;
1431
- width: 100%;
1432
- height: 100%;
1433
- }
1434
-
1435
1698
  .hover-container-outline {
1436
1699
  position: absolute;
1437
1700
  pointer-events: none;
@@ -1450,122 +1713,7 @@ defineExpose({
1450
1713
  background: rgba(240, 237, 237, 0.842);
1451
1714
  }
1452
1715
 
1453
- .resize-handle {
1454
- position: absolute;
1455
- width: 10px;
1456
- height: 10px;
1457
- background-color: #007bff;
1458
- border: 2px solid #fff;
1459
- border-radius: 50%;
1460
- pointer-events: all;
1461
- transition: all 0.2s ease;
1462
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
1463
- z-index: 999;
1464
- }
1465
-
1466
- .resize-handle.is-disabled {
1467
- cursor: default !important;
1468
- }
1469
-
1470
- .resize-handle:hover {
1471
- background-color: #0056b3;
1472
- transform: scale(1.2);
1473
- }
1474
-
1475
- .action-buttons {
1476
- display: flex;
1477
- flex-direction: column;
1478
- gap: 4px;
1479
- pointer-events: all;
1480
- background: rgba(255, 255, 255, 0.95);
1481
- padding: 6px;
1482
- border-radius: 4px;
1483
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1484
- border: 1px solid #e0e0e0;
1485
- backdrop-filter: blur(2px);
1486
- }
1487
-
1488
- .action-btn {
1489
- width: 28px;
1490
- height: 28px;
1491
- border: 1px solid #d0d0d0;
1492
- border-radius: 3px;
1493
- background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
1494
- cursor: pointer;
1495
- display: flex;
1496
- align-items: center;
1497
- justify-content: center;
1498
- font-size: 12px;
1499
- font-weight: bold;
1500
- font-family: "Arial", sans-serif;
1501
- color: #495057;
1502
- transition: all 0.2s ease;
1503
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1504
- }
1505
-
1506
- .action-btn:hover {
1507
- background: linear-gradient(to bottom, #e3f2fd, #bbdefb);
1508
- border-color: #90caf9;
1509
- transform: translateY(-1px);
1510
- box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
1511
- color: #1976d2;
1512
- }
1513
-
1514
- .edit-btn:hover {
1515
- background: #e3f2fd;
1516
- }
1517
-
1518
- .delete-btn:hover {
1519
- background: #ffebee;
1520
- border-color: #f44336;
1521
- }
1522
-
1523
- .name-editor-container {
1524
- pointer-events: all;
1525
- }
1526
-
1527
- .name-input {
1528
- width: calc(100% - 20px);
1529
- padding: 2px 4px;
1530
- border: 2px solid #007bff;
1531
- border-radius: 6px;
1532
- font-size: 12px;
1533
- font-weight: 600;
1534
- background: #fff;
1535
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1536
- outline: none;
1537
- text-align: center;
1538
- }
1539
-
1540
- .name-input:focus {
1541
- border-color: #0056b3;
1542
- box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
1543
- }
1544
-
1545
- .name-text-box-container {
1546
- pointer-events: all;
1547
- }
1548
-
1549
- .name-text-box {
1550
- border: 1px dashed #007bff;
1551
- background: rgba(255, 255, 255, 0.2);
1552
- cursor: pointer;
1553
- pointer-events: all;
1554
- transition: all 0.2s ease;
1555
- border-radius: 4px;
1556
- box-shadow: 0 0 0 1px rgba(0, 123, 255, 0.1);
1557
- height: 100%;
1558
- display: flex;
1559
- align-items: center;
1560
- justify-content: center;
1561
- }
1562
-
1563
- .name-text-box:hover {
1564
- border-color: #0056b3;
1565
- background: rgba(0, 123, 255, 0.05);
1566
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
1567
- transform: scale(1.02);
1568
- }
1716
+ /* 删除了选择框、调整大小手柄和操作按钮相关的样式,这些样式已经移到了 SelectionBox 组件中 */
1569
1717
 
1570
1718
  .resize-ghost {
1571
1719
  position: absolute;
@@ -1611,8 +1759,20 @@ defineExpose({
1611
1759
  z-index: 1000;
1612
1760
  }
1613
1761
 
1614
- .border-btn {
1615
- padding-bottom: 4px;
1616
- border-bottom: 1px solid #e0e0e0;
1762
+ /* 高亮覆盖层样式 - 性能优化方案 */
1763
+ .highlight-overlay {
1764
+ position: absolute;
1765
+ pointer-events: none;
1766
+ box-sizing: border-box;
1767
+ z-index: 1001;
1768
+ border-radius: 2px;
1769
+ }
1770
+
1771
+ .highlight-overlay.blue {
1772
+ border: 3px solid #1890ff;
1773
+ }
1774
+
1775
+ .highlight-overlay.red {
1776
+ border: 3px solid #f56c6c;
1617
1777
  }
1618
1778
  </style>