@tmagic/editor 1.8.0-beta.3 → 1.8.0-beta.4

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 (66) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +15 -1
  2. package/dist/es/hooks/use-code-block-edit.js +6 -3
  3. package/dist/es/hooks/use-data-source-edit.js +5 -2
  4. package/dist/es/hooks/use-stage.js +6 -3
  5. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  6. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +6 -2
  7. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +3 -0
  8. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +42 -29
  9. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +4 -2
  10. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  11. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +5 -2
  12. package/dist/es/layouts/history-list/composables.js +53 -1
  13. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  14. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
  16. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  17. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  18. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  19. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  20. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
  21. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
  22. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
  23. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
  24. package/dist/es/services/codeBlock.js +25 -10
  25. package/dist/es/services/dataSource.js +24 -10
  26. package/dist/es/services/editor.js +98 -46
  27. package/dist/es/services/history.js +2 -0
  28. package/dist/es/services/keybinding.js +3 -3
  29. package/dist/es/style.css +16 -4
  30. package/dist/es/utils/content-menu.js +17 -9
  31. package/dist/style.css +16 -4
  32. package/dist/tmagic-editor.umd.cjs +343 -144
  33. package/package.json +7 -7
  34. package/src/components/CompareForm.vue +19 -1
  35. package/src/hooks/use-code-block-edit.ts +4 -3
  36. package/src/hooks/use-data-source-edit.ts +2 -2
  37. package/src/hooks/use-stage.ts +3 -3
  38. package/src/layouts/NavMenu.vue +1 -1
  39. package/src/layouts/history-list/Bucket.vue +6 -2
  40. package/src/layouts/history-list/BucketTab.vue +3 -0
  41. package/src/layouts/history-list/GroupRow.vue +20 -3
  42. package/src/layouts/history-list/HistoryDiffDialog.vue +2 -1
  43. package/src/layouts/history-list/HistoryListPanel.vue +4 -0
  44. package/src/layouts/history-list/PageTab.vue +5 -1
  45. package/src/layouts/history-list/composables.ts +60 -0
  46. package/src/layouts/props-panel/PropsPanel.vue +5 -1
  47. package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
  48. package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
  49. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  50. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  51. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
  52. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  53. package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
  54. package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
  55. package/src/layouts/workspace/viewer/Stage.vue +1 -1
  56. package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
  57. package/src/services/codeBlock.ts +33 -9
  58. package/src/services/dataSource.ts +30 -8
  59. package/src/services/editor.ts +111 -34
  60. package/src/services/history.ts +7 -0
  61. package/src/services/keybinding.ts +3 -3
  62. package/src/theme/history-list-panel.scss +15 -1
  63. package/src/theme/props-panel.scss +3 -3
  64. package/src/type.ts +58 -2
  65. package/src/utils/content-menu.ts +18 -9
  66. package/types/index.d.ts +96 -25
@@ -120,9 +120,19 @@ class CodeBlock extends BaseService {
120
120
  public async setCodeDslById(
121
121
  id: Id,
122
122
  codeConfig: Partial<CodeBlockContent>,
123
- { changeRecords, doNotPushHistory = false }: HistoryOpOptionsWithChangeRecords = {},
123
+ {
124
+ changeRecords,
125
+ doNotPushHistory = false,
126
+ historyDescription,
127
+ historySource,
128
+ }: HistoryOpOptionsWithChangeRecords = {},
124
129
  ): Promise<void> {
125
- this.setCodeDslByIdSync(id, codeConfig, true, { changeRecords, doNotPushHistory });
130
+ this.setCodeDslByIdSync(id, codeConfig, true, {
131
+ changeRecords,
132
+ doNotPushHistory,
133
+ historyDescription,
134
+ historySource,
135
+ });
126
136
  }
127
137
 
128
138
  /**
@@ -141,7 +151,12 @@ class CodeBlock extends BaseService {
141
151
  id: Id,
142
152
  codeConfig: Partial<CodeBlockContent>,
143
153
  force = true,
144
- { changeRecords, doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
154
+ {
155
+ changeRecords,
156
+ doNotPushHistory = false,
157
+ historyDescription,
158
+ historySource,
159
+ }: HistoryOpOptionsWithChangeRecords = {},
145
160
  ): void {
146
161
  const codeDsl = this.getCodeDsl();
147
162
 
@@ -172,7 +187,13 @@ class CodeBlock extends BaseService {
172
187
  const newContent = cloneDeep(codeDsl[id]);
173
188
 
174
189
  if (!doNotPushHistory) {
175
- historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords, historyDescription });
190
+ historyService.pushCodeBlock(id, {
191
+ oldContent,
192
+ newContent,
193
+ changeRecords,
194
+ historyDescription,
195
+ source: historySource,
196
+ });
176
197
  }
177
198
 
178
199
  this.emit('addOrUpdate', id, codeDsl[id]);
@@ -268,7 +289,7 @@ class CodeBlock extends BaseService {
268
289
  */
269
290
  public async deleteCodeDslByIds(
270
291
  codeIds: Id[],
271
- { doNotPushHistory = false, historyDescription }: HistoryOpOptions = {},
292
+ { doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {},
272
293
  ): Promise<void> {
273
294
  const currentDsl = await this.getCodeDsl();
274
295
 
@@ -281,7 +302,7 @@ class CodeBlock extends BaseService {
281
302
  delete currentDsl[id];
282
303
 
283
304
  if (oldContent && !doNotPushHistory) {
284
- historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription });
305
+ historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription, source: historySource });
285
306
  }
