@mx-sose-front/mx-sose-graph 1.1.3 → 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 (46) hide show
  1. package/dist/index.d.ts +177 -9
  2. package/dist/index.esm.js +4569 -63119
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.umd.js +1 -39
  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 +10 -10
  9. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
  10. package/src/components/InteractionLayer.vue +323 -157
  11. package/src/components/LineStyle/LineStyleMarker.vue +1 -1
  12. package/src/components/{NameEditor.vue → NameEditor/NameEditor.vue} +4 -4
  13. package/src/components/{SelectionBox.vue → SelectionBox/SelectionBox.vue} +5 -5
  14. package/src/components/Shape/Block.vue +1 -1
  15. package/src/constants/edgeShapeKeys.ts +43 -3
  16. package/src/constants/index.ts +19 -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 +106 -147
  35. package/src/utils/drag.ts +10 -5
  36. package/src/utils/edgeUtils.ts +3 -4
  37. package/src/utils/graphDragService.ts +27 -23
  38. package/src/utils/iconLoader.ts +7 -7
  39. package/src/utils/keyboardUtils.ts +195 -32
  40. package/src/utils/pinUtils.ts +1 -2
  41. package/src/utils/shapeOps/shapeOps.ts +168 -0
  42. package/src/utils/viewportCulling.ts +193 -0
  43. package/src/view/graph.vue +115 -60
  44. package/src/utils/highlightUtils.ts +0 -162
  45. package/src/utils/nameEditUtils.ts +0 -137
  46. /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
@@ -5,9 +5,11 @@
5
5
  @mousedown="onLayerMouseDown" @mouseup="onLayerMouseUp" @click="onLayerClick"
6
6
  @contextmenu.prevent="handleContextMenu">
7
7
  <!-- 只在"选中对象是画布(diagram)"时显示四个角手柄 -->
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" />
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>
11
13
  <!-- 命中容器的高亮矩形(虚线框) -->
12
14
  <div v-if="hoverRect" :class="[
13
15
  'hover-container-outline',
@@ -16,13 +18,14 @@
16
18
  'is-valid': graphStore.hoverNestable === true,
17
19
  },
18
20
  ]" :style="{
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
- }" />
21
+ left: hoverRect.x - 5 + 'px',
22
+ top: hoverRect.y - 5 + 'px',
23
+ width: hoverRect.width + 10 + 'px',
24
+ height: hoverRect.height + 10 + 'px',
25
+ }" />
24
26
  <!-- 框选预览矩形 -->
25
- <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 })" />
26
29
  <!-- 拖动和缩放的预览框 -->
27
30
  <component v-for="g in allGhosts" :key="g.id" class="ghost-shape" :is="getShapeComponent(g)" :shape="g"
28
31
  :style="getGhostShapeStyle(g)" />
@@ -40,12 +43,20 @@
40
43
  <!-- 连接层逻辑 - 当 connectShapeData 存在时显示 -->
41
44
  <div v-if="connectShapeData && diagramBounds" class="connect-layer" :style="layerStyle">
42
45
  <!-- 连线起点的黑点 - 只在连接时显示 -->
43
- <div v-if="isConnecting" class="connect-dot-direct" :style="dotStyle"></div>
46
+ <div v-show="isConnecting" class="connect-dot-direct" :style="dotStyle"></div>
44
47
 
45
48
  <!-- 连线 -->
46
- <ConnectionLine v-if="showLine" :show-line="showLine" :points="linePoints" :shape-key="connectShapeData?.shapeKey"
47
- :style="svgStyle" />
49
+ <ConnectionLine v-show="showLine" :show-line="showLine" :points="linePoints"
50
+ :shape-key="connectShapeData?.shapeKey" :style="svgStyle" />
48
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
+ }" />
49
60
  </div>
50
61
  </template>
51
62
 
@@ -67,8 +78,8 @@ import type {
67
78
  ExternalCreateDragState,
68
79
  } from "../types/interactionLayer";
69
80
  import { useGraphStore } from "../store/graphStore";
