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

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.
@@ -5,28 +5,9 @@
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
+ <SelectionBox v-for="s in graphStore.marqueeShapes" :key="s.id" :shape="s" :is-busy="isBusy"
9
+ :is-multi-selected="isMultiSelected" @resize-start="startResize" @action-button-click="clickActionButton"
10
+ @model-type-property-id-click="clickModelTypePropertyIdButton" />
30
11
  <!-- 命中容器的高亮矩形(虚线框) -->
31
12
  <div v-if="hoverRect" :class="[
32
13
  'hover-container-outline',
@@ -35,39 +16,26 @@
35
16
  'is-valid': graphStore.hoverNestable === true,
36
17
  },
37
18
  ]" :style="{
38
- left: hoverRect.x - 5 + 'px',
39
- top: hoverRect.y - 5 + 'px',
40
- width: hoverRect.width + 10 + 'px',
41
- height: hoverRect.height + 10 + 'px',
42
- }" />
19
+ left: hoverRect.x - 5 + 'px',
20
+ top: hoverRect.y - 5 + 'px',
21
+ width: hoverRect.width + 10 + 'px',
22
+ height: hoverRect.height + 10 + 'px',
23
+ }" />
43
24
  <!-- 框选预览矩形 -->
44
25
  <div v-if="marqueeRect" class="marquee-rect" :style="getMarqueeStyle(marqueeRect)" />
45
26
  <!-- 拖动和缩放的预览框 -->
46
27
  <component v-for="g in allGhosts" :key="g.id" class="ghost-shape" :is="getShapeComponent(g)" :shape="g"
47
28
  :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>
29
+ <!-- 名称编辑组件 -->
30
+ <NameEditor :selected-shape="graphStore.selectedShape" :can-edit="!graphStore.pendingNestedIds.includes(
31
+ graphStore.selectedShape?.id || ''
32
+ )
33
+ " :is-editing-name="isEditingName" :editing-name="editingName" :name-edit-manager="nameEditManager" />
66
34
 
67
35
  <!-- 使用右键菜单组件 -->
68
36
  <ContextMenu v-if="selectedShape && !isMultiSelected" :visible="showContextMenu" :selected-shape="selectedShape"
69
37
  :position="contextMenuPosition" @update:visible="showContextMenu = $event"
70
- @delete="ContextMenuUtils.handleDelete(contextMenuTarget)" @show-property-panel="onLayerDblClick(true);" />
38
+ @show-property-panel="onLayerDblClick(true)" />
71
39
 
72
40
  <!-- 连接层逻辑 - 当 connectShapeData 存在时显示 -->
73
41
  <div v-if="connectShapeData && diagramBounds" class="connect-layer" :style="layerStyle">
@@ -90,12 +58,17 @@ import {
90
58
  onMounted,
91
59
  type CSSProperties,
92
60
  watch,
61
+ watchEffect,
93
62
  } from "vue";
94
63
  import type { Shape } from "../types";
95
64
  import { InteractionLayerEmits } from "../types/interactionLayer";
96
- import type { InteractionLayerProps, ExternalCreateDragState } from "../types/interactionLayer";
65
+ import type {
66
+ InteractionLayerProps,
67
+ ExternalCreateDragState,
68
+ } from "../types/interactionLayer";
97
69
  import { useGraphStore } from "../store/graphStore";
98
- import { resizeHandles } from "../constants/index";
70
+ import SelectionBox from "./SelectionBox.vue";
71
+ import NameEditor from "./NameEditor.vue";
99
72
 
100
73
  // 工具:几何/命中/样式/拖拽
101
74
  import {
@@ -111,17 +84,10 @@ import {
111
84
  } from "../utils/geom";
112
85
  import { pickTarget } from "../utils/hittest";
113
86
  import {
114
- ShapeConfig,
115
- selectionBoxStyle,
116
- handleStyle,
117
87
  adjustCanvasToFitAllShapes,
118
88
  actionButtonsStyle,
119
- nameTextBoxContainerStyle,
120
- nameTextBoxStyle,
121
- nameEditorContainerStyle,
122
- nameInputStyle,
123
89
  getMarqueeStyle,
124
- getLayerStyle
90
+ getLayerStyle,
125
91
  } from "../utils/diagram";
126
92
  import { withDrag } from "../utils/dom";
127
93
  import { checkNestViaFront } from "../utils/policy";
@@ -136,13 +102,12 @@ import { isCompartment } from "../utils/compartment";
136
102
  import { HighlightUtils } from "../utils/highlightUtils";
137
103
  import { ContextMenuUtils } from "../utils/contextMenuUtils";
138
104
  // 静态导入图片资源
139
- import { getIcon } from "../utils/iconLoader";
140
105
  import { getUuid } from "../utils/index";
141
106
  import { ElMessage } from "element-plus";
142
- import { snapPinToParentEdge, snapPinPointerOnMove } from '../utils/pinUtils';
143
- import { createKeyboardHandler } from '../utils/keyboardUtils';
107
+ import { snapPinToParentEdge, snapPinPointerOnMove } from "../utils/pinUtils";
108
+ import { createKeyboardHandler } from "../utils/keyboardUtils";
144
109
  import { NameEditManager } from "../utils/nameEditUtils";
145
- import { guardOperate } from "../utils/license-guard"
110
+ import { guardOperate } from "../utils/license-guard";
146
111
  import { createResizeUtils } from "../utils/resizeUtils";
147
112
 
148
113
  const props = defineProps<InteractionLayerProps>();
@@ -155,7 +120,7 @@ const graphStore = useGraphStore();
155
120
  const { selectedShape, connectMode } = storeToRefs(graphStore);
156
121
 
157
122
  // 是否正在“外部创建拖拽”(
158
- const isExternalCreateDragging = ref(false)
123
+ const isExternalCreateDragging = ref(false);
159
124
  // 光标样式(仅在按下时切换)
160
125
  const cursorStyle = ref<"default" | "pointer">("default");
161
126
  // 缩放时使用的预览框
@@ -186,7 +151,7 @@ const allGhosts = computed<Shape[]>(() => {
186
151
  const byId = new Map<string, Shape>();
187
152
  // 收集所有Ghost形状
188
153
  if (!isExternalCreateDragging.value) {
189
- graphStore.ghostShadow.forEach(g => byId.set(g.id, g))
154
+ graphStore.ghostShadow.forEach((g) => byId.set(g.id, g));
190
155
  }
191
156
  // 缩放时的预览
192
157
  resizeGhostShadow.value.forEach((g) => byId.set(g.id, g));
@@ -218,32 +183,42 @@ const nameEditManager = new NameEditManager({
218
183
  if (graphStore.selectedShape) {
219
184
  emit("editName", graphStore.selectedShape, newName, oldName);
220
185
  }
221
- }
186
+ },
222
187
  });
223
188
 
224
189
  // 缩放工具实例
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 });
190
+ const resizeUtils = createResizeUtils(
191
+ layerRef,
192
+ {
193
+ packages: props.packages,
194
+ diagram: props.diagram,
195
+ taggedValueLabels: props.taggedValueLabels,
235
196
  },
236
- onShapeUpdate: (id, updates) => {
237
- graphStore.updateShape(id, updates);
197
+ {
198
+ onResizeStart: (target) => {
199
+ eventBus.emit("resize-start", { target });
200
+ },
201
+ onResizeEnd: (target) => {
202
+ eventBus.emit("resize-end", { target });
203
+ },
204
+ onShapeUpdate: (id, updates) => {
205
+ graphStore.updateShape(id, updates);
206
+ },
238
207
  }
239
- });
208
+ );
240
209
 