286
307
 
287
308
  this.emit('remove', id);
@@ -373,6 +394,8 @@ class CodeBlock extends BaseService {
373
394
  const list = historyService.getCodeBlockStepList(id);
374
395
  const entry = list[index];
375
396
  if (!entry?.applied) return null;
397
+ // 更新类步骤(前后 content 都存在)必须带 changeRecords 才支持回滚,否则只能整内容替换,会冲掉后续无关变更。
398
+ if (entry.step.oldContent && entry.step.newContent && !entry.step.changeRecords?.length) return null;
376
399
  const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
377
400
  return await this.applyRevertStep(entry.step, description);
378
401
  }
@@ -471,13 +494,13 @@ class CodeBlock extends BaseService {
471
494
 
472
495
  // 原本是新增 → revert 即删除
473
496
  if (oldContent === null && newContent) {
474
- await this.deleteCodeDslByIds([id], { historyDescription });
497
+ await this.deleteCodeDslByIds([id], { historyDescription, historySource: 'rollback' });
475
498
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
476
499
  }
477
500
 
478
501
  // 原本是删除 → revert 即写回
479
502
  if (oldContent && newContent === null) {
480
- this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
503
+ this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
481
504
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
482
505
  }
483
506
 
@@ -500,11 +523,12 @@ class CodeBlock extends BaseService {
500
523
  this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldContent) : patched, true, {
501
524
  changeRecords,
502
525
  historyDescription,
526
+ historySource: 'rollback',
503
527
  });
504
528
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
505
529
  }
506
530
 
507
- this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
531
+ this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
508
532
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
509
533
  }
510
534
 
