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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +1 -1
  2. package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
  3. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +4 -1
  4. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +8 -2
  5. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +25 -8
  6. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
  7. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +25 -12
  8. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +27 -5
  9. package/dist/es/index.js +6 -3
  10. package/dist/es/initService.js +1 -1
  11. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
  12. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -55
  13. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
  14. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
  15. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
  16. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +335 -213
  17. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +36 -7
  18. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
  19. package/dist/es/layouts/history-list/composables.js +73 -32
  20. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
  21. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
  22. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +16 -3
  23. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +23 -1
  24. package/dist/es/services/codeBlock.js +96 -37
  25. package/dist/es/services/dataSource.js +70 -26
  26. package/dist/es/services/editor.js +266 -104
  27. package/dist/es/services/history.js +271 -212
  28. package/dist/es/services/ui.js +3 -0
  29. package/dist/es/style.css +53 -7
  30. package/dist/es/utils/data-source/index.js +2 -0
  31. package/dist/es/utils/editor.js +103 -49
  32. package/dist/es/utils/history.js +232 -0
  33. package/dist/es/utils/indexed-db.js +86 -0
  34. package/dist/es/utils/undo-redo.js +60 -1
  35. package/dist/style.css +53 -7
  36. package/dist/tmagic-editor.umd.cjs +2064 -1000
  37. package/package.json +7 -7
  38. package/src/components/CompareForm.vue +3 -1
  39. package/src/components/ToolButton.vue +2 -2
  40. package/src/fields/CodeSelectCol.vue +7 -1
  41. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +16 -2
  42. package/src/fields/DataSourceFields.vue +37 -8
  43. package/src/fields/DataSourceMethodSelect.vue +9 -4
  44. package/src/fields/DataSourceMethods.vue +42 -21
  45. package/src/fields/StyleSetter/components/Border.vue +15 -6
  46. package/src/index.ts +1 -0
  47. package/src/initService.ts +1 -1
  48. package/src/layouts/Framework.vue +1 -1
  49. package/src/layouts/history-list/Bucket.vue +34 -71
  50. package/src/layouts/history-list/BucketTab.vue +46 -54
  51. package/src/layouts/history-list/GroupRow.vue +146 -111
  52. package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
  53. package/src/layouts/history-list/HistoryListPanel.vue +298 -115
  54. package/src/layouts/history-list/InitialRow.vue +28 -9
  55. package/src/layouts/history-list/PageTab.vue +57 -51
  56. package/src/layouts/history-list/composables.ts +157 -36
  57. package/src/layouts/page-bar/PageBar.vue +4 -2
  58. package/src/layouts/sidebar/Sidebar.vue +6 -1
  59. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +30 -2
  60. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +26 -1
  61. package/src/services/codeBlock.ts +113 -37
  62. package/src/services/dataSource.ts +94 -40
  63. package/src/services/editor.ts +376 -136
  64. package/src/services/history.ts +306 -209
  65. package/src/services/ui.ts +7 -0
  66. package/src/theme/history-list-panel.scss +72 -5
  67. package/src/theme/page-bar.scss +0 -4
  68. package/src/theme/style-setter/border.scss +4 -1
  69. package/src/type.ts +183 -64
  70. package/src/utils/data-source/index.ts +2 -0
  71. package/src/utils/editor.ts +166 -60
  72. package/src/utils/history.ts +308 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/indexed-db.ts +122 -0
  75. package/src/utils/undo-redo.ts +88 -0
  76. package/types/index.d.ts +813 -302
@@ -32,12 +32,14 @@ import type {
32
32
  AsyncHookPlugin,
33
33
  CodeBlockStepValue,
34
34
  CodeState,
35
+ DslOpWithHistoryIdsResult,
35
36
  HistoryOpOptions,
36
37
  HistoryOpOptionsWithChangeRecords,
37
38
  } from '@editor/type';
38
39
  import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
39
40
  import { getEditorConfig } from '@editor/utils/config';
40
41
  import { COPY_CODE_STORAGE_KEY } from '@editor/utils/editor';
42
+ import { describeRevertStep, getLastPushedHistoryIds } from '@editor/utils/history';
41
43
 
42
44
  import BaseService from './BaseService';
43
45
 