241
210
  // 解构缩放相关的变量
242
- const {
243
- isResizing,
244
- groupGhost,
245
- startResize,
246
- } = resizeUtils;
211
+ const { isResizing, groupGhost, startResize } = resizeUtils;
212
+
213
+ // 名称输入框引用
214
+ const nameInput = ref<HTMLInputElement | null>(null);
215
+
216
+ // 监听nameInput ref变化,将其传递给NameEditManager
217
+ watchEffect(() => {
218
+ if (nameInput.value) {
219
+ nameEditManager.setNameInput(nameInput.value);
220
+ }
221
+ });
247
222
 
248
223
  // 从名称编辑管理器获取响应式状态
249
224
  const { isEditingName, editingName } = nameEditManager.editingState;
@@ -319,15 +294,17 @@ const highlightUtils = new HighlightUtils(graphStore);
319
294
 
320
295
  // 高亮相关状态 - 使用计算属性从工具类获取
321
296
  const highlightedShape = computed(() => highlightUtils.getHighlightedShape());
322
- const sourceShape = ref<Shape | null>(null)
323
- const recordClickPoint = ref({ x: 0, y: 0 })
297
+ const sourceShape = ref<Shape | null>(null);
298
+ const recordClickPoint = ref({ x: 0, y: 0 });
324
299
  // 高亮相关状态
325
300
  const highlightTimeout = ref<ReturnType<typeof setTimeout> | null>(null); // 高亮定时器
326
301
 
327
302
  // 正在交互:缩放中 或 元素拖动中
328
- const isBusy = computed(() =>
329
- isResizing.value || (graphStore.isDragging && graphStore.ghostShadow.length > 0)
330
- )
303
+ const isBusy = computed(
304
+ () =>
305
+ isResizing.value ||
306
+ (graphStore.isDragging && graphStore.ghostShadow.length > 0)
307
+ );
331
308
 
332
309
  // 监听所有可能影响菜单显示的操作状态
