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

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.
@@ -1,20 +1,27 @@
1
1
  import type { CSSProperties } from "vue";
2
2
  import type { Shape } from "@/types";
3
- import { BlockKeyMap, DiagramKeyMap } from "../constants";
3
+ import { ShapeKeyMap, DiagramKeyMap, PinKeyMap, EdgeKeyMap } from "../constants";
4
4
  import { getShapeComponentByKey } from "./shape-registry";
5
- import { PinKeyMap } from "../constants";
6
5
 
7
- // 根据 shapeKey 匹配具体组件
6
+ // 根据 shapeKey 匹配具体组件 (shapeType: 'shape')
8
7
  function getComponentByShapeKey(shapeKey: string) {
9
- const componentName = (BlockKeyMap as any)[shapeKey] || shapeKey;
8
+ const componentName = (ShapeKeyMap as any)[shapeKey] || shapeKey;
10
9
  return getShapeComponentByKey(componentName) || componentName;
11
10
  }
11
+
12
+ // 根据 shapeKey 匹配 Pin 组件 (shapeType: 'pin')
12
13
  function getComponentByPinKey(shapeKey: string) {
13
14
  const componentName = (PinKeyMap as any)[shapeKey] || shapeKey;
14
15
  return getShapeComponentByKey(componentName) || componentName;
15
16
  }
16
17
 
17
- // 根据 shapeKey 匹配图类型组件
18
+ // 根据 shapeKey 匹配 Edge 组件 (shapeType: 'edge')
19
+ function getComponentByEdgeKey(shapeKey: string) {
20
+ const componentName = (EdgeKeyMap as any)[shapeKey] || shapeKey;
21
+ return getShapeComponentByKey(componentName) || componentName;
22
+ }
23
+
24
+ // 根据 shapeKey 匹配 Edge 组件 (shapeType: 'diagram')
18
25
  export function getComponentByDiagramKey(shapeKey: string) {
19
26
  const componentName = (DiagramKeyMap as any)[shapeKey] || shapeKey;
20
27
  return getShapeComponentByKey(componentName) || componentName;
@@ -31,7 +38,7 @@ export const getShapeComponent = (shape: Shape) => {
31
38
  case "diagram":
32
39
  return getComponentByDiagramKey(shapeKey);
33
40
  case "edge":
34
- return getComponentByShapeKey(shapeKey);
41
+ return getComponentByEdgeKey(shapeKey);
35
42
  default:
36
43
  return "ShapeComponent";
37
44
  }
@@ -1,5 +1,5 @@
1
- import { defineStore } from 'pinia'
2
- import { ref, computed, shallowReactive, nextTick, type Ref } from 'vue'
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed, shallowReactive, shallowRef, triggerRef, nextTick, type Ref } from 'vue'
3
3
  import type { Shape, OwnerPayload, ShapeSizeUpdatePayload } from '../types'
4
4
  import { eventBus } from './eventBus'
5
5
  import { getPolicy, checkNestViaFront, } from '../utils/policy'
