@tmagic/editor 1.8.0-beta.27 → 1.8.0-beta.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
2
2
  /**
3
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
3
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
4
4
  *
5
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
5
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
6
6
  */
7
7
  var stickyAddButton = (text) => ({
8
8
  scrollLastItemIntoView: true,
9
+ header: { sticky: true },
9
10
  addButtonConfig: {
10
11
  sticky: true,
11
12
  text,
@@ -168,10 +168,20 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
168
168
  });
169
169
  });
170
170
  };
171
+ /**
172
+ * root 是否已经不是编辑器当前的 root。
173
+ *
174
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
175
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
176
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
177
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
178
+ */
179
+ const isStaleRoot = (value) => toRaw(editorService.get("root")) !== toRaw(value);
171
180
  const updateStageDsl = async (value) => {
172
181
  const stage = await getStage();
173
182
  const runtime = await stage.renderer?.getRuntime();
174
183
  const app = await getTMagicApp();
184
+ if (isStaleRoot(value)) return;
175
185
  if (!app?.dataSourceManager) runtime?.updateRootConfig?.(cloneDeep$1(toRaw(value)));
176
186
  const page = editorService.get("page");
177
187
  const node = editorService.get("node");
@@ -182,6 +192,7 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
182
192
  if (value) {
183
193
  depService.clearIdleTasks();
184
194
  await (typeof Worker === "undefined" ? collectIdle(value.items, true) : depService.collectByWorker(value));
195
+ if (isStaleRoot(value)) return;
185
196
  const dsl = cloneDeep$1(toRaw(value));
186
197
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) updateNode(app.dataSourceManager.compiledNode(node), dsl);
187
198
  runtime?.updateRootConfig?.(dsl);
@@ -192,7 +203,7 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
192
203
  depService.addTarget(createDataSourceMethodTarget(ds, reactive({})));
193
204
  depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
194
205
  };
195
- const rootChangeHandler = (value, preValue) => {
206
+ const rootChangeHandler = (value) => {
196
207
  if (!value) return;
197
208
  value.codeBlocks = value.codeBlocks || {};
198
209
  value.dataSources = value.dataSources || [];
@@ -218,7 +229,8 @@ var initServiceEvents = (props, emit, { editorService, codeBlockService, dataSou
218
229
  editorService.set("parent", null);
219
230
  editorService.set("page", null);
220
231
  }
221
- if (toRaw(value) !== toRaw(preValue)) emit("update:modelValue", value);
232
+ if (isStaleRoot(value)) return;
233
+ if (toRaw(props.modelValue) !== toRaw(value)) emit("update:modelValue", value);
222
234
  })();
223
235
  };