333
310
  const shouldCloseMenu = computed(() => {
@@ -337,12 +314,7 @@ const shouldCloseMenu = computed(() => {
337
314
  // 预览框(ghost)的 bounds,仅在缩放时存在
338
315
  type Rect = { x: number; y: number; width: number; height: number };
339
316
 
340
-
341
- const clickActionButton = (event: MouseEvent, value: string, shape: Shape) => {
342
- // 阻止事件冒泡,避免触发 onLayerClick
343
- event.stopPropagation();
344
- event.preventDefault();
345
-
317
+ const clickActionButton = (value: string, shape: Shape) => {
346
318
  // 如果正在编辑名称,先触发失焦以保存当前编辑的内容
347
319
  if (isEditingName.value) {
348
320
  nameEditManager.handleBlur(graphStore.selectedShape);
@@ -351,9 +323,9 @@ const clickActionButton = (event: MouseEvent, value: string, shape: Shape) => {
351
323
  // 清除选中状态,避免第一次点击时取消选中导致需要点击两次
352
324
  graphStore.clearSelection();
353
325
 
354
- graphStore.setConnectMode('action')
355
- emit('actionButtonClick', value, shape);
356
- }
326
+ graphStore.setConnectMode("action");
327
+ emit("actionButtonClick", value, shape);
328
+ };
357
329
 
358
330
  const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
359
331
  // 如果正在编辑名称,先触发失焦以保存当前编辑的内容
@@ -361,16 +333,12 @@ const clickModelTypePropertyIdButton = (value: string, shape: Shape) => {
361
333
  nameEditManager.handleBlur(graphStore.selectedShape);
362
334
  }
363
335
 
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);
336
+ emit("modelTypePropertyIdButtonClick", value, shape);
337
+ };
370
338
 
371
- // 名称编辑
372
- const startEditName = async () => {
373
- await nameEditManager.startEdit(graphStore.selectedShape);
339
+ // 名称编辑处理
340
+ const handleEditName = (shape: Shape, newName: string, oldName: string) => {
341
+ emit("editName", shape, newName, oldName);
374
342
  };
375
343
 
376
344
  // 属性面板
@@ -394,11 +362,8 @@ const onLayerClick = (evt: MouseEvent) => {
394
362
  showContextMenu.value = false;
395
363
  }
396
364
 
397
- // 检查是否点击了name-text-box
365
+ // 检查是否点击了name-text-box(由NameEditor组件内部处理)
398
366
  if (target.classList.contains("name-text-box")) {
399
- if (nameEditManager.canEdit(graphStore.selectedShape)) {
400
- startEditName();
401
- }
402
367
  return;
403
368
  }
404
369
 
@@ -428,18 +393,21 @@ const onLayerClick = (evt: MouseEvent) => {
428
393
  const clickY = localPoint.y;
429
394
 
430
395
  // 判断点击位置是否有图形
431
- const hasShapeAtPoint = EdgeUtils.isEndPointInShape(graphStore.shapes, { x: clickX, y: clickY });
396
+ const hasShapeAtPoint = EdgeUtils.isEndPointInShape(graphStore.shapes, {
397
+ x: clickX,
398
+ y: clickY,
399
+ });
432
400
 
433
401
  // 修改:无论是否在action模式下,只要在连接状态且点击空白处,都创建新图元
434
402
  if (!hasShapeAtPoint) {
435
403
  // 使用 cloneDeep 克隆 sourceShape
436
404
  if (!!sourceShape.value?.parenShapeId) {
437
405
  isConnecting.value = false;
438
- graphStore.setConnectMode('connect')
406
+ graphStore.setConnectMode("connect");
439
407
  highlightShape(null, false); // 取消图元高亮
440
408
  highlightUtils.clearHighlightTimeout();
441
409
  return;
442
- };
410
+ }
443
411
  const newShape = _.cloneDeep(foundSourceShape);
444
412
 
445
413
  // 修改 id(使用 getUuid)
@@ -451,8 +419,9 @@ const onLayerClick = (evt: MouseEvent) => {
451
419
  (menu) => menu.code === props.connectShapeData?.shapeKey
452
420
  );
453
421
  // 获取 targetModels,优先使用 currentMenu 中的值
454
- const targetModels = currentMenu?.targetCreateModel?.split(',')
455
- || props.connectShapeData?.targetCreateModel?.split(',');
422
+ const targetModels =
423
+ currentMenu?.targetCreateModel?.split(",") ||
424
+ props.connectShapeData?.targetCreateModel?.split(",");
456
425
 
457
426
  // 如果只有一个 targetModel,使用它作为 shapeKey
458
427
  if (targetModels?.length === 1) {
@@ -474,11 +443,16 @@ const onLayerClick = (evt: MouseEvent) => {
474
443
  y: newShapeY,
475
444
  width: defaultWidth,
476
445
  height: defaultHeight,
477
- modelId: '',
446
+ modelId: "",
478
447
  };
479
448
 
480
449
  // 添加新 shape 到画布,只传递 shapeKey 和 x, y 坐标
481
- emit('actionButtonAdd', { shapeKey: newShape.shapeKey, x: newShapeX, y: newShapeY, diagramId: diagramId });
450
+ emit("actionButtonAdd", {
451
+ shapeKey: newShape.shapeKey,
452
+ x: newShapeX,
453
+ y: newShapeY,
454
+ diagramId: diagramId,
455
+ });
482
456
  return;
483
457
  } else {
484
458
  handleConnectLayerClick(evt);
@@ -489,7 +463,7 @@ const onLayerClick = (evt: MouseEvent) => {
489
463
 
490
464
  // 处理线条点击事件
491
465
  const handleEdgeClick = (shape: Shape, event: MouseEvent) => {
492
- console.log('通过edge-click事件选中的线条数据:', shape);
466
+ console.log("通过edge-click事件选中的线条数据:", shape);
493
467
  graphStore.selectShape(shape);
494
468
  event.stopPropagation();
495
469
  };
@@ -499,12 +473,16 @@ const DRAG_THRESHOLD = 4;
499
473
  const onLayerMouseDown = (evt: MouseEvent) => {
500
474
  return guardOperate(async () => {
501
475
  // 若点击的是名称虚线框/容器,避免触发清选或框选(Pin 的名称可能在外部)
502
- const t = evt.target as HTMLElement | null
503
- if (t && (t.classList?.contains('name-text-box') || t.closest('.name-text-box-container'))) {
476
+ const t = evt.target as HTMLElement | null;
477
+ if (
478
+ t &&
479
+ (t.classList?.contains("name-text-box") ||
480
+ t.closest(".name-text-box-container"))
481
+ ) {
504
482
  // 不改变当前选中;让后续 click 事件去触发 startEditName
505
- evt.stopPropagation()
506
- evt.preventDefault()
507
- return
483
+ evt.stopPropagation();
484
+ evt.preventDefault();
485
+ return;
508
486
  }
509
487
  if (isResizing.value || isEditingName.value) return;
510
488
  if (graphStore.isDragging) graphStore.endDragShape();
@@ -528,11 +506,21 @@ const onLayerMouseDown = (evt: MouseEvent) => {
528
506
  showContextMenu.value = false;
529
507
  }
530
508
 
531
- cursorStyle.value = (hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin") ? "pointer" : "default";
509
+ cursorStyle.value =
510
+ hit.kind === "shape" || hit.kind === "edge" || hit.kind === "pin"
511
+ ? "pointer"
512
+ : "default";
532
513
  // 进入“框选”的条件:
533
- const wantMarquee = evt.shiftKey || hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin";
514
+ const wantMarquee =
515
+ evt.shiftKey ||
516
+ (hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin");
534
517
  if (wantMarquee) {
535
- if ((hit.kind !== "shape" && hit.kind !== "edge" && hit.kind !== "pin") && !evt.shiftKey) {
518
+ if (
519
+ hit.kind !== "shape" &&
520
+ hit.kind !== "edge" &&
521
+ hit.kind !== "pin" &&
522
+ !evt.shiftKey
523
+ ) {
536
524
  graphStore.clearSelection();
537
525
  }
538
526
  startMarquee(pt);
@@ -542,13 +530,17 @@ const onLayerMouseDown = (evt: MouseEvent) => {
542
530
  // 双击触发不同逻辑
543
531
  if (evt.detail === 2 && graphStore.marqueeShapes.length == 1) {
544
532
  const selectedShape = graphStore.selectedShape;
545
- console.log('双击选中的图元:', selectedShape);
533
+ console.log("双击选中的图元:", selectedShape);
546
534
 
547
535
  // 判断是否为Diagram组件
548
- if (selectedShape && selectedShape.shapeType !== 'edge' && props.diagram?.includes(selectedShape.shapeKey)) {
536
+ if (
537
+ selectedShape &&
538
+ selectedShape.shapeType !== "edge" &&
539
+ props.diagram?.includes(selectedShape.shapeKey)
540
+ ) {
549
541
  // Diagram组件的特殊双击逻辑
550
542
  // 这里可以添加你想要的其他逻辑,例如发射自定义事件
551
- emit('diagramDoubleClick', selectedShape);
543
+ emit("diagramDoubleClick", selectedShape);
552
544
  // console.log(selectedShape,'Diagram组件双击事件');
553
545
  // 不打开属性面板
554
546
  } else {
@@ -563,7 +555,12 @@ const onLayerMouseDown = (evt: MouseEvent) => {
563
555
  const { shape } = hit;
564
556
 
565
557
  // 打印选中元素的数据信息 - 确保每次点击都能看到
566
- console.log('点击选中的' + (hit.kind === 'edge' ? '线条' : hit.kind === 'pin' ? 'Pin' : '图元') + '数据信息:', shape);
558
+ console.log(
559
+ "点击选中的" +
560
+ (hit.kind === "edge" ? "线条" : hit.kind === "pin" ? "Pin" : "图元") +
561
+ "数据信息:",
562
+ shape
563
+ );
567
564
 
568
565
  const isMulti = graphStore.selectedIds.length > 1;
569
566
  const clickedInSelection = graphStore.selectedIds.includes(shape.id);
@@ -598,12 +595,25 @@ const onLayerMouseDown = (evt: MouseEvent) => {
598
595
  // 如果是 pin 类型,需要在移动过程中将“指针位置”校正为吸附后的指针坐标
599
596
  let targetPt = curr;
600
597
  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);
598
+ const draggedShape = graphStore.shapes.find(
599
+ (x) => x.id === ids[0]
600
+ );
601
+ if (
602
+ draggedShape &&
603
+ draggedShape.shapeType === "pin" &&
604
+ draggedShape.parenShapeId
605
+ ) {
606
+ const parentShape = graphStore.shapes.find(
607
+ (x) => x.id === draggedShape.parenShapeId
608
+ );
604
609
  if (parentShape) {
605
610
  // 使用移动专用的吸附方法:根据 dragOffset 计算应传入 moveDraggedShape 的指针坐标
606
- targetPt = snapPinPointerOnMove(curr, parentShape, draggedShape, graphStore.dragOffset || undefined);
611
+ targetPt = snapPinPointerOnMove(
612
+ curr,
613
+ parentShape,
614
+ draggedShape,
615
+ graphStore.dragOffset || undefined
616
+ );
607
617
  }
608
618
  }
609
619
  }
@@ -623,7 +633,7 @@ const onLayerMouseDown = (evt: MouseEvent) => {
623
633
  }
624
634
  );
625
635
  }
626
- })
636
+ });
627
637
  };
628
638
  // 框选部分
629
639
  const startMarquee = (anchor: { x: number; y: number }) => {
@@ -695,7 +705,11 @@ const contextMenuTarget = ref<Shape | null>(null);
695
705
  const isConnectAllowed = ref(false);
696
706
 
697
707
  // 高亮图元的边框样式 - 使用工具类实现
698
- const highlightShape = (shape: Shape | null, isHighlight: boolean, isValidSource: boolean = true) => {
708
+ const highlightShape = (
709
+ shape: Shape | null,
710
+ isHighlight: boolean,
711
+ isValidSource: boolean = true
712
+ ) => {
699
713
  highlightUtils.highlightShape(shape, isHighlight, isValidSource);
700
714
  };
701
715
 
@@ -724,7 +738,7 @@ const handleContextMenu = (event: MouseEvent) => {
724
738
  } else {
725
739
  showContextMenu.value = false;
726
740
  }
727
- })
741
+ });
728
742
  };
729
743
 
730
744
  // 关闭右键菜单
@@ -747,35 +761,38 @@ watch(shouldCloseMenu, (shouldClose) => {
747
761
  closeMenu();
748
762
  }
749
763
  });
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;
764
+ watch(
765
+ () => props.actionButtonShapeDataId,
766
+ (newVal) => {
767
+ if (newVal) {
768
+ const foundShape = graphStore.shapes.find((x) => x.id === newVal);
769
+ if (foundShape) {
770
+ // 连接 sourceShape 和新 shape
771
+ if (sourceShape.value) {
772
+ const connectionData = EdgeUtils.completeConnection(
773
+ sourceShape.value,
774
+ foundShape,
775
+ { x: recordClickPoint.value.x, y: recordClickPoint.value.y },
776
+ currentConnectPoint.value,
777
+ graphStore.shapes
778
+ );
779
+
780
+ if (connectionData) {
781
+ emit("connectEnd", connectionData);
782
+ graphStore.setConnectMode("connect");
783
+ isConnecting.value = false;
784
+ highlightUtils.clearHighlightTimeout();
785
+ if (highlightTimeout.value) {
786
+ clearTimeout(highlightTimeout.value);
787
+ highlightTimeout.value = null;
788
+ }
772
789
  }
773
790
  }
774
791
  }
792
+ // 处理 actionButtonShapeData 的逻辑
775
793
  }
776
- // 处理 actionButtonShapeData 的逻辑
777
794
  }
778
- });
795
+ );
779
796
 
780
797
  // 鼠标移动事件处理
781
798
  const handleMouseMove = (event: MouseEvent) => {
@@ -818,21 +835,32 @@ const checkHoverTarget = (x: number, y: number) => {
818
835
  if (hoverShape) {
819
836
  // 检查连接有效性
820
837
  let targetModels = props.connectShapeData?.targetModels;
821
- if (connectMode.value === 'action') {
838
+ if (connectMode.value === "action") {
822
839
  targetModels = props.connectShapeData?.scenarioMenus?.find(
823
840
  (menu) => menu.code === props.connectShapeData?.shapeKey
824
841
  )?.targetModels;
825
842
  }
826
843
  let isAllowed = true;
827
- if (targetModels && Array.isArray(targetModels) && targetModels.length > 0) {
828
- const hoverShapeType = hoverShape.shapeKey || '';
844
+ if (
845
+ targetModels &&
846
+ Array.isArray(targetModels) &&
847
+ targetModels.length > 0
848
+ ) {
849
+ const hoverShapeType = hoverShape.shapeKey || "";
829
850
  isAllowed = targetModels.includes(hoverShapeType);
830
851
  }
831
852
  // 检查parenShapeId是否匹配(无论hoverShape是否改变都要检查)
832
853
  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
854
+ const sourceShape = graphStore.shapes.find(
855
+ (it) => it.id === props.connectShapeData?.sourceId
856
+ );
857
+ if (
858
+ sourceShape &&
859
+ sourceShape.parenShapeId !== hoverShape.parenShapeId &&
860
+ hoverShape.shapeType !== "pin" &&
861
+ sourceShape.shapeType !== "pin"
862
+ ) {
863
+ isAllowed = false;
836
864
  }
837
865
  }
838
866
  // 只有当hoverShape改变且connectShapeData存在时才发射事件
@@ -841,19 +869,25 @@ const checkHoverTarget = (x: number, y: number) => {
841
869
  // 使用更严格的优先级判断逻辑,确保只要有一个属性有实际值就使用它
842
870
  let sourceModelId;
843
871
  // 如果modelId是有效字符串或数字,使用modelId
844
- if (props.connectShapeData.modelId && props.connectShapeData.modelId.toString().trim() !== '') {
872
+ if (
873
+ props.connectShapeData.modelId &&
874
+ props.connectShapeData.modelId.toString().trim() !== ""
875
+ ) {
845
876
  sourceModelId = props.connectShapeData.modelId;
846
877
  }
847
878
  // 否则,如果sourceModelId是有效字符串或数字,使用sourceModelId
848
- else if (props.connectShapeData.sourceModelId && props.connectShapeData.sourceModelId.toString().trim() !== '') {
879
+ else if (
880
+ props.connectShapeData.sourceModelId &&
881
+ props.connectShapeData.sourceModelId.toString().trim() !== ""
882
+ ) {
849
883
  sourceModelId = props.connectShapeData.sourceModelId;
850
884
  }
851
885
  // 只有当sourceModelId有值时就发射事件,并将isAllowed的值一并传递
852
886
  if (sourceModelId) {
853
- eventBus.emit('edge-check', {
887
+ eventBus.emit("edge-check", {
854
888
  sourceModelId: sourceModelId, // 显式指定键值对,避免属性简写可能带来的混淆
855
889
  targetModelId: hoverShape.modelId,
856
- isAllowed: isAllowed // 将验证结果一并发射
890
+ isAllowed: isAllowed, // 将验证结果一并发射
857
891
  });
858
892
  }
859
893
  }
@@ -919,7 +953,11 @@ watch(
919
953
  () => props.edgeCheck,
920
954
  (newEdgeCheck) => {
921
955
  // 只有在连接状态下且有高亮图元时才更新
922
- if (isConnecting.value && highlightedShape.value && newEdgeCheck !== undefined) {
956
+ if (
957
+ isConnecting.value &&
958
+ highlightedShape.value &&
959
+ newEdgeCheck !== undefined
960
+ ) {
923
961
  // 使用后端验证结果更新高亮颜色
924
962
  highlightShape(highlightedShape.value, true, newEdgeCheck);
925
963
  }
@@ -933,7 +971,7 @@ const handleConnectLayerClick = (event: MouseEvent) => {
933
971
  const hit = pickTarget(graphStore.shapes, localPoint);
934
972
 
935
973
  if (
936
- ['shape', 'pin'].includes(hit.kind) &&
974
+ ["shape", "pin"].includes(hit.kind) &&
937
975
  hit.shape?.id &&
938
976
  props.connectShapeData?.sourceId &&
939
977
  hit.shape?.id !== props.connectShapeData.sourceId
@@ -956,14 +994,20 @@ const completeConnection = (
956
994
  if (!isConnectAllowed.value) {
957
995
  isConnecting.value = false;
958
996
  highlightShape(null, false); // 取消图元高亮
959
- graphStore.setConnectMode('connect')
997
+ graphStore.setConnectMode("connect");
960
998
  highlightUtils.clearHighlightTimeout();
961
- ElMessage.error('当前目标图元类型不符合连接要求');
999
+ ElMessage.error("当前目标图元类型不符合连接要求");
962
1000
  return;
963
1001
  }
964
1002
 
965
1003
  // 嵌套情况下只能连接同一个父图元
966
- if (sourceShape && clickedShape && sourceShape.parenShapeId !== clickedShape.parenShapeId && (clickedShape.shapeType !== 'pin' && sourceShape.shapeType !== 'pin')) {
1004
+ if (
1005
+ sourceShape &&
1006
+ clickedShape &&
1007
+ sourceShape.parenShapeId !== clickedShape.parenShapeId &&
1008
+ clickedShape.shapeType !== "pin" &&
1009
+ sourceShape.shapeType !== "pin"
1010
+ ) {
967
1011
  isConnecting.value = false;
968
1012
  highlightShape(null, false); // 取消图元高亮
969
1013
  highlightUtils.clearHighlightTimeout();
@@ -972,33 +1016,32 @@ const completeConnection = (
972
1016
  // 检查目标图元类型是否符合targetModels要求
973
1017
  const targetModels = props.connectShapeData?.targetModels;
974
1018
  if (targetModels && Array.isArray(targetModels) && targetModels.length > 0) {
975
-
976
- const clickedShapeType = clickedShape.shapeKey || '';
1019
+ const clickedShapeType = clickedShape.shapeKey || "";
977
1020
  if (!targetModels.includes(clickedShapeType)) {
978
1021
  isConnecting.value = false;
979
1022
  highlightShape(null, false); // 取消图元高亮
980
1023
  highlightUtils.clearHighlightTimeout();
981
1024
  // alert('当前目标图元类型不符合连接要求');
982
- ElMessage.error('当前目标图元类型不符合连接要求');
1025
+ ElMessage.error("当前目标图元类型不符合连接要求");
983
1026
  return;
984
1027
  }
985
1028
  }
986
1029
 
987
1030
  // 检查是否已存在相同类型的边
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
1031
+ const existingEdge = graphStore.shapes.find(
1032
+ (shape: Shape) =>
1033
+ shape.shapeType === "edge" &&
1034
+ shape.sourceId === props.connectShapeData?.sourceId &&
1035
+ shape.targetId === clickedShape.id &&
1036
+ shape.shapeKey === props.connectShapeData?.shapeKey
993
1037
  );
994
1038
 
995
-
996
1039
  // 如果边已存在,错误提示并返回
997
1040
  if (existingEdge) {
998
1041
  // alert('同类型的边已经存在,不能重复添加');
999
- ElMessage.error('同类型的边已经存在,不能重复添加');
1042
+ ElMessage.error("同类型的边已经存在,不能重复添加");
1000
1043
  isConnecting.value = false;
1001
- graphStore.setConnectMode('connect')
1044
+ graphStore.setConnectMode("connect");
1002
1045
  highlightShape(null, false); // 取消图元高亮
1003
1046
  highlightUtils.clearHighlightTimeout();
1004
1047
  return;
@@ -1013,22 +1056,27 @@ const completeConnection = (
1013
1056
  );
1014
1057
  if (connectionData) {
1015
1058
  // ServiceObjectFlow 特殊处理:需要在 sourcePoint 和 targetPoint 创建 pin
1016
- if (props.connectShapeData?.shapeKey === 'ServiceObjectFlow' && sourceShape && connectionData.sourcePoint && connectionData.targetPoint) {
1059
+ if (
1060
+ props.connectShapeData?.shapeKey?.toLowerCase().includes("objectflow") &&
1061
+ sourceShape &&
1062
+ connectionData.sourcePoint &&
1063
+ connectionData.targetPoint
1064
+ ) {
1017
1065
  const result = EdgeUtils.handleServiceObjectFlowConnection(
1018
1066
  sourceShape,
1019
1067
  clickedShape,
1020
1068
  connectionData
1021
1069
  );
1022
- emit('objectFlowConnectEnd', {
1070
+ emit("objectFlowConnectEnd", {
1023
1071
  connectionData: result.connectionData,
1024
1072
  outputPinBounds: result.outputPinBounds,
1025
- inputPinBounds: result.inputPinBounds
1073
+ inputPinBounds: result.inputPinBounds,
1026
1074
  });
1027
1075
  } else {
1028
- (connectionData as any).sourceShape = sourceShape
1029
- emit('connectEnd', connectionData);
1076
+ (connectionData as any).sourceShape = sourceShape;
1077
+ emit("connectEnd", connectionData);
1030
1078
  }
1031
- graphStore.setConnectMode('connect')
1079
+ graphStore.setConnectMode("connect");
1032
1080
  isConnecting.value = false;
1033
1081
  highlightShape(null, false); // 取消图元高亮
1034
1082
  highlightUtils.clearHighlightTimeout();
@@ -1058,7 +1106,8 @@ const initializeConnectPoint = () => {
1058
1106
  props.diagramBounds // 传递图表边界
1059
1107
  );
1060
1108
 
1061
- if (initialPoint && sourceShape) { // 确保sourceShape存在,防止从工具栏拖拽时错误初始化
1109
+ if (initialPoint && sourceShape) {
1110
+ // 确保sourceShape存在,防止从工具栏拖拽时错误初始化
1062
1111
  currentConnectPoint.value = initialPoint;
1063
1112
 
1064
1113
  if (!isConnecting.value) {
@@ -1164,7 +1213,7 @@ const cancelConnection = () => {
1164
1213
  mousePosition,
1165
1214
  targetConnectPoint,
1166
1215
  targetShape,
1167
- showLine
1216
+ showLine,
1168
1217
  },
1169
1218
  highlightUtils
1170
1219
  );
@@ -1183,7 +1232,7 @@ const externalCreateDragState: ExternalCreateDragState = {
1183
1232
  creatingId: null,
1184
1233
  pendingShape: null,
1185
1234
  isDragging: false,
1186
- isCheckInFlight: false
1235
+ isCheckInFlight: false,
1187
1236
  };
1188
1237
 
1189
1238
  const resetExternalCreateDragState = () => {
@@ -1198,7 +1247,7 @@ const resetExternalCreateDragState = () => {
1198
1247
  const cleanupInertShapes = async () => {
1199
1248
  const arr = graphStore.shapes as any[];
1200
1249
  for (let i = arr.length - 1; i >= 0; i--) {
1201
- if ('inert' in arr[i]) {
1250
+ if ("inert" in arr[i]) {
1202
1251
  arr.splice(i, 1);
1203
1252
  }
1204
1253
  }
@@ -1206,99 +1255,140 @@ const cleanupInertShapes = async () => {
1206
1255
  };
1207
1256
 
1208
1257
  //拖动中添加元素并触发嵌套逻辑,
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)
1258
+ const continueExternalCreateDrag = async (payload: {
1259
+ clientX: number;
1260
+ clientY: number;
1261
+ shapeData?: any;
1262
+ }) => {
1263
+ externalCreateDragState.isDragging = true;
1264
+ isExternalCreateDragging.value = true;
1265
+ const pt = clientToLocalPoint(
1266
+ payload.clientX,
1267
+ payload.clientY,
1268
+ layerRef.value
1269
+ );
1213
1270
  if (payload.shapeData) {
1214
- const s = payload.shapeData
1215
- const isCmp = isCompartment(s as Shape)
1271
+ const s = payload.shapeData;
1272
+ const isCmp = isCompartment(s as Shape);
1216
1273
  externalCreateDragState.pendingShape = {
1217
1274
  ...s,
1218
1275
  bounds: {
1219
1276
  x: pt.x,
1220
1277
  y: pt.y,
1221
1278
  width: s.bounds?.width ?? 180,
1222
- height: isCmp ? 120 : (s.bounds?.height ?? 80),
1279
+ height: isCmp ? 120 : s.bounds?.height ?? 80,
1223
1280
  },
1224
1281
  inert: false,
1225
- }
1282
+ };
1226
1283
  }
1227
- if (!externalCreateDragState.creatingId && externalCreateDragState.pendingShape && isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)) {
1228
- const draft = externalCreateDragState.pendingShape
1284
+ if (
1285
+ !externalCreateDragState.creatingId &&
1286
+ externalCreateDragState.pendingShape &&
1287
+ isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)
1288
+ ) {
1289
+ const draft = externalCreateDragState.pendingShape;
1229
1290
  try {
1230
- graphStore.addShape(draft)
1231
- graphStore.startDrag([draft.id], pt)
1232
- externalCreateDragState.creatingId = draft.id
1233
- externalCreateDragState.pendingShape = null
1291
+ graphStore.addShape(draft);
1292
+ graphStore.startDrag([draft.id], pt);
1293
+ externalCreateDragState.creatingId = draft.id;
1294
+ externalCreateDragState.pendingShape = null;
1234
1295
  } finally {
1235
- externalCreateDragState.isCheckInFlight = false
1296
+ externalCreateDragState.isCheckInFlight = false;
1236
1297
  }
1237
1298
  }
1238
- externalCreateDragState.pendingShape = null
1299
+ externalCreateDragState.pendingShape = null;
1239
1300
 
1240
- if (!externalCreateDragState.creatingId) return
1301
+ if (!externalCreateDragState.creatingId) return;
1241
1302
 
1242
- let targetPt = pt
1243
- const s = graphStore.shapes.find(x => x.id === externalCreateDragState.creatingId)
1244
- if (s && s.shapeType === 'pin') {
1303
+ let targetPt = pt;
1304
+ const s = graphStore.shapes.find(
1305
+ (x) => x.id === externalCreateDragState.creatingId
1306
+ );
1307
+ if (s && s.shapeType === "pin") {
1245
1308
  if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1246
- const parent = graphStore.shapes.find(x => x.id === graphStore.hoverContainerId)
1309
+ const parent = graphStore.shapes.find(
1310
+ (x) => x.id === graphStore.hoverContainerId
1311
+ );
1247
1312
  if (parent) {
1248
- const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(pt, parent, s)
1249
- targetPt = { x: adjustedX, y: adjustedY }
1313
+ const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(
1314
+ pt,
1315
+ parent,
1316
+ s
1317
+ );
1318
+ targetPt = { x: adjustedX, y: adjustedY };
1250
1319
  }
1251
1320
  }
1252
1321
  }
1253
1322
 
1254
- graphStore.moveDraggedShape(targetPt)
1255
- }
1323
+ graphStore.moveDraggedShape(targetPt);
1324
+ };
1256
1325
 
1257
1326
  //拖拽结束后重新发送事件到front中调用接口
1258
- const finishExternalCreateDrag = async (payload: { clientX: number; clientY: number }) => {
1327
+ const finishExternalCreateDrag = async (payload: {
1328
+ clientX: number;
1329
+ clientY: number;
1330
+ }) => {
1259
1331
  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),
1332
+ if (!externalCreateDragState.creatingId) return;
1333
+ const pt = clientToLocalPoint(
1334
+ payload.clientX,
1335
+ payload.clientY,
1336
+ layerRef.value
1337
+ );
1338
+ if (
1339
+ isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)
1340
+ ) {
1341
+ graphStore.moveDraggedShape(pt);
1342
+ await nextTick();
1343
+ const s: any = (graphStore.shapes || []).find(
1344
+ (x: any) => x.id == externalCreateDragState.creatingId
1345
+ );
1346
+ if ((s && s.shapeType == "shape") || s.shapeType == "pin") {
1347
+ const pure = _.omit(s, ["inert"]);
1348
+ pure.bounds = {
1349
+ // 覆盖为新的 bounds
1350
+ ...pure.bounds,
1270
1351
  x: pt.x,
1271
1352
  y: pt.y,
1272
- }
1353
+ };
1273
1354
  // 先推断这次 drop 的“候选父节点”
1274
- let parent: Shape | null = null
1355
+ let parent: Shape | null = null;
1275
1356
  if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1276
- parent = (graphStore.shapes as any[]).find(
1277
- (x: any) => x.id === graphStore.hoverContainerId
1278
- ) || null
1357
+ parent =
1358
+ (graphStore.shapes as any[]).find(
1359
+ (x: any) => x.id === graphStore.hoverContainerId
1360
+ ) || null;
1279
1361
  }
1280
1362
  // 如果是 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
1363
+ if (pure.shapeType === "pin" && parent) {
1364
+ const { x: adjustedX, y: adjustedY } = snapPinToParentEdge(
1365
+ pt,
1366
+ parent,
1367
+ pure
1368
+ );
1369
+ pure.bounds.x = adjustedX;
1370
+ pure.bounds.y = adjustedY;
1285
1371
  pure.parenShapeId = parent.id;
1286
1372
  // 将吸附后的坐标同步回 ghost
1287
- graphStore.moveDraggedShape({ x: adjustedX, y: adjustedY })
1373
+ graphStore.moveDraggedShape({ x: adjustedX, y: adjustedY });
1288
1374
  }
1289
1375
  try {
1290
- const { ok } = await checkNestViaFront(pure as Shape, parent, graphStore.shapes[0])
1376
+ const { ok } = await checkNestViaFront(
1377
+ pure as Shape,
1378
+ parent,
1379
+ graphStore.shapes[0]
1380
+ );
1291
1381
  if (!ok) {
1292
- graphStore.setHoverState(null, false)
1293
- await cleanupInertShapes()
1294
- resetExternalCreateDragState()
1295
- return
1382
+ graphStore.setHoverState(null, false);
1383
+ await cleanupInertShapes();
1384
+ resetExternalCreateDragState();
1385
+ return;
1296
1386
  } else {
1297
- graphStore.canDropOnCanvas = true
1387
+ graphStore.canDropOnCanvas = true;
1298
1388
  }
1299
1389
  } catch (error) {
1300
- await cleanupInertShapes()
1301
- resetExternalCreateDragState()
1390
+ await cleanupInertShapes();
1391
+ resetExternalCreateDragState();
1302
1392
  }
1303
1393
  // 同步对外通知
1304
1394
  // 先构造基础数据
@@ -1311,70 +1401,92 @@ const finishExternalCreateDrag = async (payload: { clientX: number; clientY: num
1311
1401
  type: s.type,
1312
1402
  nodeType: s.shapeKey,
1313
1403
  icon: s.icon,
1314
- }
1404
+ };
1315
1405
  // 如果是 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
1406
+ if (pure.shapeType === "pin" && parent) {
1407
+ const adjustedClientPt = localToClientPoint(
1408
+ pure.bounds.x,
1409
+ pure.bounds.y,
1410
+ layerRef.value
1411
+ );
1412
+ payloadData.coordinate.clientX = adjustedClientPt.clientX;
1413
+ payloadData.coordinate.clientY = adjustedClientPt.clientY;
1320
1414
  }
1321
1415
  // 如果当前图元的shapeKey存在于ownerRequiredShapeKeys当中,再补 ownerId 字段
1322
1416
  // @todo OperationalPort合并到ownerRequiredShapeKeys
1323
1417
  if (graphStore.ownerRequiredShapeKeys.includes(pure.shapeKey)) {
1324
- payloadData.ownerId = parent?.modelId
1418
+ payloadData.ownerId = parent?.modelId;
1325
1419
  }
1326
1420
  // 发送事件
1327
1421
  await new Promise<void>((resolve, reject) => {
1328
- eventBus.emit('addShape', {
1422
+ eventBus.emit("addShape", {
1329
1423
  ...payloadData,
1330
1424
  resolve,
1331
1425
  reject,
1332
- })
1333
- })
1334
- if (pure.shapeType === 'pin') {
1335
- graphStore.endDragShape('pinDrop')
1426
+ });
1427
+ });
1428
+ if (pure.shapeType === "pin") {
1429
+ graphStore.endDragShape("pinDrop");
1336
1430
  } else {
1337
- graphStore.endDragShape('addEntity') // 提交拖拽(触发嵌套/吸附等 finalize)
1431
+ graphStore.endDragShape("addEntity"); // 提交拖拽(触发嵌套/吸附等 finalize)
1338
1432
  }
1339
1433
  }
1340
1434
  }
1341
1435
  } catch (error) {
1342
- graphStore.setHoverState(null, false)
1343
- await cleanupInertShapes()
1436
+ graphStore.setHoverState(null, false);
1437
+ await cleanupInertShapes();
1344
1438
  } finally {
1345
- await nextTick()
1346
- await cleanupInertShapes()
1347
- graphStore.setHoverState(null, false)
1348
- resetExternalCreateDragState()
1439
+ await nextTick();
1440
+ await cleanupInertShapes();
1441
+ graphStore.setHoverState(null, false);
1442
+ resetExternalCreateDragState();
1349
1443
  }
1350
- }
1444
+ };
1351
1445
  // 根据 graphStore.canDropOnCanvas 决定是否显示禁用放置图标
1352
1446
  const onCanvasDragOver = (e: DragEvent) => {
1353
- e.preventDefault() // 必须,否则不会触发 drop
1354
- if (!e.dataTransfer) return
1447
+ e.preventDefault(); // 必须,否则不会触发 drop
1448
+ if (!e.dataTransfer) return;
1355
1449
  if (graphStore.canDropOnCanvas) {
1356
- e.dataTransfer.dropEffect = 'copy' // 显示带 + 的复制光标
1450
+ e.dataTransfer.dropEffect = "copy"; // 显示带 + 的复制光标
1357
1451
  } else {
1358
- e.dataTransfer.dropEffect = 'none' //显示禁止的圈圈光标
1452
+ e.dataTransfer.dropEffect = "none"; //显示禁止的圈圈光标
1359
1453
  }
1360
- }
1454
+ };
1361
1455
 
1362
1456
  const onCanvasDrop = (e: DragEvent) => {
1363
1457
  if (!graphStore.canDropOnCanvas) {
1364
1458
  // 不允许丢,直接 return
1365
- return
1459
+ return;
1366
1460
  }
1367
- }
1461
+ };
1462
+
1368
1463
  // 创建键盘事件处理器
1369
1464
  const keyboardHandler = createKeyboardHandler({
1370
1465
  onDelete: () => { },
1371
1466
  onEditProperty: () => onLayerDblClick(true),
1372
1467
  onCancelConnection: cancelConnection,
1373
- isEditingName: () => isEditingName.value
1468
+ isEditingName: () => isEditingName.value,
1469
+ onCopy: () => {
1470
+ // 获取当前选中的图元
1471
+ const selectedShapes = graphStore.selectedIds
1472
+ .map((id) => graphStore.shapes.find((s) => s.id === id))
1473
+ .filter(Boolean) as Shape[];
1474
+
1475
+ if (selectedShapes.length === 0) {
1476
+ return;
1477
+ }
1478
+
1479
+ // 使用ContextMenuUtils存储复制的图元
1480
+ ContextMenuUtils.setCopiedShapes(selectedShapes);
1481
+ },
1482
+ onPaste: () => {
1483
+ // 使用ContextMenuUtils处理粘贴
1484
+ ContextMenuUtils.handlePaste(graphStore.selectedShape);
1485
+ },
1374
1486
  });
1375
1487
 
1376
1488
  onMounted(() => {
1377
- window.addEventListener('keydown', keyboardHandler);
1489
+ window.addEventListener("keydown", keyboardHandler);
1378
1490
  // 添加连接层相关的事件监听
1379
1491
  if (props.connectShapeData) {
1380
1492
  document.addEventListener("mousemove", handleMouseMove);
@@ -1383,17 +1495,30 @@ onMounted(() => {
1383
1495
  }
1384
1496
 
1385
1497
  //拖动结束后更新情景菜单
1386
- eventBus.on('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
1498
+ eventBus.on("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1499
+
1500
+ // 监听粘贴图元事件
1501
+ eventBus.on("paste-shapes", (shapes: Shape[]) => {
1502
+ shapes.forEach((shape) => {
1503
+ graphStore.addShape(shape);
1504
+ });
1505
+ });
1506
+
1507
+ // 监听选择图元事件
1508
+ eventBus.on("select-shapes", (ids: string[]) => {
1509
+ graphStore.clearSelection();
1510
+ graphStore.selectMany(ids);
1511
+ });
1387
1512
  });
1388
1513
  onUnmounted(() => {
1389
1514
  offDrag?.();
1390
1515
  // 清理连接层相关的事件监听
1391
1516
  document.removeEventListener("mousemove", handleMouseMove);
1392
1517
  document.removeEventListener("mouseleave", handleMouseLeave);
1393
- window.removeEventListener('keydown', keyboardHandler);
1518
+ window.removeEventListener("keydown", keyboardHandler);
1394
1519
  // 重置名称编辑状态
1395
1520
  nameEditManager.reset();
1396
- eventBus.off('shape-drag-end-updateScenarioMenu', actionButtonsStyle)
1521
+ eventBus.off("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1397
1522
  highlightUtils.clearHighlightTimeout();
1398
1523
  // 清理高亮工具实例
1399
1524
  highlightUtils.dispose();
@@ -1404,7 +1529,7 @@ defineExpose({
1404
1529
  finishExternalCreateDrag,
1405
1530
  handleEdgeClick,
1406
1531
  getBoundingClientRect: () => layerRef.value?.getBoundingClientRect(),
1407
- })
1532
+ });
1408
1533
  </script>
1409
1534
 
1410
1535
  <style scoped>
@@ -1416,22 +1541,11 @@ defineExpose({
1416
1541
  height: 100%;
1417
1542
  /* 应用与画布相同的缩放变换 */
1418
1543
  transform-origin: 0 0;
1419
- transform: scale(v-bind('graphStore.currentScale'));
1544
+ transform: scale(v-bind("graphStore.currentScale"));
1420
1545
  pointer-events: all;
1421
1546
  z-index: 999;
1422
1547
  }
1423
1548
 
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
1549
  .hover-container-outline {
1436
1550
  position: absolute;
1437
1551
  pointer-events: none;
@@ -1450,122 +1564,7 @@ defineExpose({
1450
1564
  background: rgba(240, 237, 237, 0.842);
1451
1565
  }
1452
1566
 
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
- }
1567
+ /* 删除了选择框、调整大小手柄和操作按钮相关的样式,这些样式已经移到了 SelectionBox 组件中 */
1569
1568
 
1570
1569
  .resize-ghost {
1571
1570
  position: absolute;
@@ -1610,9 +1609,4 @@ defineExpose({
1610
1609
  pointer-events: none;
1611
1610
  z-index: 1000;
1612
1611
  }
1613
-
1614
- .border-btn {
1615
- padding-bottom: 4px;
1616
- border-bottom: 1px solid #e0e0e0;
1617
- }
1618
1612
  </style>