@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.
- package/dist/index.d.ts +16 -3
- package/dist/index.esm.js +637 -564
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/edgeShapeKeys.ts +4 -2
- package/src/constants/index.ts +306 -295
- package/src/render/shape-renderer.ts +13 -6
- package/src/store/graphStore.ts +24 -42
- package/src/utils/contextMenuUtils.ts +40 -9
- package/src/utils/graphDragService.ts +10 -9
- package/src/utils/shapeOps/shapeOps.ts +35 -7
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { CSSProperties } from "vue";
|
|
2
2
|
import type { Shape } from "@/types";
|
|
3
|
-
import {
|
|
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 = (
|
|
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
|
|
41
|
+
return getComponentByEdgeKey(shapeKey);
|
|
35
42
|
default:
|
|
36
43
|
return "ShapeComponent";
|
|
37
44
|
}
|
package/src/store/graphStore.ts
CHANGED
|
@@ -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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|