@tmagic/editor 1.8.0-beta.2 → 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 (84) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +9 -0
  2. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +46 -28
  3. package/dist/es/editorProps.js +2 -0
  4. package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +2 -5
  5. package/dist/es/hooks/use-code-block-edit.js +6 -3
  6. package/dist/es/hooks/use-data-source-edit.js +5 -2
  7. package/dist/es/hooks/use-stage.js +8 -4
  8. package/dist/es/index.js +3 -1
  9. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +2 -5
  10. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  11. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +37 -14
  12. package/dist/es/layouts/history-list/BucketTab.js +5 -0
  13. package/dist/es/layouts/history-list/{CodeBlockTab.vue_vue_type_script_setup_true_lang.js → BucketTab.vue_vue_type_script_setup_true_lang.js} +30 -16
  14. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +91 -60
  15. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +82 -30
  16. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +140 -66
  17. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +15 -9
  18. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +15 -6
  19. package/dist/es/layouts/history-list/composables.js +72 -2
  20. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  21. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  22. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
  23. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  24. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  25. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  26. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  27. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
  28. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
  29. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
  30. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
  31. package/dist/es/services/codeBlock.js +25 -10
  32. package/dist/es/services/dataSource.js +24 -10
  33. package/dist/es/services/editor.js +98 -46
  34. package/dist/es/services/history.js +7 -2
  35. package/dist/es/services/keybinding.js +3 -3
  36. package/dist/es/style.css +60 -16
  37. package/dist/es/utils/content-menu.js +17 -9
  38. package/dist/style.css +60 -16
  39. package/dist/tmagic-editor.umd.cjs +795 -443
  40. package/package.json +7 -7
  41. package/src/Editor.vue +8 -0
  42. package/src/components/CompareForm.vue +50 -19
  43. package/src/editorProps.ts +7 -0
  44. package/src/fields/CodeLink.vue +2 -5
  45. package/src/hooks/use-code-block-edit.ts +4 -3
  46. package/src/hooks/use-data-source-edit.ts +2 -2
  47. package/src/hooks/use-stage.ts +4 -3
  48. package/src/index.ts +2 -0
  49. package/src/layouts/CodeEditor.vue +2 -5
  50. package/src/layouts/NavMenu.vue +1 -1
  51. package/src/layouts/history-list/Bucket.vue +58 -29
  52. package/src/layouts/history-list/BucketTab.vue +80 -0
  53. package/src/layouts/history-list/GroupRow.vue +110 -58
  54. package/src/layouts/history-list/HistoryDiffDialog.vue +53 -33
  55. package/src/layouts/history-list/HistoryListPanel.vue +165 -52
  56. package/src/layouts/history-list/InitialRow.vue +17 -6
  57. package/src/layouts/history-list/PageTab.vue +25 -7
  58. package/src/layouts/history-list/composables.ts +92 -1
  59. package/src/layouts/props-panel/PropsPanel.vue +5 -1
  60. package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
  61. package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
  62. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  63. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  64. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
  65. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  66. package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
  67. package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
  68. package/src/layouts/workspace/viewer/Stage.vue +1 -1
  69. package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
  70. package/src/services/codeBlock.ts +33 -9
  71. package/src/services/dataSource.ts +30 -8
  72. package/src/services/editor.ts +111 -34
  73. package/src/services/history.ts +10 -0
  74. package/src/services/keybinding.ts +3 -3
  75. package/src/theme/history-list-panel.scss +67 -14
  76. package/src/theme/props-panel.scss +3 -3
  77. package/src/type.ts +146 -0
  78. package/src/utils/content-menu.ts +18 -9
  79. package/types/index.d.ts +387 -156
  80. package/dist/es/layouts/history-list/CodeBlockTab.js +0 -5
  81. package/dist/es/layouts/history-list/DataSourceTab.js +0 -5
  82. package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +0 -62
  83. package/src/layouts/history-list/CodeBlockTab.vue +0 -61
  84. package/src/layouts/history-list/DataSourceTab.vue +0 -61
