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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +10 -6
  2. package/dist/es/index.js +6 -4
  3. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +2 -2
  4. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +38 -26
  5. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +5 -1
  6. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +163 -310
  7. package/dist/es/layouts/history-list/composables.js +47 -128
  8. package/dist/es/layouts/history-list/useHistoryList.js +56 -0
  9. package/dist/es/layouts/history-list/useHistoryRevert.js +304 -0
  10. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +2 -2
  11. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +3 -3
  12. package/dist/es/services/codeBlock.js +32 -28
  13. package/dist/es/services/dataSource.js +43 -34
  14. package/dist/es/services/editor.js +31 -26
  15. package/dist/es/services/history.js +268 -384
  16. package/dist/es/style.css +12 -0
  17. package/dist/es/utils/editor.js +2 -2
  18. package/dist/es/utils/history.js +35 -47
  19. package/dist/style.css +12 -0
  20. package/dist/tmagic-editor.umd.cjs +3808 -3090
  21. package/package.json +7 -7
  22. package/src/components/CompareForm.vue +14 -6
  23. package/src/index.ts +2 -0
  24. package/src/layouts/NavMenu.vue +2 -2
  25. package/src/layouts/history-list/GroupRow.vue +10 -0
  26. package/src/layouts/history-list/HistoryDiffDialog.vue +6 -1
  27. package/src/layouts/history-list/HistoryListPanel.vue +60 -270
  28. package/src/layouts/history-list/PageTab.vue +4 -4
  29. package/src/layouts/history-list/composables.ts +82 -180
  30. package/src/layouts/history-list/useHistoryList.ts +60 -0
  31. package/src/layouts/history-list/useHistoryRevert.ts +400 -0
  32. package/src/layouts/sidebar/Sidebar.vue +2 -2
  33. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +5 -4
  34. package/src/services/codeBlock.ts +30 -29
  35. package/src/services/dataSource.ts +37 -37
  36. package/src/services/editor.ts +32 -29
  37. package/src/services/history.ts +340 -431
  38. package/src/theme/history-list-panel.scss +14 -0
  39. package/src/type.ts +179 -100
  40. package/src/utils/editor.ts +2 -2
  41. package/src/utils/history.ts +52 -74
  42. package/types/index.d.ts +435 -309
@@ -330,6 +330,20 @@
330
330
  font-weight: 400; // 防止被合并组头部的粗体继承
331
331
  }
332
332
 
333
+ // 「操作人」徽标:浅蓝描边胶囊,弱化展示操作人,仅在 step.operator 有值时渲染。
334
+ .m-editor-history-list-item-operator {
335
+ flex: 0 0 auto;
336
+ padding: 0 6px;
337
+ border: 1px solid #c6e2ff;
338
+ border-radius: 8px;
339
+ font-size: 10px;
340
+ line-height: 14px;
341
+ color: #409eff;
342
+ background-color: #ecf5ff;
343
+ white-space: nowrap;
344
+ font-weight: 400; // 防止被合并组头部的粗体继承
345
+ }
346
+
333
347
  // 「已保存」徽标:绿色实心胶囊,标记最近一次保存对应的历史记录(与 historyService.markSaved 对应)。
