@nocobase/client-v2 2.1.0-beta.23 → 2.1.0-beta.25

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 (42) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/components/Grid/index.d.ts +5 -3
  4. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  5. package/es/flow/models/base/GridModel.d.ts +19 -2
  6. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  7. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  8. package/es/index.mjs +100 -100
  9. package/lib/index.js +100 -100
  10. package/package.json +6 -5
  11. package/src/BaseApplication.tsx +4 -0
  12. package/src/__tests__/globalDeps.test.ts +1 -0
  13. package/src/__tests__/remotePlugins.test.ts +27 -0
  14. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  15. package/src/flow/actions/dataScope.tsx +6 -4
  16. package/src/flow/actions/dataScopeFilter.ts +70 -0
  17. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  18. package/src/flow/components/Grid/index.tsx +66 -20
  19. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  20. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  21. package/src/flow/models/base/BlockGridModel.tsx +2 -2
  22. package/src/flow/models/base/GridModel.tsx +428 -195
  23. package/src/flow/models/base/__tests__/BlockGridModel.dragOverlayConfig.test.ts +44 -0
  24. package/src/flow/models/base/__tests__/GridModel.computeOverlayRect.test.ts +29 -0
  25. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +181 -2
  26. package/src/flow/models/base/__tests__/GridModel.resizeLayout.test.ts +124 -0
  27. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +55 -15
  28. package/src/flow/models/blocks/details/DetailsGridModel.tsx +6 -6
  29. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  30. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +54 -14
  31. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  32. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +45 -0
  33. package/src/flow/models/blocks/form/FormGridModel.tsx +6 -6
  34. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  35. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  36. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  37. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  38. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  39. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  40. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
  41. package/src/utils/globalDeps.ts +4 -0
  42. package/src/utils/requirejs.ts +1 -1
@@ -9,25 +9,32 @@
9
9
 
