@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.
- package/dist/index.d.ts +120 -4
- package/dist/index.esm.js +989 -616
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +2 -2
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -1
- package/src/components/InteractionLayer.vue +93 -3
- 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 +256 -92
- package/src/utils/containers.ts +4 -2
- package/src/utils/contextMenuUtils.ts +40 -9
- package/src/utils/drag.ts +48 -8
- package/src/utils/graphDragService.ts +10 -9
- package/src/utils/keyboardUtils.ts +25 -11
- package/src/utils/shapeOps/shapeOps.ts +104 -32
- package/src/view/graph.vue +4 -2
package/src/utils/drag.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// utils/drag.ts
|
|
1
|
+
// utils/drag.ts
|
|
2
2
|
import type { Shape } from '../types'
|
|
3
3
|
import { getBounds, getDiagramRect, clampPointToRect } from './geom'
|
|
4
4
|
import { useGraphStore } from '../store/graphStore'
|
|
@@ -31,9 +31,11 @@ export const buildDragSnapshot = (
|
|
|
31
31
|
shapes: Shape[],
|
|
32
32
|
ids: string[],
|
|
33
33
|
) => {
|
|
34
|
+
const graphStore = useGraphStore();
|
|
35
|
+
const byId = graphStore.shapeMap;
|
|
34
36
|
const dragBase: Record<string, Rect> = {}
|
|
35
37
|
ids.forEach(id => {
|
|
36
|
-
const s =
|
|
38
|
+
const s = byId.get(id); if (!s) return
|
|
37
39
|
const b = getBounds(s)
|
|
38
40
|
dragBase[id] = { x: b.x, y: b.y, width: b.width, height: b.height }
|
|
39
41
|
})
|
|
@@ -61,7 +63,8 @@ export const stepSingleDrag = (
|
|
|
61
63
|
constrainEdges: { left?: boolean; top?: boolean; right?: boolean; bottom?: boolean },
|
|
62
64
|
hitPointer?: { x: number; y: number },
|
|
63
65
|
) => {
|
|
64
|
-
const
|
|
66
|
+
const graphStore = useGraphStore();
|
|
67
|
+
const s = graphStore.shapeMap.get(id); if (!s) return { ghost: {}, hover: null }
|
|
65
68
|
const base = dragBase[id]; if (!base) return { ghost: {}, hover: null }
|
|
66
69
|
const container = getDiagramRect(shapes)
|
|
67
70
|
|
|
@@ -101,6 +104,8 @@ export const stepGroupDrag = (
|
|
|
101
104
|
dragAnchor: { x: number; y: number },
|
|
102
105
|
groupBaseBox: Rect,
|
|
103
106
|
) => {
|
|
107
|
+
const graphStore = useGraphStore();
|
|
108
|
+
const byId = graphStore.shapeMap;
|
|
104
109
|
const diagramRect = getDiagramRect(shapes)
|
|
105
110
|
const dxRaw = pointer.x - dragAnchor.x
|
|
106
111
|
const dyRaw = pointer.y - dragAnchor.y
|
|
@@ -120,14 +125,14 @@ export const stepGroupDrag = (
|
|
|
120
125
|
// 构建整组 ghost
|
|
121
126
|
const ghost: Record<string, Rect> = {}
|
|
122
127
|
for (const id of draggingIds) {
|
|
123
|
-
const s =
|
|
128
|
+
const s = byId.get(id); if (!s) continue
|
|
124
129
|
const b = getBounds(s)
|
|
125
130
|
ghost[id] = { x: b.x + dx, y: b.y + dy, width: b.width, height: b.height }
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
// —— hover 统一改为“指针命中 topmost 容器”
|
|
129
134
|
// 用主拖动项获取 diagramId(也可以存你当前图的 id)
|
|
130
|
-
const priShape =
|
|
135
|
+
const priShape = byId.get(primaryId)
|
|
131
136
|
const diagramId = priShape?.diagramId || ''
|
|
132
137
|
|
|
133
138
|
const hoverShape = pickContainerByPointerTopmost(
|
|
@@ -155,9 +160,22 @@ export const commitDrag = (
|
|
|
155
160
|
updateShape: (id: string, updates: Partial<Shape>) => void,
|
|
156
161
|
baseMap?: Record<string, Rect>
|
|
157
162
|
) => {
|
|
163
|
+
const graphStore = useGraphStore();
|
|
164
|
+
const parentChildMap = graphStore.parentChildMap;
|
|
158
165
|
const changedIds = new Set<string>()
|
|
159
166
|
|
|
160
|
-
//
|
|
167
|
+
// 递归收集所有后代 ID
|
|
168
|
+
const collectAllDescendants = (parentId: string): string[] => {
|
|
169
|
+
const result: string[] = []
|
|
170
|
+
const children = parentChildMap.get(parentId) || []
|
|
171
|
+
for (const childId of children) {
|
|
172
|
+
result.push(childId)
|
|
173
|
+
result.push(...collectAllDescendants(childId))
|
|
174
|
+
}
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 仅把"参与拖拽的节点"的 ghost 写回;不处理父子关系,也不扩父或夹回
|
|
161
179
|
for (const id of draggingIds) {
|
|
162
180
|
const s = shapes.find(x => x.id === id)
|
|
163
181
|
const r = ghost[id] // 参与拖拽的 id 都应当在 ghost 里
|
|
@@ -172,12 +190,34 @@ export const commitDrag = (
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
const pb = (baseMap?.[id] ?? (s.bounds as Rect))
|
|
175
|
-
const
|
|
176
|
-
|
|
193
|
+
const dx = nb.x - pb.x
|
|
194
|
+
const dy = nb.y - pb.y
|
|
195
|
+
const same = dx === 0 && dy === 0 && nb.width === pb.width && nb.height === pb.height
|
|
177
196
|
|
|
178
197
|
if (!same) {
|
|
179
198
|
updateShape(id, { bounds: nb })
|
|
180
199
|
changedIds.add(id)
|
|
200
|
+
|
|
201
|
+
// 性能优化:同步移动所有后代元素(只在父元素位置变化时)
|
|
202
|
+
if (dx !== 0 || dy !== 0) {
|
|
203
|
+
const descendants = collectAllDescendants(id)
|
|
204
|
+
for (const descId of descendants) {
|
|
205
|
+
// 跳过已经在 draggingIds 中的元素(避免重复处理)
|
|
206
|
+
if (draggingIds.includes(descId)) continue
|
|
207
|
+
const descShape = shapes.find(x => x.id === descId)
|
|
208
|
+
if (!descShape?.bounds) continue
|
|
209
|
+
const descBounds = descShape.bounds as Rect
|
|
210
|
+
updateShape(descId, {
|
|
211
|
+
bounds: {
|
|
212
|
+
x: Math.round(descBounds.x + dx),
|
|
213
|
+
y: Math.round(descBounds.y + dy),
|
|
214
|
+
width: descBounds.width,
|
|
215
|
+
height: descBounds.height,
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
changedIds.add(descId)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
|
|
@@ -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 },
|
|
@@ -31,11 +31,13 @@ const KEYBOARD_MOVE_DEBOUNCE_DELAY = 1000; // 1000ms 防抖延迟
|
|
|
31
31
|
*/
|
|
32
32
|
const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<string> => {
|
|
33
33
|
const affectedIds = new Set<string>();
|
|
34
|
+
const shapeMap = graphStore.shapeMap;
|
|
34
35
|
|
|
35
36
|
movedIds.forEach(id => {
|
|
36
37
|
affectedIds.add(id);
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
// 使用 shapeMap O(1) 查找
|
|
40
|
+
const shape = shapeMap.get(id);
|
|
39
41
|
if (!shape) return;
|
|
40
42
|
|
|
41
43
|
// 添加父元素
|
|
@@ -58,7 +60,8 @@ const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<st
|
|
|
58
60
|
let guard = 0;
|
|
59
61
|
while (currentParentId && guard++ < 100) {
|
|
60
62
|
affectedIds.add(currentParentId);
|
|
61
|
-
|
|
63
|
+
// 使用 shapeMap O(1) 查找
|
|
64
|
+
const parent = shapeMap.get(currentParentId);
|
|
62
65
|
currentParentId = parent?.parenShapeId;
|
|
63
66
|
}
|
|
64
67
|
});
|
|
@@ -74,10 +77,11 @@ const emitKeyboardMoveEnd = (graphStore: any) => {
|
|
|
74
77
|
|
|
75
78
|
// 收集所有受影响的图元
|
|
76
79
|
const affectedIds = collectAffectedShapeIds(graphStore, keyboardMovingShapeIds);
|
|
80
|
+
const shapeMap = graphStore.shapeMap;
|
|
77
81
|
|
|
78
|
-
// 组装 payloads(深拷贝)
|
|
82
|
+
// 组装 payloads(深拷贝) - 使用 shapeMap O(1) 查找
|
|
79
83
|
const payloads = Array.from(affectedIds)
|
|
80
|
-
.map(id =>
|
|
84
|
+
.map(id => shapeMap.get(id))
|
|
81
85
|
.filter(Boolean)
|
|
82
86
|
.map(s => _.cloneDeep(s));
|
|
83
87
|
|
|
@@ -286,8 +290,14 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
286
290
|
e.preventDefault();
|
|
287
291
|
const step = 1; // 移动距离
|
|
288
292
|
|
|
289
|
-
// 获取画布信息(diagram类型的shape
|
|
290
|
-
|
|
293
|
+
// 获取画布信息(diagram类型的shape)- 遍历一次找 diagram
|
|
294
|
+
let canvas: any = null;
|
|
295
|
+
for (const shape of graphStore.shapes) {
|
|
296
|
+
if (shape.shapeType === 'diagram') {
|
|
297
|
+
canvas = shape;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
291
301
|
const canvasX = canvas?.bounds?.x || 0;
|
|
292
302
|
const canvasY = canvas?.bounds?.y || 0;
|
|
293
303
|
const canvasWidth = canvas?.bounds?.width || 300; // 最小画布宽度
|
|
@@ -299,10 +309,13 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
299
309
|
const canvasMaxX = canvasWidth + canvasX;
|
|
300
310
|
const canvasMaxY = canvasHeight + canvasY;
|
|
301
311
|
|
|
302
|
-
// 获取所有选中的图元
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
312
|
+
// 获取所有选中的图元 - 使用 shapeMap O(1) 查找
|
|
313
|
+
const shapeMap = graphStore.shapeMap;
|
|
314
|
+
const selectedShapes: any[] = [];
|
|
315
|
+
for (const id of graphStore.selectedIds) {
|
|
316
|
+
const shape = shapeMap.get(id);
|
|
317
|
+
if (shape) selectedShapes.push(shape);
|
|
318
|
+
}
|
|
306
319
|
|
|
307
320
|
// ==================== 防抖机制:记录初始状态 ====================
|
|
308
321
|
// 在第一次移动时记录初始bounds
|
|
@@ -362,7 +375,8 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
362
375
|
// 查找并计算所有子图元的新位置
|
|
363
376
|
const childIds = graphStore.parentChildMap.get(shape.id) || [];
|
|
364
377
|
childIds.forEach(childId => {
|
|
365
|
-
|
|
378
|
+
// 使用 shapeMap O(1) 查找
|
|
379
|
+
const child = shapeMap.get(childId);
|
|
366
380
|
if (child && child.bounds) {
|
|
367
381
|
// 如果子图元也在选中列表中,跳过它
|
|
368
382
|
// 因为它会在遍历 selectedShapes 时被单独处理
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
export type ShapeId = string | number
|
|
5
5
|
export type ShapeOp = "add" | "update" | "delete" | "upsert" | "replace"
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 变更详情,用于增量更新外部索引
|
|
9
|
+
*/
|
|
10
|
+
export interface ShapeChangeDetail<Shape> {
|
|
11
|
+
type: 'add' | 'update' | 'delete'
|
|
12
|
+
shape: Shape
|
|
13
|
+
oldShape?: Shape // update 时提供旧值,用于检测父节点变更
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
/**
|
|
8
17
|
* 创建一个图元操作器(每个操作器维护自己的 indexMap)
|
|
9
18
|
* 推荐在每个 store 实例里创建一次并复用(不要每次调用都 new)
|
|
@@ -11,19 +20,47 @@ export type ShapeOp = "add" | "update" | "delete" | "upsert" | "replace"
|
|
|
11
20
|
export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
12
21
|
const indexMap = new Map<ShapeId, number>()
|
|
13
22
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
* 重建索引 + 去重(保留最后出现的同 id)
|
|
24
|
+
* 说明:
|
|
25
|
+
* - 从尾到头遍历,遇到重复 id 就删掉前面的(更早的那条)
|
|
26
|
+
* - 最终 list 中保证每个 id 只剩 1 条
|
|
27
|
+
*/
|
|
28
|
+
const rebuildIndexAndDedupe = (list: Shape[]) => {
|
|
29
|
+
const seen = new Set<ShapeId>()
|
|
30
|
+
|
|
31
|
+
// 先去重:保留最后一条
|
|
32
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
33
|
+
const id = list[i]?.id
|
|
34
|
+
if (id == null) continue
|
|
35
|
+
if (seen.has(id)) {
|
|
36
|
+
list.splice(i, 1) // 删除更早的重复项
|
|
37
|
+
} else {
|
|
38
|
+
seen.add(id)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 再重建索引
|
|
17
43
|
indexMap.clear()
|
|
18
44
|
for (let i = 0; i < list.length; i++) {
|
|
19
45
|
indexMap.set(list[i].id, i)
|
|
20
46
|
}
|
|
21
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 确保索引可用:
|
|
50
|
+
* - 不仅检查 size,还校验 indexMap 指向的元素是否真的匹配 id
|
|
51
|
+
* - 一旦发现错位/重复,直接 rebuild
|
|
52
|
+
*/
|
|
22
53
|
const ensureIndex = (list: Shape[]) => {
|
|
23
|
-
if (indexMap.size
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
54
|
+
if (indexMap.size !== list.length) {
|
|
55
|
+
rebuildIndexAndDedupe(list)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
// 校验映射是否仍然正确(防止 sort/reorder 导致错位)
|
|
59
|
+
for (const [id, idx] of indexMap) {
|
|
60
|
+
if (list[idx]?.id !== id) {
|
|
61
|
+
rebuildIndexAndDedupe(list)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
27
64
|
}
|
|
28
65
|
}
|
|
29
66
|
|
|
@@ -41,13 +78,15 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
41
78
|
return ids
|
|
42
79
|
}
|
|
43
80
|
|
|
44
|
-
/** O(1) 删除:swap last + pop
|
|
45
|
-
const removeByIdFast = (list: Shape[], id: ShapeId) => {
|
|
81
|
+
/** O(1) 删除:swap last + pop(会改变数组顺序),返回被删除的元素 */
|
|
82
|
+
const removeByIdFast = (list: Shape[], id: ShapeId): Shape | null => {
|
|
46
83
|
const idx = indexMap.get(id)
|
|
47
|
-
if (idx == null) return
|
|
84
|
+
if (idx == null) return null
|
|
48
85
|
|
|
49
86
|
const lastIdx = list.length - 1
|
|
50
|
-
if (lastIdx < 0) return
|
|
87
|
+
if (lastIdx < 0) return null
|
|
88
|
+
|
|
89
|
+
const removed = list[idx]
|
|
51
90
|
|
|
52
91
|
if (idx !== lastIdx) {
|
|
53
92
|
const moved = list[lastIdx]
|
|
@@ -57,52 +96,57 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
57
96
|
|
|
58
97
|
list.pop()
|
|
59
98
|
indexMap.delete(id)
|
|
60
|
-
return
|
|
99
|
+
return removed
|
|
61
100
|
}
|
|
62
101
|
|
|
63
102
|
/** upsert:有则整条替换,无则 push 新增 */
|
|
64
|
-
const upsertOne = (list: Shape[], incoming: Shape) => {
|
|
103
|
+
const upsertOne = (list: Shape[], incoming: Shape): { changed: boolean; isAdd: boolean; oldShape?: Shape } => {
|
|
65
104
|
const id = incoming?.id
|
|
66
|
-
if (id == null) return false
|
|
105
|
+
if (id == null) return { changed: false, isAdd: false }
|
|
67
106
|
|
|
68
107
|
const idx = indexMap.get(id)
|
|
69
108
|
if (idx == null) {
|
|
70
109
|
list.push(incoming)
|
|
71
110
|
indexMap.set(id, list.length - 1)
|
|
111
|
+
return { changed: true, isAdd: true }
|
|
72
112
|
} else {
|
|
113
|
+
const oldShape = list[idx]
|
|
73
114
|
list[idx] = incoming
|
|
115
|
+
return { changed: true, isAdd: false, oldShape }
|
|
74
116
|
}
|
|
75
|
-
return true
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
/** update:仅存在才替换 */
|
|
79
|
-
const updateOne = (list: Shape[], incoming: Shape) => {
|
|
120
|
+
const updateOne = (list: Shape[], incoming: Shape): { changed: boolean; oldShape?: Shape } => {
|
|
80
121
|
const id = incoming?.id
|
|
81
|
-
if (id == null) return false
|
|
122
|
+
if (id == null) return { changed: false }
|
|
82
123
|
|
|
83
124
|
const idx = indexMap.get(id)
|
|
84
|
-
if (idx == null) return false
|
|
125
|
+
if (idx == null) return { changed: false }
|
|
85
126
|
|
|
127
|
+
const oldShape = list[idx]
|
|
86
128
|
list[idx] = incoming
|
|
87
|
-
return true
|
|
129
|
+
return { changed: true, oldShape }
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
/**
|
|
91
133
|
* add:不存在才新增
|
|
92
|
-
* -
|
|
134
|
+
* - 已存在时选择"替换"保证最终一致
|
|
93
135
|
*/
|
|
94
|
-
const addOne = (list: Shape[], incoming: Shape) => {
|
|
136
|
+
const addOne = (list: Shape[], incoming: Shape): { changed: boolean; isAdd: boolean; oldShape?: Shape } => {
|
|
95
137
|
const id = incoming?.id
|
|
96
|
-
if (id == null) return false
|
|
138
|
+
if (id == null) return { changed: false, isAdd: false }
|
|
97
139
|
|
|
98
140
|
const idx = indexMap.get(id)
|
|
99
141
|
if (idx == null) {
|
|
100
142
|
list.push(incoming)
|
|
101
143
|
indexMap.set(id, list.length - 1)
|
|
144
|
+
return { changed: true, isAdd: true }
|
|
102
145
|
} else {
|
|
146
|
+
const oldShape = list[idx]
|
|
103
147
|
list[idx] = incoming
|
|
148
|
+
return { changed: true, isAdd: false, oldShape }
|
|
104
149
|
}
|
|
105
|
-
return true
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
/**
|
|
@@ -112,8 +156,9 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
112
156
|
list: Shape[]
|
|
113
157
|
payload: Shape[] | ShapeId[]
|
|
114
158
|
op: ShapeOp
|
|
115
|
-
}) => {
|
|
159
|
+
}): { changedShapes: Shape[]; changeDetails: ShapeChangeDetail<Shape>[] } => {
|
|
116
160
|
const { list, payload, op } = args
|
|
161
|
+
const changeDetails: ShapeChangeDetail<Shape>[] = []
|
|
117
162
|
|
|
118
163
|
// ==================== replace:全量覆盖(就地覆盖数组内容) ====================
|
|
119
164
|
if (op === "replace") {
|
|
@@ -130,8 +175,8 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
130
175
|
for (let i = 0; i < list.length; i++) {
|
|
131
176
|
indexMap.set(list[i].id, i)
|
|
132
177
|
}
|
|
133
|
-
// replace
|
|
134
|
-
return { changedShapes: incomingShapes }
|
|
178
|
+
// replace 场景:返回空的 changeDetails,调用方应该全量重建索引
|
|
179
|
+
return { changedShapes: incomingShapes, changeDetails: [] }
|
|
135
180
|
}
|
|
136
181
|
// 轻量保证索引可用
|
|
137
182
|
ensureIndex(list)
|
|
@@ -141,9 +186,12 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
141
186
|
if (op === "delete") {
|
|
142
187
|
const ids = extractIds(payload as any)
|
|
143
188
|
for (const id of ids) {
|
|
144
|
-
removeByIdFast(list, id)
|
|
189
|
+
const removed = removeByIdFast(list, id)
|
|
190
|
+
if (removed) {
|
|
191
|
+
changeDetails.push({ type: 'delete', shape: removed })
|
|
192
|
+
}
|
|
145
193
|
}
|
|
146
|
-
return { changedShapes }
|
|
194
|
+
return { changedShapes, changeDetails }
|
|
147
195
|
}
|
|
148
196
|
|
|
149
197
|
const incomingShapes = payload as Shape[]
|
|
@@ -151,15 +199,39 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
|
151
199
|
if (!incoming) continue
|
|
152
200
|
|
|
153
201
|
if (op === "upsert") {
|
|
154
|
-
|
|
202
|
+
const result = upsertOne(list, incoming)
|
|
203
|
+
if (result.changed) {
|
|
204
|
+
changedShapes.push(incoming)
|
|
205
|
+
changeDetails.push({
|
|
206
|
+
type: result.isAdd ? 'add' : 'update',
|
|
207
|
+
shape: incoming,
|
|
208
|
+
oldShape: result.oldShape
|
|
209
|
+
})
|
|
210
|
+
}
|
|
155
211
|
} else if (op === "update") {
|
|
156
|
-
|
|
212
|
+
const result = updateOne(list, incoming)
|
|
213
|
+
if (result.changed) {
|
|
214
|
+
changedShapes.push(incoming)
|
|
215
|
+
changeDetails.push({
|
|
216
|
+
type: 'update',
|
|
217
|
+
shape: incoming,
|
|
218
|
+
oldShape: result.oldShape
|
|
219
|
+
})
|
|
220
|
+
}
|
|
157
221
|
} else if (op === "add") {
|
|
158
|
-
|
|
222
|
+
const result = addOne(list, incoming)
|
|
223
|
+
if (result.changed) {
|
|
224
|
+
changedShapes.push(incoming)
|
|
225
|
+
changeDetails.push({
|
|
226
|
+
type: result.isAdd ? 'add' : 'update',
|
|
227
|
+
shape: incoming,
|
|
228
|
+
oldShape: result.oldShape
|
|
229
|
+
})
|
|
230
|
+
}
|
|
159
231
|
}
|
|
160
232
|
}
|
|
161
233
|
|
|
162
|
-
return { changedShapes }
|
|
234
|
+
return { changedShapes, changeDetails }
|
|
163
235
|
}
|
|
164
236
|
|
|
165
237
|
return {
|
package/src/view/graph.vue
CHANGED
|
@@ -360,7 +360,8 @@ const updateShapes = (shapes: Shape[]) => {
|
|
|
360
360
|
})
|
|
361
361
|
clearEdgeStyleCache() // 批量更新时清空 edge 样式缓存,避免旧数据残留
|
|
362
362
|
// graphStore.updateShapes(shapes)
|
|
363
|
-
|
|
363
|
+
// 使用 clearAll 清空数据和索引,然后用 replace 重新加载
|
|
364
|
+
graphStore.clearAll()
|
|
364
365
|
graphStore.updateShapes(shapes, 'replace');
|
|
365
366
|
// 在图形批量更新后(通常是从后端加载数据),初始化所有连线的端点
|
|
366
367
|
// 确保连线不会横跨图元,而是从合适的位置连接
|
|
@@ -388,7 +389,8 @@ function sweepShapesWithInert() {
|
|
|
388
389
|
let removed = 0
|
|
389
390
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
390
391
|
if ('inert' in arr[i]) {
|
|
391
|
-
|
|
392
|
+
// 使用 removeShape 确保索引同步
|
|
393
|
+
graphStore.removeShape(arr[i].id)
|
|
392
394
|
removed++
|
|
393
395
|
}
|
|
394
396
|
}
|