334
348
  .m-editor-history-list-item-saved {
335
349
  flex: 0 0 auto;
package/src/type.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import type { Component } from 'vue';
19
+ import type { AppContext, Component } from 'vue';
20
20
  import type EventEmitter from 'events';
21
21
  import type * as Monaco from 'monaco-editor';
22
22
  import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
@@ -33,7 +33,7 @@ import type {
33
33
  MPage,
34
34
  MPageFragment,
35
35
  } from '@tmagic/core';
36
- import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
36
+ import type { ChangeRecord, FormConfig, FormState, TableColumnConfig, TypeFunction } from '@tmagic/form';
37
37
  import type StageCore from '@tmagic/stage';
38
38
  import type {
39
39
  CanDropIn,
@@ -780,13 +780,18 @@ export interface StepDiffItem<T = unknown> {
780
780
  *
781
781
  * 泛型 `T` 为 `diff` 中变化内容的快照类型(页面节点 `MNode` / 代码块 `CodeBlockContent` / 数据源 `DataSourceSchema`)。
782
782
  */
783
- export interface BaseStepValue<T = unknown> {
783
+ export interface BaseStepValue<T = unknown, U extends Record<string, any> = {}> {
784
784
  /**
785
785
  * 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
786
786
  * 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
787
- * 注意与各自的 `id`(关联的页面 / 代码块 / 数据源 id)区分。
787
+ * 注意与 `data.id`(关联的页面 / 代码块 / 数据源 id)区分。
788
788
  */
789
789
  uuid: string;
790
+ /**
791
+ * 关联目标信息:`id` 为关联的页面 / 代码块 / 数据源等资源 id(也是历史栈的分组 key),
792
+ * `name` 为展示名。所有历史类型统一携带。
793
+ */
794
+ data: { name: string; id: Id };
790
795
  /** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
791
796
  opType: HistoryOpType;
792
797
  /**
@@ -820,64 +825,96 @@ export interface BaseStepValue<T = unknown> {
820
825
  * 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
821
826
  */
822
827
  rootStep?: boolean;
828
+ /** 操作人 */
829
+ operator?: string;
830
+ /** 扩展信息 */
831
+ extra?: U;
823
832
  }
824
833
  // #endregion BaseStepValue
825
834
 
826
- // #region StepValue
827
- export interface StepValue extends BaseStepValue<MNode> {
828
- /** 页面信息 */
829
- data: { name: string; id: Id };
830
- /** 操作前选中的节点 ID,用于撤销后恢复选择状态 */
831
- selectedBefore: Id[];
832
- /** 操作后选中的节点 ID,用于重做后恢复选择状态 */
833
- selectedAfter: Id[];
834
- modifiedNodeIds: Map<Id, Id>;
835
+ // #region StepExtra
836
+ /**
837
+ * 历史记录的扩展上下文({@link BaseStepValue.extra})。
838
+ * 内置字段供 `page` 类型在撤销 / 重做时恢复选区与受影响节点;扩展类型可自由附加其它键。
839
+ */
840
+ export interface StepExtra {
841
+ /** 操作前选中的节点 ID,用于撤销后恢复选择状态(page 类型) */
842
+ selectedBefore?: Id[];
843
+ /** 操作后选中的节点 ID,用于重做后恢复选择状态(page 类型) */
844
+ selectedAfter?: Id[];
845
+ /** 本次操作涉及的节点 id 集合(page 类型) */
846
+ modifiedNodeIds?: Map<Id, Id>;
847
+ [key: string]: any;
835
848
  }
849
+ // #endregion StepExtra
850
+
851
+ // #region StepValue
852
+ /**
853
+ * 页面节点历史记录条目(`diff` 内容为 {@link MNode})。结构已与代码块 / 数据源统一收敛到
854
+ * {@link BaseStepValue}:关联 id 见 `data.id`,选区等上下文见 `extra`。
855
+ */
856
+ export type StepValue = BaseStepValue<MNode, StepExtra>;
836
857
  // #endregion StepValue
837
858
 
838
859
  // #region CodeBlockStepValue
839
860
  /**
840
- * 代码块历史记录条目。按 codeBlock.id 分组保存到 historyState.codeBlockState。
841
- * 变更内容统一由 `diff`(单项)表达,每项见 {@link StepDiffItem}
842
- * - 新增(opType 'add'):仅 `newSchema`(新内容);
843
- * - 更新(opType 'update'):`oldSchema` + `newSchema`,并可带 `changeRecords` 做局部更新;
844
- * - 删除(opType 'remove'):仅 `oldSchema`(删除前内容)。
861
+ * 代码块历史记录条目(`diff` 内容为 {@link CodeBlockContent}),按 `data.id`(codeBlock.id
862
+ * 分组保存到 historyState.steps.codeBlock。结构与 {@link StepValue} / {@link DataSourceStepValue}
863
+ * 一致,仅 `diff` 快照类型不同。
845
864
  */
846
- export interface CodeBlockStepValue extends BaseStepValue<CodeBlockContent> {
847
- /** 关联的代码块 id */
848
- id: Id;
849
- }
865
+ export type CodeBlockStepValue = BaseStepValue<CodeBlockContent>;
850
866
  // #endregion CodeBlockStepValue
851
867
 
852
868
  // #region DataSourceStepValue
853
869
  /**
854
- * 数据源历史记录条目。按 dataSource.id 分组保存到 historyState.dataSourceState。
855
- * 变更内容统一由 `diff`(单项)表达,每项见 {@link StepDiffItem}
856
- * - 新增(opType 'add'):仅 `newSchema`(新 schema);
857
- * - 更新(opType 'update'):`oldSchema` + `newSchema`,并可带 `changeRecords` 做局部更新;
858
- * - 删除(opType 'remove'):仅 `oldSchema`(删除前 schema)。
870
+ * 数据源历史记录条目(`diff` 内容为 {@link DataSourceSchema}),按 `data.id`(dataSource.id
871
+ * 分组保存到 historyState.steps.dataSource。结构与 {@link StepValue} / {@link CodeBlockStepValue}
872
+ * 一致,仅 `diff` 快照类型不同。
859
873
  */
860
- export interface DataSourceStepValue extends BaseStepValue<DataSourceSchema> {
861
- /** 关联的数据源 id */
862
- id: Id;
863
- }
874
+ export type DataSourceStepValue = BaseStepValue<DataSourceSchema>;
864
875
  // #endregion DataSourceStepValue
865
876
 
877
+ // #region HistorySteps
878
+ /**
879
+ * 历史记录类型标识:内置 `page` / `codeBlock` / `dataSource`,并允许业务扩展自定义类型。
880
+ * `(string & {})` 保留对内置字面量的智能提示,同时不限制扩展取值。
881
+ */
882
+ export type HistoryStepType = 'page' | 'codeBlock' | 'dataSource' | (string & {});
883
+
884
+ /**
885
+ * 全部历史栈的统一容器,按「类型 -> id -> UndoRedo 栈」两级分组。
886
+ *
887
+ * - `page`:页面历史栈,按 page.id 分组(每页一份 UndoRedo);
888
+ * - `codeBlock`:代码块历史栈,按 codeBlock.id 分组;
889
+ * - `dataSource`:数据源历史栈,按 dataSource.id 分组;
890
+ * - 其余键:业务通过 {@link HistoryService.registerStepType} 注册的自定义历史类型。
891
+ *
892
+ * 所有类型(含扩展类型)一视同仁:均按 id 独立分栈、独立 undo/redo,且都可通过
893
+ * {@link HistoryService.setMarker} 在 index 0 种入 `initial` 基线(撤销 / 回滚不会越过该基线)。
894
+ */
895
+ export interface HistorySteps {
896
+ page: Record<Id, UndoRedo<StepValue>>;
897
+ codeBlock: Record<Id, UndoRedo<CodeBlockStepValue>>;
898
+ dataSource: Record<Id, UndoRedo<DataSourceStepValue>>;
899
+ /** 扩展历史类型:按 id 分组的 UndoRedo 栈。 */
900
+ [stepType: string]: Record<Id, UndoRedo<any>>;
901
+ }
902
+ // #endregion HistorySteps
903
+
866
904
  export interface HistoryState {
867
- pageId?: Id;
868
- pageSteps: Record<Id, UndoRedo<StepValue>>;
869
- canRedo: boolean;
870
- canUndo: boolean;
871
905
  /**
872
- * 代码块历史栈,按 codeBlock.id 分组(每个代码块独立一份 UndoRedo)。
873
- * 与页面/节点无关,支持独立 undo/redo
906
+ * 全部历史栈的统一容器(页面 / 代码块 / 数据源 / 扩展类型),见 {@link HistorySteps}。
907
+ * 各类型互不影响,支持按 id 独立 undo/redo;是否可撤销 / 重做改用 {@link HistoryService.canUndo} /
908
+ * {@link HistoryService.canRedo}(按 stepType + id 查询)替代旧的全局 canUndo / canRedo 字段。
874
909
  */
875
- codeBlockState: Record<Id, UndoRedo<CodeBlockStepValue>>;
910
+ steps: HistorySteps;
876
911
  /**
877
- * 数据源历史栈,按 dataSource.id 分组(每个数据源独立一份 UndoRedo)。
878
- * 与页面/节点无关,支持独立 undo/redo。
912
+ * 各历史类型的展示名称,用于历史面板({@link HistorySteps} tab / 分组标题等)。
913
+ * 内置 `page` / `codeBlock` / `dataSource` 有默认中文名(页面 / 代码块 / 数据源),
914
+ * 扩展类型可通过 {@link HistoryService.registerStepType} 的 `name` 选项或
915
+ * {@link HistoryService.setStepName} 登记;读取请用 {@link HistoryService.getStepName}。
879
916
  */
880
- dataSourceState: Record<Id, UndoRedo<DataSourceStepValue>>;
917
+ stepNames: Record<string, string>;
881
918
  }
882
919
 
883
920
  // #region PersistedHistoryState
@@ -888,14 +925,16 @@ export interface HistoryState {
888
925
  export interface PersistedHistoryState {
889
926
  /** 快照结构版本号,便于后续兼容升级。 */
890
927
  version: number;
891
- /** 保存时的活动页 id。 */
892
- pageId?: Id;
893
- /** 各页面历史栈的序列化快照,按 pageId 分组。 */
894
- pageSteps: Record<Id, SerializedUndoRedo<StepValue>>;
895
- /** 各代码块历史栈的序列化快照,按 codeBlockId 分组。 */
896
- codeBlockState: Record<Id, SerializedUndoRedo<CodeBlockStepValue>>;
897
- /** 各数据源历史栈的序列化快照,按 dataSourceId 分组。 */
898
- dataSourceState: Record<Id, SerializedUndoRedo<DataSourceStepValue>>;
928
+ /**
929
+ * 全部历史栈的序列化快照,按「类型 -> id」两级分组,与 {@link HistorySteps} 对应。
930
+ * 内置 `page` / `codeBlock` / `dataSource`,并包含业务注册的扩展类型。
931
+ */
932
+ steps: {
933
+ page: Record<Id, SerializedUndoRedo<StepValue>>;
934
+ codeBlock: Record<Id, SerializedUndoRedo<CodeBlockStepValue>>;
935
+ dataSource: Record<Id, SerializedUndoRedo<DataSourceStepValue>>;
936
+ [stepType: string]: Record<Id, SerializedUndoRedo<any>>;
937
+ };
899
938
  /** 保存时间戳(毫秒)。 */
900
939
  savedAt: number;
901
940
  }
@@ -923,9 +962,9 @@ export interface HistoryPersistOptions {
923
962
  /**
924
963
  * 历史面板用:当前页面的一条历史步骤(包含位置和是否已应用)。
925
964
  */
926
- export interface PageHistoryStepEntry {
965
+ export interface HistoryStepEntry<T> {
927
966
  /** 步骤内容 */
928
- step: StepValue;
967
+ step: T;
929
968
  /** 在所属栈中的索引(0 为最早) */
930
969
  index: number;
931
970
  /** 是否处于"已应用"段(即位于栈游标之前)。撤销后变为 false。 */
@@ -935,63 +974,39 @@ export interface PageHistoryStepEntry {
935
974
  }
936
975
 
937
976
  /**
938
- * 页面历史面板分组。
939
- * - 连续修改同一目标节点(updatedItems[0].oldNode.id 一致)的 'update' 步骤合并成一组;
940
- * - 多节点更新 / add / remove 始终独立成组(无法明确归属单一目标)。
941
- * - targetId 为 undefined 表示"无明确目标"(如 add/remove/多节点 update),不参与合并。
977
+ * 历史面板分组(页面 / 数据源 / 代码块 / 扩展类型统一结构)。
978
+ *
979
+ * 把指定历史栈的步骤列表按"目标"做相邻合并:
980
+ * - 连续修改同一目标(单实体 update,targetId 一致)的多步合并成一组,组内可展开查看每步;
981
+ * - add / remove / 多实体 update 始终独立成组(无法明确归属单一目标);
982
+ * - targetId 为 undefined 表示"无明确目标",不参与合并。
983
+ *
984
+ * 各类型仅 `kind` 与 step 快照类型不同,统一由泛型描述:
985
+ * - 页面:`HistoryGroup<StepValue>`,`kind: 'page'`,`id` 为 pageId,`targetId` 为被改节点 id;
986
+ * - 数据源:`HistoryGroup<DataSourceStepValue>`,`kind: 'data-source'`,`id` 为 dataSource.id;
987
+ * - 代码块:`HistoryGroup<CodeBlockStepValue>`,`kind: 'code-block'`,`id` 为 codeBlock.id。
942
988
  */
943
- export interface PageHistoryGroup {
944
- kind: 'page';
945
- /** 所属页面 id */
946
- pageId: Id;
947
- /** 该分组的操作类型 */
989
+ export interface HistoryGroup<T extends BaseStepValue = BaseStepValue> {
990
+ /** 历史类型标识:page / code-block / data-source(扩展类型同理)。 */
991
+ kind: string;
992
+ /** 所属栈 id(page 为 pageId,代码块 / 数据源为对应资源 id)。 */
993
+ id: Id;
994
+ /** 该分组的操作类型。 */
948
995
  opType: HistoryOpType;
949
996
  /**
950
- * 合并的目标节点 id;只有"单节点 update"才有值,并按此 id 与相邻同 id 的 update 合并。
951
- * undefined 表示该分组不可被合并(add / remove / 多节点 update)。
997
+ * 合并的目标 id:仅"单实体 update"有值,并按此与相邻同 id 的 update 合并。
998
+ * undefined 表示该分组不可被合并(add / remove / 多实体 update)。
952
999
  */
953
1000
  targetId?: Id;
954
- /** 目标节点的可读名(取最后一步的 newNode.name/type/id */
1001
+ /** 目标可读名(取最后一步快照的 name/type/id)。 */
955
1002
  targetName?: string;
956
- /** 组内所有步骤,按时间正序 */
957
- steps: PageHistoryStepEntry[];
958
- /** 组内最后一步是否已应用 */
1003
+ /** 组内所有步骤,按时间正序。 */
1004
+ steps: { step: T; index: number; applied: boolean; isCurrent?: boolean }[];
1005
+ /** 组内最后一步是否已应用。 */
959
1006
  applied: boolean;
960
1007
  /** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组)。 */
961
1008
  isCurrent?: boolean;
962
1009
  }
963
-
964
- /**
965
- * 代码块历史面板分组。
966
- * - 同一 codeBlockId 的栈内,相邻的 'update' 操作会合并成一个 group;
967
- * - 'add' / 'remove' 始终独立成组(语义上是一次性事件)。
968
- */
969
- export interface CodeBlockHistoryGroup {
970
- kind: 'code-block';
971
- /** 关联的 codeBlock id */
972
- id: Id;
973
- /** 该分组的操作类型 */
974
- opType: HistoryOpType;
975
- /** 组内所有步骤,按时间正序 */
976
- steps: { step: CodeBlockStepValue; index: number; applied: boolean; isCurrent?: boolean }[];
977
- /** 组内最后一步是否已应用,用于整组的状态展示 */
978
- applied: boolean;
979
- /** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
980
- isCurrent?: boolean;
981
- }
982
-
983
- /**
984
- * 数据源历史面板分组,结构同 CodeBlockHistoryGroup。
985
- */
986
- export interface DataSourceHistoryGroup {
987
- kind: 'data-source';
988
- id: Id;
989
- opType: HistoryOpType;
990
- steps: { step: DataSourceStepValue; index: number; applied: boolean; isCurrent?: boolean }[];
991
- applied: boolean;
992
- /** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
993
- isCurrent?: boolean;
994
- }
995
1010
  // #endregion HistoryListEntry
996
1011
 
997
1012
  export enum KeyBindingCommand {
@@ -1205,6 +1220,21 @@ export interface EditorEvents {
1205
1220
  'history-change': [data: MPage | MPageFragment];
1206
1221
  }
1207
1222
 
1223
+ export interface HistoryEvents {
1224
+ change: [
1225
+ state: BaseStepValue | StepValue | CodeBlockStepValue | DataSourceStepValue,
1226
+ stepType: HistoryStepType,
1227
+ id: Id,
1228
+ ];
1229
+ 'code-block-history-change': [id: Id, state: CodeBlockStepValue];
1230
+ 'data-source-history-change': [id: Id, state: DataSourceStepValue];
1231
+ 'restore-from-indexed-db': [snapshot: PersistedHistoryState | null];
1232
+ 'save-to-indexed-db': [snapshot: PersistedHistoryState];
1233
+ 'mark-saved': [{ kind: HistoryStepType; id?: Id }];
1234
+ clear: [{ id: Id; stepType: HistoryStepType }];
1235
+ 'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
1236
+ }
1237
+
1208
1238
  export const canUsePluginMethods = {
1209
1239
  async: [
1210
1240
  'getLayout',
@@ -1296,8 +1326,11 @@ export interface DiffDialogPayload {
1296
1326
  * 各自实现一份,作为整体注入,避免把 describe* / isStep* 拆成多个独立 props 反复透传。
1297
1327
  */
1298
1328
  export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
1299
- /** 组级描述文案生成器,接收一个 group,返回展示文本。 */
1300
- describeGroup: (_group: any) => string;
1329
+ /**
1330
+ * 组级描述文案生成器,接收一个 group,返回展示文本。
1331
+ * 不传时回退到对组内最后一步调用 {@link describeStep}(适用于不做相邻合并、每组恒为单步的历史,如数据源/代码块)。
1332
+ */
1333
+ describeGroup?: (_group: any) => string;
1301
1334
  /** 单步描述文案生成器,接收一个 step,返回展示文本(合并组展开后的子步列表用)。 */
1302
1335
  describeStep: (_step: T) => string;
1303
1336
  /** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */
@@ -1321,3 +1354,49 @@ export interface HistoryBucketConfig<T extends BaseStepValue = BaseStepValue> ex
1321
1354
  /** 是否支持「跳转到该记录」(goto),默认 true。 */
1322
1355
  gotoEnabled?: boolean;
1323
1356
  }
1357
+
1358
+ export interface UseHistoryRevertOptions {
1359
+ /**
1360
+ * 父级应用上下文,用于让动态挂载的「差异确认弹窗」继承全局组件 / 指令 / provide / 插件
1361
+ * (Element Plus、@tmagic/form 字段组件等)。未显式传入时,会自动取调用方所在组件的 appContext
1362
+ * (`getCurrentInstance()?.appContext`)。业务方若在组件 setup 之外调用,需手动传入(如 `editorApp._context`)。
1363
+ */
1364
+ appContext?: AppContext | null;
1365
+ /**
1366
+ * 透传给差异确认弹窗的 `extendState`(即 Editor 的 `extendFormState`),
1367
+ * 使对比表单中依赖业务上下文的 `display` / `disabled` 等 filterFunction 正常工作。
1368
+ */
1369
+ extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
1370
+ }
1371
+
1372
+ /**
1373
+ * 业务自有历史(如管理台「模块」)做差异对比时所需的额外渲染入参。
1374
+ * 内置的页面 / 数据源 / 代码块按 `category` 自动取表单配置,无需传这些;
1375
+ * 业务自有类别可通过 `loadConfig` 注入自定义表单配置加载逻辑。
1376
+ */
1377
+ export interface CustomDiffFormOptions {
1378
+ /**
1379
+ * 自定义差异表单配置加载逻辑(如「模块」按 c_type 重建表单配置),
1380
+ * 透传给弹窗内部的 CompareForm;缺省时按 `category` 走内置加载。
1381
+ */
1382
+ loadConfig?: CompareFormLoadConfig;
1383
+ /** 需要走 self diff 的字段类型(如模块的 mod-cond)。 */
1384
+ selfDiffFieldTypes?: string[];
1385
+ }
1386
+
1387
+ /**
1388
+ * 业务自有历史复用「单步回滚」交互({@link useHistoryRevert} 的 `confirmAndRevert`)的入参。
1389
+ * 与内置页面 / 数据源 / 代码块回滚共用「目标校验 → 差异/二次确认弹窗 → 反向回滚」流程,
1390
+ * 业务方只需提供:差异弹窗入参(可选)、表单配置加载(可选)、实际回滚执行函数。
1391
+ */
1392
+ export interface ConfirmAndRevertOptions<T = unknown> extends CustomDiffFormOptions {
1393
+ /**
1394
+ * 差异确认弹窗入参;可对比的步骤(单实体 update)传入后弹差异确认弹窗,
1395
+ * 传 null / 省略则退化为普通二次确认框(add / remove / 不可对比)。
1396
+ */
1397
+ diffPayload?: DiffDialogPayload | null;
1398
+ /** 回滚前置校验:返回 true 表示目标数据已删除等不可回滚,给出统一提示并中止。 */
1399
+ isTargetMissing?: () => boolean;
1400
+ /** 用户确认后执行的实际回滚逻辑。 */
1401
+ revert: () => T | Promise<T>;
1402
+ }
@@ -378,11 +378,11 @@ export const fixNodePosition = (config: MNode, parent: MContainer, stage: StageC
378
378
  const style = { ...(config.style || {}) };
379
379
  const baseStyle = config.style || {};
380
380
 
381
- if ('left' in baseStyle && !('right' in baseStyle)) {
381
+ if (!('right' in baseStyle)) {
382
382
  style.left = fixNodeLeft(config, parent, stage?.renderer?.contentWindow?.document);
383
383
  }
384
384
 
385
- if ('top' in baseStyle && !('bottom' in baseStyle)) {
385
+ if (!('top' in baseStyle) && !('bottom' in baseStyle)) {
386
386
  style.top = getMiddleTop(config, parent, stage);
387
387
  }
388
388
 
@@ -23,15 +23,7 @@ import type { Id } from '@tmagic/core';
23
23
  import type { ChangeRecord } from '@tmagic/form';
24
24
  import { guid } from '@tmagic/utils';
25
25
 
26
- import type {
27
- BaseStepValue,
28
- HistoryOpSource,
29
- HistoryOpType,
30
- PageHistoryGroup,
31
- PageHistoryStepEntry,
32
- StepDiffItem,
33
- StepValue,
34
- } from '@editor/type';
26
+ import type { BaseStepValue, HistoryGroup, StepDiffItem } from '@editor/type';
35
27
 
36
28
  import { UndoRedo } from './undo-redo';
37
29
 
@@ -71,17 +63,20 @@ export const detectStackOpType = (oldVal: unknown, newVal: unknown): 'add' | 're
71
63
  *
72
64
  * - `add`:oldValue = null;`remove`:newValue = null;`update`:两者都有,可带 changeRecords 做局部更新。
73
65
  * - 内容会做 cloneDeep 防止后续被外部引用篡改;opType 依据 old/new 是否为 null 推断。
74
- * - 仅负责构造 step 并返回,入栈与事件 emit 由各公共方法(pushCodeBlock / pushDataSource)自行处理。
66
+ * - 仅负责构造 step 并返回,入栈与事件 emit 由统一的 history.push(stepType, step, id) 处理。
75
67
  * - 不直接驱动业务 service,调用方负责实际写回。
76
68
  */
77
- export const createStackStep = <T, S extends BaseStepValue<T> & { id: Id }>(
69
+ export const createStackStep = <T, S extends BaseStepValue<T>>(
78
70
  id: Id,
79
- payload: {
71
+ // payload {@link BaseStepValue} 为基础:透传字段(historyDescription / source / operator / rootStep / extra)
72
+ // 随 BaseStepValue 演进自动同步,原样写入 step;自动生成字段(uuid / data / opType / diff / timestamp / saved)
73
+ // 从 payload 中排除,由本方法内部构造。oldValue / newValue / changeRecords / name 为构造 diff 与 data 用的输入。
74
+ payload: Omit<BaseStepValue<T>, 'uuid' | 'data' | 'opType' | 'diff' | 'timestamp' | 'saved'> & {
80
75
  oldValue: T | null;
81
76
  newValue: T | null;
82
77
  changeRecords?: ChangeRecord[];
83
- historyDescription?: string;
84
- source?: HistoryOpSource;
78
+ /** 展示名(缺省时从快照 name / title 推断)。 */
79
+ name?: string;
85
80
  },
86
81
  ): S | null => {
87
82
  if (!id) return null;
@@ -90,10 +85,13 @@ export const createStackStep = <T, S extends BaseStepValue<T> & { id: Id }>(
90
85
  const newSchema = payload.newValue ? cloneDeep(payload.newValue) : null;
91
86
  const changeRecords = payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined;
92
87
  const opType = detectStackOpType(payload.oldValue, payload.newValue);
88
+ // 展示名:代码块取 name,数据源取 title,取不到则留空(不影响 undo/redo,仅用于展示)。
89
+ const schema = (payload.newValue ?? payload.oldValue) as { name?: string; title?: string } | null;
90
+ const name = payload.name ?? schema?.name ?? schema?.title ?? '';
93
91
 
94
- const step: BaseStepValue<T> & { id: Id } = {
92
+ const step: BaseStepValue<T> = {
95
93
  uuid: guid(),
96
- id,
94
+ data: { name, id },
97
95
  opType,
98
96
  diff: [
99
97
  {
@@ -104,7 +102,10 @@ export const createStackStep = <T, S extends BaseStepValue<T> & { id: Id }>(
104
102
  ],
105
103
  historyDescription: payload.historyDescription,
106
104
  source: payload.source,
105
+ operator: payload.operator,
106
+ rootStep: payload.rootStep,
107
107
  timestamp: Date.now(),
108
+ extra: payload.extra,
108
109
  };
109
110
 
110
111
  return step as S;
@@ -121,68 +122,41 @@ export const markStackSaved = <S extends { saved?: boolean }>(undoRedo?: UndoRed
121
122
  };
122
123
 
123
124
  /**
124
- * 把单个「按 id 分栈」的历史栈(代码块 / 数据源)拆成若干 group:
125
- * 每条操作记录独立成组,不做相邻 update 合并(与页面历史的合并策略不同)。
125
+ * 把单个历史栈(页面 / 代码块 / 数据源 / 扩展类型)的步骤列表按"目标"做相邻合并:
126
+ * - 单实体的 'update' 按 targetId 与相邻同 targetId 的 update 合并到一个 group,组内可展开查看每步;
127
+ * - 'add' / 'remove' 始终独立成组(语义上是结构变更,不应被收纳进单实体修改组);
128
+ * - 多实体 'update'(如页面批量改属性)也独立成组(无明确单一目标,避免误合并)。
126
129
  *
127
- * 代码块与数据源除 `kind` 外结构完全一致,统一由本方法处理;`kind` 决定返回的具体分组类型。
130
+ * 各类型行为完全一致,仅 `kind` step 快照类型不同,统一由本方法处理。
128
131
  */
129
- export const mergeStackSteps = <S extends BaseStepValue, K extends 'code-block' | 'data-source'>(
130
- kind: K,
132
+ export const mergeSteps = <T extends BaseStepValue>(
133
+ kind: string,
131
134
  id: Id,
132
- list: S[],
135
+ list: T[],
133
136
  cursor: number,
134
- ): {
135
- kind: K;
136
- id: Id;
137
- opType: HistoryOpType;
138
- steps: { step: S; index: number; applied: boolean; isCurrent?: boolean }[];
139
- applied: boolean;
140
- isCurrent?: boolean;
141
- }[] => {
142
- const currentIndex = cursor - 1;
143
- return list.map((step, index) => {
144
- const applied = index < cursor;
145
- const isCurrent = index === currentIndex;
146
- return {
147
- kind,
148
- id,
149
- opType: step.opType,
150
- steps: [{ step, index, applied, isCurrent }],
151
- applied,
152
- isCurrent,
153
- };
154
- });
155
- };
156
-
157
- /**
158
- * 把页面栈拆成若干 group:
159
- * - 单节点的 'update' 按 targetId 与相邻同 targetId 的 update 合并到一个 group;
160
- * - 'add' / 'remove' 始终独立成组(语义上是结构变更,不应被收纳进单节点修改组);
161
- * - 多节点 'update'(如批量改属性)也独立成组(无明确单一目标,避免误合并)。
162
- */
163
- export const mergePageSteps = (pageId: Id, list: StepValue[], cursor: number): PageHistoryGroup[] => {
164
- const groups: PageHistoryGroup[] = [];
165
- let current: PageHistoryGroup | null = null;
137
+ ): HistoryGroup<T>[] => {
138
+ const groups: HistoryGroup<T>[] = [];
139
+ let current: HistoryGroup<T> | null = null;
166
140
  const currentIndex = cursor - 1;
167
141
  list.forEach((step, index) => {
168
142
  const applied = index < cursor;
169
143
  const isCurrent = index === currentIndex;
170
- const targetId = detectPageTargetId(step);
171
- const targetName = detectPageTargetName(step);
172
- const entry: PageHistoryStepEntry = { step, index, applied, isCurrent };
144
+ const targetId = detectTargetId(step);
145
+ const targetName = detectTargetName(step);
146
+ const entry = { step, index, applied, isCurrent };
173
147
 
174
- // 仅"单节点 update"参与合并;其它情形(add/remove/多节点 update)始终独立成组。
148
+ // 仅"单实体 update"参与合并;其它情形(add/remove/多实体 update)始终独立成组。
175
149
  const mergeable = step.opType === 'update' && targetId !== undefined;
176
150
  if (mergeable && current?.opType === 'update' && current.targetId === targetId) {
177
151
  current.steps.push(entry);
178
152
  current.applied = applied;
179
153
  if (isCurrent) current.isCurrent = true;
180
- // 保持目标名为最近一次的(节点重命名时也能反映)
154
+ // 保持目标名为最近一次的(重命名时也能反映)
181
155
  if (targetName) current.targetName = targetName;
182
156
  } else {
183
157
  current = {
184
- kind: 'page',
185
- pageId,
158
+ kind,
159
+ id,
186
160
  opType: step.opType,
187
161
  targetId: mergeable ? targetId : undefined,
188
162
  targetName,
@@ -197,37 +171,40 @@ export const mergePageSteps = (pageId: Id, list: StepValue[], cursor: number): P
197
171
  };
198
172
 
199
173
  /**
200
- * 解析 StepValue 中的"目标节点 id"用于合并:
201
- * - 单节点 update:取唯一一项 updatedItems 的节点 id
202
- * - 其它情形(多节点 update / add / remove):返回 undefined,表示不参与合并。
174
+ * 解析 step 中的"目标 id"用于合并:
175
+ * - 单实体 update:取唯一一项 diff 的快照 id;快照无 id 时(如 CodeBlockContent)回退到 `step.data.id`
176
+ * (即资源 id),使代码块 / 数据源同样能按资源合并相邻 update;
177
+ * - 其它情形(多实体 update / add / remove):返回 undefined,表示不参与合并。
203
178
  */
204
- export const detectPageTargetId = (step: StepValue): Id | undefined => {
179
+ export const detectTargetId = (step: BaseStepValue): Id | undefined => {
205
180
  if (step.opType !== 'update') return undefined;
206
181
  const items = step.diff;
207
182
  if (items?.length !== 1) return undefined;
208
- return items[0].newSchema?.id ?? items[0].oldSchema?.id;
183
+ const newSchema = items[0].newSchema as { id?: Id } | undefined;
184
+ const oldSchema = items[0].oldSchema as { id?: Id } | undefined;
185
+ return newSchema?.id ?? oldSchema?.id ?? step.data?.id;
209
186
  };
210
187
 
211
- /** 解析 StepValue 中的目标节点可读名(用于 UI 展示)。 */
212
- export const detectPageTargetName = (step: StepValue): string | undefined => {
188
+ /** 解析 step 中的目标可读名(用于 UI 展示)。 */
189
+ export const detectTargetName = (step: BaseStepValue): string | undefined => {
213
190
  const items = step.diff;
214
191
  if (step.opType === 'update') {
215
192
  if (items?.length === 1) {
216
- const node = items[0].newSchema || items[0].oldSchema;
193
+ const node = (items[0].newSchema || items[0].oldSchema) as { name?: string; type?: string; id?: Id } | undefined;
217
194
  return (node?.name as string) || (node?.type as string) || (node?.id !== undefined ? `${node.id}` : undefined);
218
195
  }
219
196
  return items?.length ? `${items.length} 个节点` : undefined;
220
197
  }
221
198
  if (step.opType === 'add') {
222
199
  if (items?.length === 1) {
223
- const n = items[0].newSchema;
200
+ const n = items[0].newSchema as { name?: string; type?: string; id?: Id } | undefined;
224
201
  return (n?.name as string) || (n?.type as string) || `${n?.id}`;
225
202
  }
226
203
  return items?.length ? `${items.length} 个节点` : undefined;
227
204
  }
228
205
  if (step.opType === 'remove') {
229
206
  if (items?.length === 1) {
230
- const n = items[0].oldSchema;
207
+ const n = items[0].oldSchema as { name?: string; type?: string; id?: Id } | undefined;
231
208
  return (n?.name as string) || (n?.type as string) || `${n?.id}`;
232
209
  }
233
210
  return items?.length ? `${items.length} 个节点` : undefined;
@@ -297,10 +274,11 @@ export const getOrCreateStack = <T>(stacks: Record<Id, UndoRedo<T>>, id: Id): Un
297
274
  };
298
275
 
299
276
  /**
300
- * 撤销下限:当页面栈 index 0 是 `opType: 'initial'` 的基线 step 时为 1(基线不可被撤销),否则为 0。
301
- * 用于把 cursor 钉在基线之上,保证 undo / canUndo / goto 都不会越过初始基线。
277
+ * 撤销下限:当栈 index 0 是 `opType: 'initial'` 的基线 step 时为 1(基线不可被撤销),否则为 0。
278
+ * 适用于所有历史类型(page / codeBlock / dataSource / 扩展),把 cursor 钉在基线之上,
279
+ * 保证 undo / canUndo / goto 都不会越过初始基线。
302
280
  */
303
- export const undoFloor = (undoRedo: UndoRedo<StepValue>): number => {
281
+ export const undoFloor = (undoRedo: UndoRedo<any>): number => {
304
282
  return undoRedo.getElementList()[0]?.opType === 'initial' ? 1 : 0;
305
283
  };
306
284