@@ -74,44 +74,132 @@ export const useGraphStore = defineStore('graph', () => {
74
74
  const cutShapeIds = ref<Set<string>>(new Set())
75
75
  // 剪贴板中图元的数量(用于工具栏按钮状态同步)
76
76
  const copiedShapesCount = ref<number>(0)
77
-
77
+ // 拖动时的后代坐标快照(只在 startDrag 时计算一次,避免每帧重算)
78
+ // key: 被拖动的父元素 ID, value: 所有后代的坐标快照
79
+ type DescendantSnapshot = { id: string; x: number; y: number; width: number; height: number };
80
+ const dragDescendantsSnapshot = ref<Map<string, DescendantSnapshot[]>>(new Map())
78
81
  // 计算属性
79
82
  const shapeCount = computed(() => shapes.value.length)
80
83
  const hasSelectedShape = computed(() => selectedShape.value !== null)
81
84
 
85
+ // ========== 增量索引:手动维护,避免 computed 全量重建 ==========
86
+
87
+ /**
88
+ * 图元ID索引映射(增量维护版本)
89
+ * 用于快速通过ID查找图元对象
90
+ * key: 图元 ID, value: 图元对象
91
+ * 性能优化: O(1) 增删改查,避免每次 shapes 变化都 O(n) 重建
92
+ */
93
+ const _shapeMapRef = shallowRef(new Map<string, Shape>())
94
+ // 使用 computed 包装,确保 Pinia 正确解包
95
+ const shapeMap = computed(() => _shapeMapRef.value)
96
+
82
97
  /**
83
- * 父子关系索引映射
98
+ * 父子关系索引映射(增量维护版本)
84
99
  * 用于快速查找某个图元的所有直接子图元
85
100
  * key: 父图元 ID, value: 子图元 ID 数组
86
- * 性能优化: O(n) 构建, O(1) 查询
101
+ * 性能优化: O(1) 查询,增删时只更新受影响的条目
87
102
  */
88
- const parentChildMap = computed(() => {
89
- const map = new Map<string, string[]>();
103
+ const _parentChildMapRef = shallowRef(new Map<string, string[]>())
104
+ // 使用 computed 包装,确保 Pinia 正确解包
105
+ const parentChildMap = computed(() => _parentChildMapRef.value)
106
+
107
+ // ========== 索引维护内部方法 ==========
108
+
109
+ /** 添加图元到索引 */
110
+ function _indexAdd(shape: Shape) {
111
+ _shapeMapRef.value.set(shape.id, shape)
112
+ if (shape.parenShapeId) {
113
+ const children = _parentChildMapRef.value.get(shape.parenShapeId)
114
+ if (children) {
115
+ if (!children.includes(shape.id)) {
116
+ children.push(shape.id)
117
+ }
118
+ } else {
119
+ _parentChildMapRef.value.set(shape.parenShapeId, [shape.id])
120
+ }
121
+ }
122
+ }
123
+
124
+ /** 从索引中移除图元 */
125
+ function _indexRemove(shapeId: string) {
126
+ const shape = _shapeMapRef.value.get(shapeId)
127
+ if (!shape) return
128
+ _shapeMapRef.value.delete(shapeId)
129
+ if (shape.parenShapeId) {
130
+ const children = _parentChildMapRef.value.get(shape.parenShapeId)
131
+ if (children) {
132
+ const idx = children.indexOf(shapeId)
133
+ if (idx !== -1) children.splice(idx, 1)
134
+ if (children.length === 0) {
135
+ _parentChildMapRef.value.delete(shape.parenShapeId)
136
+ }
137
+ }
138
+ }
139
+ // 如果这个图元是父节点,清理它的子节点列表
140
+ _parentChildMapRef.value.delete(shapeId)
141
+ }
142
+
143
+ /** 更新图元索引(处理父节点变更) */
144
+ function _indexUpdate(shape: Shape, oldParentId: string | null | undefined) {
145
+ _shapeMapRef.value.set(shape.id, shape)
146
+ const newParentId = shape.parenShapeId ?? null
147
+ const normalizedOldParent = oldParentId ?? null
148
+
149
+ if (normalizedOldParent !== newParentId) {
150
+ // 从旧父节点移除
151
+ if (normalizedOldParent) {
152
+ const oldChildren = _parentChildMapRef.value.get(normalizedOldParent)
153
+ if (oldChildren) {
154
+ const idx = oldChildren.indexOf(shape.id)
155
+ if (idx !== -1) oldChildren.splice(idx, 1)
156
+ if (oldChildren.length === 0) {
157
+ _parentChildMapRef.value.delete(normalizedOldParent)
158
+ }
159
+ }
160
+ }
161
+ // 添加到新父节点
162
+ if (newParentId) {
163
+ const newChildren = _parentChildMapRef.value.get(newParentId)
164
+ if (newChildren) {
165
+ if (!newChildren.includes(shape.id)) {
166
+ newChildren.push(shape.id)
167
+ }
168
+ } else {
169
+ _parentChildMapRef.value.set(newParentId, [shape.id])
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ /** 全量重建索引(用于 replace 操作或初始化) */
176
+ function _rebuildIndex() {
177
+ const newShapeMap = new Map<string, Shape>()
178
+ const newParentChildMap = new Map<string, string[]>()
179
+
90
180
  shapes.value.forEach(shape => {
181
+ newShapeMap.set(shape.id, shape)
91
182
  if (shape.parenShapeId) {
92
- if (!map.has(shape.parenShapeId)) {
93
- map.set(shape.parenShapeId, []);
183
+ const children = newParentChildMap.get(shape.parenShapeId)
184
+ if (children) {
185
+ children.push(shape.id)
186
+ } else {
187
+ newParentChildMap.set(shape.parenShapeId, [shape.id])
94
188
  }
95
- map.get(shape.parenShapeId)!.push(shape.id);
96
189
  }
97
- });
98
- return map;
99
- })
190
+ })
191
+
192
+ _shapeMapRef.value = newShapeMap
193
+ _parentChildMapRef.value = newParentChildMap
194
+ triggerRef(_shapeMapRef)
195
+ triggerRef(_parentChildMapRef)
196
+ }
100
197
 
101
- /**
102
- * 图元ID索引映射
103
- * 用于快速通过ID查找图元对象
104
- * key: 图元 ID, value: 图元对象
105
- * 性能优化: O(n) 构建, O(1) 查询
106
- * 解决嵌套图元场景下大量 find() 调用导致的性能问题
107
- */
108
- const shapeMap = computed(() => {
109
- const map = new Map<string, Shape>();
110
- shapes.value.forEach(shape => {
111
- map.set(shape.id, shape);
112
- });
113
- return map;
114
- })
198
+ /** 触发索引的响应式更新 */
199
+ function _triggerIndexUpdate() {
200
+ triggerRef(_shapeMapRef)
201
+ triggerRef(_parentChildMapRef)
202
+ }
115
203
 
116
204
  //图元shapeKey
117
205
  const taggedValueLabels = ref<string[]>([]) // 隔间组件的图元类型
@@ -148,8 +236,11 @@ export const useGraphStore = defineStore('graph', () => {
148
236
  }
149
237
 
150
238
  shapes.value.push(shape)
239
+ // 增量更新索引
240
+ _indexAdd(shape)
241
+ _triggerIndexUpdate()
151
242
  // 通过事件总线发送事件
152
- eventBus.emit('shape-added', shape)
243
+ // eventBus.emit('shape-added', shape)
153
244
 
154
245
  // 自动扩父逻辑:如果图元有父元素且需要自动扩父,触发扩父逻辑
155
246
  const shouldAutoExpand = options?.autoExpandParent !== false // 默认为 true
@@ -164,16 +255,10 @@ export const useGraphStore = defineStore('graph', () => {
164
255
  const removeShape = (shapeId: string) => {
165
256
  const index = shapes.value.findIndex(s => s.id == shapeId)
166
257
  if (index > -1) {
167
- const removedShape = shapes.value.splice(index, 1)[0]
168
- // 通过事件总线发送事件
169
- // eventBus.emit('shape-removed', removedShape)
170
- // 若该图元有父,且父是隔间:从 comparents 里移除这个子 Shape
171
- // if (removedShape.parenShapeId) {
172
- // const parent = shapes.value.find(s => s.id == removedShape.parenShapeId)
173
- // if (parent && isShapeTypeNotInPackageTypes(parent)) {
174
- // removeChildShapeFromCompartment(parent, removedShape, updateShape)
175
- // }
176
- // }
258
+ // 增量更新索引(在删除数组元素之前)
259
+ _indexRemove(shapeId)
260
+ shapes.value.splice(index, 1)
261
+ _triggerIndexUpdate()
177
262
  // 如果删除的是当前选中的形状,清除选中状态
178
263
  if (selectedShape.value?.id === shapeId) {
179
264
  selectedShape.value = null
@@ -208,6 +293,8 @@ export const useGraphStore = defineStore('graph', () => {
208
293
  const updateShapeRaw = (shapeId: string, updates: Partial<Shape>, id: 'id' | 'modelId' = 'id') => {
209
294
  const shape = shapes.value.find(s => s[id] === shapeId)
210
295
  if (shape) {
296
+ // 记录旧的父节点ID,用于索引更新
297
+ const oldParentId = shape.parenShapeId
211
298
  // 对 Pin 做强保护:永不允许把 parenShapeId 置空/undefined
212
299
  if (shape.shapeType === 'pin' && 'parenShapeId' in updates) {
213
300
  const v: any = (updates as any).parenShapeId
@@ -224,6 +311,9 @@ export const useGraphStore = defineStore('graph', () => {
224
311
  const nextBounds = updates.bounds ? { ...shape.bounds, ...updates.bounds } : shape.bounds
225
312
  const nextStyle = updates.style ? { ...(shape.style || {}), ...(updates.style as any) } : shape.style
226
313
  Object.assign(shape, updates, { bounds: nextBounds, style: nextStyle })
314
+ // 增量更新索引(处理父节点变更)
315
+ _indexUpdate(shape, oldParentId)
316
+ _triggerIndexUpdate()
227
317
  eventBus.emit('shape-updated', shape, updates)
228
318
  }
229
319
  }
@@ -282,32 +372,46 @@ export const useGraphStore = defineStore('graph', () => {
282
372
  }
283
373
  // 被框选中的多个 shape/映射 selectedIds.value中的数据
284
374
  const marqueeShapes = computed(() => {
285
- const byId = (id: string) => shapes.value.find(s => s.id === id)
286
- const isShape = (x: Shape | undefined): x is Shape => !!x
287
375
  const pending = new Set(pendingNestedIds.value)
288
- return selectedIds.value
289
- .map(byId)
290
- .filter(isShape)
291
- .filter(s => !pending.has(s.id))
376
+ const result: Shape[] = []
377
+ // 使用 shapeMap 进行 O(1) 查找,避免 O(n²) 的 find
378
+ for (const id of selectedIds.value) {
379
+ if (pending.has(id)) continue
380
+ const shape = shapeMap.value.get(id)
381
+ if (shape) result.push(shape)
382
+ }
383
+ return result
292
384
  })
293
385
  //获取元素拖动预览框
386
+ // 性能优化:只返回"直接被拖动"的图元的 ghost,不包含后代
387
+ // 后代图元会跟随父元素移动,不需要单独显示 ghost
294
388
  const ghostShadow = computed<Shape[]>(() => {
389
+ // 如果没有在拖动,直接返回空数组
390
+ if (!isDragging.value || draggingIds.value.length === 0) return []
391
+
295
392
  const out: Shape[] = []
296
393
  // 兜底:防止被持久化还原成非数组
297
394
  const pendingList = Array.isArray(pendingNestedIds.value)
298
395
  ? pendingNestedIds.value
299
396
  : []
300
397
  const pendingSet = new Set(pendingList)
301
- // shapeMap O(1) 查找原始图元,避免 find O(N)
398
+ // 只为"直接被选中拖动"的图元创建 ghost,不包含后代
399
+ // 这样可以大幅减少 ghost 数量(从 3000 减少到用户实际选中的数量)
400
+ const directDragSet = new Set(dragSelectionSnapshot.value.length > 0
401
+ ? dragSelectionSnapshot.value
402
+ : draggingIds.value.slice(0, 1)) // 至少保留主拖动项
403
+
302
404
  const map = shapeMap.value
303
- for (const id in dragGhost) {
405
+ for (const id of directDragSet) {
304
406
  if (pendingSet.has(id)) continue
407
+ const ghostRect = dragGhost[id]
408
+ if (!ghostRect) continue
305
409
  const orig = map.get(id)
306
410
  if (!orig) continue
307
411
 
308
412
  out.push({
309
413
  ...orig,
310
- bounds: { ...dragGhost[id] },
414
+ bounds: { ...ghostRect },
311
415
  meta: { ...(orig as any).meta, isGhost: true } as any,
312
416
  } as Shape)
313
417
  }
@@ -316,18 +420,22 @@ export const useGraphStore = defineStore('graph', () => {
316
420
  // 选择一组(用于框选/Shift 叠加)
317
421
  const selectMany = (ids: string[]) => {
318
422
  selectedIds.value = Array.from(new Set(ids))
423
+ // 使用 shapeMap 进行 O(1) 查找,避免 O(n) 的 find
319
424
  selectedShape.value = ids.length
320
- ? (shapes.value.find(s => s.id === ids[0]) ?? null)
425
+ ? (shapeMap.value.get(ids[0]) ?? null)
321
426
  : null
322
427
  }
323
428
  // 清空选择
324
429
  const clearSelection = () => selectShape(null)
325
430
  // 全选图元
326
431
  const selectAll = () => {
327
- // 获取所有非diagram类型的图元
328
- const allShapeIds = shapes.value
329
- .filter(shape => shape.shapeType?.toLowerCase() !== 'diagram')
330
- .map(shape => shape.id)
432
+ // 优化:直接遍历一次收集 id,避免 filter + map 两次遍历
433
+ const allShapeIds: string[] = []
434
+ for (const shape of shapes.value) {
435
+ if (shape.shapeType?.toLowerCase() !== 'diagram') {
436
+ allShapeIds.push(shape.id)
437
+ }
438
+ }
331
439
  selectMany(allShapeIds)
332
440
  }
333
441
  // 设置图表标题
@@ -341,7 +449,7 @@ export const useGraphStore = defineStore('graph', () => {
341
449
  /**
342
450
  * 批量操作图元(新增 / 修改 / 删除)
343
451
  * @param payload - 新增/修改时传 Shape[];删除时可传 Shape[](只需要 id)或 id 数组
344
- * @param op - 操作类型:add | update | delete | upsert(默认)
452
+ * @param op - 操作类型:add | update | delete | upsert(默认)| replace
345
453
  * @param options - 额外选项(保持你原来的 autoExpandParents)
346
454
  */
347
455
  const updateShapes = (
@@ -351,11 +459,44 @@ export const useGraphStore = defineStore('graph', () => {
351
459
  ) => {
352
460
  // 先把 comparents[*].comparentShapes 铺平同步到运行时
353
461
  // const expanded = syncComparentShapesIntoRuntime(newShapes)
354
- const { changedShapes } = applyShapeOp({
462
+ const { changedShapes, changeDetails } = applyShapeOp({
355
463
  list: shapes.value,
356
464
  payload: payload as any, // delete 时是 id[],其余是 Shape[]
357
465
  op,
358
466
  })
467
+
468
+ // 根据操作类型选择索引更新策略
469
+ if (op === 'replace' || changeDetails.length === 0) {
470
+ // replace 操作或无变更详情时,全量重建索引
471
+ _rebuildIndex()
472
+ } else {
473
+ // 增量更新索引
474
+ for (const detail of changeDetails) {
475
+ if (detail.type === 'add') {
476
+ _indexAdd(detail.shape)
477
+ } else if (detail.type === 'delete') {
478
+ // 删除时 shape 已经从数组移除,需要手动清理索引
479
+ _shapeMapRef.value.delete(detail.shape.id)
480
+ const parentId = (detail.shape as any).parenShapeId
481
+ if (parentId) {
482
+ const children = _parentChildMapRef.value.get(parentId)
483
+ if (children) {
484
+ const idx = children.indexOf(detail.shape.id)
485
+ if (idx !== -1) children.splice(idx, 1)
486
+ if (children.length === 0) {
487
+ _parentChildMapRef.value.delete(parentId)
488
+ }
489
+ }
490
+ }
491
+ _parentChildMapRef.value.delete(detail.shape.id)
492
+ } else if (detail.type === 'update') {
493
+ const oldParentId = (detail.oldShape as any)?.parenShapeId
494
+ _indexUpdate(detail.shape, oldParentId)
495
+ }
496
+ }
497
+ _triggerIndexUpdate()
498
+ }
499
+
359
500
  pendingNestedIds.value = []
360
501
  // eventBus.emit('shapes-updated', newShapes)
361
502
  // hydrateAllComparents()
@@ -375,33 +516,16 @@ export const useGraphStore = defineStore('graph', () => {
375
516
  //清空图元数据
376
517
  const clearAll = () => {
377
518
  shapes.value = []
519
+ // 清空索引
520
+ _shapeMapRef.value = new Map()
521
+ _parentChildMapRef.value = new Map()
522
+ triggerRef(_shapeMapRef)
523
+ triggerRef(_parentChildMapRef)
378
524
  diagrams.value = []
379
525
  selectedShape.value = null
380
526
  pendingNestedIds.value = []
381
527
  eventBus.emit('shapes-cleared')
382
528
  }
383
-
384
- /** 把所有隔间父的 comparents 从 comparentsJSON 还原;若没有 JSON 就按 parenShapeId 兜底重建 */
385
- // const hydrateAllComparents = () => {
386
- // if (comparentsHydratedOnce.value) return
387
- // if (!shapes.value.length) return
388
- // let hadAnyJSON = false
389
- // for (const p of shapes.value) {
390
- // if (isShapeTypeNotInPackageTypes(p)) {
391
- // const any = p as any
392
- // if (typeof any.comparentsJSON === 'string' && any.comparentsJSON.length) {
393
- // hydrateComparentsFromJSON(p, updateShape)
394
- // hadAnyJSON = true
395
- // }
396
- // }
397
- // }
398
- // // 历史数据可能还没有 comparentsJSON:按父子关系兜底重建一次
399
- // if (!hadAnyJSON) {
400
- // rebuildAllCompartmentChildrenAsShapes(shapes.value, updateShape)
401
- // }
402
-
403
- // comparentsHydratedOnce.value = true
404
- // }
405
529
  //拖动元素相关操作
406
530
  // 开始拖动已有元素(仅对 shape 生效)
407
531
  const byId = (id: string) => shapes.value.find(s => s.id === id) || null
@@ -409,17 +533,13 @@ export const useGraphStore = defineStore('graph', () => {
409
533
  startDrag([shapeId], pointer)
410
534
  }
411
535
  const startDrag = (ids: string[], pointer: { x: number; y: number }) => {
412
- // 记录“当前真正被用户选中”的快照 —— 结束时恢复
536
+ // 记录"当前真正被用户选中"的快照 结束时恢复
413
537
  dragSelectionSnapshot.value = selectedIds.value.length ? [...selectedIds.value] : [ids[0]]
414
538
  primaryDragId.value = ids[0] || null
415
539
 
416
- // 展开“选中项 + 后代”的集合(你现有的逻辑保留)
417
- const expanded: string[] = []
418
- ids.forEach(id => {
419
- expanded.push(id)
420
- collectDescendantIds(shapes.value, id).forEach(cid => expanded.push(cid))
421
- })
422
- const validIds = Array.from(new Set(expanded)).filter(id => {
540
+ // 性能优化:只对"直接选中"的图元建立 dragBase/dragGhost
541
+ // 后代图元会跟随父元素移动,不需要单独创建 ghost,大幅减少渲染开销
542
+ const validIds = ids.filter(id => {
423
543
  const s = byId(id); return s && getPolicy(s!).draggable
424
544
  })
425
545
  if (!validIds.length) return
@@ -433,6 +553,32 @@ export const useGraphStore = defineStore('graph', () => {
433
553
  draggingIds.value = validIds
434
554
  dragAnchor.value = { x: pointer.x, y: pointer.y }
435
555
 
556
+ // 构建后代坐标快照(只计算一次,供预览框渲染使用)
557
+ const newSnapshot = new Map<string, { id: string; x: number; y: number; width: number; height: number }[]>()
558
+ for (const parentId of validIds) {
559
+ const descendants: { id: string; x: number; y: number; width: number; height: number }[] = []
560
+ // 递归收集所有后代
561
+ const collectDescendants = (pid: string) => {
562
+ const childIds = _parentChildMapRef.value.get(pid) || []
563
+ for (const childId of childIds) {
564
+ const childShape = _shapeMapRef.value.get(childId)
565
+ if (childShape?.bounds) {
566
+ descendants.push({
567
+ id: childId,
568
+ x: childShape.bounds.x ?? 0,
569
+ y: childShape.bounds.y ?? 0,
570
+ width: childShape.bounds.width ?? 0,
571
+ height: childShape.bounds.height ?? 0,
572
+ })
573
+ }
574
+ collectDescendants(childId) // 递归
575
+ }
576
+ }
577
+ collectDescendants(parentId)
578
+ newSnapshot.set(parentId, descendants)
579
+ }
580
+ dragDescendantsSnapshot.value = newSnapshot
581
+
436
582
  const currentDiagram = shapes.value.find(s => s.shapeType === 'diagram')
437
583
  if (currentDiagram) {
438
584
  dragBaseCanvasSize.value = {
@@ -589,6 +735,7 @@ export const useGraphStore = defineStore('graph', () => {
589
735
  }
590
736
  // 结束拖动:提交位置/尺寸 + 处理归属 + 规范层级
591
737
  const endDragShape = async (source?: string) => {
738
+ // 防并发:同一次拖拽只允许进入一次
592
739
  if (!isDragging.value) return
593
740
  // 提交拖动几何(commitDrag 只是“位置/大小”的更新)
594
741
  const changedIds = commitDrag(
@@ -632,7 +779,6 @@ export const useGraphStore = defineStore('graph', () => {
632
779
  pendingNestedIds,
633
780
  addShape,
634
781
  })
635
-
636
782
  if (nestResult.cancelled) {
637
783
  // 嵌套校验失败,已经在 graphDragService 里回滚了位置/父级,这里只需要收尾
638
784
  cleanupAfterDrag()
@@ -652,7 +798,6 @@ export const useGraphStore = defineStore('graph', () => {
652
798
  1,
653
799
  )
654
800
  }
655
-
656
801
  // —— Pin 终极兜底:不允许在本次拖拽后变成“无父” ——
657
802
  for (const id of changedIds) {
658
803
  const node = shapes.value.find(s => s.id === id)
@@ -671,13 +816,33 @@ export const useGraphStore = defineStore('graph', () => {
671
816
  })
672
817
  //对“本次直接拖动的隔间”做一次自动扩容检查
673
818
  autoExpandMovedCompartmentsAfterDrag(shapes.value, changedIds, updateShape)
819
+ // 把 clone 也并入“需要同步 comparents 的变更集合”
820
+ const allReparentIds = [...changedIds, ...clonedIds]
821
+ // prevParentById 要补齐 clone 的“拖动前父”,因为 clone 之前不存在
822
+ const prevMapForComparents = { ...prevParentById }
823
+ for (const cid of clonedIds) {
824
+ // clone 之前不存在 => 认为 beforePid = null / ''
825
+ prevMapForComparents[cid] = null
826
+ }
674
827
  // 处理comparents字段
675
828
  syncShowComparentsByReparent(
676
829
  shapes.value,
677
- changedIds,
678
- prevParentById,
830
+ allReparentIds,
831
+ prevMapForComparents,
679
832
  (id, u) => updateShapeRaw(id, u)
680
833
  )
834
+ // 在生成 payloads 之前,把父扩容先做掉
835
+ const shapeId = ownerPayload?.shapeId
836
+ if (shapeId != null) {
837
+ const child = shapes.value.find(s => s.id == shapeId)
838
+ if (child) {
839
+ expandParentByChild({
840
+ shapes: shapes.value,
841
+ child,
842
+ updateShape,
843
+ })
844
+ }
845
+ }
681
846
  // 收集受影响的所有 shape(自身 + 父 + 子树 + 祖先)
682
847
  const affectedIds = collectAffectedShapeIds(shapes.value, changedIds, clonedIds)
683
848
  //组装 payloads(深拷贝),给更新接口用
@@ -688,7 +853,6 @@ export const useGraphStore = defineStore('graph', () => {
688
853
  ownerPayload,
689
854
  updateShape,
690
855
  })
691
-
692
856
  // 先同步调整画布大小,确保获取到最新的画布数据
693
857
  adjustCanvasToFitAllShapes()
694
858
 
@@ -705,11 +869,8 @@ export const useGraphStore = defineStore('graph', () => {
705
869
  }
706
870
  }
707
871
  }
708
- console.log("drag end payloads:", payloads, ownerPayload);
709
-
710
872
  //对外只暴露一个事件:shape-drag-end
711
873
  eventBus.emit('shape-drag-end', payloads, ownerPayload, onNestDone)
712
-
713
874
  // 清理拖拽状态
714
875
  cleanupAfterDrag()
715
876
  }
@@ -770,6 +931,7 @@ export const useGraphStore = defineStore('graph', () => {
770
931
  dragSelectionSnapshot.value = []
771
932
  primaryDragId.value = null
772
933
  dragBaseCanvasSize.value = null
934
+ dragDescendantsSnapshot.value = new Map() // 清理后代快照
773
935
  for (const k in dragGhost) delete dragGhost[k]
774
936
  }
775
937
  //设置当前打开的画布id
@@ -925,6 +1087,8 @@ export const useGraphStore = defineStore('graph', () => {
925
1087
  shapeMap, // 图元ID索引映射 (性能优化)
926
1088
  marqueeShapes,
927
1089
  ghostShadow,
1090
+ dragDescendantsSnapshot, // 拖动时的后代坐标快照
1091
+ dragBase, // 拖动开始时的坐标快照
928
1092
  scales,
929
1093
  activeDiagramId,
930
1094
  // 当前活动画布的缩放比例
@@ -709,9 +709,11 @@ const pointInRect = (pt: { x: number; y: number }, r: Rect, margin = 0) =>
709
709
  pt.y <= r.y + r.height - margin
710
710
 
711
711
  const depthOf = (shapes: Shape[], id: string): number => {
712
+ const graphStore = useGraphStore();
713
+ const byId = graphStore.shapeMap;
712
714
  let d = 0
713
- let p = shapes.find(s => s.id === id)?.parenShapeId ?? null
714
- while (p) { d++; p = shapes.find(s => s.id === p)?.parenShapeId ?? null }
715
+ let p = byId.get(id)?.parenShapeId ?? null
716
+ while (p) { d++; p = byId.get(p)?.parenShapeId ?? null }
715
717
  return d
716
718
  }
717
719
 
@@ -163,10 +163,43 @@ export class ContextMenuUtils {
163
163
  }
164
164
 
165
165
  /**
166
- * 清除剪切状态
167
- * 当按 ESC 取消或其他需要清除剪切状态的场景调用
166
+ * 检查剪贴板是否有内容
167
+ */
168
+ static hasClipboardContent(): boolean {
169
+ return this.copiedShapes.length > 0;
170
+ }
171
+
172
+ /**
173
+ * 清空剪贴板(粘贴完成后调用)
174
+ */
175
+ static clearClipboard() {
176
+ this.copiedShapes = [];
177
+ this.operationType = 'copy';
178
+
179
+ // 清空 GraphStore 中的剪贴板数量
180
+ const graphStore = useGraphStore();
181
+ graphStore.setCopiedShapesCount(0);
182
+ console.log('剪贴板已清空');
183
+ }
184
+
185
+ /**
186
+ * 清除剪切状态(只清除SVG遮盖层,不清除剪贴板数据)
187
+ * 当点击空白处时调用
168
188
  */
169
189
  static clearCutState() {
190
+ // 只清除剪切遮盖层
191
+ if (this.operationType === 'cut') {
192
+ const graphStore = useGraphStore();
193
+ graphStore.clearCutShapeIds();
194
+ }
195
+ // 注意:不清除 copiedShapes 和 operationType,保留剪贴板数据
196
+ }
197
+
198
+ /**
199
+ * 完全清除剪切状态和剪贴板数据
200
+ * 当需要完全重置时调用(如按ESC取消操作)
201
+ */
202
+ static clearAll() {
170
203
  if (this.operationType === 'cut') {
171
204
  const graphStore = useGraphStore();
172
205
  graphStore.clearCutShapeIds();
@@ -224,7 +257,7 @@ export class ContextMenuUtils {
224
257
  /**
225
258
  * 处理粘贴
226
259
  */
227
- static handlePaste(target: any) {
260
+ static handlePaste(_target?: any) {
228
261
  if (this.copiedShapes.length === 0) {
229
262
  return;
230
263
  }
@@ -237,17 +270,15 @@ export class ContextMenuUtils {
237
270
  operationType: this.operationType
238
271
  });
239
272
 
240
- // 如果是剪切操作,粘贴后清除剪切状态遮盖层和剪贴板
273
+ // 如果是剪切操作,粘贴后清除剪切状态遮盖层
241
274
  if (this.operationType === 'cut') {
242
275
  const graphStore = useGraphStore();
243
276
  graphStore.clearCutShapeIds();
244
-
245
- // 剪切粘贴后清空剪贴板数量
246
- graphStore.setCopiedShapesCount(0);
247
- this.copiedShapes = [];
248
- this.operationType = 'copy';
249
277
  }
250
278
 
279
+ // 粘贴完成后清空剪贴板(无论是复制还是剪切)
280
+ this.clearClipboard();
281
+
251
282
  console.log('已粘贴的图元:', pastedShapes, '操作类型:', this.operationType);
252
283
  }
253
284