@mx-sose-front/mx-sose-graph 1.1.5 → 1.1.7

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
  }
@@ -74,7 +74,6 @@ export const useGraphStore = defineStore('graph', () => {
74
74
  const cutShapeIds = ref<Set<string>>(new Set())
75
75
  // 剪贴板中图元的数量(用于工具栏按钮状态同步)
76
76
  const copiedShapesCount = ref<number>(0)
77
-
78
77
  // 计算属性
79
78
  const shapeCount = computed(() => shapes.value.length)
80
79
  const hasSelectedShape = computed(() => selectedShape.value !== null)
@@ -149,7 +148,7 @@ export const useGraphStore = defineStore('graph', () => {
149
148
 
150
149
  shapes.value.push(shape)
151
150
  // 通过事件总线发送事件
152
- eventBus.emit('shape-added', shape)
151
+ // eventBus.emit('shape-added', shape)
153
152
 
154
153
  // 自动扩父逻辑:如果图元有父元素且需要自动扩父,触发扩父逻辑
155
154
  const shouldAutoExpand = options?.autoExpandParent !== false // 默认为 true
@@ -164,16 +163,6 @@ export const useGraphStore = defineStore('graph', () => {
164
163
  const removeShape = (shapeId: string) => {
165
164
  const index = shapes.value.findIndex(s => s.id == shapeId)
166
165
  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
- // }
177
166
  // 如果删除的是当前选中的形状,清除选中状态
178
167
  if (selectedShape.value?.id === shapeId) {
179
168
  selectedShape.value = null
@@ -380,28 +369,6 @@ export const useGraphStore = defineStore('graph', () => {
380
369
  pendingNestedIds.value = []
381
370
  eventBus.emit('shapes-cleared')
382
371
  }
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
372
  //拖动元素相关操作
406
373
  // 开始拖动已有元素(仅对 shape 生效)
407
374
  const byId = (id: string) => shapes.value.find(s => s.id === id) || null
@@ -589,6 +556,7 @@ export const useGraphStore = defineStore('graph', () => {
589
556
  }
590
557
  // 结束拖动:提交位置/尺寸 + 处理归属 + 规范层级
591
558
  const endDragShape = async (source?: string) => {
559
+ // 防并发:同一次拖拽只允许进入一次
592
560
  if (!isDragging.value) return
593
561
  // 提交拖动几何(commitDrag 只是“位置/大小”的更新)
594
562
  const changedIds = commitDrag(
@@ -632,7 +600,6 @@ export const useGraphStore = defineStore('graph', () => {
632
600
  pendingNestedIds,
633
601
  addShape,
634
602
  })
635
-
636
603
  if (nestResult.cancelled) {
637
604
  // 嵌套校验失败,已经在 graphDragService 里回滚了位置/父级,这里只需要收尾
638
605
  cleanupAfterDrag()
@@ -652,7 +619,6 @@ export const useGraphStore = defineStore('graph', () => {
652
619
  1,
653
620
  )
654
621
  }
655
-
656
622
  // —— Pin 终极兜底:不允许在本次拖拽后变成“无父” ——
657
623
  for (const id of changedIds) {
658
624
  const node = shapes.value.find(s => s.id === id)
@@ -671,13 +637,33 @@ export const useGraphStore = defineStore('graph', () => {
671
637
  })
672
638
  //对“本次直接拖动的隔间”做一次自动扩容检查
673
639
  autoExpandMovedCompartmentsAfterDrag(shapes.value, changedIds, updateShape)
640
+ // 把 clone 也并入“需要同步 comparents 的变更集合”
641
+ const allReparentIds = [...changedIds, ...clonedIds]
642
+ // prevParentById 要补齐 clone 的“拖动前父”,因为 clone 之前不存在
643
+ const prevMapForComparents = { ...prevParentById }
644
+ for (const cid of clonedIds) {
645
+ // clone 之前不存在 => 认为 beforePid = null / ''
646
+ prevMapForComparents[cid] = null
647
+ }
674
648
  // 处理comparents字段
675
649
  syncShowComparentsByReparent(
676
650
  shapes.value,
677
- changedIds,
678
- prevParentById,
651
+ allReparentIds,
652
+ prevMapForComparents,
679
653
  (id, u) => updateShapeRaw(id, u)
680
654
  )
655
+ // 在生成 payloads 之前,把父扩容先做掉
656
+ const shapeId = ownerPayload?.shapeId
657
+ if (shapeId != null) {
658
+ const child = shapes.value.find(s => s.id === shapeId)
659
+ if (child) {
660
+ expandParentByChild({
661
+ shapes: shapes.value,
662
+ child,
663
+ updateShape,
664
+ })
665
+ }
666
+ }
681
667
  // 收集受影响的所有 shape(自身 + 父 + 子树 + 祖先)
682
668
  const affectedIds = collectAffectedShapeIds(shapes.value, changedIds, clonedIds)
683
669
  //组装 payloads(深拷贝),给更新接口用
@@ -688,7 +674,6 @@ export const useGraphStore = defineStore('graph', () => {
688
674
  ownerPayload,
689
675
  updateShape,
690
676
  })
691
-
692
677
  // 先同步调整画布大小,确保获取到最新的画布数据
693
678
  adjustCanvasToFitAllShapes()
694
679
 
@@ -705,11 +690,8 @@ export const useGraphStore = defineStore('graph', () => {
705
690
  }
706
691
  }
707
692
  }
708
- console.log("drag end payloads:", payloads, ownerPayload);
709
-
710
693
  //对外只暴露一个事件:shape-drag-end
711
694
  eventBus.emit('shape-drag-end', payloads, ownerPayload, onNestDone)
712
-
713
695
  // 清理拖拽状态
714
696
  cleanupAfterDrag()
715
697
  }
@@ -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
 
@@ -77,13 +77,14 @@ export async function applyReparentAndClone(options: {
77
77
  const graphStore = useGraphStore()
78
78
  let ownerPayload: OwnerPayload | null = null
79
79
  const clonedIds: string[] = []
80
- // 只在嵌套/脱离时刷新
81
- const parentsToRefresh = new Set<string>();
82
- const normPid = (pid: any): string | null => {
83
- const v = pid ?? null;
84
- return v === "" ? null : v;
85
- };
86
- for (const id of changedIds) {
80
+ // 只保留顶层变更:父也在 changedIds 中的,认为是后代(跳过克隆)
81
+ const changedSet = new Set(changedIds)
82
+ const topLevelChangedIds = changedIds.filter(id => {
83
+ const beforePid = prevParentById[id] ?? null
84
+ const afterPid = shapes.find(s => s.id === id)?.parenShapeId ?? null
85
+ return !(beforePid && changedSet.has(beforePid)) && !(afterPid && changedSet.has(afterPid))
86
+ })
87
+ for (const id of topLevelChangedIds) {
87
88
  const after = shapes.find(s => s.id === id)
88
89
  if (!after) continue
89
90
 
@@ -132,7 +133,6 @@ export async function applyReparentAndClone(options: {
132
133
  parenShapeId: prevParentById[id] || '',
133
134
  bounds: baseBounds ? { ...baseBounds } : dropBounds,
134
135
  })
135
-
136
136
  // 放进画布
137
137
  addShape(clonedShape)
138
138
  pendingNestedIds.value.push(newId)
@@ -144,7 +144,7 @@ export async function applyReparentAndClone(options: {
144
144
  })
145
145
 
146
146
  const cloneInStore = shapes.find(s => s.id === newId) || clonedShape
147
- const shapeDataForAdd: any = _.cloneDeep(cloneInStore)
147
+ const shapeDataForAdd: any = _.cloneDeep(cloneInStore)
148
148
  const payloadData: any = {
149
149
  coordinate: {
150
150
  clientX: dropBounds.x,
@@ -322,6 +322,7 @@ export const createOnNestDoneCallback = (options: {
322
322
  if (!expanded) return
323
323
  const afterParent = JSON.stringify(parent.bounds ?? {})
324
324
  if (afterParent == beforeParent) return
325
+ return { id: parent.id, bounds: afterParent }
325
326
  // 只更新当前嵌套的父图元(parent)
326
327
  // eventBus.emit('shape-size-update', [
327
328
  // { id: parent.id, bounds: afterParent },
@@ -11,19 +11,47 @@ export type ShapeOp = "add" | "update" | "delete" | "upsert" | "replace"
11
11
  export function createShapeOperator<Shape extends { id: ShapeId }>() {
12
12
  const indexMap = new Map<ShapeId, number>()
13
13
  /**
14
- * 重建索引
15
- */
16
- const resetIndex = (list: Shape[]) => {
14
+ * 重建索引 + 去重(保留最后出现的同 id)
15
+ * 说明:
16
+ * - 从尾到头遍历,遇到重复 id 就删掉前面的(更早的那条)
17
+ * - 最终 list 中保证每个 id 只剩 1 条
18
+ */
19
+ const rebuildIndexAndDedupe = (list: Shape[]) => {
20
+ const seen = new Set<ShapeId>()
21
+
22
+ // 先去重:保留最后一条
23
+ for (let i = list.length - 1; i >= 0; i--) {
24
+ const id = list[i]?.id
25
+ if (id == null) continue
26
+ if (seen.has(id)) {
27
+ list.splice(i, 1) // 删除更早的重复项
28
+ } else {
29
+ seen.add(id)
30
+ }
31
+ }
32
+
33
+ // 再重建索引
17
34
  indexMap.clear()
18
35
  for (let i = 0; i < list.length; i++) {
19
36
  indexMap.set(list[i].id, i)
20
37
  }
21
38
  }
39
+ /**
40
+ * 确保索引可用:
41
+ * - 不仅检查 size,还校验 indexMap 指向的元素是否真的匹配 id
42
+ * - 一旦发现错位/重复,直接 rebuild
43
+ */
22
44
  const ensureIndex = (list: Shape[]) => {
23
- if (indexMap.size === list.length) return
24
- indexMap.clear()
25
- for (let i = 0; i < list.length; i++) {
26
- indexMap.set(list[i].id, i)
45
+ if (indexMap.size !== list.length) {
46
+ rebuildIndexAndDedupe(list)
47
+ return
48
+ }
49
+ // 校验映射是否仍然正确(防止 sort/reorder 导致错位)
50
+ for (const [id, idx] of indexMap) {
51
+ if (list[idx]?.id !== id) {
52
+ rebuildIndexAndDedupe(list)
53
+ return
54
+ }
27
55
  }
28
56
  }
29
57