10
10
  import { DragCancelEvent, DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
11
11
  import { uid } from '@formily/shared';
12
+ import type { FlowModelRendererProps } from '@nocobase/flow-engine';
12
13
  import {
14
+ buildLayoutSnapshot,
13
15
  DndProvider,
14
16
  DragHandler,
15
- Droppable,
16
17
  DragOverlayConfig,
18
+ Droppable,
17
19
  EMPTY_COLUMN_UID,
18
- tExpr,
19
- findModelUidPosition,
20
+ findModelUidLayoutPosition,
20
21
  FlowModel,
21
- MemoFlowModelRenderer,
22
- buildLayoutSnapshot,
23
22
  getSlotKey,
23
+ GridCellV2,
24
24
  GridLayoutData,
25
+ GridLayoutPath,
26
+ GridLayoutV2,
27
+ GridRowV2,
28
+ isSameGridLayout,
25
29
  LayoutSlot,
30
+ MemoFlowModelRenderer,
31
+ normalizeGridLayout,
32
+ projectLayoutToLegacyRows,
26
33
  Rect,
27
34
  resolveDropIntent,
28
35
  simulateLayoutForSlot,
36
+ tExpr,
29
37
  } from '@nocobase/flow-engine';
30
- import type { FlowModelRendererProps } from '@nocobase/flow-engine';
31
38
  import { Space } from 'antd';
32
39
  import _ from 'lodash';
33
40
  import React from 'react';
@@ -60,9 +67,12 @@ interface DragState {
60
67
  containerEl: HTMLDivElement | null;
61
68
  containerRect: Rect;
62
69
  pointerOrigin?: { x: number; y: number };
70
+ pointerPosition?: { x: number; y: number };
63
71
  activeSlotKey: string | null;
64
72
  previewLayout?: GridLayoutData;
65
73
  refreshTimer?: ReturnType<typeof setTimeout> | null;
74
+ cleanupListeners?: () => void;
75
+ generatedIds: Map<string, string>;
66
76
  }
67
77
 
68
78
  const getClientPoint = (event: any): { x: number; y: number } | null => {
@@ -107,6 +117,53 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
107
117
  ];
108
118
  private dragState?: DragState;
109
119
  private _memoItemFlowSettings?: Exclude<FlowModelRendererProps['showFlowSettings'], boolean>;
120
+
121
+ private updateDragPointerPosition = (event: Event) => {
122
+ if (!this.dragState) {
123
+ return;
124
+ }
125
+
126
+ const point = getClientPoint(event);
127
+ if (!point) {
128
+ return;
129
+ }
130
+
131
+ this.dragState.pointerPosition = point;
132
+ };
133
+
134
+ private handleDragScroll = () => {
135
+ if (!this.dragState) {
136
+ return;
137
+ }
138
+
139
+ this.scheduleSnapshotRefresh();
140
+ };
141
+
142
+ private bindDragDocumentListeners() {
143
+ const ownerDocument = this.getDragContainer()?.ownerDocument || (typeof document === 'undefined' ? null : document);
144
+ if (!ownerDocument) {
145
+ return undefined;
146
+ }
147
+
148
+ const options: AddEventListenerOptions = { capture: true, passive: true };
149
+ const removeOptions: EventListenerOptions = { capture: true };
150
+ const ownerWindow = ownerDocument.defaultView;
151
+
152
+ ownerDocument.addEventListener('pointermove', this.updateDragPointerPosition, options);
153
+ ownerDocument.addEventListener('mousemove', this.updateDragPointerPosition, options);
154
+ ownerDocument.addEventListener('touchmove', this.updateDragPointerPosition, options);
155
+ ownerDocument.addEventListener('scroll', this.handleDragScroll, options);
156
+ ownerWindow?.addEventListener('scroll', this.handleDragScroll, options);
157
+
158
+ return () => {
159
+ ownerDocument.removeEventListener('pointermove', this.updateDragPointerPosition, removeOptions);
160
+ ownerDocument.removeEventListener('mousemove', this.updateDragPointerPosition, removeOptions);
161
+ ownerDocument.removeEventListener('touchmove', this.updateDragPointerPosition, removeOptions);
162
+ ownerDocument.removeEventListener('scroll', this.handleDragScroll, removeOptions);
163
+ ownerWindow?.removeEventListener('scroll', this.handleDragScroll, removeOptions);
164
+ };
165
+ }
166
+
110
167
  protected deriveRowOrder(rows: Record<string, string[][]>, provided?: string[]) {
111
168
  const order: string[] = [];
112
169
  const used = new Set<string>();
@@ -165,69 +222,199 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
165
222
  return this._memoItemFlowSettings;
166
223
  }
167
224
 
168
- onMount(): void {
169
- super.onMount();
170
- this.emitter.on('onSubModelAdded', (model: FlowModel) => {
171
- if (!model.isNew) {
172
- return;
225
+ getItemUids() {
226
+ return (this.subModels.items || []).map((item) => item.uid);
227
+ }
228
+
229
+ protected normalizeLayoutFromSource(source?: Partial<GridLayoutData>): GridLayoutV2 {
230
+ const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
231
+ return normalizeGridLayout({
232
+ layout: source?.layout ?? this.props.layout ?? params.layout,
233
+ rows: source?.rows ?? this.props.rows ?? params.rows,
234
+ sizes: source?.sizes ?? this.props.sizes ?? params.sizes,
235
+ rowOrder: source?.rowOrder ?? this.props.rowOrder ?? params.rowOrder,
236
+ itemUids: this.getItemUids(),
237
+ gridUid: this.uid,
238
+ logger: console,
239
+ });
240
+ }
241
+
242
+ getGridLayout(): GridLayoutV2 {
243
+ return this.normalizeLayoutFromSource();
244
+ }
245
+
246
+ syncLayoutProps(layout: GridLayoutV2) {
247
+ const projection = projectLayoutToLegacyRows(layout);
248
+ this.setProps('layout', layout);
249
+ this.setProps('rows', projection.rows);
250
+ this.setProps('sizes', projection.sizes);
251
+ this.setProps('rowOrder', projection.rowOrder);
252
+ }
253
+
254
+ setGridStepLayout(layout: GridLayoutV2) {
255
+ this.setStepParams(GRID_FLOW_KEY, {
256
+ [GRID_STEP]: {
257
+ layout,
258
+ },
259
+ });
260
+ }
261
+
262
+ private findLayoutRowByPath(layout: GridLayoutV2, path?: GridLayoutPath): GridRowV2 | null {
263
+ if (!path?.length) {
264
+ return null;
265
+ }
266
+
267
+ let rows = layout.rows;
268
+ for (let index = 0; index < path.length; index += 1) {
269
+ const entry = path[index];
270
+ const row = rows.find((candidate) => candidate.id === entry.rowId);
271
+ if (!row) {
272
+ return null;
173
273
  }
174
- this.resetRows(true);
274
+ if (index === path.length - 1) {
275
+ return row;
276
+ }
277
+ const cell = row.cells.find((candidate) => candidate.id === entry.cellId);
278
+ rows = cell?.rows || [];
279
+ }
175
280
 
176
- // 在 sizes 中新加一行
177
- // 1. 获取当前 modelUid 所在的位置
178
- const position = findModelUidPosition(model.uid, this.props.rows || {});
179
- // 2. 根据位置,在 sizes 中添加一行
180
- if (position) {
181
- const newSizes = _.cloneDeep(this.props.sizes || {});
182
- newSizes[position.rowId] = [24]; // 默认新行宽度为 24
183
- const { rowOrder } = this.normalizeRowsWithOrder(this.props.rows || {}, this.props.rowOrder);
184
- const orderedSizes = this.orderSizesByRowOrder(newSizes, rowOrder);
185
- this.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
186
- rows: this.props.rows || {},
187
- sizes: orderedSizes,
188
- rowOrder,
189
- });
281
+ return null;
282
+ }
283
+
284
+ private collectCellItemsForResize(cell: GridCellV2): string[] {
285
+ if (Array.isArray(cell.items)) {
286
+ return [...cell.items];
287
+ }
288
+ return (cell.rows || []).flatMap((row) =>
289
+ row.cells.flatMap((childCell) => this.collectCellItemsForResize(childCell)),
290
+ );
291
+ }
292
+
293
+ private buildResizedCells(row: GridRowV2, cells: string[][]): GridCellV2[] {
294
+ const usedCellIndexes = new Set<number>();
295
+
296
+ return cells.map((items, index) => {
297
+ const matchedIndex = row.cells.findIndex((cell, cellIndex) => {
298
+ return !usedCellIndexes.has(cellIndex) && _.isEqual(this.collectCellItemsForResize(cell), items);
299
+ });
190
300
 
191
- this.setProps('sizes', orderedSizes);
192
- this.setProps('rowOrder', rowOrder);
301
+ if (matchedIndex !== -1) {
302
+ usedCellIndexes.add(matchedIndex);
303
+ const matchedCell = row.cells[matchedIndex];
304
+ if (matchedCell.rows && !matchedCell.items) {
305
+ return _.cloneDeep(matchedCell);
306
+ }
307
+ return {
308
+ id: matchedCell.id,
309
+ items: [...items],
310
+ };
193
311
  }
312
+
313
+ return {
314
+ id: `${row.id}:cell:${index}:${uid()}`,
315
+ items: [...items],
316
+ };
194
317
  });
195
- this.emitter.on('onSubModelDestroyed', (model: FlowModel) => {
196
- const modelUid = model.uid;
318
+ }
197
319
 
198
- // 1. 获取当前 modelUid 所在的位置
199
- const position = findModelUidPosition(modelUid, this.props.rows || {});
320
+ private getResizeRowPath(path?: GridLayoutPath): GridLayoutPath {
321
+ if (!path?.length) {
322
+ return [];
323
+ }
200
324
 
201
- // 2. 重置 rows
202
- this.resetRows(true);
325
+ const rowPath = path.map((entry) => ({ ...entry }));
326
+ delete rowPath[rowPath.length - 1].cellId;
327
+ return rowPath;
328
+ }
203
329
 
204
- // 3. 根据位置清空 sizes 中对应的值
205
- if (position) {
206
- const rows = this.props.rows || {};
207
- const newSizes = _.cloneDeep(this.props.sizes || {});
330
+ private getResizeContainerWidth(path?: GridLayoutPath) {
331
+ const rowPath = this.getResizeRowPath(path);
332
+ const fallbackWidth = this.gridContainerRef.current?.clientWidth || 0;
333
+ if (!rowPath.length) {
334
+ return fallbackWidth;
335
+ }
208
336
 
209
- // 如果列变空了,移除该列
210
- if (rows[position.rowId]?.[position.columnIndex] === undefined) {
211
- newSizes[position.rowId]?.splice(position.columnIndex, 1);
212
- }
337
+ const rows = Array.from(this.gridContainerRef.current?.querySelectorAll<HTMLElement>('[data-grid-row-id]') || []);
338
+ const rowElement = rows.find((element) => {
339
+ try {
340
+ return _.isEqual(JSON.parse(element.dataset.gridPath || '[]'), rowPath);
341
+ } catch {
342
+ return false;
343
+ }
344
+ });
345
+ return rowElement?.parentElement?.clientWidth || fallbackWidth;
346
+ }
213
347
 
214
- // 如果行变空了,移除该行
215
- if (rows[position.rowId] === undefined) {
216
- delete newSizes[position.rowId];
217
- }
348
+ private resizeGridLayout({
349
+ direction,
350
+ resizeDistance,
351
+ model,
352
+ }: {
353
+ direction: 'left' | 'right';
354
+ resizeDistance: number;
355
+ model: FlowModel;
356
+ }): { layout: GridLayoutV2; moveDistance: number } | null {
357
+ const layout = this.getGridLayout();
358
+ const position = findModelUidLayoutPosition(layout, model.uid);
359
+ if (!position) {
360
+ return null;
361
+ }
218
362
 
219
- const { rowOrder } = this.normalizeRowsWithOrder(rows, this.props.rowOrder);
220
- const orderedSizes = this.orderSizesByRowOrder(newSizes, rowOrder);
363
+ const row = this.findLayoutRowByPath(layout, position.path);
364
+ if (!row) {
365
+ return null;
366
+ }
221
367
 
222
- this.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
223
- rows,
224
- sizes: orderedSizes,
225
- rowOrder,
226
- });
368
+ const gridContainerWidth = this.getResizeContainerWidth(position.path);
369
+ const rowCells = row.cells.map((cell) => this.collectCellItemsForResize(cell));
370
+ const { newRows, newSizes, moveDistance } = recalculateGridSizes({
371
+ position: {
372
+ rowId: row.id,
373
+ columnIndex: position.cellIndex,
374
+ itemIndex: position.itemIndex,
375
+ },
376
+ direction,
377
+ resizeDistance,
378
+ prevMoveDistance: this.prevMoveDistance,
379
+ oldRows: { [row.id]: rowCells },
380
+ oldSizes: { [row.id]: row.sizes || [] },
381
+ gutter: this.context.themeToken?.marginBlock ?? 16,
382
+ gridContainerWidth,
383
+ });
227
384
 
228
- this.setProps('sizes', orderedSizes);
229
- this.setProps('rowOrder', rowOrder);
385
+ const nextLayout = _.cloneDeep(layout);
386
+ const resizedRow = this.findLayoutRowByPath(nextLayout, position.path);
387
+ if (!resizedRow) {
388
+ return null;
389
+ }
390
+
391
+ resizedRow.cells = this.buildResizedCells(row, newRows[row.id] || []);
392
+ resizedRow.sizes = newSizes[row.id] || [];
393
+
394
+ return {
395
+ layout: normalizeGridLayout({
396
+ layout: nextLayout,
397
+ itemUids: this.getItemUids(),
398
+ gridUid: this.uid,
399
+ logger: console,
400
+ }),
401
+ moveDistance,
402
+ };
403
+ }
404
+
405
+ onMount(): void {
406
+ super.onMount();
407
+ this.emitter.on('onSubModelAdded', (model: FlowModel) => {
408
+ if (!model.isNew) {
409
+ return;
230
410
  }
411
+ this.resetRows(true);
412
+ this.setGridStepLayout(this.props.layout);
413
+ });
414
+ this.emitter.on('onSubModelDestroyed', (model: FlowModel) => {
415
+ const modelUid = model.uid;
416
+ this.resetRows(true);
417
+ this.setGridStepLayout(this.props.layout);
231
418
 
232
419
  // 删除筛选配置
233
420
  this.context.filterManager?.removeFilterConfig({ targetId: modelUid });
@@ -240,57 +427,33 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
240
427
  });
241
428
 
242
429
  this.emitter.on('onResizeLeft', ({ resizeDistance, model }) => {
243
- const position = findModelUidPosition(model.uid, this.props.rows || {});
244
- const gridContainerWidth = this.gridContainerRef.current?.clientWidth || 0;
245
- const { newRows, newSizes, moveDistance } = recalculateGridSizes({
246
- position,
247
- direction: 'left',
248
- resizeDistance,
249
- prevMoveDistance: this.prevMoveDistance,
250
- oldRows: this.props.rows || {},
251
- oldSizes: this.props.sizes || {},
252
- gutter: this.context.themeToken.marginBlock,
253
- gridContainerWidth,
254
- });
255
- this.prevMoveDistance = moveDistance;
256
- this.setProps('rows', newRows);
257
- this.setProps('sizes', newSizes);
430
+ const resized = this.resizeGridLayout({ direction: 'left', resizeDistance, model });
431
+ if (resized) {
432
+ this.prevMoveDistance = resized.moveDistance;
433
+ this.syncLayoutProps(resized.layout);
434
+ }
258
435
  });
259
436
  this.emitter.on('onResizeRight', ({ resizeDistance, model }) => {
260
- const position = findModelUidPosition(model.uid, this.props.rows || {});
261
- const gridContainerWidth = this.gridContainerRef.current?.clientWidth || 0;
262
- const { newRows, newSizes, moveDistance } = recalculateGridSizes({
263
- position,
264
- direction: 'right',
265
- resizeDistance,
266
- prevMoveDistance: this.prevMoveDistance,
267
- oldRows: this.props.rows || {},
268
- oldSizes: this.props.sizes || {},
269
- gutter: this.context.themeToken.marginBlock,
270
- gridContainerWidth,
271
- });
272
- this.prevMoveDistance = moveDistance;
273
- this.setProps('rows', newRows);
274
- this.setProps('sizes', newSizes);
437
+ const resized = this.resizeGridLayout({ direction: 'right', resizeDistance, model });
438
+ if (resized) {
439
+ this.prevMoveDistance = resized.moveDistance;
440
+ this.syncLayoutProps(resized.layout);
441
+ }
275
442
  });
276
443
  this.emitter.on('onResizeBottom', ({ resizeDistance, model }) => {});
277
444
  this.emitter.on('onResizeEnd', () => {
278
445
  this.prevMoveDistance = 0;
279
- this.saveGridLayout();
446
+ this.saveGridLayout(this.props.layout);
280
447
  });
281
448
  }
282
449
 
283
- saveGridLayout(layout?: GridLayoutData) {
284
- const sourceRows = layout?.rows ?? this.props.rows ?? {};
285
- const sourceRowOrder = layout?.rowOrder ?? this.props.rowOrder;
286
- const { rows, rowOrder } = this.normalizeRowsWithOrder(sourceRows, sourceRowOrder);
287
- const sizes = this.orderSizesByRowOrder(layout?.sizes ?? this.props.sizes ?? {}, rowOrder);
288
- this.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
289
- rows,
290
- sizes,
291
- rowOrder,
292
- });
293
- this.setProps('rowOrder', rowOrder);
450
+ saveGridLayout(layout?: GridLayoutData | GridLayoutV2) {
451
+ const normalizedLayout =
452
+ layout && 'version' in layout
453
+ ? normalizeGridLayout({ layout, itemUids: this.getItemUids(), gridUid: this.uid, logger: console })
454
+ : this.normalizeLayoutFromSource(layout as any);
455
+ this.setGridStepLayout(normalizedLayout);
456
+ this.syncLayoutProps(normalizedLayout);
294
457
  this.saveStepParams();
295
458
  }
296
459
 
@@ -332,19 +495,11 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
332
495
 
333
496
  resetRows(syncProps = false) {
334
497
  const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
335
- const mergedRows = this.mergeRowsWithItems(params.rows || {});
336
- const { rows, rowOrder } = this.normalizeRowsWithOrder(mergedRows, params.rowOrder);
337
- const sizes = this.orderSizesByRowOrder(params.sizes || {}, rowOrder);
338
- this.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
339
- rows,
340
- sizes,
341
- rowOrder,
342
- });
498
+ const layout = this.normalizeLayoutFromSource(params);
499
+ this.setGridStepLayout(layout);
343
500
 
344
501
  if (syncProps) {
345
- this.setProps('rows', rows);
346
- this.setProps('sizes', sizes);
347
- this.setProps('rowOrder', rowOrder);
502
+ this.syncLayoutProps(layout);
348
503
  }
349
504
  }
350
505
 
@@ -352,6 +507,10 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
352
507
  if (!this.dragState) {
353
508
  return null;
354
509
  }
510
+ if (this.dragState.pointerPosition) {
511
+ return this.dragState.pointerPosition;
512
+ }
513
+
355
514
  const origin = this.dragState.pointerOrigin;
356
515
  if (origin) {
357
516
  return {
@@ -407,7 +566,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
407
566
  return;
408
567
  }
409
568
  if (this.dragState.refreshTimer) {
410
- clearTimeout(this.dragState.refreshTimer);
569
+ return;
411
570
  }
412
571
  this.dragState.refreshTimer = setTimeout(() => {
413
572
  if (!this.dragState) {
@@ -415,9 +574,24 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
415
574
  }
416
575
  this.dragState.refreshTimer = null;
417
576
  this.updateLayoutSnapshot();
577
+ this.refreshPreviewFromPointer();
418
578
  }, 16);
419
579
  }
420
580
 
581
+ private refreshPreviewFromPointer() {
582
+ if (!this.dragState) {
583
+ return;
584
+ }
585
+
586
+ const point = this.dragState.pointerPosition || this.dragState.pointerOrigin;
587
+ if (!point) {
588
+ return;
589
+ }
590
+
591
+ const slot = this.resolveDragSlot(point);
592
+ this.applyPreview(slot);
593
+ }
594
+
421
595
  /**
422
596
  * 根据 slot 类型、位置和配置计算最终的 overlay 尺寸和位置
423
597
  */
@@ -448,6 +622,17 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
448
622
  height: baseRect.height,
449
623
  };
450
624
  }
