@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.
- package/dist/index.d.ts +177 -9
- package/dist/index.esm.js +4569 -63119
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -39
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +10 -1
- package/src/components/ContextMenu/ContextMenu.vue +10 -10
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
- package/src/components/InteractionLayer.vue +323 -157
- package/src/components/LineStyle/LineStyleMarker.vue +1 -1
- package/src/components/{NameEditor.vue → NameEditor/NameEditor.vue} +4 -4
- package/src/components/{SelectionBox.vue → SelectionBox/SelectionBox.vue} +5 -5
- package/src/components/Shape/Block.vue +1 -1
- package/src/constants/edgeShapeKeys.ts +43 -3
- package/src/constants/index.ts +19 -4
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useHighlight.ts +223 -0
- package/src/hooks/useNameEdit.ts +234 -0
- package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
- package/src/index.ts +4 -1
- package/src/render/shape-renderer.ts +59 -46
- package/src/statics/icons/createMenu/show.png +0 -0
- package/src/statics/icons/createMenu/tree.png +0 -0
- 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
- package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
- package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
- package/src/store/graphStore.ts +185 -65
- package/src/types/index.ts +4 -2
- package/src/types/interactionLayer.ts +1 -0
- package/src/utils/batchAutoExpand.ts +65 -0
- package/src/utils/compartment.ts +78 -4
- package/src/utils/containers.ts +24 -10
- package/src/utils/contextMenuUtils.ts +106 -147
- package/src/utils/drag.ts +10 -5
- package/src/utils/edgeUtils.ts +3 -4
- package/src/utils/graphDragService.ts +27 -23
- package/src/utils/iconLoader.ts +7 -7
- package/src/utils/keyboardUtils.ts +195 -32
- package/src/utils/pinUtils.ts +1 -2
- package/src/utils/shapeOps/shapeOps.ts +168 -0
- package/src/utils/viewportCulling.ts +193 -0
- package/src/view/graph.vue +115 -60
- package/src/utils/highlightUtils.ts +0 -162
- package/src/utils/nameEditUtils.ts +0 -137
- /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
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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-
|
|
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-
|
|
46
|
+
<div v-show="isConnecting" class="connect-dot-direct" :style="dotStyle"></div>
|
|
44
47
|
|
|
45
48
|
<!-- 连线 -->
|
|
46
|
-
<ConnectionLine v-
|
|
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 {
|
|
119
|
+
import { useNameEdit } from "../hooks/useNameEdit";
|
|
110
120
|
import { guardOperate } from "../utils/license-guard";
|
|
111
|
-
import {
|
|
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 =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
//
|
|
987
|
+
// 如果没有悬停在目标上
|
|
988
|
+
// 清除防抖定时器
|
|
989
|
+
if (edgeCheckDebounceTimer) {
|
|
990
|
+
clearTimeout(edgeCheckDebounceTimer);
|
|
991
|
+
edgeCheckDebounceTimer = null;
|
|
992
|
+
}
|
|
993
|
+
// 重置lastHoverShapeId
|
|
904
994
|
lastHoverShapeId = null;
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1352
|
-
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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({
|
|
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.
|
|
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>
|