@@ -48,18 +50,6 @@ const canUsePluginMethods = {
48
50
 
49
51
  type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
50
52
 
51
- /**
52
- * 「回滚」生成的新 step 简短描述。仅 service 层使用。
53
- */
54
- const describeRevertCodeBlockStep = (step: CodeBlockStepValue): string => {
55
- const { oldContent, newContent, changeRecords, id } = step;
56
- if (oldContent === null && newContent) return `撤回新增 ${newContent.name || newContent.id || id}`;
57
- if (oldContent && newContent === null) return `还原已删除的 ${oldContent.name || oldContent.id || id}`;
58
- const name = newContent?.name || oldContent?.name || `${id}`;
59
- const propPath = changeRecords?.[0]?.propPath;
60
- return propPath ? `还原 ${name} · ${propPath}` : `还原 ${name}`;
61
- };
62
-
63
53
  class CodeBlock extends BaseService {
64
54
  private state = reactive<CodeState>({
65
55
  codeDsl: null,
@@ -69,6 +59,17 @@ class CodeBlock extends BaseService {
69
59
  paramsColConfig: undefined,
70
60
  });
71
61
 
62
+ /**
63
+ * 最近一次写入历史栈的代码块历史记录 uuid(单条写入场景:新增 / 更新)。
64
+ * 供 setCodeDslById(Sync)AndGetHistoryId 取回,普通方法不读取它。
65
+ */
66
+ private lastPushedHistoryId: string | null = null;
67
+ /**
68
+ * deleteCodeDslByIds 一次删除多个代码块时,按写入顺序收集的历史记录 uuid 列表。
69
+ * 在 deleteCodeDslByIds 入口处重置,供 deleteCodeDslByIdsAndGetHistoryId 取回。
70
+ */
71
+ private lastDeletedHistoryIds: string[] = [];
72
+
72
73
  constructor() {
73
74
  super([
74
75
  ...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })),
@@ -187,13 +188,14 @@ class CodeBlock extends BaseService {
187
188
  const newContent = cloneDeep(codeDsl[id]);
188
189
 
189
190
  if (!doNotPushHistory) {
190
- historyService.pushCodeBlock(id, {
191
- oldContent,
192
- newContent,
193
- changeRecords,
194
- historyDescription,
195
- source: historySource,
196
- });
191
+ this.lastPushedHistoryId =
192
+ historyService.pushCodeBlock(id, {
193
+ oldContent,
194
+ newContent,
195
+ changeRecords,
196
+ historyDescription,
197
+ source: historySource,
198
+ })?.uuid ?? null;
197
199
  }
198
200
 
199
201
  this.emit('addOrUpdate', id, codeDsl[id]);
@@ -295,6 +297,8 @@ class CodeBlock extends BaseService {
295
297
 
296
298
  if (!currentDsl) return;
297
299
 
300
+ this.lastDeletedHistoryIds = [];
301
+
298
302
  codeIds.forEach((id) => {
299
303
  // 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
300
304
  const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
@@ -302,13 +306,65 @@ class CodeBlock extends BaseService {
302
306
  delete currentDsl[id];
303
307
 
304
308
  if (oldContent && !doNotPushHistory) {
305
- historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription, source: historySource });
309
+ const uuid = historyService.pushCodeBlock(id, {
310
+ oldContent,
311
+ newContent: null,
312
+ historyDescription,
313
+ source: historySource,
314
+ })?.uuid;
315
+ if (uuid) this.lastDeletedHistoryIds.push(uuid);
306
316
  }
307
317
 
308
318
  this.emit('remove', id);
309
319
  });
310
320
  }
311
321
 
322
+ // #region AndGetHistoryId
323
+ /**
324
+ * 下列 *AndGetHistoryId 方法与对应的写入方法行为完全一致,
325
+ * 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入的历史 uuid 列表,
326
+ * 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
327
+ *
328
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时 historyIds 为 `[]`。
329
+ */
330
+
331
+ /** 等价于 {@link setCodeDslById},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
332
+ public async setCodeDslByIdAndGetHistoryId(
333
+ id: Id,
334
+ codeConfig: Partial<CodeBlockContent>,
335
+ options: HistoryOpOptionsWithChangeRecords = {},
336
+ ): Promise<DslOpWithHistoryIdsResult<void>> {
337
+ this.lastPushedHistoryId = null;
338
+ await this.setCodeDslById(id, codeConfig, options);
339
+ return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
340
+ }
341
+
342
+ /** 等价于 {@link setCodeDslByIdSync},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
343
+ public setCodeDslByIdSyncAndGetHistoryId(
344
+ id: Id,
345
+ codeConfig: Partial<CodeBlockContent>,
346
+ force = true,
347
+ options: HistoryOpOptionsWithChangeRecords = {},
348
+ ): DslOpWithHistoryIdsResult<void> {
349
+ this.lastPushedHistoryId = null;
350
+ this.setCodeDslByIdSync(id, codeConfig, force, options);
351
+ return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
352
+ }
353
+
354
+ /**
355
+ * 等价于 {@link deleteCodeDslByIds},并额外返回本次写入的全部历史记录 uuid(按删除顺序)。
356
+ * 一次删除多个代码块会产生多条历史记录;未写入任何历史时 historyIds 为 `[]`。
357
+ */
358
+ public async deleteCodeDslByIdsAndGetHistoryId(
359
+ codeIds: Id[],
360
+ options: HistoryOpOptions = {},
361
+ ): Promise<DslOpWithHistoryIdsResult<void>> {
362
+ this.lastDeletedHistoryIds = [];
363
+ await this.deleteCodeDslByIds(codeIds, options);
364
+ return { result: undefined, historyIds: [...this.lastDeletedHistoryIds] };
365
+ }
366
+ // #endregion AndGetHistoryId
367
+
312
368
  public setParamsColConfig(config: TableColumnConfig): void {
313
369
  this.state.paramsColConfig = config;
314
370
  }
@@ -395,11 +451,29 @@ class CodeBlock extends BaseService {
395
451
  const entry = list[index];
396
452
  if (!entry?.applied) return null;
397
453
  // 更新类步骤(前后 content 都存在)必须带 changeRecords 才支持回滚,否则只能整内容替换,会冲掉后续无关变更。
398
- if (entry.step.oldContent && entry.step.newContent && !entry.step.changeRecords?.length) return null;
399
- const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
454
+ const { oldSchema, newSchema, changeRecords } = entry.step.diff?.[0] ?? {};
455
+
456
+ if (oldSchema && newSchema && !changeRecords?.length) return null;
457
+ const description = `回滚 #${index + 1}: ${describeRevertStep<CodeBlockContent>(entry.step.id, entry.step.diff?.[0], (s) => s.name)}`;
400
458
  return await this.applyRevertStep(entry.step, description);
401
459
  }
402
460
 
461
+ /**
462
+ * 通过历史记录 uuid 回滚代码块历史步骤,语义同 {@link revert},
463
+ * 仅无需调用方再传 codeBlockId 与 index:内部会按 uuid 在全部代码块栈中定位对应步骤后再回滚。
464
+ * 按数组顺序依次回滚,返回与入参同序的结果列表(某项失败时为 `null`)。
465
+ *
466
+ * @param uuids 目标历史记录的 uuid 列表,通常由 {@link setCodeDslByIdAndGetHistoryId} 等方法返回的 `historyIds`
467
+ */
468
+ public async revertById(uuids: string[]): Promise<(CodeBlockStepValue | null)[]> {
469
+ const results: (CodeBlockStepValue | null)[] = [];
470
+ for (const uuid of uuids) {
471
+ const location = historyService.findCodeBlockStepLocationByUuid(uuid);
472
+ results.push(location ? await this.revert(location.id, location.index) : null);
473
+ }
474
+ return results;
475
+ }
476
+
403
477
  /**
404
478
  * 生成代码块唯一id
405
479
  * @returns {Id} 代码块唯一id
@@ -490,21 +564,22 @@ class CodeBlock extends BaseService {
490
564
  step: CodeBlockStepValue,
491
565
  historyDescription: string,
492
566
  ): Promise<CodeBlockStepValue | null> {
493
- const { id, oldContent, newContent, changeRecords } = step;
567
+ const { id } = step;
568
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
494
569
 
495
570
  // 原本是新增 → revert 即删除
496
- if (oldContent === null && newContent) {
571
+ if (!oldSchema && newSchema) {
497
572
  await this.deleteCodeDslByIds([id], { historyDescription, historySource: 'rollback' });
498
573
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
499
574
  }
500
575
 
501
576
  // 原本是删除 → revert 即写回
502
- if (oldContent && newContent === null) {
503
- this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
577
+ if (oldSchema && !newSchema) {
578
+ this.setCodeDslByIdSync(id, cloneDeep(oldSchema), true, { historyDescription, historySource: 'rollback' });
504
579
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
505
580
  }
506
581
 
507
- if (!oldContent || !newContent) return null;
582
+ if (!oldSchema || !newSchema) return null;
508
583
 
509
584
  // 原本是更新 → 把 oldContent 写回;优先按 changeRecords 局部 patch
510
585
  if (changeRecords?.length) {
@@ -517,10 +592,10 @@ class CodeBlock extends BaseService {
517
592
  fallbackToFullReplace = true;
518
593
  break;
519
594
  }
520
- const value = cloneDeep(getValueByKeyPath(record.propPath, oldContent));
595
+ const value = cloneDeep(getValueByKeyPath(record.propPath, oldSchema));
521
596
  setValueByKeyPath(record.propPath, value, patched);
522
597
  }
523
- this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldContent) : patched, true, {
598
+ this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldSchema) : patched, true, {
524
599
  changeRecords,
525
600
  historyDescription,
526
601
  historySource: 'rollback',
@@ -528,7 +603,7 @@ class CodeBlock extends BaseService {
528
603
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
529
604
  }
530
605
 
531
- this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
606
+ this.setCodeDslByIdSync(id, cloneDeep(oldSchema), true, { historyDescription, historySource: 'rollback' });
532
607
  return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
533
608
  }
534
609
 
@@ -547,31 +622,32 @@ class CodeBlock extends BaseService {
547
622
  * @param reverse true=撤销,false=重做
548
623
  */
549
624
  private async applyHistoryStep(step: CodeBlockStepValue, reverse: boolean): Promise<void> {
550
- const { id, oldContent, newContent, changeRecords } = step;
625
+ const { id } = step;
626
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
551
627
 
552
628
  // 新增 / 删除:直接 set 或 delete,不走 patch 逻辑
553
- if (oldContent === null && newContent) {
629
+ if (!oldSchema && newSchema) {
554
630
  if (reverse) {
555
631
  await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
556
632
  } else {
557
- this.setCodeDslByIdSync(id, cloneDeep(newContent), true, { doNotPushHistory: true });
633
+ this.setCodeDslByIdSync(id, cloneDeep(newSchema), true, { doNotPushHistory: true });
558
634
  }
559
635
  return;
560
636
  }
561
637
 
562
- if (oldContent && newContent === null) {
638
+ if (oldSchema && !newSchema) {
563
639
  if (reverse) {
564
- this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { doNotPushHistory: true });
640
+ this.setCodeDslByIdSync(id, cloneDeep(oldSchema), true, { doNotPushHistory: true });
565
641
  } else {
566
642
  await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
567
643
  }
568
644
  return;
569
645
  }
570
646
 
571
- if (!oldContent || !newContent) return;
647
+ if (!oldSchema || !newSchema) return;
572
648
 
573
649
  // 更新场景:优先按 changeRecords 局部 patch;缺省退化为整内容替换
574
- const sourceForValues = reverse ? oldContent : newContent;
650
+ const sourceForValues = reverse ? oldSchema : newSchema;
575
651
 
576
652
  if (changeRecords?.length) {
577
653
  const current = this.getCodeContentById(id);
@@ -13,12 +13,14 @@ import storageService, { Protocol } from '@editor/services/storage';
13
13
  import type {
14
14
  DataSourceStepValue,
15
15
  DatasourceTypeOption,
16
+ DslOpWithHistoryIdsResult,
16
17
  HistoryOpOptions,
17
18
  HistoryOpOptionsWithChangeRecords,
18
19
  SyncHookPlugin,
19
20
  } from '@editor/type';
20
21
  import { getFormConfig, getFormValue } from '@editor/utils/data-source';
21
22
  import { COPY_DS_STORAGE_KEY } from '@editor/utils/editor';
23
+ import { describeRevertStep, getLastPushedHistoryIds } from '@editor/utils/history';
22
24
 
23
25
  import BaseService from './BaseService';
24
26
 
@@ -54,19 +56,6 @@ const canUsePluginMethods = {
54
56
 
55
57
  type SyncMethodName = Writable<(typeof canUsePluginMethods)['sync']>;
56
58
 
57
- /**
58
- * 「回滚」生成的新 step 简短描述。
59
- * 仅在 service 层使用,避免依赖 UI 层 composables。
60
- */
61
- const describeRevertDataSourceStep = (step: DataSourceStepValue): string => {
62
- const { oldSchema, newSchema, changeRecords, id } = step;
63
- if (oldSchema === null && newSchema) return `撤回新增 ${newSchema.title || newSchema.id || id}`;
64
- if (oldSchema && newSchema === null) return `还原已删除的 ${oldSchema.title || oldSchema.id || id}`;
65
- const title = newSchema?.title || oldSchema?.title || `${id}`;
66
- const propPath = changeRecords?.[0]?.propPath;
67
- return propPath ? `还原 ${title} · ${propPath}` : `还原 ${title}`;
68
- };
69
-
70
59
  class DataSource extends BaseService {
71
60
  private state = reactive<State>({
72
61
  datasourceTypeList: [],
@@ -78,6 +67,13 @@ class DataSource extends BaseService {
78
67
  methods: {},
79
68
  });
80
69
 
70
+ /**
71
+ * 最近一次写入历史栈的数据源历史记录 uuid。
72
+ * 供 *AndGetHistoryId 系列方法在调用 add / update / remove 后取回本次产生的历史记录 id;
73
+ * 普通方法不读取它,调用前由 *AndGetHistoryId 重置为 null。
74
+ */
75
+ private lastPushedHistoryId: string | null = null;
76
+
81
77
  constructor() {
82
78
  super(canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false })));
83
79
  }
@@ -141,12 +137,13 @@ class DataSource extends BaseService {
141
137
  this.get('dataSources').push(newConfig);
142
138
 
143
139
  if (!doNotPushHistory) {
144
- historyService.pushDataSource(newConfig.id, {
145
- oldSchema: null,
146
- newSchema: newConfig,
147
- historyDescription,
148
- source: historySource,
149
- });
140
+ this.lastPushedHistoryId =
141
+ historyService.pushDataSource(newConfig.id, {
142
+ oldSchema: null,
143
+ newSchema: newConfig,
144
+ historyDescription,
145
+ source: historySource,
146
+ })?.uuid ?? null;
150
147
  }
151
148
 
152
149
  this.emit('add', newConfig);
@@ -181,13 +178,14 @@ class DataSource extends BaseService {
181
178
  dataSources[index] = newConfig;
182
179
 
183
180
  if (!doNotPushHistory) {
184
- historyService.pushDataSource(newConfig.id, {
185
- oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
186
- newSchema: newConfig,
187
- changeRecords,
188
- historyDescription,
189
- source: historySource,
190
- });
181
+ this.lastPushedHistoryId =
182
+ historyService.pushDataSource(newConfig.id, {
183
+ oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
184
+ newSchema: newConfig,
185
+ changeRecords,
186
+ historyDescription,
187
+ source: historySource,
188
+ })?.uuid ?? null;
191
189
  }
192
190
 
193
191
  this.emit('update', newConfig, {
@@ -212,17 +210,55 @@ class DataSource extends BaseService {
212
210
  dataSources.splice(index, 1);
213
211
 
214
212
  if (oldConfig && !doNotPushHistory) {
215
- historyService.pushDataSource(id, {
216
- oldSchema: cloneDeep(oldConfig),
217
- newSchema: null,
218
- historyDescription,
219
- source: historySource,
220
- });
213
+ this.lastPushedHistoryId =
214
+ historyService.pushDataSource(id, {
215
+ oldSchema: cloneDeep(oldConfig),
216
+ newSchema: null,
217
+ historyDescription,
218
+ source: historySource,
219
+ })?.uuid ?? null;
221
220
  }
222
221
 
223
222
  this.emit('remove', id);
224
223
  }
225
224
 
225
+ // #region AndGetHistoryId
226
+ /**
227
+ * 下列 *AndGetHistoryId 方法与对应的 add / update / remove 行为完全一致,
228
+ * 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入历史栈的 uuid 列表({@link DataSourceStepValue.uuid}),
229
+ * 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
230
+ *
231
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时 historyIds 为 `[]`。
232
+ */
233
+
234
+ /** 等价于 {@link add},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
235
+ public addAndGetHistoryId(
236
+ config: DataSourceSchema,
237
+ options: HistoryOpOptions = {},
238
+ ): DslOpWithHistoryIdsResult<DataSourceSchema> {
239
+ this.lastPushedHistoryId = null;
240
+ const result = this.add(config, options);
241
+ return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
242
+ }
243
+
244
+ /** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
245
+ public updateAndGetHistoryId(
246
+ config: DataSourceSchema,
247
+ options: HistoryOpOptionsWithChangeRecords = {},
248
+ ): DslOpWithHistoryIdsResult<DataSourceSchema> {
249
+ this.lastPushedHistoryId = null;
250
+ const result = this.update(config, options);
251
+ return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
252
+ }
253
+
254
+ /** 等价于 {@link remove},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
255
+ public removeAndGetHistoryId(id: string, options: HistoryOpOptions = {}): DslOpWithHistoryIdsResult<void> {
256
+ this.lastPushedHistoryId = null;
257
+ this.remove(id, options);
258
+ return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
259
+ }
260
+ // #endregion AndGetHistoryId
261
+
226
262
  /**
227
263
  * 撤销指定数据源的最近一次变更。
228
264
  *
@@ -298,11 +334,27 @@ class DataSource extends BaseService {
298
334
  const entry = list[index];
299
335
  if (!entry?.applied) return null;
300
336
  // 更新类步骤(前后 schema 都存在)必须带 changeRecords 才支持回滚,否则只能整 schema 替换,会冲掉后续无关变更。
301
- if (entry.step.oldSchema && entry.step.newSchema && !entry.step.changeRecords?.length) return null;
302
- const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
337
+ const { oldSchema, newSchema, changeRecords } = entry.step.diff?.[0] ?? {};
338
+ if (oldSchema && newSchema && !changeRecords?.length) return null;
339
+ const description = `回滚 #${index + 1}: ${describeRevertStep<DataSourceSchema>(entry.step.id, entry.step.diff?.[0], (s) => s.title)}`;
303
340
  return this.applyRevertStep(entry.step, description);
304
341
  }
305
342
 
343
+ /**
344
+ * 通过历史记录 uuid 回滚数据源历史步骤,语义同 {@link revert},
345
+ * 仅无需调用方再传 dataSourceId 与 index:内部会按 uuid 在全部数据源栈中定位对应步骤后再回滚。
346
+ * 按数组顺序依次回滚,返回与入参同序的结果列表(某项失败时为 `null`)。
347
+ *
348
+ * @param uuids 目标历史记录的 uuid 列表,通常由 {@link addAndGetHistoryId} 等方法返回的 `historyIds`
349
+ */
350
+ public revertById(uuids: string[]): (DataSourceStepValue | null)[] {
351
+ return uuids.map((uuid) => {
352
+ const location = historyService.findDataSourceStepLocationByUuid(uuid);
353
+ if (!location) return null;
354
+ return this.revert(location.id, location.index);
355
+ });
356
+ }
357
+
306
358
  public createId(): string {
307
359
  return `ds_${guid()}`;
308
360
  }
@@ -383,16 +435,17 @@ class DataSource extends BaseService {
383
435
  * 同构,差异仅在于走对应的公共 add / update / remove 而不是带 doNotPushHistory 的版本。
384
436
  */
385
437
  private applyRevertStep(step: DataSourceStepValue, historyDescription: string): DataSourceStepValue | null {
386
- const { id, oldSchema, newSchema, changeRecords } = step;
438
+ const { id } = step;
439
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
387
440
 
388
441
  // 原本是新增 → revert 即删除
389
- if (oldSchema === null && newSchema) {
442
+ if (!oldSchema && newSchema) {
390
443
  this.remove(`${id}`, { historyDescription, historySource: 'rollback' });
391
444
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
392
445
  }
393
446
 
394
447
  // 原本是删除 → revert 即重新加回
395
- if (oldSchema && newSchema === null) {
448
+ if (oldSchema && !newSchema) {
396
449
  this.add(cloneDeep(oldSchema), { historyDescription, historySource: 'rollback' });
397
450
  return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
398
451
  }
@@ -440,10 +493,11 @@ class DataSource extends BaseService {
440
493
  * @param reverse true=撤销,false=重做
441
494
  */
442
495
  private applyHistoryStep(step: DataSourceStepValue, reverse: boolean): void {
443
- const { id, oldSchema, newSchema, changeRecords } = step;
496
+ const { id } = step;
497
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
444
498
 
445
499
  // 新增 / 删除:直接 add 或 remove,不走 patch 逻辑
446
- if (oldSchema === null && newSchema) {
500
+ if (!oldSchema && newSchema) {
447
501
  if (reverse) {
448
502
  this.remove(`${id}`, { doNotPushHistory: true });
449
503
  } else {
@@ -452,7 +506,7 @@ class DataSource extends BaseService {
452
506
  return;
453
507
  }
454
508
 
455
- if (oldSchema && newSchema === null) {
509
+ if (oldSchema && !newSchema) {
456
510
  if (reverse) {
457
511
  this.add(cloneDeep(oldSchema), { doNotPushHistory: true });
458
512
  } else {