625
+ case 'item-edge': {
626
+ const edgeConfig = config?.columnEdge?.[slot.direction];
627
+ const width = edgeConfig?.width ?? baseRect.width;
628
+ const offsetLeft = edgeConfig?.offsetLeft ?? 0;
629
+ return {
630
+ top: baseRect.top,
631
+ left: baseRect.left + offsetLeft,
632
+ width,
633
+ height: baseRect.height,
634
+ };
635
+ }
451
636
  case 'row-gap': {
452
637
  const gapConfig = config?.rowGap?.[slot.position];
453
638
  const height = gapConfig?.height ?? baseRect.height;
@@ -479,6 +664,25 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
479
664
  }
480
665
  }
481
666
 
667
+ private getHitTestSlot(slot: LayoutSlot): LayoutSlot {
668
+ return {
669
+ ...slot,
670
+ // 命中区域与实际显示的 overlay 保持一致,避免鼠标仍在蓝框内时命中跳走
671
+ rect: this.computeOverlayRect(slot),
672
+ };
673
+ }
674
+
675
+ private resolveDragSlot(point: { x: number; y: number }): LayoutSlot | null {
676
+ if (!this.dragState?.slots.length) {
677
+ return null;
678
+ }
679
+
680
+ return resolveDropIntent(
681
+ point,
682
+ this.dragState.slots.map((slot) => this.getHitTestSlot(slot)),
683
+ );
684
+ }
685
+
482
686
  private applyPreview(slot: LayoutSlot | null) {
483
687
  if (!this.dragState) {
484
688
  return;
@@ -503,24 +707,32 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
503
707
  sourceUid: this.dragState.sourceUid,
504
708
  layout: this.dragState.snapshot,
505
709
  generateRowId: uid,
710
+ generatedIds: this.dragState.generatedIds,
711
+ generateId: () => uid(),
506
712
  });