@@ -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,
@@ -254,6 +255,7 @@ class History extends BaseService {
254
255
  public push(state: StepValue, pageId?: Id): StepValue | null {
255
256
  const undoRedo = this.getUndoRedo(pageId);
256
257
  if (!undoRedo) return null;
258
+ if (state.timestamp === undefined) state.timestamp = Date.now();
257
259
  undoRedo.pushElement(state);
258
260
  // 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
259
261
  if (pageId === undefined || `${pageId}` === `${this.state.pageId}`) {
@@ -279,6 +281,8 @@ class History extends BaseService {
279
281
  changeRecords?: ChangeRecord[];
280
282
  /** 可选的人类可读描述(如「修改按钮颜色」),仅用于历史面板展示。 */
281
283
  historyDescription?: string;
284
+ /** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
285
+ source?: HistoryOpSource;
282
286
  },
283
287
  ): CodeBlockStepValue | null {
284
288
  if (!codeBlockId) return null;
@@ -289,6 +293,8 @@ class History extends BaseService {
289
293
  newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
290
294
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
291
295
  historyDescription: payload.historyDescription,
296
+ source: payload.source,
297
+ timestamp: Date.now(),
292
298
  };
293
299
 
294
300
  this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
@@ -308,6 +314,8 @@ class History extends BaseService {
308
314
  changeRecords?: ChangeRecord[];
309
315
  /** 可选的人类可读描述,仅用于历史面板展示。 */
310
316
  historyDescription?: string;
317
+ /** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
318
+ source?: HistoryOpSource;
311
319
  },
312
320
  ): DataSourceStepValue | null {
313
321
  if (!dataSourceId) return null;
@@ -318,6 +326,8 @@ class History extends BaseService {
318
326
  newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
319
327
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
320
328
  historyDescription: payload.historyDescription,
329
+ source: payload.source,
330
+ timestamp: Date.now(),
321
331
  };
322
332
 
323
333
  this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
@@ -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;
@@ -125,19 +125,19 @@
125
125
  &.is-merged {
126
126
  margin: 4px 0;
127
127
  padding: 4px 8px 6px;
128
- background-color: rgba(144, 105, 219, 0.06);
129
- border: 1px solid rgba(144, 105, 219, 0.18);
130
- border-left: 3px solid #9069db;
128
+ background-color: rgba(47, 84, 235, 0.06);
129
+ border: 1px solid rgba(47, 84, 235, 0.18);
130
+ border-left: 3px solid #2f54eb;
131
131
  border-radius: 4px;
132
132
 
133
133
  // 卡片本体已经有背景色,hover 状态以更深的同色提示交互
134
134
  &:hover {
135
- background-color: rgba(144, 105, 219, 0.1);
135
+ background-color: rgba(47, 84, 235, 0.1);
136
136
  }
137
137
 
138
138
  .m-editor-history-list-group-head {
139
139
  font-weight: 600;
140
- color: #5b3fa5;
140
+ color: #1d39c4;
141
141
  }
142
142
 
143
143
  // 已撤销态:整张卡片去色
@@ -169,7 +169,7 @@
169
169
  margin: 6px 0 0 6px;
170
170
  padding: 0;
171
171
  list-style: none;
172
- border-left: 1px dashed rgba(144, 105, 219, 0.45);
172
+ border-left: 1px dashed rgba(47, 84, 235, 0.45);
173
173
 
174
174
  li {
175
175
  display: flex;
@@ -185,7 +185,7 @@
185
185
  cursor: pointer;
186
186
 
187
187
  &:hover {
188
- background-color: rgba(144, 105, 219, 0.1);
188
+ background-color: rgba(47, 84, 235, 0.1);
189
189
  }
190
190
  }
191
191
 
@@ -222,6 +222,16 @@
222
222
  white-space: nowrap;
223
223
  }
224
224
 
225
+ // 操作时间:弱化展示,紧贴在描述之后、各操作按钮之前。
226
+ .m-editor-history-list-item-time {
227
+ flex: 0 0 auto;
228
+ color: #a8abb2;
229
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
230
+ font-size: 11px;
231
+ font-weight: 400; // 防止被合并组头部的粗体继承
232
+ white-space: nowrap;
233
+ }
234
+
225
235
  .m-editor-history-list-item-op {
226
236
  flex: 0 0 auto;
227
237
  padding: 0 6px;
@@ -240,7 +250,7 @@
240
250
  }
241
251
 
242
252
  &.op-update {
243
- background-color: #409eff;
253
+ background-color: #e6a23c;
244
254
  }
245
255
 
246
256
  &.op-initial {
@@ -271,6 +281,20 @@
271
281
  white-space: nowrap;
272
282
  }
273
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
+
274
298
  // 「合并 N 步」徽标:紫色实心胶囊,与合并组卡片色系一致,醒目区分单步条目。
275
299
  .m-editor-history-list-item-merge {
276
300
  flex: 0 0 auto;
@@ -279,7 +303,7 @@
279
303
  font-size: 10px;
280
304
  line-height: 16px;
281
305
  color: #fff;
282
- background-color: #9069db;
306
+ background-color: #2f54eb;
283
307
  font-weight: 500;
284
308
  letter-spacing: 0.2px;
285
309
  }
@@ -300,21 +324,39 @@
300
324
  }
301
325
  }
302
326
 
327
+ // 「跳转」按钮:将历史游标移动到该 step,替代原先点击整行跳转的交互。
328
+ // 使用与组卡片一致的紫色色系,与「查看差异」「回滚」区分开。
329
+ .m-editor-history-list-item-goto {
330
+ flex: 0 0 auto;
331
+ padding: 0 6px;
332
+ border-radius: 2px;
333
+ font-size: 10px;
334
+ line-height: 16px;
335
+ color: #606266;
336
+ background-color: rgba(96, 98, 102, 0.1);
337
+ cursor: pointer;
338
+ user-select: none;
339
+
340
+ &:hover {
341
+ background-color: rgba(96, 98, 102, 0.18);
342
+ }
343
+ }
344
+
303
345
  // 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。
304
- // 使用与「查看差异」不同的色调(橙黄),用来区分"可逆操作"与"只读对比"。
346
+ // 使用红色色调,强调其为"破坏性/可逆操作",与「查看差异」「跳转」区分开。
305
347
  .m-editor-history-list-item-revert {
306
348
  flex: 0 0 auto;
307
349
  padding: 0 6px;
308
350
  border-radius: 2px;
309
351
  font-size: 10px;
310
352
  line-height: 16px;
311
- color: #e6a23c;
312
- background-color: rgba(230, 162, 60, 0.12);
353
+ color: #f56c6c;
354
+ background-color: rgba(245, 108, 108, 0.12);
313
355
  cursor: pointer;
314
356
  user-select: none;
315
357
 
316
358
  &:hover {
317
- background-color: rgba(230, 162, 60, 0.25);
359
+ background-color: rgba(245, 108, 108, 0.25);
318
360
  }
319
361
  }
320
362
 
@@ -369,6 +411,17 @@
369
411
  flex-direction: column;
370
412
  }
371
413
 
414
+ .m-editor-history-diff-dialog-notice {
415
+ margin-bottom: 8px;
416
+ padding: 8px 12px;
417
+ background-color: #fdf6ec;
418
+ border: 1px solid #faecd8;
419
+ border-radius: 4px;
420
+ color: #e6a23c;
421
+ font-size: 13px;
422
+ line-height: 1.5;
423
+ }
424
+
372
425
  .m-editor-history-diff-dialog-header {
373
426
  display: flex;
374
427
  align-items: center;
@@ -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 {