224
236
  const nodeAddHandler = (nodes) => {
@@ -135,7 +135,7 @@ var useHistoryRevert = (options = {}, services) => {
135
135
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
136
136
  * 用户取消时返回 false,调用方据此中止回滚。
137
137
  */
138
- const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。");
138
+ const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?");
139
139
  /**
140
140
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,
141
141
  * 无法对比的步骤(add / remove / 多节点更新)回退到普通二次确认框。
package/dist/es/style.css CHANGED
@@ -478,18 +478,48 @@ fieldset.m-fieldset .m-form-tip {
478
478
  flex-shrink: 0;
479
479
  gap: 8px;
480
480
  }
481
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
482
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
481
+ .m-fields-group-list > .m-fields-group-list-item {
482
+ --m-group-list-item-footer-bottom: var(
483
+ --m-group-list-nested-footer-bottom,
484
+ 0px
485
+ );
486
+ }
487
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
488
+ --m-group-list-item-footer-bottom: calc(
489
+ var(--m-group-list-nested-footer-bottom, 0px) +
490
+ var(--m-group-list-footer-height)
491
+ );
492
+ }
493
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
494
+ --m-group-list-nested-footer-bottom: var(
495
+ --m-group-list-item-footer-bottom,
496
+ 0px
497
+ );
498
+ }
499
+ .m-fields-group-list.is-header-sticky {
500
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
501
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
502
+ }
503
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
504
+ --m-group-list-item-sticky-top: var(
505
+ --m-group-list-nested-sticky-top,
506
+ var(--m-group-list-header-sticky-top, 0px)
507
+ );
508
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
509
+ --m-group-list-child-list-sticky-top: calc(
510
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
511
+ );
512
+ --m-group-list-child-list-header-z: calc(
513
+ var(--m-group-list-item-header-z) - 1
514
+ );
515
+ }
516
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
517
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
483
518
  position: sticky;
484
- top: var(--m-group-list-header-sticky-top, 0px);
485
- z-index: 7;
519
+ top: var(--m-group-list-item-sticky-top);
520
+ z-index: var(--m-group-list-item-header-z);
486
521
  background-color: var(--m-group-list-header-bg, #fff);
487
522
  }
488
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
489
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
490
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
491
- z-index: 6;
492
- }
493
523
  .m-fields-group-list .el-table__empty-block {
494
524
  width: 100%;
495
525
  min-height: 0;
@@ -507,7 +537,7 @@ fieldset.m-fieldset .m-form-tip {
507
537
  }
508
538
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
509
539
  position: sticky;
510
- bottom: 0;
540
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
511
541
  z-index: 7;
512
542
  margin-bottom: 0;
513
543
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -521,10 +551,6 @@ fieldset.m-fieldset .m-form-tip {
521
551
  width: 100%;
522
552
  height: var(--m-group-list-footer-button-height);
523
553
  }
524
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
525
- bottom: var(--m-group-list-footer-height);
526
- z-index: 7;
527
- }
528
554
 
529
555
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
530
556
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
package/dist/style.css CHANGED
@@ -478,18 +478,48 @@ fieldset.m-fieldset .m-form-tip {
478
478
  flex-shrink: 0;
479
479
  gap: 8px;
480
480
  }
481
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
482
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
481
+ .m-fields-group-list > .m-fields-group-list-item {
482
+ --m-group-list-item-footer-bottom: var(
483
+ --m-group-list-nested-footer-bottom,
484
+ 0px
485
+ );
486
+ }
487
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
488
+ --m-group-list-item-footer-bottom: calc(
489
+ var(--m-group-list-nested-footer-bottom, 0px) +
490
+ var(--m-group-list-footer-height)
491
+ );
492
+ }
493
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
494
+ --m-group-list-nested-footer-bottom: var(
495
+ --m-group-list-item-footer-bottom,
496
+ 0px
497
+ );
498
+ }
499
+ .m-fields-group-list.is-header-sticky {
500
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
501
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
502
+ }
503
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
504
+ --m-group-list-item-sticky-top: var(
505
+ --m-group-list-nested-sticky-top,
506
+ var(--m-group-list-header-sticky-top, 0px)
507
+ );
508
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
509
+ --m-group-list-child-list-sticky-top: calc(
510
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
511
+ );
512
+ --m-group-list-child-list-header-z: calc(
513
+ var(--m-group-list-item-header-z) - 1
514
+ );
515
+ }
516
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
517
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
483
518
  position: sticky;
484
- top: var(--m-group-list-header-sticky-top, 0px);
485
- z-index: 7;
519
+ top: var(--m-group-list-item-sticky-top);
520
+ z-index: var(--m-group-list-item-header-z);
486
521
  background-color: var(--m-group-list-header-bg, #fff);
487
522
  }
488
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
489
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
490
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
491
- z-index: 6;
492
- }
493
523
  .m-fields-group-list .el-table__empty-block {
494
524
  width: 100%;
495
525
  min-height: 0;
@@ -507,7 +537,7 @@ fieldset.m-fieldset .m-form-tip {
507
537
  }
508
538
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
509
539
  position: sticky;
510
- bottom: 0;
540
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
511
541
  z-index: 7;
512
542
  margin-bottom: 0;
513
543
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -521,10 +551,6 @@ fieldset.m-fieldset .m-form-tip {
521
551
  width: 100%;
522
552
  height: var(--m-group-list-footer-button-height);
523
553
  }
524
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
525
- bottom: var(--m-group-list-footer-height);
526
- z-index: 7;
527
- }
528
554
 
529
555
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
530
556
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
@@ -509,18 +509,48 @@ fieldset.m-fieldset .m-form-tip {
509
509
  flex-shrink: 0;
510
510
  gap: 8px;
511
511
  }
512
- .m-fields-group-list .m-fields-group-list-item > .el-card__header,
513
- .m-fields-group-list .m-fields-group-list-item > .t-card__header {
512
+ .m-fields-group-list > .m-fields-group-list-item {
513
+ --m-group-list-item-footer-bottom: var(
514
+ --m-group-list-nested-footer-bottom,
515
+ 0px
516
+ );
517
+ }
518
+ .m-fields-group-list.is-footer-sticky > .m-fields-group-list-item {
519
+ --m-group-list-item-footer-bottom: calc(
520
+ var(--m-group-list-nested-footer-bottom, 0px) +
521
+ var(--m-group-list-footer-height)
522
+ );
523
+ }
524
+ .m-fields-group-list .m-fields-group-list-item .m-fields-group-list {
525
+ --m-group-list-nested-footer-bottom: var(
526
+ --m-group-list-item-footer-bottom,
527
+ 0px
528
+ );
529
+ }
530
+ .m-fields-group-list.is-header-sticky {
531
+ --m-group-list-nested-sticky-top: var(--m-group-list-child-list-sticky-top);
532
+ --m-group-list-nested-header-z: var(--m-group-list-child-list-header-z);
533
+ }
534
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item {
535
+ --m-group-list-item-sticky-top: var(
536
+ --m-group-list-nested-sticky-top,
537
+ var(--m-group-list-header-sticky-top, 0px)
538
+ );
539
+ --m-group-list-item-header-z: var(--m-group-list-nested-header-z, 7);
540
+ --m-group-list-child-list-sticky-top: calc(
541
+ var(--m-group-list-item-sticky-top) + var(--m-group-list-header-height)
542
+ );
543
+ --m-group-list-child-list-header-z: calc(
544
+ var(--m-group-list-item-header-z) - 1
545
+ );
546
+ }
547
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .el-card__header,
548
+ .m-fields-group-list.is-header-sticky > .m-fields-group-list-item > .t-card__header {
514
549
  position: sticky;
515
- top: var(--m-group-list-header-sticky-top, 0px);
516
- z-index: 7;
550
+ top: var(--m-group-list-item-sticky-top);
551
+ z-index: var(--m-group-list-item-header-z);
517
552
  background-color: var(--m-group-list-header-bg, #fff);
518
553
  }
519
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .el-card__header,
520
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list-item > .t-card__header {
521
- top: calc(var(--m-group-list-header-sticky-top, 0px) + var(--m-group-list-header-height));
522
- z-index: 6;
523
- }
524
554
  .m-fields-group-list .el-table__empty-block {
525
555
  width: 100%;
526
556
  min-height: 0;
@@ -538,7 +568,7 @@ fieldset.m-fieldset .m-form-tip {
538
568
  }
539
569
  .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
540
570
  position: sticky;
541
- bottom: 0;
571
+ bottom: var(--m-group-list-nested-footer-bottom, 0px);
542
572
  z-index: 7;
543
573
  margin-bottom: 0;
544
574
  padding: var(--m-group-list-footer-padding-top) 0 var(--m-group-list-footer-padding-bottom);
@@ -552,10 +582,6 @@ fieldset.m-fieldset .m-form-tip {
552
582
  width: 100%;
553
583
  height: var(--m-group-list-footer-button-height);
554
584
  }
555
- .m-fields-group-list .m-fields-group-list-item .m-fields-group-list > .m-fields-group-list-footer.is-sticky-full {
556
- bottom: var(--m-group-list-footer-height);
557
- z-index: 7;
558
- }
559
585
 
560
586
  /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
561
587
  .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
@@ -8813,12 +8813,13 @@
8813
8813
  //#endregion
8814
8814
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
8815
8815
  /**
8816
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
8816
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
8817
8817
  *
8818
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
8818
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
8819
8819
  */
8820
8820
  var stickyAddButton = (text) => ({
8821
8821
  scrollLastItemIntoView: true,
8822
+ header: { sticky: true },
8822
8823
  addButtonConfig: {
8823
8824
  sticky: true,
8824
8825
  text,
@@ -13941,7 +13941,7 @@
13941
13941
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
13942
13942
  * 用户取消时返回 false,调用方据此中止回滚。
13943
13943
  */
13944
- const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。");
13944
+ const confirmRevert = () => confirmHistoryAction("确定回滚该步骤吗?");
13945
13945
  /**
13946
13946
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,
13947
13947
  * 无法对比的步骤(add / remove / 多节点更新)回退到普通二次确认框。
@@ -20181,10 +20181,20 @@
20181
20181
  });
20182
20182
  });
20183
20183
  };
20184
+ /**
20185
+ * root 是否已经不是编辑器当前的 root。
20186
+ *
20187
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
20188
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
20189
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
20190
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
20191
+ */
20192
+ const isStaleRoot = (value) => (0, vue.toRaw)(editorService.get("root")) !== (0, vue.toRaw)(value);
20184
20193
  const updateStageDsl = async (value) => {
20185
20194
  const stage = await getStage();
20186
20195
  const runtime = await stage.renderer?.getRuntime();
20187
20196
  const app = await getTMagicApp();
20197
+ if (isStaleRoot(value)) return;
20188
20198
  if (!app?.dataSourceManager) runtime?.updateRootConfig?.(cloneDeep$1((0, vue.toRaw)(value)));
20189
20199
  const page = editorService.get("page");
20190
20200
  const node = editorService.get("node");
@@ -20195,6 +20205,7 @@
20195
20205
  if (value) {
20196
20206
  depService.clearIdleTasks();
20197
20207
  await (typeof Worker === "undefined" ? collectIdle(value.items, true) : depService.collectByWorker(value));
20208
+ if (isStaleRoot(value)) return;
20198
20209
  const dsl = cloneDeep$1((0, vue.toRaw)(value));
20199
20210
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) for (const node of (0, _tmagic_utils.getNodes)((0, _tmagic_utils.getDepNodeIds)(dsl.dataSourceDeps), dsl.items)) (0, _tmagic_core.updateNode)(app.dataSourceManager.compiledNode(node), dsl);
20200
20211
  runtime?.updateRootConfig?.(dsl);
@@ -20205,7 +20216,7 @@
20205
20216
  depService.addTarget((0, _tmagic_core.createDataSourceMethodTarget)(ds, (0, vue.reactive)({})));
20206
20217
  depService.addTarget((0, _tmagic_core.createDataSourceCondTarget)(ds, (0, vue.reactive)({})));
20207
20218
  };
20208
- const rootChangeHandler = (value, preValue) => {
20219
+ const rootChangeHandler = (value) => {
20209
20220
  if (!value) return;
20210
20221
  value.codeBlocks = value.codeBlocks || {};
20211
20222
  value.dataSources = value.dataSources || [];
@@ -20231,7 +20242,8 @@
20231
20242
  editorService.set("parent", null);
20232
20243
  editorService.set("page", null);
20233
20244
  }
20234
- if ((0, vue.toRaw)(value) !== (0, vue.toRaw)(preValue)) emit("update:modelValue", value);
20245
+ if (isStaleRoot(value)) return;
20246
+ if ((0, vue.toRaw)(props.modelValue) !== (0, vue.toRaw)(value)) emit("update:modelValue", value);
20235
20247
  })();
20236
20248
  };
20237
20249
  const nodeAddHandler = (nodes) => {
@@ -20680,12 +20692,13 @@
20680
20692
  //#endregion
20681
20693
  //#region packages/editor/src/fields/configs/stickyAddButton.ts
20682
20694
  /**
20683
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
20695
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
20684
20696
  *
20685
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
20697
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
20686
20698
  */
20687
20699
  var stickyAddButton = (text) => ({
20688
20700
  scrollLastItemIntoView: true,
20701
+ header: { sticky: true },
20689
20702
  addButtonConfig: {
20690
20703
  sticky: true,
20691
20704
  text,
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.27",
2
+ "version": "1.8.0-beta.28",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -63,11 +63,11 @@
63
63
  "moveable": "^0.53.0",
64
64
  "serialize-javascript": "^7.0.7",
65
65
  "sortablejs": "^1.15.7",
66
- "@tmagic/design": "1.8.0-beta.27",
67
- "@tmagic/stage": "1.8.0-beta.27",
68
- "@tmagic/table": "1.8.0-beta.27",
69
- "@tmagic/form": "1.8.0-beta.27",
70
- "@tmagic/utils": "1.8.0-beta.27"
66
+ "@tmagic/design": "1.8.0-beta.28",
67
+ "@tmagic/stage": "1.8.0-beta.28",
68
+ "@tmagic/utils": "1.8.0-beta.28",
69
+ "@tmagic/form": "1.8.0-beta.28",
70
+ "@tmagic/table": "1.8.0-beta.28"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/events": "^3.0.3",
@@ -81,7 +81,7 @@
81
81
  "type-fest": "^5.2.0",
82
82
  "typescript": "^6.0.3",
83
83
  "vue": "^3.5.41",
84
- "@tmagic/core": "1.8.0-beta.27"
84
+ "@tmagic/core": "1.8.0-beta.28"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "typescript": {
@@ -17,12 +17,13 @@
17
17
  */
18
18
 
19
19
  /**
20
- * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
20
+ * 属性面板列表字段共用的吸底全宽「添加」按钮与吸顶标题,展开到 group-list 配置上。
21
21
  *
22
- * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
22
+ * 吸底按钮会盖住列表底部,吸顶标题会盖住列表顶部,新增的项必须同时滚进视口才看得见。
23
23
  */
24
24
  export const stickyAddButton = (text: string) => ({
25
25
  scrollLastItemIntoView: true as const,
26
+ header: { sticky: true as const },
26
27
  addButtonConfig: {
27
28
  sticky: true as const,
28
29
  text,
@@ -427,12 +427,26 @@ export const initServiceEvents = (
427
427
  });
428
428
  };
429
429
 
430
+ /**
431
+ * root 是否已经不是编辑器当前的 root。
432
+ *
433
+ * `editorService.set('root', v)` 是同步赋值后再派发 `root-change`,因此派发那一刻 `v` 一定
434
+ * 就是当前 root;但处理 `root-change` 要等 stage / runtime / 依赖收集等异步过程,期间 root
435
+ * 可能被新的一次整体替换(外部重设 DSL、更新 root 节点、源码保存等)顶掉,此时手上的 `v`
436
+ * 已是过期快照,继续用它刷画布或回写给外部都会与新 root 互相覆盖。
437
+ */
438
+ const isStaleRoot = (value: MApp | null) => toRaw(editorService.get('root')) !== toRaw(value);
439
+
430
440
  const updateStageDsl = async (value: MApp | null) => {
431
441
  const stage = await getStage();
432
442
 
433
443
  const runtime = await stage.renderer?.getRuntime();
434
444
  const app = await getTMagicApp();
435
445
 
446
+ // 等 stage / runtime 就绪期间 root 已被替换:新 root 的刷新可能已经完成,
447
+ // 再把旧 dsl 推给 runtime 会让画布回退到旧内容
448
+ if (isStaleRoot(value)) return;
449
+
436
450
  if (!app?.dataSourceManager) {
437
451
  runtime?.updateRootConfig?.(cloneDeep(toRaw(value))!);
438
452
  }
@@ -449,6 +463,8 @@ export const initServiceEvents = (
449
463
 
450
464
  await (typeof Worker === 'undefined' ? collectIdle(value.items, true) : depService.collectByWorker(value));
451
465
 
466
+ if (isStaleRoot(value)) return;
467
+
452
468
  const dsl = cloneDeep(toRaw(value));
453
469
  if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) {
454
470
  for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) {
@@ -466,7 +482,7 @@ export const initServiceEvents = (
466
482
  depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
467
483
  };
468
484
 
469
- const rootChangeHandler = (value: MApp | null, preValue?: MApp | null) => {
485
+ const rootChangeHandler = (value: MApp | null) => {
470
486
  if (!value) return;
471
487
 
472
488
  value.codeBlocks = value.codeBlocks || {};
@@ -509,7 +525,13 @@ export const initServiceEvents = (
509
525
  editorService.set('page', null);
510
526
  }
511
527
 
512
- if (toRaw(value) !== toRaw(preValue)) {
528
+ // 上面的 select 是异步的,期间 root 可能已被替换,过期快照不能再回写给外部:
529
+ // 两次整体替换各自持有一个快照时,回写会把对方的 root 顶掉,外部 modelValue 变化又会
530
+ // 重新 set root,两条链路无休止地交替下去(表现为编辑器卡死)
531
+ if (isStaleRoot(value)) return;
532
+
533
+ // 外部已经持有这个 root(如本次变化就是 modelValue 传进来的)时无需回写
534
+ if (toRaw(props.modelValue) !== toRaw(value)) {
513
535
  emit('update:modelValue', value);
514
536
  }
515
537
  })();
@@ -204,10 +204,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
204
204
  * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
205
205
  * 用户取消时返回 false,调用方据此中止回滚。
206
206
  */
207
- const confirmRevert = (): Promise<boolean> =>
208
- confirmHistoryAction(
209
- '确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。',
210
- );
207
+ const confirmRevert = (): Promise<boolean> => confirmHistoryAction('确定回滚该步骤吗?');
211
208
 
212
209
  /**
213
210
  * 「回滚」统一确认入口:可差异对比的步骤动态挂载 HistoryDiffDialog 走差异确认弹窗,