507
713
 
714
+ if (
715
+ preview.layout &&
716
+ this.dragState.snapshot.layout &&
717
+ isSameGridLayout(preview.layout, this.dragState.snapshot.layout)
718
+ ) {
719
+ this.applyPreview(null);
720
+ return;
721
+ }
722
+
508
723
  this.dragState.previewLayout = {
509
724
  rows: _.cloneDeep(preview.rows),
510
725
  sizes: _.cloneDeep(preview.sizes),
511
726
  rowOrder: _.cloneDeep(preview.rowOrder),
727
+ layout: _.cloneDeep(preview.layout),
512
728
  };
513
729
 
514
- const container = this.getDragContainer();
515
- const scrollTop = container?.scrollTop ?? 0;
516
- const scrollLeft = container?.scrollLeft ?? 0;
517
-
518
730
  // 计算最终的 overlay 矩形
519
731
  const rect = this.computeOverlayRect(slot);
520
732
 
521
733
  const overlay: DragOverlayState = {
522
- top: rect.top - this.dragState.containerRect.top + scrollTop,
523
- left: rect.left - this.dragState.containerRect.left + scrollLeft,
734
+ top: rect.top - this.dragState.containerRect.top,
735
+ left: rect.left - this.dragState.containerRect.left,
524
736
  width: rect.width,
525
737
  height: rect.height,
526
738
  type: slot.type,
@@ -531,14 +743,15 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
531
743
 
532
744
  handleDragStart(event: DragStartEvent) {
533
745
  const sourceUid = event.active.id as string;
534
- const { rows, rowOrder } = this.normalizeRowsWithOrder(this.props.rows || {}, this.props.rowOrder);
535
- const sizes = this.orderSizesByRowOrder(this.props.sizes || {}, rowOrder);
746
+ const layout = this.normalizeLayoutFromSource();
747
+ const projection = projectLayoutToLegacyRows(layout);
536
748
  this.dragState = {
537
749
  sourceUid,
538
750
  snapshot: {
539
- rows: _.cloneDeep(rows),
540
- sizes: _.cloneDeep(sizes),
541
- rowOrder,
751
+ rows: _.cloneDeep(projection.rows),
752
+ sizes: _.cloneDeep(projection.sizes),
753
+ rowOrder: _.cloneDeep(projection.rowOrder),
754
+ layout: _.cloneDeep(layout),
542
755
  },
543
756
  slots: [],
544
757
  containerEl: this.gridContainerRef.current,
@@ -547,7 +760,9 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
547
760
  activeSlotKey: null,
548
761
  previewLayout: undefined,
549
762
  refreshTimer: null,
763
+ generatedIds: new Map(),
550
764
  };
765
+ this.dragState.cleanupListeners = this.bindDragDocumentListeners();
551
766
  this.setProps('dragOverlayRect', null);
552
767
  this.updateLayoutSnapshot();
553
768
  this.scheduleSnapshotRefresh();
@@ -568,7 +783,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
568
783
  return;
569
784
  }
570
785
 
571
- const slot = resolveDropIntent(point, this.dragState.slots);
786
+ const slot = this.resolveDragSlot(point);
572
787
  this.applyPreview(slot);
573
788
  }
