@logicflow/extension 2.2.0-alpha.5 → 2.2.0-alpha.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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/CHANGELOG.md +15 -0
  3. package/dist/index.min.js +1 -1
  4. package/dist/index.min.js.map +1 -1
  5. package/es/bpmn-elements-adapter/json2xml.d.ts +2 -1
  6. package/es/bpmn-elements-adapter/json2xml.js +18 -4
  7. package/es/bpmn-elements-adapter/xml2json.js +2 -7
  8. package/es/components/control/index.js +3 -3
  9. package/es/index.d.ts +1 -0
  10. package/es/index.js +2 -0
  11. package/es/materials/curved-edge/index.js +41 -25
  12. package/es/pool/LaneModel.d.ts +90 -0
  13. package/es/pool/LaneModel.js +252 -0
  14. package/es/pool/LaneView.d.ts +40 -0
  15. package/es/pool/LaneView.js +202 -0
  16. package/es/pool/PoolModel.d.ts +120 -0
  17. package/es/pool/PoolModel.js +586 -0
  18. package/es/pool/PoolView.d.ts +17 -0
  19. package/es/pool/PoolView.js +76 -0
  20. package/es/pool/constant.d.ts +15 -0
  21. package/es/pool/constant.js +17 -0
  22. package/es/pool/index.d.ts +89 -0
  23. package/es/pool/index.js +524 -0
  24. package/es/pool/utils.d.ts +19 -0
  25. package/es/pool/utils.js +33 -0
  26. package/lib/bpmn-elements-adapter/json2xml.d.ts +2 -1
  27. package/lib/bpmn-elements-adapter/json2xml.js +19 -4
  28. package/lib/bpmn-elements-adapter/xml2json.js +2 -7
  29. package/lib/components/control/index.js +3 -3
  30. package/lib/index.d.ts +1 -0
  31. package/lib/index.js +2 -0
  32. package/lib/materials/curved-edge/index.js +41 -25
  33. package/lib/pool/LaneModel.d.ts +90 -0
  34. package/lib/pool/LaneModel.js +255 -0
  35. package/lib/pool/LaneView.d.ts +40 -0
  36. package/lib/pool/LaneView.js +205 -0
  37. package/lib/pool/PoolModel.d.ts +120 -0
  38. package/lib/pool/PoolModel.js +589 -0
  39. package/lib/pool/PoolView.d.ts +17 -0
  40. package/lib/pool/PoolView.js +79 -0
  41. package/lib/pool/constant.d.ts +15 -0
  42. package/lib/pool/constant.js +20 -0
  43. package/lib/pool/index.d.ts +89 -0
  44. package/lib/pool/index.js +527 -0
  45. package/lib/pool/utils.d.ts +19 -0
  46. package/lib/pool/utils.js +38 -0
  47. package/package.json +5 -5
  48. package/src/bpmn-elements-adapter/json2xml.ts +18 -4
  49. package/src/bpmn-elements-adapter/xml2json.ts +2 -8
  50. package/src/components/control/index.ts +3 -3
  51. package/src/dynamic-group/index.ts +0 -1
  52. package/src/index.ts +2 -0
  53. package/src/materials/curved-edge/index.ts +47 -30
  54. package/src/pool/LaneModel.ts +226 -0
  55. package/src/pool/LaneView.ts +220 -0
  56. package/src/pool/PoolModel.ts +631 -0
  57. package/src/pool/PoolView.ts +75 -0
  58. package/src/pool/constant.ts +19 -0
  59. package/src/pool/index.ts +621 -0
  60. package/src/pool/utils.ts +46 -0
  61. package/stats.html +1 -1