@@ -129,7 +129,10 @@ class DataSource extends BaseService {
129
129
  * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
130
130
  * @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
131
131
  */
132
- public add(config: DataSourceSchema, { doNotPushHistory = false, historyDescription }: HistoryOpOptions = {}) {
132
+ public add(
133
+ config: DataSourceSchema,
134
+ { doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {},
135
+ ) {
133
136
  const newConfig = {
134
137
  ...config,
135
138
  id: config.id && !this.getDataSourceById(config.id) ? config.id : this.createId(),
@@ -138,7 +141,12 @@ class DataSource extends BaseService {
138
141
  this.get('dataSources').push(newConfig);
139
142
 
140
143
  if (!doNotPushHistory) {
141
- historyService.pushDataSource(newConfig.id, { oldSchema: null, newSchema: newConfig, historyDescription });
144
+ historyService.pushDataSource(newConfig.id, {
145
+ oldSchema: null,
146
+ newSchema: newConfig,
147
+ historyDescription,
148
+ source: historySource,
149
+ });
142
150
  }
143
151
 
144
152
  this.emit('add', newConfig);
@@ -156,7 +164,12 @@ class DataSource extends BaseService {
156
164
  */
157
165
  public update(
158
166
  config: DataSourceSchema,
159
- { changeRecords = [], doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
167
+ {
168
+ changeRecords = [],
169
+ doNotPushHistory = false,
170
+ historyDescription,
171
+ historySource,
172
+ }: HistoryOpOptionsWithChangeRecords = {},
160
173
  ) {
161
174
  const dataSources = this.get('dataSources');
162
175
 
@@ -173,6 +186,7 @@ class DataSource extends BaseService {
173
186
  newSchema: newConfig,
174
187
  changeRecords,
175
188
  historyDescription,
189
+ source: historySource,
176
190
  });
177
191
  }
178
192
 
@@ -191,14 +205,19 @@ class DataSource extends BaseService {
191
205
  * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
192
206
  * @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
193
207
  */
194
- public remove(id: string, { doNotPushHistory = false, historyDescription }: HistoryOpOptions = {}) {
208
+ public remove(id: string, { doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {}) {
195
209
  const dataSources = this.get('dataSources');
196
210
  const index = dataSources.findIndex((ds) => ds.id === id);
197
211
  const oldConfig = index !== -1 ? dataSources[index] : null;
198
212
  dataSources.splice(index, 1);
199
213
 
200
214
  if (oldConfig && !doNotPushHistory) {
201
- historyService.pushDataSource(id, { oldSchema: cloneDeep(oldConfig), newSchema: null, historyDescription });
215
+ historyService.pushDataSource(id, {
216
+ oldSchema: cloneDeep(oldConfig),
217
+ newSchema: null,
218
+ historyDescription,
219
+ source: historySource,
220
+ });
202
221
  }
203
222
 
204
223
  this.emit('remove', id);
@@ -278,6 +297,8 @@ class DataSource extends BaseService {
278
297
  const list = historyService.getDataSourceStepList(id);
279
298
  const entry = list[index];
280
299
  if (!entry?.applied) return null;
300
+ // 更新类步骤(前后 schema 都存在)必须带 changeRecords 才支持回滚,否则只能整 schema 替换,会冲掉后续无关变更。
301
+ if (entry.step.oldSchema && entry.step.newSchema && !entry.step.changeRecords?.length) return null;
281
302
  const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
282
303
  return this.applyRevertStep(entry.step, description);
283
304
  }
@@ -366,13 +387,13 @@ class DataSource extends BaseService {
366
387
 
367
388
  // 原本是新增 → revert 即删除
368
389
  if (oldSchema === null && newSchema) {
369
- this.remove(`${id}`, { historyDescription });
390
+ this.remove(`${id}`, { historyDescription, historySource: 'rollback' });
370
391
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
371
392
  }
372
393
 
373
394
  // 原本是删除 → revert 即重新加回
374
395
  if (oldSchema && newSchema === null) {
375
- this.add(cloneDeep(oldSchema), { historyDescription });
396
+ this.add(cloneDeep(oldSchema), { historyDescription, historySource: 'rollback' });
376
397
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
377
398
  }
378
399
 
@@ -395,11 +416,12 @@ class DataSource extends BaseService {
395
416
  this.update(fallbackToFullReplace ? cloneDeep(oldSchema) : patched, {
396
417
  changeRecords,
397
418
  historyDescription,
419
+ historySource: 'rollback',
398
420
  });
399
421
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
400
422
  }
401
423
 
402
- this.update(cloneDeep(oldSchema), { historyDescription });
424
+ this.update(cloneDeep(oldSchema), { historyDescription, historySource: 'rollback' });
403
425
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
404
426
  }
405
427
 
@@ -36,6 +36,7 @@ import type {
36
36
  DslOpOptions,
37
37
  EditorEvents,
38
38
  EditorNodeInfo,
39
+ HistoryOpSource,
39
40
  HistoryOpType,
40
41
  PastePosition,
41
42
  StepValue,
@@ -406,7 +407,13 @@ class Editor extends BaseService {
406
407
  public async add(
407
408
  addNode: AddMNode | MNode[],
408
409
  parent?: MContainer | null,
409
- { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
410
+ {
411
+ doNotSelect = false,
412
+ doNotSwitchPage = false,
413
+ doNotPushHistory = false,
414
+ historyDescription,
415
+ historySource,
416
+ }: DslOpOptions = {},
410
417
  ): Promise<MNode | MNode[]> {
411
418
  this.captureSelectionBeforeOp();
412
419
 
@@ -466,9 +473,8 @@ class Editor extends BaseService {
466
473
  if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
467
474
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
468
475
  if (!doNotPushHistory) {
469
- this.pushOpHistory(
470
- 'add',
471
- {
476
+ this.pushOpHistory('add', {
477
+ extra: {
472
478
  nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
473
479
  parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
474
480
  indexMap: Object.fromEntries(
@@ -478,9 +484,10 @@ class Editor extends BaseService {
478
484
  }),
479
485
  ),
480
486
  },
481
- { name: pageForOp?.name || '', id: pageForOp!.id },
487
+ pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
482
488
  historyDescription,
483
- );
489
+ source: historySource,
490
+ });
484
491
  } else {
485
492
  this.selectionBeforeOp = null;
486
493
  }
@@ -577,7 +584,13 @@ class Editor extends BaseService {
577
584
  */
578
585
  public async remove(
579
586
  nodeOrNodeList: MNode | MNode[],
580
- { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
587
+ {
588
+ doNotSelect = false,
589
+ doNotSwitchPage = false,
590
+ doNotPushHistory = false,
591
+ historyDescription,
592
+ historySource,
593
+ }: DslOpOptions = {},
581
594
  ): Promise<void> {
582
595
  this.captureSelectionBeforeOp();
583
596
 
@@ -606,7 +619,12 @@ class Editor extends BaseService {
606
619
 
607
620
  if (removedItems.length > 0 && pageForOp) {
608
621
  if (!doNotPushHistory) {
609
- this.pushOpHistory('remove', { removedItems }, pageForOp, historyDescription);
622
+ this.pushOpHistory('remove', {
623
+ extra: { removedItems },
624
+ pageData: pageForOp,
625
+ historyDescription,
626
+ source: historySource,
627
+ });
610
628
  } else {
611
629
  this.selectionBeforeOp = null;
612
630
  }
@@ -704,11 +722,12 @@ class Editor extends BaseService {
704
722
  changeRecordList?: ChangeRecord[][];
705
723
  doNotPushHistory?: boolean;
706
724
  historyDescription?: string;
725
+ historySource?: HistoryOpSource;
707
726
  } = {},
708
727
  ): Promise<MNode | MNode[]> {
709
728
  this.captureSelectionBeforeOp();
710
729
 
711
- const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription } = data;
730
+ const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
712
731
 
713
732
  const nodes = Array.isArray(config) ? config : [config];
714
733
 
@@ -726,9 +745,8 @@ class Editor extends BaseService {
726
745
  if (curNodes.length) {
727
746
  if (!doNotPushHistory) {
728
747
  const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
729
- this.pushOpHistory(
730
- 'update',
731
- {
748
+ this.pushOpHistory('update', {
749
+ extra: {
732
750
  updatedItems: updateData.map((d) => ({
733
751
  oldNode: cloneDeep(d.oldNode),
734
752
  newNode: cloneDeep(toRaw(d.newNode)),
@@ -737,9 +755,10 @@ class Editor extends BaseService {
737
755
  changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
738
756
  })),
739
757
  },
740
- { name: pageForOp?.name || '', id: pageForOp!.id },
758
+ pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
741
759
  historyDescription,
742
- );
760
+ source: historySource,
761
+ });
743
762
  } else {
744
763
  this.selectionBeforeOp = null;
745
764
  }
@@ -763,7 +782,7 @@ class Editor extends BaseService {
763
782
  public async sort(
764
783
  id1: Id,
765
784
  id2: Id,
766
- { doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
785
+ { doNotSelect = false, doNotPushHistory = false, historySource }: DslOpOptions = {},
767
786
  ): Promise<void> {
768
787
  this.captureSelectionBeforeOp();
769
788
 
@@ -783,7 +802,7 @@ class Editor extends BaseService {
783
802
 
784
803
  parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
785
804
 
786
- await this.update(parent, { doNotPushHistory });
805
+ await this.update(parent, { doNotPushHistory, historySource });
787
806
  if (!doNotSelect) {
788
807
  await this.select(node);
789
808
  }
@@ -836,7 +855,13 @@ class Editor extends BaseService {
836
855
  public async paste(
837
856
  position: PastePosition = {},
838
857
  collectorOptions?: TargetOptions,
839
- { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
858
+ {
859
+ doNotSelect = false,
860
+ doNotSwitchPage = false,
861
+ doNotPushHistory = false,
862
+ historyDescription,
863
+ historySource,
864
+ }: DslOpOptions = {},
840
865
  ): Promise<MNode | MNode[] | void> {
841
866
  const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
842
867
  if (!Array.isArray(config)) return;
@@ -857,7 +882,13 @@ class Editor extends BaseService {
857
882
  propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
858
883
  }
859
884
 
860
- return this.add(pasteConfigs, parent, { doNotSelect, doNotSwitchPage, doNotPushHistory });
885
+ return this.add(pasteConfigs, parent, {
886
+ doNotSelect,
887
+ doNotSwitchPage,
888
+ doNotPushHistory,
889
+ historyDescription,
890
+ historySource,
891
+ });
861
892
  }
862
893
 
863
894
  public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
@@ -893,14 +924,14 @@ class Editor extends BaseService {
893
924
  */
894
925
  public async alignCenter(
895
926
  config: MNode | MNode[],
896
- { doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
927
+ { doNotSelect = false, doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
897
928
  ): Promise<MNode | MNode[]> {
898
929
  const nodes = Array.isArray(config) ? config : [config];
899
930
  const stage = this.get('stage');
900
931
 
901
932
  const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
902
933
 
903
- const newNode = await this.update(newNodes, { doNotPushHistory });
934
+ const newNode = await this.update(newNodes, { doNotPushHistory, historyDescription, historySource });
904
935
 
905
936
  if (!doNotSelect) {
906
937
  if (newNodes.length > 1) {
@@ -919,7 +950,10 @@ class Editor extends BaseService {
919
950
  * @param options 可选配置
920
951
  * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
921
952
  */
922
- public async moveLayer(offset: number | LayerOffset, { doNotPushHistory = false }: DslOpOptions = {}): Promise<void> {
953
+ public async moveLayer(
954
+ offset: number | LayerOffset,
955
+ { doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
956
+ ): Promise<void> {
923
957
  this.captureSelectionBeforeOp();
924
958
 
925
959
  const root = this.get('root');
@@ -960,10 +994,15 @@ class Editor extends BaseService {
960
994
  const pageForOp = this.getNodeInfo(node.id, false).page;
961
995
  this.pushOpHistory(
962
996
  'update',
997
+
963
998
  {
964
- updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
999
+ extra: {
1000
+ updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
1001
+ },
1002
+ pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
1003
+ historyDescription,
1004
+ source: historySource,
965
1005
  },
966
- { name: pageForOp?.name || '', id: pageForOp!.id },
967
1006
  );
968
1007
  } else {
969
1008
  this.selectionBeforeOp = null;
@@ -989,7 +1028,13 @@ class Editor extends BaseService {
989
1028
  public async moveToContainer(
990
1029
  config: MNode | MNode[],
991
1030
  targetId: Id,
992
- { doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
1031
+ {
1032
+ doNotSelect = false,
1033
+ doNotSwitchPage = false,
1034
+ doNotPushHistory = false,
1035
+ historyDescription,
1036
+ historySource,
1037
+ }: DslOpOptions = {},
993
1038
  ): Promise<MNode | MNode[]> {
994
1039
  const isBatch = Array.isArray(config);
995
1040
  const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
@@ -1052,7 +1097,12 @@ class Editor extends BaseService {
1052
1097
  newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
1053
1098
  }));
1054
1099
  const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
1055
- this.pushOpHistory('update', { updatedItems }, historyPage);
1100
+ this.pushOpHistory('update', {
1101
+ extra: { updatedItems },
1102
+ pageData: historyPage,
1103
+ historyDescription,
1104
+ source: historySource,
1105
+ });
1056
1106
  } else {
1057
1107
  this.selectionBeforeOp = null;
1058
1108
  }
@@ -1064,7 +1114,7 @@ class Editor extends BaseService {
1064
1114
  config: MNode | MNode[],
1065
1115
  targetParent: MContainer,
1066
1116
  targetIndex: number,
1067
- { doNotPushHistory = false }: DslOpOptions = {},
1117
+ { doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
1068
1118
  ) {
1069
1119
  this.captureSelectionBeforeOp();
1070
1120
 
@@ -1127,7 +1177,12 @@ class Editor extends BaseService {
1127
1177
  }
1128
1178
  if (!doNotPushHistory) {
1129
1179
  const pageForOp = this.getNodeInfo(configs[0].id, false).page;
1130
- this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
1180
+ this.pushOpHistory('update', {
1181
+ extra: { updatedItems },
1182
+ pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
1183
+ historyDescription,
1184
+ source: historySource,
1185
+ });
1131
1186
  } else {
1132
1187
  this.selectionBeforeOp = null;
1133
1188
  }
@@ -1184,6 +1239,12 @@ class Editor extends BaseService {
1184
1239
  const root = this.get('root');
1185
1240
  if (!root) return null;
1186
1241
 
1242
+ // 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
1243
+ if (step.opType === 'update') {
1244
+ const items = step.updatedItems ?? [];
1245
+ if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
1246
+ }
1247
+
1187
1248
  // 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
1188
1249
  let revertedStep: StepValue | null = null;
1189
1250
  const captureRevert = (s: StepValue) => {
@@ -1193,7 +1254,7 @@ class Editor extends BaseService {
1193
1254
 
1194
1255
  const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
1195
1256
  // revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
1196
- const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription } as const;
1257
+ const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription, historySource: 'rollback' } as const;
1197
1258
 
1198
1259
  try {
1199
1260
  switch (step.opType) {
@@ -1241,7 +1302,7 @@ class Editor extends BaseService {
1241
1302
  return cloneDeep(oldNode);
1242
1303
  });
1243
1304
  if (configs.length) {
1244
- await this.update(configs, { historyDescription });
1305
+ await this.update(configs, { historyDescription, historySource: 'rollback' });
1245
1306
  }
1246
1307
  break;
1247
1308
  }
@@ -1285,14 +1346,21 @@ class Editor extends BaseService {
1285
1346
  return cursor;
1286
1347
  }
1287
1348
 
1288
- public async move(left: number, top: number, { doNotPushHistory = false }: DslOpOptions = {}) {
1349
+ public async move(
1350
+ left: number,
1351
+ top: number,
1352
+ { doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
1353
+ ) {
1289
1354
  const node = toRaw(this.get('node'));
1290
1355
  if (!node || isPage(node)) return;
1291
1356
 
1292
1357
  const newStyle = calcMoveStyle(node.style || {}, left, top);
1293
1358
  if (!newStyle) return;
1294
1359
 
1295
- await this.update({ id: node.id, type: node.type, style: newStyle }, { doNotPushHistory });
1360
+ await this.update(
1361
+ { id: node.id, type: node.type, style: newStyle },
1362
+ { doNotPushHistory, historyDescription, historySource },
1363
+ );
1296
1364
  }
1297
1365
 
1298
1366
  public resetState() {
@@ -1350,9 +1418,17 @@ class Editor extends BaseService {
1350
1418
 
1351
1419
  private pushOpHistory(
1352
1420
  opType: HistoryOpType,
1353
- extra: Partial<StepValue>,
1354
- pageData: { name: string; id: Id },
1355
- historyDescription?: string,
1421
+ {
1422
+ extra,
1423
+ pageData,
1424
+ historyDescription,
1425
+ source,
1426
+ }: {
1427
+ extra: Partial<StepValue>;
1428
+ pageData: { name: string; id: Id };
1429
+ historyDescription?: string;
1430
+ source?: HistoryOpSource;
1431
+ },
1356
1432
  ) {
1357
1433
  const step: StepValue = {
1358
1434
  data: pageData,
@@ -1363,6 +1439,7 @@ class Editor extends BaseService {
1363
1439
  ...extra,
1364
1440
  };
1365
1441
  if (historyDescription) step.historyDescription = historyDescription;
1442
+ if (source) step.source = source;
1366
1443
  // 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
1367
1444
  // 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
1368
1445
  historyService.push(step, pageData.id);
@@ -27,6 +27,7 @@ import type {
27
27
  CodeBlockStepValue,
28
28
  DataSourceHistoryGroup,
29
29
  DataSourceStepValue,
30
+ HistoryOpSource,
30
31
  HistoryState,
31
32
  PageHistoryGroup,
32
33
  PageHistoryStepEntry,
@@ -280,6 +281,8 @@ class History extends BaseService {
280
281
  changeRecords?: ChangeRecord[];
281
282
  /** 可选的人类可读描述(如「修改按钮颜色」),仅用于历史面板展示。 */
282
283
  historyDescription?: string;
284
+ /** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
285
+ source?: HistoryOpSource;
283
286
  },
284
287
  ): CodeBlockStepValue | null {
285
288
  if (!codeBlockId) return null;
@@ -290,6 +293,7 @@ class History extends BaseService {
290
293
  newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
291
294
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
292
295
  historyDescription: payload.historyDescription,
296
+ source: payload.source,
293
297
  timestamp: Date.now(),
294
298
  };
295
299
 
@@ -310,6 +314,8 @@ class History extends BaseService {
310
314
  changeRecords?: ChangeRecord[];
311
315
  /** 可选的人类可读描述,仅用于历史面板展示。 */
312
316
  historyDescription?: string;
317
+ /** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
318
+ source?: HistoryOpSource;
313
319
  },
314
320
  ): DataSourceStepValue | null {
315
321
  if (!dataSourceId) return null;
@@ -320,6 +326,7 @@ class History extends BaseService {
320
326
  newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
321
327
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
322
328
  historyDescription: payload.historyDescription,
329
+ source: payload.source,
323
330
  timestamp: Date.now(),
324
331
  };
325
332
 
@@ -20,7 +20,7 @@ class Keybinding extends BaseService {
20
20
  const nodes = editorService.get('nodes');
21
21
 
22
22
  if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
23
- editorService.remove(nodes);
23
+ editorService.remove(nodes, { historySource: 'shortcut' });
24
24
  },
25
25
  [KeyBindingCommand.COPY_NODE]: () => {
26
26
  const nodes = editorService.get('nodes');
@@ -31,11 +31,11 @@ class Keybinding extends BaseService {
31
31
 
32
32
  if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
33
33
  editorService.copy(nodes);
34
- editorService.remove(nodes);
34
+ editorService.remove(nodes, { historySource: 'shortcut' });
35
35
  },
36
36
  [KeyBindingCommand.PASTE_NODE]: () => {
37
37
  const nodes = editorService.get('nodes');
38
- nodes && editorService.paste({ offsetX: 10, offsetY: 10 });
38
+ nodes && editorService.paste({ offsetX: 10, offsetY: 10 }, undefined, { historySource: 'shortcut' });
39
39
  },
40
40
  [KeyBindingCommand.UNDO]: () => {
41
41
  editorService.undo();
@@ -13,7 +13,7 @@
13
13
  position: absolute;
14
14
  top: 4px;
15
15
  right: 4px;
16
- z-index: 1;
16
+ z-index: 2;
17
17
  display: flex;
18
18
  align-items: center;
19
19
  height: 40px;
@@ -281,6 +281,20 @@
281
281
  white-space: nowrap;
282
282
  }
283
283
 
284
+ // 「操作途径」徽标:浅灰描边胶囊,弱化展示来源(画布 / 图层 / 配置面板…),不抢占描述焦点。
285
+ .m-editor-history-list-item-source {
286
+ flex: 0 0 auto;
287
+ padding: 0 6px;
288
+ border: 1px solid #dcdfe6;
289
+ border-radius: 8px;
290
+ font-size: 10px;
291
+ line-height: 14px;
292
+ color: #909399;
293
+ background-color: #f4f4f5;
294
+ white-space: nowrap;
295
+ font-weight: 400; // 防止被合并组头部的粗体继承
296
+ }
297
+
284
298
  // 「合并 N 步」徽标:紫色实心胶囊,与合并组卡片色系一致,醒目区分单步条目。
285
299
  .m-editor-history-list-item-merge {
286
300
  flex: 0 0 auto;
@@ -58,7 +58,7 @@
58
58
  position: absolute;
59
59
  right: 15px;
60
60
  bottom: 15px;
61
- z-index: 30;
61
+ z-index: 32;
62
62
  opacity: 0.5;
63
63
 
64
64
  &:hover {
@@ -70,7 +70,7 @@
70
70
  position: absolute;
71
71
  right: 15px;
72
72
  bottom: 60px;
73
- z-index: 30;
73
+ z-index: 31;
74
74
  opacity: 0.5;
75
75
 
76
76
  &:hover {
@@ -82,7 +82,7 @@
82
82
  position: absolute;
83
83
  left: 0;
84
84
  top: 0;
85
- z-index: 10;
85
+ z-index: 31;
86
86
  }
87
87
 
88
88
  .m-editor-resizer {