574
789
 
@@ -581,6 +796,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
581
796
  clearTimeout(this.dragState.refreshTimer);
582
797
  }
583
798
 
799
+ this.dragState.cleanupListeners?.();
584
800
  this.dragState = undefined;
585
801
  this.setProps('dragOverlayRect', null);
586
802
  }
@@ -592,10 +808,14 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
592
808
 
593
809
  const previewLayout = this.dragState.previewLayout;
594
810
  if (previewLayout) {
595
- this.setProps('rows', _.cloneDeep(previewLayout.rows));
596
- this.setProps('sizes', _.cloneDeep(previewLayout.sizes));
597
- if (previewLayout.rowOrder) {
598
- this.setProps('rowOrder', _.cloneDeep(previewLayout.rowOrder));
811
+ if (previewLayout.layout) {
812
+ this.syncLayoutProps(_.cloneDeep(previewLayout.layout));
813
+ } else {
814
+ this.setProps('rows', _.cloneDeep(previewLayout.rows));
815
+ this.setProps('sizes', _.cloneDeep(previewLayout.sizes));
816
+ if (previewLayout.rowOrder) {
817
+ this.setProps('rowOrder', _.cloneDeep(previewLayout.rowOrder));
818
+ }
599
819
  }
600
820
  this.saveGridLayout(previewLayout);
601
821
  }
@@ -617,73 +837,90 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
617
837
  * - 运行态仅在判断为“整列/整行都不可见”时做过滤,不写回 props/stepParams,布局元数据保持不变。
618
838
  */
619
839
  private getVisibleLayout() {
620
- const rawRows = (this.props.rows || {}) as Record<string, string[][]>;
621
- const rawSizes = (this.props.sizes || {}) as Record<string, number[]>;
622
-
623
- const baseRows: Record<string, string[][]> = this.context.isMobileLayout
624
- ? transformRowsToSingleColumn(rawRows)
625
- : rawRows;
626
- const baseSizes: Record<string, number[]> = this.context.isMobileLayout ? {} : rawSizes;
627
-
628
- const { rows: orderedBaseRows, rowOrder } = this.normalizeRowsWithOrder(baseRows, this.props.rowOrder);
629
- const orderedBaseSizes = this.orderSizesByRowOrder(baseSizes, rowOrder);
840
+ const rawLayout = this.normalizeLayoutFromSource();
841
+ const baseLayout = this.context.isMobileLayout
842
+ ? normalizeGridLayout({
843
+ rows: transformRowsToSingleColumn(projectLayoutToLegacyRows(rawLayout).rows),
844
+ itemUids: this.getItemUids(),
845
+ })
846
+ : rawLayout;
847
+ const baseProjection = projectLayoutToLegacyRows(baseLayout);
848
+ const { rows: orderedBaseRows, rowOrder } = this.normalizeRowsWithOrder(
849
+ baseProjection.rows,
850
+ baseProjection.rowOrder,
851
+ );
852
+ const orderedBaseSizes = this.orderSizesByRowOrder(baseProjection.sizes, rowOrder);
630
853
 
631
854
  // 配置态:不做任何过滤,保持完整布局
632
855
  if (this.context.flowSettingsEnabled) {
633
- return { rows: orderedBaseRows, sizes: orderedBaseSizes };
856
+ return { layout: baseLayout, rows: orderedBaseRows, sizes: orderedBaseSizes };
634
857
  }
635
858
 
636
859
  const items = this.subModels?.items || [];
637
860
  if (!items.length) {
638
- return { rows: baseRows, sizes: baseSizes };
861
+ return { layout: baseLayout, rows: baseProjection.rows, sizes: baseProjection.sizes };
639
862
  }
640
863
 
641
864
  const modelByUid = new Map(items.map((m: FlowModel) => [m.uid, m]));
642
865
 
643
- const rows: Record<string, string[][]> = {};
644
- const sizes: Record<string, number[]> = {};
645
-
646
- rowOrder.forEach((rowKey) => {
647
- const rowCells = orderedBaseRows[rowKey];
648
- const rowSizes = orderedBaseSizes[rowKey] || [];
649
- const keptCells: string[][] = [];
650
- const keptSizes: number[] = [];
651
-
652
- rowCells.forEach((cell, idx) => {
653
- cell = cell.filter((uid) => {
654
- if (uid === EMPTY_COLUMN_UID) {
655
- return false;
656
- }
657
- return modelByUid.get(uid)?.hidden !== true;
658
- });
659
- // 只要当前单元格中存在“未显式 hidden 的模型”,就认为该列可见
660
- const hasVisibleItem = cell.some((uid) => {
661
- if (uid === EMPTY_COLUMN_UID) return false;
662
- const model = modelByUid.get(uid);
663
- // model 不存在时视为可见,避免误删;只有明确 hidden === true 的才视为不可见
664
- return !model || !model.hidden;
665
- });
666
-
667
- if (hasVisibleItem) {
668
- keptCells.push(cell);
669
- keptSizes.push(rowSizes[idx]);
670
- }
671
- });
866
+ const filterRows = (rows: GridLayoutV2['rows']): GridLayoutV2['rows'] => {
867
+ return rows
868
+ .map((row) => {
869
+ const cells: GridLayoutV2['rows'][number]['cells'] = [];
870
+ const keptSizes: number[] = [];
871
+
872
+ row.cells.forEach((cell, index) => {
873
+ const sourceSize = row.sizes?.[index];
874
+ const keepSize = Number.isFinite(sourceSize) && sourceSize > 0 ? sourceSize : 1;
875
+ if (cell.rows) {
876
+ const childRows = filterRows(cell.rows);
877
+ if (childRows.length) {
878
+ cells.push({ ...cell, rows: childRows });
879
+ keptSizes.push(keepSize);
880
+ }
881
+ return;
882
+ }
883
+
884
+ const cellItems = (cell.items || []).filter((uid) => {
885
+ if (uid === EMPTY_COLUMN_UID) {
886
+ return false;
887
+ }
888
+ return modelByUid.get(uid)?.hidden !== true;
889
+ });
890
+ const hasVisibleItem = cellItems.some((uid) => {
891
+ const model = modelByUid.get(uid);
892
+ return !model || !model.hidden;
893
+ });
894
+ if (hasVisibleItem) {
895
+ cells.push({ ...cell, items: cellItems });
896
+ keptSizes.push(keepSize);
897
+ }
898
+ });
899
+
900
+ return cells.length
901
+ ? {
902
+ ...row,
903
+ cells,
904
+ sizes: keptSizes,
905
+ }
906
+ : null;
907
+ })
908
+ .filter(Boolean) as GridLayoutV2['rows'];
909
+ };
672
910
 
673
- // 如果该行至少有一列存在可见 block,则保留该行
674
- if (keptCells.length > 0) {
675
- rows[rowKey] = keptCells;
676
- if (keptSizes.length > 0) {
677
- sizes[rowKey] = keptSizes;
678
- }
679
- }
911
+ const visibleLayout = normalizeGridLayout({
912
+ layout: {
913
+ version: 2,
914
+ rows: filterRows(baseLayout.rows),
915
+ },
680
916
  });
917
+ const projection = projectLayoutToLegacyRows(visibleLayout);
681
918
 
682
- return { rows, sizes };
919
+ return { layout: visibleLayout, rows: projection.rows, sizes: projection.sizes };
683
920
  }
684
921
 
685
922
  render() {
686
- const { rows, sizes } = this.getVisibleLayout();
923
+ const { layout, rows, sizes } = this.getVisibleLayout();
687
924
  const hasAnyVisibleRow = Object.keys(rows || {}).length > 0;
688
925
 
689
926
  return (
@@ -704,6 +941,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
704
941
  <Grid
705
942
  rowGap={this.props.rowGap}
706
943
  colGap={this.props.colGap}
944
+ layout={layout}
707
945
  rows={rows}
708
946
  sizes={sizes}
709
947
  dragOverlayRect={this.props.dragOverlayRect}
@@ -789,34 +1027,29 @@ GridModel.registerFlow({
789
1027
  },
790
1028
  grid: {
791
1029
  uiSchema: {
792
- rows: {
793
- title: tExpr('Rows'),
1030
+ layout: {
1031
+ title: tExpr('Layout'),
794
1032
  'x-decorator': 'FormItem',
795
1033
  'x-component': JsonEditor,
796
1034
  'x-component-props': {
797
1035
  autoSize: { minRows: 10, maxRows: 20 },
798
- description: tExpr('Configure the rows and columns of the grid.'),
1036
+ description: tExpr('Configure the nested layout of the grid.'),
799
1037
  },
800
1038
  },
801
- sizes: {
802
- title: tExpr('Sizes'),
803
- 'x-decorator': 'FormItem',
804
- 'x-component': JsonEditor,
805
- 'x-component-props': {
806
- rows: 5,
807
- },
808
- description: tExpr(
809
- 'Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.',
810
- ),
811
- },
812
1039
  },
813
1040
  async handler(ctx, params) {
814
- const mergedRows = ctx.model.mergeRowsWithItems(params.rows || {});
815
- const { rows, rowOrder } = (ctx.model as GridModel).normalizeRowsWithOrder(mergedRows, params.rowOrder);
816
- const sizes = (ctx.model as GridModel).orderSizesByRowOrder(params.sizes || {}, rowOrder);
817
- ctx.model.setProps('rows', rows);
818
- ctx.model.setProps('sizes', sizes);
819
- ctx.model.setProps('rowOrder', rowOrder);
1041
+ const model = ctx.model as GridModel;
1042
+ const layout = normalizeGridLayout({
1043
+ layout: params.layout,
1044
+ rows: params.rows,
1045
+ sizes: params.sizes,
1046
+ rowOrder: params.rowOrder,
1047
+ itemUids: model.getItemUids(),
1048
+ gridUid: model.uid,
1049
+ logger: console,
1050
+ });
1051
+ model.setGridStepLayout(layout);
1052
+ model.syncLayoutProps(layout);
820
1053
  },
821
1054
  },
822
1055
  },