70
- import SelectionBox from "./SelectionBox.vue";
71
- import NameEditor from "./NameEditor.vue";
81
+ import SelectionBox from "./SelectionBox/SelectionBox.vue";
82
+ import NameEditor from "./NameEditor/NameEditor.vue";
72
83
 
73
84
  // 工具:几何/命中/样式/拖拽
74
85
  import {
@@ -99,16 +110,16 @@ import { storeToRefs } from "pinia";
99
110
  import { EdgeUtils } from "../utils/edgeUtils";
100
111
  import ConnectionLine from "./LineStyle/ConnectionLine.vue";
101
112
  import { isCompartment } from "../utils/compartment";
102
- import { HighlightUtils } from "../utils/highlightUtils";
103
113
  import { ContextMenuUtils } from "../utils/contextMenuUtils";
104
114
  // 静态导入图片资源
105
115
  import { getUuid } from "../utils/index";
106
116
  import { ElMessage } from "element-plus";
107
117
  import { snapPinToParentEdge, snapPinPointerOnMove } from "../utils/pinUtils";
108
118
  import { createKeyboardHandler } from "../utils/keyboardUtils";
109
- import { NameEditManager } from "../utils/nameEditUtils";
119
+ import { useNameEdit } from "../hooks/useNameEdit";
110
120
  import { guardOperate } from "../utils/license-guard";
111
- import { createResizeUtils } from "../utils/resizeUtils";
121
+ import { useResize } from "../hooks/useResize";
122
+ import { useHighlight, type IHighlightUtils } from "../hooks/useHighlight";
112
123
 
113
124
  const props = defineProps<InteractionLayerProps>();
114
125
 
@@ -177,17 +188,19 @@ const getGhostShapeStyle = (shape: Shape): CSSProperties => {
177
188
  // 根层引用:用于本地坐标换算
178
189
  const layerRef = ref<HTMLDivElement | null>(null);
179
190
 
180
- // 名称编辑管理器
181
- const nameEditManager = new NameEditManager({
182
- onNameChange: (oldName, newName) => {
183
- if (graphStore.selectedShape) {
184
- emit("editName", graphStore.selectedShape, newName, oldName);
185
- }
191
+ // 名称编辑 - 使用 Composable
192
+ const nameEditManager = useNameEdit({
193
+ getSelectedShape: () => graphStore.selectedShape,
194
+ onNameChange: (shape, oldName, newName) => {
195
+ emit("editName", shape, newName, oldName);
186
196
  },
187
197
  });
188
198
 
189
- // 缩放工具实例
190
- const resizeUtils = createResizeUtils(
199
+ // 解构名称编辑状态(方便在组件内使用)
200
+ const { isEditingName, editingName } = nameEditManager;
201
+
202
+ // 缩放 - 使用 Composable
203
+ const { isResizing, groupGhost, startResize } = useResize(
191
204
  layerRef,
192
205
  {
193
206
  packages: props.packages,
@@ -207,21 +220,6 @@ const resizeUtils = createResizeUtils(
207
220
  }
208
221
  );
209
222
 
210
- // 解构缩放相关的变量
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
- });
222
-
223
- // 从名称编辑管理器获取响应式状态
224
- const { isEditingName, editingName } = nameEditManager.editingState;
225
223
  // 是否在画布内
226
224
  const isMouseInside = ref(false);
227
225
  // 记录最近一次 mousemove(用于 rAF 合并)
@@ -289,15 +287,29 @@ const isConnecting = ref(false); // 是否正在连接状态
289
287
  const targetConnectPoint = ref({ x: 0, y: 0 }); // 目标连接点
290
288
  const targetShape = ref<Shape | null>(null); // 目标图形
291
289
 
292
- // 高亮工具实例
293
- 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
+ };
294
305
 
295
- // 高亮相关状态 - 使用计算属性从工具类获取
296
- const highlightedShape = computed(() => highlightUtils.getHighlightedShape());
306
+ // 保持兼容性:highlightedShape 计算属性
307
+ const highlightedShape = computed(() => {
308
+ if (!highlightedShapeId.value) return null;
309
+ return graphStore.shapes.find(s => s.id === highlightedShapeId.value) || null;
310
+ });
297
311
  const sourceShape = ref<Shape | null>(null);
298
312
  const recordClickPoint = ref({ x: 0, y: 0 });
299
- // 高亮相关状态
300
- const highlightTimeout = ref<ReturnType<typeof setTimeout> | null>(null); // 高亮定时器
301
313
 
302
314
  // 正在交互:缩放中 或 元素拖动中
303
315
  const isBusy = computed(
@@ -405,7 +417,7 @@ const onLayerClick = (evt: MouseEvent) => {
405
417
  isConnecting.value = false;
406
418
  graphStore.setConnectMode("connect");
407
419
  highlightShape(null, false); // 取消图元高亮
408
- highlightUtils.clearHighlightTimeout();
420
+ clearHighlightTimeout();
409
421
  return;
410
422
  }
411
423
  const newShape = _.cloneDeep(foundSourceShape);
@@ -522,6 +534,8 @@ const onLayerMouseDown = (evt: MouseEvent) => {
522
534
  !evt.shiftKey
523
535
  ) {
524
536
  graphStore.clearSelection();
537
+ // 点击空白处时,清除剪切状态
538
+ ContextMenuUtils.clearCutState();
525
539
  }
526
540
  startMarquee(pt);
527
541
  evt.preventDefault();
@@ -571,6 +585,8 @@ const onLayerMouseDown = (evt: MouseEvent) => {
571
585
  // 其他情况:单选当前元素
572
586
  graphStore.clearSelection();
573
587
  graphStore.selectShape(shape);
588
+ // 选中其他图元时,清除剪切状态
589
+ ContextMenuUtils.clearCutState();
574
590
  }
575
591
  // 选区此时已是“正确”的:要么多选集合,要么当前单选
576
592
  const ids = graphStore.selectedIds.length
@@ -617,12 +633,15 @@ const onLayerMouseDown = (evt: MouseEvent) => {
617
633
  }
618
634
  }
619
635
  }
620
- graphStore.moveDraggedShape(targetPt);
636
+ // pin 拖动:targetPt 用于落位/ghost,hover 命中用真实鼠标点,避免 hoverContainerId 来回切换闪动
637
+ // graphStore.moveDraggedShape(targetPt, { hitPointer: curr });
638
+ scheduleMoveDraggedShape(targetPt, { hitPointer: curr });
621
639
  }
622
640
  },
623
641
  () => {
624
642
  // 只有在“真的开始拖拽”后,才结束拖拽
625
643
  if (started) {
644
+ flushMoveDraggedShape();
626
645
  graphStore.endDragShape();
627
646
  } else {
628
647
  // 纯点击:啥也不做(已完成选中),避免误触发 reparent/zIndex
@@ -656,6 +675,22 @@ const startMarquee = (anchor: { x: number; y: number }) => {
656
675
  width: 0,
657
676
  height: 0,
658
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
+ };
659
694
  // 拖拽生命周期
660
695
  offDrag?.();
661
696
  offDrag = null;
@@ -665,28 +700,49 @@ const startMarquee = (anchor: { x: number; y: number }) => {
665
700
  // 当前指针的本地坐标
666
701
  const currRaw = toLocalPoint(e, layerRef.value);
667
702
  // 只对左/上做夹取(右/下不限制)
668
- const currClamped = clampPointToRect(
703
+ latestPoint = clampPointToRect(
669
704
  currRaw,
670
705
  { x: 0, y: 0, width: 0, height: 0 },
671
706
  container,
672
707
  edges
673
708
  );
709
+
674
710
  // 根据“锚点 & 当前点”得到框选矩形(会自动处理反向拖拽)
675
- const rect = rectFromPoints(marqueeAnchor.value!, currClamped);
676
- marqueeRect.value = rect;
677
- // 只选“完全位于框内”的图元(排除 diagram)
678
- const ids = graphStore.shapes
679
- .filter((s) => s.shapeType?.toLowerCase?.() !== "diagram")
680
- .filter((s) => rectContainsRect(rect, getBounds(s)))
681
- .map((s) => s.id);
682
- // 多选:所有被框中的图元都进入选中态
683
- graphStore.selectMany(ids);
711
+ // rAF 合并更新,避免 mousemove 触发过多响应式刷新
712
+ if (raf) return;
713
+ raf = requestAnimationFrame(updateRect);
684
714
  },
685
715
  // 结束框选,清理预览
686
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
+ // 清理预览
687
742
  marqueeAnchor.value = null;
688
743
  marqueeRect.value = null;
689
744
  cursorStyle.value = "default";
745
+ offDrag = null;
690
746
  }
691
747
  );
692
748
  };
@@ -704,15 +760,6 @@ const contextMenuTarget = ref<Shape | null>(null);
704
760
  // 是否允许连接当前高亮的图元
705
761
  const isConnectAllowed = ref(false);
706
762
 
707
- // 高亮图元的边框样式 - 使用工具类实现
708
- const highlightShape = (
709
- shape: Shape | null,
710
- isHighlight: boolean,
711
- isValidSource: boolean = true
712
- ) => {
713
- highlightUtils.highlightShape(shape, isHighlight, isValidSource);
714
- };
715
-
716
763
  // 处理右键点击事件
717
764
  const handleContextMenu = (event: MouseEvent) => {
718
765
  return guardOperate(async () => {
@@ -728,7 +775,8 @@ const handleContextMenu = (event: MouseEvent) => {
728
775
  graphStore.shapes,
729
776
  graphStore.selectShape,
730
777
  graphStore.isDragging,
731
- isResizing.value
778
+ isResizing.value,
779
+ graphStore.currentScale // 传入当前缩放比例
732
780
  );
733
781
 
734
782
  if (hitShape) {
@@ -778,14 +826,32 @@ watch(
778
826
  );
779
827
 
780
828
  if (connectionData) {
781
- emit("connectEnd", 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
+ }
782
852
  graphStore.setConnectMode("connect");
783
853
  isConnecting.value = false;
784
- highlightUtils.clearHighlightTimeout();
785
- if (highlightTimeout.value) {
786
- clearTimeout(highlightTimeout.value);
787
- highlightTimeout.value = null;
788
- }
854
+ clearHighlightTimeout();
789
855
  }
790
856
  }
791
857
  }
@@ -819,6 +885,10 @@ const handleMouseMove = (event: MouseEvent) => {
819
885
  // 用于跟踪上一次的hoverShape,避免重复发射事件
820
886
  let lastHoverShapeId: string | null = null;
821
887
 
888
+ // 用于防抖 edge-check 事件和高亮,只有鼠标停留一段时间后才触发
889
+ let edgeCheckDebounceTimer: ReturnType<typeof setTimeout> | null = null;
890
+ const EDGE_CHECK_DEBOUNCE_DELAY = 150; // 150ms 防抖延迟
891
+
822
892
  const checkHoverTarget = (x: number, y: number) => {
823
893
  if (!props.diagramBounds) return;
824
894
 
@@ -830,7 +900,7 @@ const checkHoverTarget = (x: number, y: number) => {
830
900
  props.diagramBounds,
831
901
  props.connectShapeData?.sourceId
832
902
  );
833
- highlightUtils.clearHighlightTimeout();
903
+ clearHighlightTimeout();
834
904
 
835
905
  if (hoverShape) {
836
906
  // 检查连接有效性
@@ -863,50 +933,69 @@ const checkHoverTarget = (x: number, y: number) => {
863
933
  isAllowed = false;
864
934
  }
865
935
  }
866
- // 只有当hoverShape改变且connectShapeData存在时才发射事件
867
- if (lastHoverShapeId !== hoverShape.id && props.connectShapeData) {
868
- lastHoverShapeId = hoverShape.id;
869
- // 使用更严格的优先级判断逻辑,确保只要有一个属性有实际值就使用它
870
- let sourceModelId;
871
- // 如果modelId是有效字符串或数字,使用modelId
872
- if (
873
- props.connectShapeData.modelId &&
874
- props.connectShapeData.modelId.toString().trim() !== ""
875
- ) {
876
- sourceModelId = props.connectShapeData.modelId;
877
- }
878
- // 否则,如果sourceModelId是有效字符串或数字,使用sourceModelId
879
- else if (
880
- props.connectShapeData.sourceModelId &&
881
- props.connectShapeData.sourceModelId.toString().trim() !== ""
882
- ) {
883
- sourceModelId = props.connectShapeData.sourceModelId;
936
+
937
+ // hoverShape 改变时,清除之前的防抖定时器
938
+ if (lastHoverShapeId !== hoverShape.id) {
939
+ // 清除旧的防抖定时器
940
+ if (edgeCheckDebounceTimer) {
941
+ clearTimeout(edgeCheckDebounceTimer);
942
+ edgeCheckDebounceTimer = null;
884
943
  }
885
- // 只有当sourceModelId有值时就发射事件,并将isAllowed的值一并传递
886
- if (sourceModelId) {
887
- eventBus.emit("edge-check", {
888
- sourceModelId: sourceModelId, // 显式指定键值对,避免属性简写可能带来的混淆
889
- targetModelId: hoverShape.modelId,
890
- isAllowed: isAllowed, // 将验证结果一并发射
891
- });
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);
892
984
  }
893
985
  }
894
- // 高亮目标图元 - 根据isAllowed决定高亮颜色(实时计算,不依赖异步更新的props.edgeCheck)
895
- // true为蓝色,false为红色
896
- // 直接使用isAllowed(前端实时验证结果),确保快速移动时颜色能立即更新
897
- // 如果后端验证结果不同,会在props.edgeCheck更新后通过watch或其他机制再次更新
898
- highlightUtils.setHighlightTimeout(() => {
899
- highlightShape(hoverShape, true, isAllowed);
900
- isConnectAllowed.value = isAllowed;
901
- }, 10);
902
986
  } else {
903
- // 如果没有悬停在目标上,重置lastHoverShapeId
987
+ // 如果没有悬停在目标上
988
+ // 清除防抖定时器
989
+ if (edgeCheckDebounceTimer) {
990
+ clearTimeout(edgeCheckDebounceTimer);
991
+ edgeCheckDebounceTimer = null;
992
+ }
993
+ // 重置lastHoverShapeId
904
994
  lastHoverShapeId = null;
905
- // 延迟取消高亮
906
- highlightUtils.setHighlightTimeout(() => {
907
- highlightShape(null, false);
908
- isConnectAllowed.value = false;
909
- }, 60);
995
+ // 立即取消高亮(无需延迟,因为高亮本身已有防抖)
996
+ clearHighlightTimeout();
997
+ highlightShape(null, false);
998
+ isConnectAllowed.value = false;
910
999
  }
911
1000
  };
912
1001
 
@@ -942,6 +1031,13 @@ watch(
942
1031
  () => isConnecting.value,
943
1032
  (newVal) => {
944
1033
  if (!newVal) {
1034
+ // 清除防抖定时器
1035
+ if (edgeCheckDebounceTimer) {
1036
+ clearTimeout(edgeCheckDebounceTimer);
1037
+ edgeCheckDebounceTimer = null;
1038
+ }
1039
+ // 重置 lastHoverShapeId
1040
+ lastHoverShapeId = null;
945
1041
  isConnectAllowed.value = false;
946
1042
  highlightShape(null, false); // 取消图元高亮
947
1043
  }
@@ -995,7 +1091,7 @@ const completeConnection = (
995
1091
  isConnecting.value = false;
996
1092
  highlightShape(null, false); // 取消图元高亮
997
1093
  graphStore.setConnectMode("connect");
998
- highlightUtils.clearHighlightTimeout();
1094
+ clearHighlightTimeout();
999
1095
  ElMessage.error("当前目标图元类型不符合连接要求");
1000
1096
  return;
1001
1097
  }
@@ -1010,7 +1106,7 @@ const completeConnection = (
1010
1106
  ) {
1011
1107
  isConnecting.value = false;
1012
1108
  highlightShape(null, false); // 取消图元高亮
1013
- highlightUtils.clearHighlightTimeout();
1109
+ clearHighlightTimeout();
1014
1110
  return;
1015
1111
  }
1016
1112
  // 检查目标图元类型是否符合targetModels要求
@@ -1020,7 +1116,7 @@ const completeConnection = (
1020
1116
  if (!targetModels.includes(clickedShapeType)) {
1021
1117
  isConnecting.value = false;
1022
1118
  highlightShape(null, false); // 取消图元高亮
1023
- highlightUtils.clearHighlightTimeout();
1119
+ clearHighlightTimeout();
1024
1120
  // alert('当前目标图元类型不符合连接要求');
1025
1121
  ElMessage.error("当前目标图元类型不符合连接要求");
1026
1122
  return;
@@ -1043,7 +1139,7 @@ const completeConnection = (
1043
1139
  isConnecting.value = false;
1044
1140
  graphStore.setConnectMode("connect");
1045
1141
  highlightShape(null, false); // 取消图元高亮
1046
- highlightUtils.clearHighlightTimeout();
1142
+ clearHighlightTimeout();
1047
1143
  return;
1048
1144
  }
1049
1145
  // 使用工具类完成连接,传递当前的shapes列表以支持差异化路由
@@ -1079,7 +1175,7 @@ const completeConnection = (
1079
1175
  graphStore.setConnectMode("connect");
1080
1176
  isConnecting.value = false;
1081
1177
  highlightShape(null, false); // 取消图元高亮
1082
- highlightUtils.clearHighlightTimeout();
1178
+ clearHighlightTimeout();
1083
1179
  }
1084
1180
  };
1085
1181
  // 鼠标离开事件处理
@@ -1088,7 +1184,7 @@ const handleMouseLeave = () => {
1088
1184
  showLine.value = false;
1089
1185
  }
1090
1186
  highlightShape(null, false); // 取消图元高亮
1091
- highlightUtils.clearHighlightTimeout();
1187
+ clearHighlightTimeout();
1092
1188
  };
1093
1189
 
1094
1190
  // 初始化连接点位置
@@ -1222,7 +1318,10 @@ const cancelConnection = () => {
1222
1318
  // 监听 connectShapeData 变化,重新初始化连接点
1223
1319
  watch(
1224
1320
  () => props.connectShapeData,
1225
- () => {
1321
+ (newVal) => {
1322
+ if (newVal && !newVal.scenarioMenus) {
1323
+ graphStore.setConnectMode('connect');
1324
+ }
1226
1325
  initializeConnectPoint();
1227
1326
  },
1228
1327
  { deep: true }
@@ -1319,8 +1418,9 @@ const continueExternalCreateDrag = async (payload: {
1319
1418
  }
1320
1419
  }
1321
1420
  }
1322
-
1323
- graphStore.moveDraggedShape(targetPt);
1421
+ // 外部创建 pin:targetPt 是期望的最终落位,但 hover 命中仍用真实鼠标 pt,避免闪动
1422
+ // graphStore.moveDraggedShape(targetPt, { hitPointer: pt });
1423
+ scheduleMoveDraggedShape(targetPt, { hitPointer: pt });
1324
1424
  };
1325
1425
 
1326
1426
  //拖拽结束后重新发送事件到front中调用接口
@@ -1338,7 +1438,29 @@ const finishExternalCreateDrag = async (payload: {
1338
1438
  if (
1339
1439
  isInsideCanvasClient(payload.clientX, payload.clientY, layerRef.value)
1340
1440
  ) {
1341
- graphStore.moveDraggedShape(pt);
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(
1450
+ (x: any) => x.id === graphStore.hoverContainerId
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 });
1342
1464
  await nextTick();
1343
1465
  const s: any = (graphStore.shapes || []).find(
1344
1466
  (x: any) => x.id == externalCreateDragState.creatingId
@@ -1348,29 +1470,17 @@ const finishExternalCreateDrag = async (payload: {
1348
1470
  pure.bounds = {
1349
1471
  // 覆盖为新的 bounds
1350
1472
  ...pure.bounds,
1351
- x: pt.x,
1352
- y: pt.y,
1473
+ x: targetPt.x,
1474
+ y: targetPt.y,
1353
1475
  };
1354
- // 先推断这次 drop 的“候选父节点”
1355
- let parent: Shape | null = null;
1356
- if (graphStore.hoverContainerId && graphStore.hoverNestable !== false) {
1357
- parent =
1358
- (graphStore.shapes as any[]).find(
1359
- (x: any) => x.id === graphStore.hoverContainerId
1360
- ) || null;
1361
- }
1362
1476
  // 如果是 pin 类型,调整位置吸附到父图元最近的边
1363
1477
  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;
1478
+ // 这里直接沿用上面计算出的 targetPt,避免再次出现“先 pt 后吸附”的一帧回跳
1479
+ pure.bounds.x = targetPt.x;
1480
+ pure.bounds.y = targetPt.y;
1371
1481
  pure.parenShapeId = parent.id;
1372
- // 将吸附后的坐标同步回 ghost
1373
- graphStore.moveDraggedShape({ x: adjustedX, y: adjustedY });
1482
+ // 将吸附后的坐标同步回 ghost(hover 命中仍用真实鼠标点)
1483
+ graphStore.moveDraggedShape(targetPt, { hitPointer: pt });
1374
1484
  }
1375
1485
  try {
1376
1486
  const { ok } = await checkNestViaFront(
@@ -1466,6 +1576,7 @@ const keyboardHandler = createKeyboardHandler({
1466
1576
  onEditProperty: () => onLayerDblClick(true),
1467
1577
  onCancelConnection: cancelConnection,
1468
1578
  isEditingName: () => isEditingName.value,
1579
+ isTextareaDialogOpen: () => props.isTextareaDialogOpen || false, // 传递对话框状态
1469
1580
  onCopy: () => {
1470
1581
  // 获取当前选中的图元
1471
1582
  const selectedShapes = graphStore.selectedIds
@@ -1476,14 +1587,61 @@ const keyboardHandler = createKeyboardHandler({
1476
1587
  return;
1477
1588
  }
1478
1589
 
1479
- // 使用ContextMenuUtils存储复制的图元
1480
- ContextMenuUtils.setCopiedShapes(selectedShapes);
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);
1481
1605
  },
1482
1606
  onPaste: () => {
1483
1607
  // 使用ContextMenuUtils处理粘贴
1484
1608
  ContextMenuUtils.handlePaste(graphStore.selectedShape);
1485
1609
  },
1486
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
+ };
1487
1645
 
1488
1646
  onMounted(() => {
1489
1647
  window.addEventListener("keydown", keyboardHandler);
@@ -1497,13 +1655,6 @@ onMounted(() => {
1497
1655
  //拖动结束后更新情景菜单
1498
1656
  eventBus.on("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1499
1657
 
1500
- // 监听粘贴图元事件
1501
- eventBus.on("paste-shapes", (shapes: Shape[]) => {
1502
- shapes.forEach((shape) => {
1503
- graphStore.addShape(shape);
1504
- });
1505
- });
1506
-
1507
1658
  // 监听选择图元事件
1508
1659
  eventBus.on("select-shapes", (ids: string[]) => {
1509
1660
  graphStore.clearSelection();
@@ -1516,13 +1667,11 @@ onUnmounted(() => {
1516
1667
  document.removeEventListener("mousemove", handleMouseMove);
1517
1668
  document.removeEventListener("mouseleave", handleMouseLeave);
1518
1669
  window.removeEventListener("keydown", keyboardHandler);
1519
- // 重置名称编辑状态
1520
- nameEditManager.reset();
1521
1670
  eventBus.off("shape-drag-end-updateScenarioMenu", actionButtonsStyle);
1522
- highlightUtils.clearHighlightTimeout();
1523
- // 清理高亮工具实例
1524
- highlightUtils.dispose();
1525
1671
  if (rafId) cancelAnimationFrame(rafId);
1672
+ if (dragMoveRafId) cancelAnimationFrame(dragMoveRafId);
1673
+ dragMoveRafId = null;
1674
+ latestDragMove = null;
1526
1675
  });
1527
1676
  defineExpose({
1528
1677
  continueExternalCreateDrag,
@@ -1609,4 +1758,21 @@ defineExpose({
1609
1758
  pointer-events: none;
1610
1759
  z-index: 1000;
1611
1760
  }
1761
+
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;
1777
+ }
1612
1778
  </style>