@@ -0,0 +1,631 @@
1
+ /**
2
+ * 基于DynamicGroup重新实现的泳池节点组件
3
+ * 充分利用DynamicGroup的分组管理能力,实现完整的泳道功能
4
+ */
5
+ import LogicFlow, { GraphModel, BaseEdgeModel } from '@logicflow/core'
6
+ import { computed } from 'mobx'
7
+ import { forEach, merge, cloneDeep, isEmpty } from 'lodash-es'
8
+ import {
9
+ DynamicGroupNodeModel,
10
+ IGroupNodeProperties,
11
+ } from '../dynamic-group/model'
12
+ import { poolConfig, laneConfig } from './constant'
13
+
14
+ // import { LaneModel } from './NewLane'
15
+
16
+ import NodeConfig = LogicFlow.NodeConfig
17
+
18
+ export class PoolModel extends DynamicGroupNodeModel {
19
+ // 泳池特定属性
20
+ // 标题区域大小:如果是垂直方向,指代的就是标题区的宽度,如果是水平方向,指代的就是标题区的高度
21
+ titleSize: number = poolConfig.titleSize
22
+ poolConfig: typeof poolConfig = poolConfig
23
+ readonly isPool: boolean = true
24
+
25
+ // 标记是否已创建默认泳道
26
+ _defaultLaneCreated: boolean = false
27
+ constructor(data: NodeConfig<IGroupNodeProperties>, graphModel: GraphModel) {
28
+ super(data, graphModel)
29
+ }
30
+
31
+ @computed get isHorizontal() {
32
+ return this.properties?.direction === 'horizontal'
33
+ }
34
+
35
+ initNodeData(data: LogicFlow.NodeConfig<IGroupNodeProperties>) {
36
+ super.initNodeData(data)
37
+ if (data.properties) {
38
+ // 泳池基础配置
39
+ this.width = data.properties?.width || poolConfig.defaultWidth
40
+ this.height = data.properties?.height || poolConfig.defaultHeight
41
+ }
42
+
43
+ // 动态分组配置
44
+ this.autoResize = false
45
+ this.isRestrict = true
46
+ this.transformWithContainer = true
47
+ this.resizable = false
48
+ this.rotatable = false
49
+ this.autoToFront = false
50
+
51
+ // 允许文本编辑
52
+ this.text.editable = true
53
+
54
+ // 初始化文本位置
55
+ this.updateTextPosition()
56
+ this.addEventListeners()
57
+ this.resizePool()
58
+ }
59
+
60
+ // 增加监听事件
61
+ addEventListeners() {
62
+ this.graphModel.eventCenter.on('node:resize', ({ data, index }) => {
63
+ // 如果resize的是子泳道
64
+ if (this.children.has(data.id)) {
65
+ // 检查是否为泳道节点的尺寸变化
66
+ const resizedNode = this.graphModel.getNodeModelById(data.id)
67
+ if (!resizedNode || !resizedNode.type || resizedNode.type !== 'lane') {
68
+ return
69
+ }
70
+
71
+ // 获取所有子泳道
72
+ const lanes = this.getLanes()
73
+
74
+ // 更新泳池宽高
75
+ let newWidth: number
76
+ let newHeight: number
77
+ let deltaX: number = 0
78
+ let deltaY: number = 0
79
+ if (this.isHorizontal) {
80
+ // 横向布局:
81
+ // 泳池宽度 = 最大泳道宽度 + 标题区域
82
+
83
+ const totalLaneHeight = lanes.reduce(
84
+ (sum, lane) => sum + lane.height,
85
+ 0,
86
+ )
87
+ newWidth = resizedNode.width + poolConfig.titleSize
88
+ // 泳池高度 = 所有泳道高度之和
89
+ newHeight = totalLaneHeight
90
+ } else {
91
+ // 竖向布局:
92
+ // 泳池高度 = 最大泳道高度 + 标题区域
93
+ const totalLaneWidth = lanes.reduce(
94
+ (sum, lane) => sum + lane.width,
95
+ 0,
96
+ )
97
+ newHeight = resizedNode.height + poolConfig.titleSize
98
+ // 泳池宽度 = 所有泳道宽度之和
99
+ newWidth = totalLaneWidth
100
+ }
101
+ // 根据拖拽控制点方向计算位移方向
102
+ // ResizeControlIndex: 0-左上, 1-右上, 2-右下, 3-左下
103
+ const resizeIndex = typeof index === 'number' ? index : 2
104
+ const isLeft = resizeIndex === 0 || resizeIndex === 3
105
+ const isTop = resizeIndex === 0 || resizeIndex === 1
106
+ const signX = isLeft ? -1 : 1
107
+ const signY = isTop ? -1 : 1
108
+ deltaX = signX * (newWidth - this.width)
109
+ deltaY = signY * (newHeight - this.height)
110
+ this.width = newWidth
111
+ this.height = newHeight
112
+ this.move(deltaX / 2, deltaY / 2)
113
+ }
114
+ // 重新布局泳道以适应新的泳池尺寸
115
+ this.resizeChildren()
116
+ // 更新泳池文本位置
117
+ this.updateTextPosition()
118
+ })
119
+ }
120
+
121
+ /**
122
+ * 获取需要移动的节点
123
+ * @param groupModel
124
+ */
125
+ getNodesInGroup(groupModel: DynamicGroupNodeModel): string[] {
126
+ const nodeIds: string[] = []
127
+ if (groupModel.isGroup) {
128
+ forEach(Array.from(groupModel.children), (nodeId: string) => {
129
+ const nodeModel = this.graphModel.getNodeModelById(nodeId)
130
+ // 拖拽泳道时会触发泳池的getNodesInGroup,这时泳池再触发移动的子泳道里就需要剔除当前正在拖拽的泳道
131
+ if (nodeModel && !nodeModel.isDragging) {
132
+ nodeIds.push(nodeId)
133
+ }
134
+ })
135
+ }
136
+ return nodeIds
137
+ }
138
+ /**
139
+ * 初始化文本位置 - 根据布局方向设置文本位置
140
+ */
141
+ private updateTextPosition() {
142
+ if (this.isHorizontal) {
143
+ // 横向泳池:文本显示在左侧标题区域
144
+ this.text.x = this.x - this.width / 2 + poolConfig.titleSize / 2
145
+ this.text.y = this.y
146
+ } else {
147
+ // 纵向泳池:文本显示在顶部标题区域
148
+ this.text.x = this.x
149
+ this.text.y = this.y - this.height / 2 + poolConfig.titleSize / 2
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 根据子泳道自动调整泳池尺寸
155
+ */
156
+ resizePool() {
157
+ const lanes = this.getLanes()
158
+ if (lanes.length === 0) return
159
+ let contentWidth = 0
160
+ let contentHeight = 0
161
+ if (this.isHorizontal) {
162
+ // 横向布局:计算所有泳道的边界
163
+ forEach(lanes, (lane) => {
164
+ const laneWidth = lane.width
165
+ const laneHeight = lane.height
166
+ contentWidth = Math.max(contentWidth, laneWidth)
167
+ contentHeight += laneHeight
168
+ })
169
+ // 计算新尺寸(横向布局:宽度包含标题区域)
170
+ this.width = contentWidth + poolConfig.titleSize
171
+ this.height = contentHeight
172
+ } else {
173
+ // 竖向布局:计算所有泳道的边界
174
+ forEach(lanes, (lane) => {
175
+ const laneWidth = lane.width
176
+ const laneHeight = lane.height
177
+ contentWidth += laneWidth
178
+ contentHeight = Math.max(contentHeight, laneHeight)
179
+ })
180
+ // 计算新尺寸(竖向布局:高度包含标题区域)
181
+ this.width = contentWidth
182
+ this.height = contentHeight + poolConfig.titleSize
183
+ }
184
+
185
+ // 更新文本位置
186
+ this.updateTextPosition()
187
+ }
188
+
189
+ /**
190
+ * 重新调整所有泳道布局
191
+ * @param newLanePosition 添加位置(可选):'above'|'below'|'left'|'right'
192
+ * @param newLaneId 新添加的泳道ID(可选)
193
+ */
194
+ resizeChildrenWithNewLane(
195
+ newLanePosition?: 'above' | 'below' | 'left' | 'right',
196
+ newLaneId?: string,
197
+ ) {
198
+ const lanes = this.getLanes()
199
+ const isAddingNewLane = newLanePosition && newLaneId
200
+ if (!isAddingNewLane || isEmpty(lanes)) return
201
+ let orderedLanes = [] as any[]
202
+ // 找到新创建的泳道
203
+ const newLane = lanes.find((lane) => lane.id === newLaneId)
204
+ // 先找到触发新增泳道的泳道的index
205
+ orderedLanes = lanes
206
+ .filter((lane) => lane.id !== newLaneId)
207
+ .slice()
208
+ .sort((a: any, b: any) => (this.isHorizontal ? a.y - b.y : a.x - b.x))
209
+ if (newLane) {
210
+ const refId = (newLane as any).properties?.referenceLaneId
211
+ const refIndex = refId
212
+ ? orderedLanes.findIndex((l: any) => l.id === refId)
213
+ : 0
214
+ const insertIndex = ['above', 'left'].includes(newLanePosition)
215
+ ? Math.max(refIndex, 0)
216
+ : Math.min(refIndex + 1, orderedLanes.length)
217
+ // 按顺序插入新泳道
218
+ orderedLanes.splice(insertIndex, 0, newLane)
219
+ }
220
+ if (this.isHorizontal) {
221
+ // 统一泳道宽度
222
+ const laneWidth = this.width - poolConfig.titleSize
223
+ // 计算泳道在内容区域内的分布
224
+ const newHeight = orderedLanes.reduce((sum: number, lane: any) => {
225
+ return sum + lane.height
226
+ }, 0)
227
+ let laneTopDistance: number = this.y - newHeight / 2
228
+ orderedLanes.forEach((lane: any, index: number) => {
229
+ const newLaneY = laneTopDistance + lane.height / 2
230
+ // 统一泳道文本位置
231
+ lane.text = {
232
+ ...lane.text,
233
+ x: lane.x - laneWidth / 2 + laneConfig.titleSize / 2,
234
+ y: lane.y,
235
+ }
236
+ this.moveLane(lane, lane.x, newLaneY)
237
+ // 为下一个泳道计算Y坐标
238
+ if (index < orderedLanes.length - 1) {
239
+ laneTopDistance += lane.height
240
+ }
241
+ })
242
+ this.height = newHeight
243
+ } else {
244
+ // 统一泳道高度
245
+ const laneHeight = this.height - poolConfig.titleSize
246
+ const newWidth = orderedLanes.reduce((sum: number, lane: any) => {
247
+ return sum + lane.width
248
+ }, 0)
249
+ let laneLeftDistance: number = this.x - newWidth / 2
250
+ // 遍历所有泳道,设置它们的位置
251
+ orderedLanes.forEach((lane: any, index: number) => {
252
+ const newLaneX = laneLeftDistance + lane.width / 2
253
+ // 统一泳道文本位置
254
+ lane.text = {
255
+ ...lane.text,
256
+ x: lane.x,
257
+ y: lane.y - laneHeight / 2 + laneConfig.titleSize / 2,
258
+ }
259
+ this.moveLane(lane, newLaneX, lane.y)
260
+ // 为下一个泳道计算X坐标
261
+ if (index < orderedLanes.length - 1) {
262
+ laneLeftDistance += lane.width
263
+ }
264
+ })
265
+ this.width = newWidth
266
+ }
267
+ // 更新文本位置
268
+ this.updateTextPosition()
269
+ }
270
+
271
+ moveLane(lane: any, newX, newY) {
272
+ // 更新泳子节点位置
273
+ const childrenRelPos: { id: string; dx: number; dy: number }[] = []
274
+ if (lane.children && lane.children.size > 0) {
275
+ lane.children.forEach((childId: string) => {
276
+ const childNode = this.graphModel.getNodeModelById(childId)
277
+ // 过滤掉拖拽中的节点和 Lane 类型(避免递归)
278
+ if (
279
+ childNode &&
280
+ !childNode.isDragging &&
281
+ String(childNode.type) !== 'lane'
282
+ ) {
283
+ childrenRelPos.push({
284
+ id: childId,
285
+ dx: childNode.x - lane.x,
286
+ dy: childNode.y - lane.y,
287
+ })
288
+ }
289
+ })
290
+ }
291
+ // 设置泳道位置和尺寸
292
+ lane.moveTo(newX, newY, true)
293
+ childrenRelPos.forEach(({ id, dx, dy }) => {
294
+ const childNode = this.graphModel.getNodeModelById(id)
295
+ if (childNode) {
296
+ const { x, y } = childNode
297
+ const newChildX = lane.x + dx
298
+ const newChildY = lane.y + dy
299
+ childNode.moveTo(newChildX, newChildY)
300
+ const { edges: incomingEdges } = childNode.incoming
301
+ const { edges: outgoingEdges } = childNode.outgoing
302
+ incomingEdges.forEach((edge: BaseEdgeModel) => {
303
+ edge.moveEndPoint(newChildX - x, newChildY - y)
304
+ })
305
+ outgoingEdges.forEach((edge: BaseEdgeModel) => {
306
+ edge.moveStartPoint(newChildX - x, newChildY - y)
307
+ })
308
+ }
309
+ })
310
+ }
311
+
312
+ /**
313
+ * 重新调整所有泳道布局
314
+ * @param newLanePosition 添加位置(可选):'above'|'below'|'left'|'right'
315
+ * @param newLaneId 新添加的泳道ID(可选)
316
+ */
317
+ resizeChildren() {
318
+ // 遍历所有泳道,horizontal泳道按Y轴排序,vertical泳道按X轴排序并调整位置
319
+ const lanes = this.getLanes()
320
+ if (lanes.length === 0) return
321
+
322
+ if (this.isHorizontal) {
323
+ this.height = lanes.reduce((sum: number, lane: any) => {
324
+ return sum + lane.height
325
+ }, 0)
326
+ // 遍历所有泳道,产出它们在y轴从上到下的顺序
327
+ const orderedLanes = lanes.slice().sort((a: any, b: any) => a.y - b.y)
328
+ lanes.forEach((lane: any) => {
329
+ lane.width = this.width - poolConfig.titleSize
330
+ const laneIndex = orderedLanes.findIndex(
331
+ (orderedLane: any) => orderedLane.id === lane.id,
332
+ )
333
+ // 遍历orderedLanes,计算出lane相比泳池顶部的距离
334
+ const laneTopDistance = orderedLanes.reduce(
335
+ (sum: number, orderedLane: any, index: number) => {
336
+ if (index < laneIndex && orderedLane.id !== lane.id) {
337
+ return sum + orderedLane.height
338
+ }
339
+ return sum
340
+ },
341
+ this.y - this.height / 2,
342
+ )
343
+
344
+ lane.moveTo(
345
+ this.x - this.width / 2 + poolConfig.titleSize + lane.width / 2,
346
+ laneTopDistance + lane.height / 2,
347
+ true,
348
+ )
349
+ })
350
+ } else {
351
+ this.width = lanes.reduce((sum: number, lane: any) => {
352
+ return sum + lane.width
353
+ }, 0)
354
+ // 垂直泳道按X轴排序
355
+ const orderedLanes = cloneDeep(lanes).sort((a: any, b: any) => a.x - b.x)
356
+ lanes.forEach((lane: any) => {
357
+ lane.height = this.height - poolConfig.titleSize
358
+ const laneIndex = orderedLanes.findIndex(
359
+ (orderedLane: any) => orderedLane.id === lane.id,
360
+ )
361
+ // 遍历orderedLanes,计算出lane相比泳池顶部的距离
362
+ const laneLeftDistance = orderedLanes.reduce(
363
+ (sum: number, orderedLane: any, index: number) => {
364
+ if (index < laneIndex && orderedLane.id !== lane.id) {
365
+ return sum + orderedLane.width
366
+ }
367
+ return sum
368
+ },
369
+ this.x - this.width / 2,
370
+ )
371
+ lane.moveTo(
372
+ laneLeftDistance + lane.width / 2,
373
+ this.y - this.height / 2 + poolConfig.titleSize + lane.height / 2,
374
+ true,
375
+ )
376
+ })
377
+ }
378
+ }
379
+
380
+ /**
381
+ * 获取子泳道
382
+ */
383
+ getLanes() {
384
+ const children: any[] = []
385
+ Array.from(this.children).forEach((childId) => {
386
+ const childModel = this.graphModel.getNodeModelById(childId)
387
+ if (childModel && String(childModel.type) === 'lane') {
388
+ children.push(childModel)
389
+ }
390
+ })
391
+ return children
392
+ }
393
+
394
+ /**
395
+ * 添加泳道的公共方法
396
+ * @param position 添加位置:'above'|'below'|'left'|'right'
397
+ * @param laneData 泳道数据
398
+ */
399
+ addLane(position: 'above' | 'below' | 'left' | 'right', laneData?: any) {
400
+ const lanes = this.getLanes()
401
+ if (lanes.length === 0) {
402
+ return this.createDefaultLane(laneData)
403
+ }
404
+
405
+ // 计算初始位置
406
+ let initialX = this.x
407
+ let initialY = this.y
408
+ // 参考泳道(用于定位)
409
+ const referenceLane = lanes.find((lane) => lane.id === laneData?.id)
410
+ // 用于确定新泳道尺寸的参考泳道,优先使用referenceLane,其次使用现有第一个泳道,最后回退到泳池尺寸
411
+ const sizeLane = referenceLane || lanes[0]
412
+ const laneWidth = sizeLane?.width ?? this.width
413
+ const laneHeight = sizeLane?.height ?? this.height
414
+
415
+ if (this.isHorizontal && ['above', 'below'].includes(position)) {
416
+ if (referenceLane) {
417
+ initialY =
418
+ position === 'above'
419
+ ? referenceLane.y - referenceLane.height / 2 - laneHeight / 2
420
+ : referenceLane.y + referenceLane.height / 2 + laneHeight / 2
421
+ initialX = referenceLane.x
422
+ }
423
+ }
424
+ if (!this.isHorizontal && ['left', 'right'].includes(position)) {
425
+ if (referenceLane) {
426
+ initialX =
427
+ position === 'left'
428
+ ? referenceLane.x - referenceLane.width / 2 - laneWidth / 2
429
+ : referenceLane.x + referenceLane.width / 2 + laneWidth / 2
430
+ initialY = referenceLane.y
431
+ }
432
+ }
433
+
434
+ // 确保不将referenceLaneId作为parent或者其他可能引起递归引用的属性传入
435
+ // laneData可能包含一些运行时属性,需要清理
436
+ const cleanLaneData = cloneDeep(laneData)
437
+ if (cleanLaneData) {
438
+ delete cleanLaneData.id
439
+ delete cleanLaneData.children
440
+ delete cleanLaneData.properties?.parent
441
+ delete cleanLaneData.properties?.children
442
+ }
443
+
444
+ const nodeConfig = merge(
445
+ cleanLaneData,
446
+ {
447
+ type: 'lane',
448
+ x: initialX,
449
+ y: initialY,
450
+ width: laneWidth,
451
+ height: laneHeight,
452
+ text: '新泳道',
453
+ properties: {
454
+ parent: this.id, // 确保父节点始终指向泳池
455
+ position: position, // 记录添加位置,供resizeChildren使用
456
+ referenceLaneId: referenceLane?.id, // 记录参考泳道ID
457
+ },
458
+ zIndex: this.zIndex,
459
+ },
460
+ this.properties.laneConfig,
461
+ )
462
+ const newLane = this.graphModel.addNode(nodeConfig)
463
+ this.setZIndex(this.zIndex - 1)
464
+ this.addChild(newLane.id)
465
+
466
+ // 调用优化后的resizeChildren,它会处理所有位置计算和泳池尺寸调整
467
+ this.resizeChildrenWithNewLane(position, newLane.id)
468
+ return newLane
469
+ }
470
+
471
+ setZIndex(zIndex: number) {
472
+ // this.zIndex = zIndex
473
+ this.zIndex = Math.min(zIndex, -100)
474
+ }
475
+
476
+ /**
477
+ * 在上方添加泳道
478
+ */
479
+ addChildAbove(laneData?: any) {
480
+ return this.addLane('above', laneData)
481
+ }
482
+
483
+ /**
484
+ * 在下方添加泳道
485
+ */
486
+ addChildBelow(laneData?: any) {
487
+ return this.addLane('below', laneData)
488
+ }
489
+
490
+ /**
491
+ * 在左侧添加泳道
492
+ */
493
+ addChildLeft(laneData?: any) {
494
+ return this.addLane('left', laneData)
495
+ }
496
+
497
+ /**
498
+ * 在右侧添加泳道(纵向布局专用)
499
+ */
500
+ addChildRight(laneData?: any) {
501
+ return this.addLane('right', laneData)
502
+ }
503
+
504
+ /**
505
+ * 创建默认泳道
506
+ */
507
+ createDefaultLane(laneConfig?: any) {
508
+ let newLane: any = null
509
+ // 只在没有子节点时创建默认泳道
510
+ if (this.isHorizontal) {
511
+ // 横向泳池:泳道垂直排列
512
+ const laneWidth = this.width - poolConfig.titleSize
513
+ const laneHeight = this.height
514
+ newLane = this.graphModel.addNode(
515
+ merge(
516
+ {
517
+ type: 'lane',
518
+ x: this.x - this.width / 2 + poolConfig.titleSize + laneWidth / 2,
519
+ y: this.y,
520
+ width: laneWidth,
521
+ height: laneHeight,
522
+ text: {
523
+ x: this.x - this.width / 2 + poolConfig.titleSize / 2,
524
+ y: this.y,
525
+ value: '泳道1',
526
+ },
527
+ properties: {
528
+ parent: this.id,
529
+ isHorizontal: this.isHorizontal,
530
+ },
531
+ zIndex: this.zIndex,
532
+ },
533
+ laneConfig,
534
+ ),
535
+ )
536
+ } else {
537
+ // 纵向泳池:泳道水平排列
538
+ // 修复:初始泳道在泳池中心位置,与resizeChildren逻辑保持一致
539
+ const laneWidth = this.width
540
+ const laneHeight = this.height - poolConfig.titleSize
541
+ newLane = this.graphModel.addNode(
542
+ merge(
543
+ {
544
+ type: 'lane',
545
+ x: this.x,
546
+ y:
547
+ this.y -
548
+ this.height / 2 +
549
+ poolConfig.titleSize +
550
+ (this.height - poolConfig.titleSize) / 2,
551
+ width: laneWidth,
552
+ height: laneHeight,
553
+ text: {
554
+ x: this.x,
555
+ y: this.y - this.height / 2 + poolConfig.titleSize / 2,
556
+ value: '泳道1',
557
+ },
558
+ properties: {
559
+ parent: this.id,
560
+ },
561
+ zIndex: this.zIndex,
562
+ },
563
+ laneConfig,
564
+ ),
565
+ )
566
+ }
567
+ this.setZIndex(this.zIndex - 1)
568
+ this.addChild(newLane.id)
569
+ this.resizeChildren()
570
+ this.updateTextPosition()
571
+ return newLane
572
+ }
573
+
574
+ /**
575
+ * 删除泳道
576
+ */
577
+ deleteChild(childId: string) {
578
+ const lanes = this.getLanes()
579
+ if (lanes.length <= 1) return
580
+
581
+ const laneToDelete = lanes.find((lane) => lane.id === childId)
582
+ if (!laneToDelete) return
583
+
584
+ // 移除子节点
585
+ this.removeChild(childId)
586
+ this.graphModel.deleteNode(childId)
587
+
588
+ // 重新调整泳池
589
+ this.resizePool()
590
+ this.resizeChildren()
591
+ }
592
+
593
+ getNodeStyle() {
594
+ const style = super.getNodeStyle()
595
+ style.strokeWidth = 2
596
+ return style
597
+ }
598
+
599
+ /**
600
+ * 获取文本样式
601
+ */
602
+ getTextStyle() {
603
+ const style = super.getTextStyle()
604
+ style.overflowMode = 'ellipsis'
605
+ style.strokeWidth = 2
606
+ style.textWidth = this.isHorizontal ? this.height : this.width
607
+ style.textHeight = this.isHorizontal ? this.width : this.height
608
+ if (this.isHorizontal) {
609
+ style.transform = 'rotate(-90deg)'
610
+ style.textAlign = 'center'
611
+ }
612
+ return style
613
+ }
614
+
615
+ getData(): LogicFlow.NodeData {
616
+ const data = super.getData()
617
+ // const poolModel = this.getPoolModel()
618
+ return {
619
+ ...data,
620
+ properties: {
621
+ ...data.properties,
622
+ width: this.width,
623
+ height: this.height,
624
+ },
625
+ }
626
+ }
627
+ }
628
+
629
+ export default {
630
+ PoolModel,
631
+ }
@@ -0,0 +1,75 @@
1
+ import { h } from '@logicflow/core'
2
+ import { DynamicGroupNode } from '../dynamic-group/node'
3
+ import { poolConfig } from './constant'
4
+
5
+ export class PoolView extends DynamicGroupNode {
6
+ componentDidMount(): void {
7
+ const { graphModel, model } = this.props
8
+ const index = graphModel.nodes.findIndex((node) => node.id === model.id)
9
+ const poolCount = graphModel.nodes.filter(
10
+ (node) => String(node.type) === 'pool',
11
+ ).length
12
+ // 设置一个足够低的z-index,确保泳池在所有节点的最底层
13
+ model.setZIndex(-((poolCount - index) * 100))
14
+ if (
15
+ !model.properties?.children?.length &&
16
+ !model._defaultLaneCreated &&
17
+ !model.virtual
18
+ ) {
19
+ model.createDefaultLane(model.properties?.laneConfig)
20
+ model._defaultLaneCreated = true
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 渲染泳池形状 - 根据布局方向分为标题区域和内容区域
26
+ */
27
+ getShape() {
28
+ const { model } = this.props
29
+ const {
30
+ x,
31
+ y,
32
+ width,
33
+ height,
34
+ properties: { textStyle: customTextStyle = {}, style: customStyle = {} },
35
+ isHorizontal,
36
+ } = model
37
+ const style = model.getNodeStyle()
38
+ const base = { fill: '#ffffff', stroke: '#000000', strokeWidth: 1 }
39
+ const left = x - width / 2
40
+ const top = y - height / 2
41
+
42
+ const titleRect = {
43
+ ...base,
44
+ ...style,
45
+ x: left,
46
+ y: top,
47
+ width: isHorizontal ? poolConfig.titleSize : width,
48
+ height: isHorizontal ? height : poolConfig.titleSize,
49
+ ...(isHorizontal && customTextStyle),
50
+ }
51
+ const contentRect = {
52
+ ...base,
53
+ ...style,
54
+ x: isHorizontal ? left + poolConfig.titleSize : left,
55
+ y: isHorizontal ? top : top + poolConfig.titleSize,
56
+ width: isHorizontal ? width - poolConfig.titleSize : width,
57
+ height: isHorizontal ? height : height - poolConfig.titleSize,
58
+ ...(isHorizontal && customStyle),
59
+ }
60
+ return h('g', {}, [h('rect', titleRect), h('rect', contentRect)])
61
+ }
62
+
63
+ /**
64
+ * 获取调整控制点 - 只在展开状态下显示
65
+ */
66
+ getResizeControl() {
67
+ const { resizable, isCollapsed } = this.props.model
68
+ const showResizeControl = resizable && !isCollapsed
69
+ return showResizeControl ? super.getResizeControl() : null
70
+ }
71
+ }
72
+
73
+ export default {
74
+ PoolView,
75
+ }