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