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

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 (57) 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/index.js +6 -3
  4. package/dist/es/initService.js +1 -1
  5. package/dist/es/layouts/Framework.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 +19 -55
  7. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
  8. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
  9. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
  10. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +333 -211
  11. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
  12. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
  13. package/dist/es/layouts/history-list/composables.js +73 -32
  14. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
  15. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
  16. package/dist/es/services/codeBlock.js +85 -37
  17. package/dist/es/services/dataSource.js +62 -26
  18. package/dist/es/services/editor.js +243 -100
  19. package/dist/es/services/history.js +270 -206
  20. package/dist/es/services/ui.js +3 -0
  21. package/dist/es/style.css +49 -6
  22. package/dist/es/utils/editor.js +35 -1
  23. package/dist/es/utils/history.js +223 -0
  24. package/dist/es/utils/indexed-db.js +86 -0
  25. package/dist/es/utils/undo-redo.js +60 -1
  26. package/dist/style.css +49 -6
  27. package/dist/tmagic-editor.umd.cjs +1799 -905
  28. package/package.json +7 -7
  29. package/src/components/CompareForm.vue +3 -1
  30. package/src/components/ToolButton.vue +2 -2
  31. package/src/index.ts +1 -0
  32. package/src/initService.ts +1 -1
  33. package/src/layouts/Framework.vue +1 -1
  34. package/src/layouts/history-list/Bucket.vue +34 -71
  35. package/src/layouts/history-list/BucketTab.vue +46 -54
  36. package/src/layouts/history-list/GroupRow.vue +146 -111
  37. package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
  38. package/src/layouts/history-list/HistoryListPanel.vue +296 -113
  39. package/src/layouts/history-list/InitialRow.vue +25 -9
  40. package/src/layouts/history-list/PageTab.vue +57 -51
  41. package/src/layouts/history-list/composables.ts +157 -36
  42. package/src/layouts/page-bar/PageBar.vue +4 -2
  43. package/src/layouts/sidebar/Sidebar.vue +6 -1
  44. package/src/services/codeBlock.ts +107 -37
  45. package/src/services/dataSource.ts +89 -40
  46. package/src/services/editor.ts +370 -134
  47. package/src/services/history.ts +305 -203
  48. package/src/services/ui.ts +7 -0
  49. package/src/theme/history-list-panel.scss +72 -5
  50. package/src/theme/page-bar.scss +0 -4
  51. package/src/type.ts +167 -63
  52. package/src/utils/editor.ts +41 -1
  53. package/src/utils/history.ts +298 -0
  54. package/src/utils/index.ts +2 -0
  55. package/src/utils/indexed-db.ts +122 -0
  56. package/src/utils/undo-redo.ts +88 -0
  57. package/types/index.d.ts +783 -291
@@ -4917,8 +4917,41 @@
4917
4917
  };
4918
4918
  var props_default = new Props();
4919
4919
  //#endregion
4920
+ //#region packages/editor/src/utils/config.ts
4921
+ var $TMAGIC_EDITOR = {};
4922
+ var setEditorConfig = (option) => {
4923
+ $TMAGIC_EDITOR = option;
4924
+ };
4925
+ var getEditorConfig = (key) => $TMAGIC_EDITOR[key];
4926
+ //#endregion
4920
4927
  //#region packages/editor/src/utils/undo-redo.ts
4921
- var UndoRedo = class {
4928
+ var UndoRedo = class UndoRedo {
4929
+ /**
4930
+ * 由 {@link UndoRedo.serialize} 产出的快照重建一个 UndoRedo 实例。
4931
+ * 游标会被夹紧到 [0, length] 区间,避免脏数据导致越界。
4932
+ *
4933
+ * @param options.isSavedStep 可选谓词:若提供,则把游标定位到「最近一条满足该谓词的记录」之后
4934
+ * (即恢复到最近一个已保存点);找不到匹配记录时退回快照中的原游标。
4935
+ */
4936
+ static fromSerialized(data, options = {}) {
4937
+ const undoRedo = new UndoRedo(data.listMaxSize);
4938
+ const list = Array.isArray(data.elementList) ? data.elementList.map((item) => cloneDeep$1(item)) : [];
4939
+ let cursor = Number.isFinite(data.listCursor) ? data.listCursor : list.length;
4940
+ const overflow = list.length - undoRedo.listMaxSize;
4941
+ if (overflow > 0) {
4942
+ list.splice(0, overflow);
4943
+ cursor -= overflow;
4944
+ }
4945
+ if (options.isSavedStep) {
4946
+ for (let i = list.length - 1; i >= 0; i--) if (options.isSavedStep(list[i])) {
4947
+ cursor = i + 1;
4948
+ break;
4949
+ }
4950
+ }
4951
+ undoRedo.elementList = list;
4952
+ undoRedo.listCursor = Math.max(0, Math.min(cursor, list.length));
4953
+ return undoRedo;
4954
+ }
4922
4955
  elementList;
4923
4956
  listCursor;
4924
4957
  listMaxSize;
@@ -4928,6 +4961,17 @@
4928
4961
  this.listCursor = 0;
4929
4962
  this.listMaxSize = listMaxSize > minListMaxSize ? listMaxSize : minListMaxSize;
4930
4963
  }
4964
+ /**
4965
+ * 导出当前栈的可序列化快照(深克隆,避免外部改动污染内部状态)。
4966
+ * 配合 {@link UndoRedo.fromSerialized} 可在持久化后完整还原撤销/重做栈。
4967
+ */
4968
+ serialize() {
4969
+ return {
4970
+ elementList: this.elementList.map((item) => cloneDeep$1(item)),
4971
+ listCursor: this.listCursor,
4972
+ listMaxSize: this.listMaxSize
4973
+ };
4974
+ }
4931
4975
  pushElement(element) {
4932
4976
  this.elementList.splice(this.listCursor, this.elementList.length - this.listCursor, cloneDeep$1(element));
4933
4977
  this.listCursor += 1;
@@ -4960,6 +5004,28 @@
4960
5004
  return cloneDeep$1(this.elementList[this.listCursor - 1]);
4961
5005
  }
4962
5006
  /**
5007
+ * 用 `element` 替换当前游标所在元素(cursor - 1),并丢弃其后的重做尾部(与 {@link pushElement} 的丢尾一致),
5008
+ * 游标位置保持不变(元素数量不增)。cursor 为 0(无已应用元素)时不做任何操作并返回 false。
5009
+ * 用于「连续同类记录合并」等就地替换最新一条的场景。
5010
+ */
5011
+ replaceCurrentElement(element) {
5012
+ if (this.listCursor < 1) return false;
5013
+ this.elementList.splice(this.listCursor - 1, this.elementList.length - (this.listCursor - 1), cloneDeep$1(element));
5014
+ return true;
5015
+ }
5016
+ /**
5017
+ * 对当前游标所在元素(cursor - 1)做就地更新;cursor 为 0(全部已撤销)时不做任何操作。
5018
+ * 用于给「当前步骤」打标记(如标记为已保存)等元数据写入场景。
5019
+ */
5020
+ updateCurrentElement(updater) {
5021
+ if (this.listCursor < 1) return;
5022
+ updater(this.elementList[this.listCursor - 1]);
5023
+ }
5024
+ /** 对栈内全部元素做就地更新。用于批量清理元数据(如清空所有元素的已保存标记)。 */
5025
+ updateElements(updater) {
5026
+ this.elementList.forEach(updater);
5027
+ }
5028
+ /**
4963
5029
  * 返回栈内全部元素的浅克隆数组(按时间顺序,索引 0 为最早一步)。
4964
5030
  * 仅用于历史面板等只读展示场景,不应直接修改返回值。
4965
5031
  */
@@ -4981,172 +5047,317 @@
4981
5047
  }
4982
5048
  };
4983
5049
  //#endregion
4984
- //#region packages/editor/src/services/history.ts
4985
- var History = class History extends BaseService {
4986
- /**
4987
- * 把单个代码块栈拆成若干 group:
4988
- * - 把"新增/删除"独立成组(语义上属于一次性事件,不应与 update 合并);
4989
- * - 连续 'update' 合并到同一组,组内 steps 顺序就是发生顺序。
4990
- */
4991
- static mergeCodeBlockSteps(codeBlockId, list, cursor) {
4992
- const groups = [];
4993
- let current = null;
4994
- const currentIndex = cursor - 1;
4995
- list.forEach((step, index) => {
4996
- const opType = History.detectOpType(step.oldContent, step.newContent);
4997
- const applied = index < cursor;
4998
- const isCurrent = index === currentIndex;
4999
- if (opType === "update" && current?.opType === "update") {
5000
- current.steps.push({
5001
- step,
5002
- index,
5003
- applied,
5004
- isCurrent
5005
- });
5006
- current.applied = applied;
5007
- if (isCurrent) current.isCurrent = true;
5008
- } else {
5009
- current = {
5010
- kind: "code-block",
5011
- id: codeBlockId,
5012
- opType,
5013
- steps: [{
5014
- step,
5015
- index,
5016
- applied,
5017
- isCurrent
5018
- }],
5019
- applied,
5020
- isCurrent
5021
- };
5022
- groups.push(current);
5023
- }
5024
- });
5025
- return groups;
5026
- }
5027
- static mergeDataSourceSteps(dataSourceId, list, cursor) {
5028
- const groups = [];
5029
- let current = null;
5030
- const currentIndex = cursor - 1;
5031
- list.forEach((step, index) => {
5032
- const opType = History.detectOpType(step.oldSchema, step.newSchema);
5033
- const applied = index < cursor;
5034
- const isCurrent = index === currentIndex;
5035
- if (opType === "update" && current?.opType === "update") {
5036
- current.steps.push({
5050
+ //#region packages/editor/src/utils/history.ts
5051
+ /**
5052
+ * 「回滚」生成的新 step 简短描述。代码块 / 数据源共用。
5053
+ * 二者逻辑一致,仅展示名取值字段不同(代码块取 `name`,数据源取 `title`),
5054
+ * 因此通过 `getLabel` 注入取值方式。
5055
+ *
5056
+ * @param id 关联的代码块 / 数据源 id
5057
+ * @param diff 单条变更 diff(缺省视为空)
5058
+ * @param getLabel 从快照取展示名
5059
+ */
5060
+ var describeRevertStep = (id, { oldSchema, newSchema, changeRecords } = {}, getLabel) => {
5061
+ const labelOf = (schema) => getLabel(schema) || schema.id;
5062
+ if (!oldSchema && newSchema) return `撤回新增 ${labelOf(newSchema) || id}`;
5063
+ if (oldSchema && !newSchema) return `还原已删除的 ${labelOf(oldSchema) || id}`;
5064
+ const label = newSchema && getLabel(newSchema) || oldSchema && getLabel(oldSchema) || `${id}`;
5065
+ const propPath = changeRecords?.[0]?.propPath;
5066
+ return propPath ? `还原 ${label} · ${propPath}` : `还原 ${label}`;
5067
+ };
5068
+ /**
5069
+ * 根据 old/new 是否为 null 推断 opType(与 push 时的约定一致)。
5070
+ */
5071
+ var detectStackOpType = (oldVal, newVal) => {
5072
+ if (oldVal === null && newVal !== null) return "add";
5073
+ if (oldVal !== null && newVal === null) return "remove";
5074
+ return "update";
5075
+ };
5076
+ /**
5077
+ * 构造一条代码块 / 数据源「按 id 分栈」的历史记录:两者除 payload 字段命名外完全一致。
5078
+ *
5079
+ * - `add`:oldValue = null;`remove`:newValue = null;`update`:两者都有,可带 changeRecords 做局部更新。
5080
+ * - 内容会做 cloneDeep 防止后续被外部引用篡改;opType 依据 old/new 是否为 null 推断。
5081
+ * - 仅负责构造 step 并返回,入栈与事件 emit 由各公共方法(pushCodeBlock / pushDataSource)自行处理。
5082
+ * - 不直接驱动业务 service,调用方负责实际写回。
5083
+ */
5084
+ var createStackStep = (id, payload) => {
5085
+ if (!id) return null;
5086
+ const oldSchema = payload.oldValue ? cloneDeep$1(payload.oldValue) : null;
5087
+ const newSchema = payload.newValue ? cloneDeep$1(payload.newValue) : null;
5088
+ const changeRecords = payload.changeRecords?.length ? cloneDeep$1(payload.changeRecords) : void 0;
5089
+ const opType = detectStackOpType(payload.oldValue, payload.newValue);
5090
+ return {
5091
+ uuid: (0, _tmagic_utils.guid)(),
5092
+ id,
5093
+ opType,
5094
+ diff: [{
5095
+ ...newSchema !== null ? { newSchema } : {},
5096
+ ...oldSchema !== null ? { oldSchema } : {},
5097
+ ...opType === "update" && changeRecords ? { changeRecords } : {}
5098
+ }],
5099
+ historyDescription: payload.historyDescription,
5100
+ source: payload.source,
5101
+ timestamp: Date.now()
5102
+ };
5103
+ };
5104
+ var markStackSaved = (undoRedo) => {
5105
+ if (!undoRedo) return;
5106
+ undoRedo.updateElements((element) => {
5107
+ element.saved = false;
5108
+ });
5109
+ undoRedo.updateCurrentElement((element) => {
5110
+ element.saved = true;
5111
+ });
5112
+ };
5113
+ /**
5114
+ * 把单个「按 id 分栈」的历史栈(代码块 / 数据源)拆成若干 group:
5115
+ * - 把"新增/删除"独立成组(语义上属于一次性事件,不应与 update 合并);
5116
+ * - 连续 'update' 合并到同一组,组内 steps 顺序就是发生顺序。
5117
+ *
5118
+ * 代码块与数据源除 `kind` 外结构完全一致,统一由本方法处理;`kind` 决定返回的具体分组类型。
5119
+ */
5120
+ var mergeStackSteps = (kind, id, list, cursor) => {
5121
+ const groups = [];
5122
+ let current = null;
5123
+ const currentIndex = cursor - 1;
5124
+ list.forEach((step, index) => {
5125
+ const { opType } = step;
5126
+ const applied = index < cursor;
5127
+ const isCurrent = index === currentIndex;
5128
+ if (opType === "update" && current?.opType === "update") {
5129
+ current.steps.push({
5130
+ step,
5131
+ index,
5132
+ applied,
5133
+ isCurrent
5134
+ });
5135
+ current.applied = applied;
5136
+ if (isCurrent) current.isCurrent = true;
5137
+ } else {
5138
+ current = {
5139
+ kind,
5140
+ id,
5141
+ opType,
5142
+ steps: [{
5037
5143
  step,
5038
5144
  index,
5039
5145
  applied,
5040
5146
  isCurrent
5041
- });
5042
- current.applied = applied;
5043
- if (isCurrent) current.isCurrent = true;
5044
- } else {
5045
- current = {
5046
- kind: "data-source",
5047
- id: dataSourceId,
5048
- opType,
5049
- steps: [{
5050
- step,
5051
- index,
5052
- applied,
5053
- isCurrent
5054
- }],
5055
- applied,
5056
- isCurrent
5057
- };
5058
- groups.push(current);
5059
- }
5060
- });
5061
- return groups;
5062
- }
5063
- /**
5064
- * 根据 old/new 是否为 null 推断 opType(与 push 时的约定一致)。
5065
- */
5066
- static detectOpType(oldVal, newVal) {
5067
- if (oldVal === null && newVal !== null) return "add";
5068
- if (oldVal !== null && newVal === null) return "remove";
5069
- return "update";
5070
- }
5071
- /**
5072
- * 把页面栈拆成若干 group:
5073
- * - 单节点的 'update' 按 targetId 与相邻同 targetId 的 update 合并到一个 group;
5074
- * - 'add' / 'remove' 始终独立成组(语义上是结构变更,不应被收纳进单节点修改组);
5075
- * - 多节点 'update'(如批量改属性)也独立成组(无明确单一目标,避免误合并)。
5076
- */
5077
- static mergePageSteps(pageId, list, cursor) {
5078
- const groups = [];
5079
- let current = null;
5080
- const currentIndex = cursor - 1;
5081
- list.forEach((step, index) => {
5082
- const applied = index < cursor;
5083
- const isCurrent = index === currentIndex;
5084
- const targetId = History.detectPageTargetId(step);
5085
- const targetName = History.detectPageTargetName(step);
5086
- const entry = {
5087
- step,
5088
- index,
5147
+ }],
5089
5148
  applied,
5090
5149
  isCurrent
5091
5150
  };
5092
- const mergeable = step.opType === "update" && targetId !== void 0;
5093
- if (mergeable && current?.opType === "update" && current.targetId === targetId) {
5094
- current.steps.push(entry);
5095
- current.applied = applied;
5096
- if (isCurrent) current.isCurrent = true;
5097
- if (targetName) current.targetName = targetName;
5098
- } else {
5099
- current = {
5100
- kind: "page",
5101
- pageId,
5102
- opType: step.opType,
5103
- targetId: mergeable ? targetId : void 0,
5104
- targetName,
5105
- steps: [entry],
5106
- applied,
5107
- isCurrent
5108
- };
5109
- groups.push(current);
5110
- }
5111
- });
5112
- return groups;
5151
+ groups.push(current);
5152
+ }
5153
+ });
5154
+ return groups;
5155
+ };
5156
+ /**
5157
+ * 把页面栈拆成若干 group:
5158
+ * - 单节点的 'update' 按 targetId 与相邻同 targetId 的 update 合并到一个 group;
5159
+ * - 'add' / 'remove' 始终独立成组(语义上是结构变更,不应被收纳进单节点修改组);
5160
+ * - 多节点 'update'(如批量改属性)也独立成组(无明确单一目标,避免误合并)。
5161
+ */
5162
+ var mergePageSteps = (pageId, list, cursor) => {
5163
+ const groups = [];
5164
+ let current = null;
5165
+ const currentIndex = cursor - 1;
5166
+ list.forEach((step, index) => {
5167
+ const applied = index < cursor;
5168
+ const isCurrent = index === currentIndex;
5169
+ const targetId = detectPageTargetId(step);
5170
+ const targetName = detectPageTargetName(step);
5171
+ const entry = {
5172
+ step,
5173
+ index,
5174
+ applied,
5175
+ isCurrent
5176
+ };
5177
+ const mergeable = step.opType === "update" && targetId !== void 0;
5178
+ if (mergeable && current?.opType === "update" && current.targetId === targetId) {
5179
+ current.steps.push(entry);
5180
+ current.applied = applied;
5181
+ if (isCurrent) current.isCurrent = true;
5182
+ if (targetName) current.targetName = targetName;
5183
+ } else {
5184
+ current = {
5185
+ kind: "page",
5186
+ pageId,
5187
+ opType: step.opType,
5188
+ targetId: mergeable ? targetId : void 0,
5189
+ targetName,
5190
+ steps: [entry],
5191
+ applied,
5192
+ isCurrent
5193
+ };
5194
+ groups.push(current);
5195
+ }
5196
+ });
5197
+ return groups;
5198
+ };
5199
+ /**
5200
+ * 解析 StepValue 中的"目标节点 id"用于合并:
5201
+ * - 单节点 update:取唯一一项 updatedItems 的节点 id;
5202
+ * - 其它情形(多节点 update / add / remove):返回 undefined,表示不参与合并。
5203
+ */
5204
+ var detectPageTargetId = (step) => {
5205
+ if (step.opType !== "update") return void 0;
5206
+ const items = step.diff;
5207
+ if (items?.length !== 1) return void 0;
5208
+ return items[0].newSchema?.id ?? items[0].oldSchema?.id;
5209
+ };
5210
+ /** 解析 StepValue 中的目标节点可读名(用于 UI 展示)。 */
5211
+ var detectPageTargetName = (step) => {
5212
+ const items = step.diff;
5213
+ if (step.opType === "update") {
5214
+ if (items?.length === 1) {
5215
+ const node = items[0].newSchema || items[0].oldSchema;
5216
+ return node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : void 0);
5217
+ }
5218
+ return items?.length ? `${items.length} 个节点` : void 0;
5113
5219
  }
5114
- /**
5115
- * 解析 StepValue 中的"目标节点 id"用于合并:
5116
- * - 单节点 update:取唯一一项 updatedItems 的节点 id;
5117
- * - 其它情形(多节点 update / add / remove):返回 undefined,表示不参与合并。
5118
- */
5119
- static detectPageTargetId(step) {
5120
- if (step.opType !== "update") return void 0;
5121
- const items = step.updatedItems;
5122
- if (items?.length !== 1) return void 0;
5123
- return items[0].newNode?.id ?? items[0].oldNode?.id;
5124
- }
5125
- /** 解析 StepValue 中的目标节点可读名(用于 UI 展示)。 */
5126
- static detectPageTargetName(step) {
5127
- if (step.opType === "update") {
5128
- const items = step.updatedItems;
5129
- if (items?.length === 1) {
5130
- const node = items[0].newNode || items[0].oldNode;
5131
- return node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : void 0);
5132
- }
5133
- return items?.length ? `${items.length} 个节点` : void 0;
5220
+ if (step.opType === "add") {
5221
+ if (items?.length === 1) {
5222
+ const n = items[0].newSchema;
5223
+ return n?.name || n?.type || `${n?.id}`;
5134
5224
  }
5135
- if (step.opType === "add") {
5136
- if (step.nodes?.length === 1) {
5137
- const n = step.nodes[0];
5138
- return n.name || n.type || `${n.id}`;
5139
- }
5140
- return step.nodes?.length ? `${step.nodes.length} 个节点` : void 0;
5225
+ return items?.length ? `${items.length} 个节点` : void 0;
5226
+ }
5227
+ if (step.opType === "remove") {
5228
+ if (items?.length === 1) {
5229
+ const n = items[0].oldSchema;
5230
+ return n?.name || n?.type || `${n?.id}`;
5141
5231
  }
5142
- if (step.opType === "remove") {
5143
- if (step.removedItems?.length === 1) {
5144
- const n = step.removedItems[0].node;
5145
- return n.name || n.type || `${n.id}`;
5146
- }
5147
- return step.removedItems?.length ? `${step.removedItems.length} 个节点` : void 0;
5232
+ return items?.length ? `${items.length} 个节点` : void 0;
5233
+ }
5234
+ };
5235
+ /** `Record<Id, UndoRedo>` 整体序列化为 `Record<Id, SerializedUndoRedo>`。 */
5236
+ var serializeStacks = (stacks) => {
5237
+ const result = {};
5238
+ Object.entries(stacks).forEach(([id, undoRedo]) => {
5239
+ if (undoRedo) result[id] = undoRedo.serialize();
5240
+ });
5241
+ return result;
5242
+ };
5243
+ /**
5244
+ * 把 `Record<Id, SerializedUndoRedo>` 整体还原为 `Record<Id, UndoRedo>`。
5245
+ * 还原时把每个栈的游标定位到最近一条已保存(`saved === true`)记录之后。
5246
+ */
5247
+ var deserializeStacks = (stacks = {}) => {
5248
+ const result = {};
5249
+ Object.entries(stacks).forEach(([id, serialized]) => {
5250
+ if (serialized) result[id] = UndoRedo.fromSerialized(serialized, { isSavedStep: (element) => element.saved === true });
5251
+ });
5252
+ return result;
5253
+ };
5254
+ /**
5255
+ * 按 id 从「按 id 分栈」的记录表(代码块 / 数据源)中获取(或创建)对应的 UndoRedo 栈。
5256
+ */
5257
+ var getOrCreateStack = (stacks, id) => {
5258
+ if (!stacks[id]) stacks[id] = new UndoRedo();
5259
+ return stacks[id];
5260
+ };
5261
+ /**
5262
+ * 撤销下限:当页面栈 index 0 是 `opType: 'initial'` 的基线 step 时为 1(基线不可被撤销),否则为 0。
5263
+ * 用于把 cursor 钉在基线之上,保证 undo / canUndo / goto 都不会越过初始基线。
5264
+ */
5265
+ var undoFloor = (undoRedo) => {
5266
+ return undoRedo.getElementList()[0]?.opType === "initial" ? 1 : 0;
5267
+ };
5268
+ //#endregion
5269
+ //#region packages/editor/src/utils/indexed-db.ts
5270
+ /**
5271
+ * 一组极简的、基于原生 IndexedDB 的 Promise KV 工具,避免引入额外依赖。
5272
+ * 仅用于浏览器环境;在不支持 IndexedDB 的环境(如 SSR / 部分测试环境)下会 reject。
5273
+ */
5274
+ /** 是否处于支持 IndexedDB 的环境。 */
5275
+ var isIndexedDBSupported = () => typeof indexedDB !== "undefined" && indexedDB !== null;
5276
+ /**
5277
+ * 打开(必要时升级)数据库,确保目标 objectStore 存在后返回连接。
5278
+ *
5279
+ * 由于 objectStore 只能在 `onupgradeneeded` 内创建,这里先以当前版本打开,
5280
+ * 若发现 store 不存在则关闭连接、以更高版本重开来按需创建,兼容动态 storeName。
5281
+ */
5282
+ var openIndexedDB = (dbName, storeName) => new Promise((resolve, reject) => {
5283
+ if (!isIndexedDBSupported()) {
5284
+ reject(/* @__PURE__ */ new Error("当前环境不支持 IndexedDB"));
5285
+ return;
5286
+ }
5287
+ const request = indexedDB.open(dbName);
5288
+ request.onupgradeneeded = () => {
5289
+ const db = request.result;
5290
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
5291
+ };
5292
+ request.onerror = () => reject(request.error);
5293
+ request.onsuccess = () => {
5294
+ const db = request.result;
5295
+ if (db.objectStoreNames.contains(storeName)) {
5296
+ resolve(db);
5297
+ return;
5148
5298
  }
5299
+ const nextVersion = db.version + 1;
5300
+ db.close();
5301
+ const upgradeRequest = indexedDB.open(dbName, nextVersion);
5302
+ upgradeRequest.onupgradeneeded = () => {
5303
+ const upgradeDb = upgradeRequest.result;
5304
+ if (!upgradeDb.objectStoreNames.contains(storeName)) upgradeDb.createObjectStore(storeName);
5305
+ };
5306
+ upgradeRequest.onerror = () => reject(upgradeRequest.error);
5307
+ upgradeRequest.onsuccess = () => resolve(upgradeRequest.result);
5308
+ };
5309
+ });
5310
+ /** 写入(覆盖)一条记录。value 通过结构化克隆存储,支持 Map / Set 等结构。 */
5311
+ var idbSet = async (dbName, storeName, key, value) => {
5312
+ const db = await openIndexedDB(dbName, storeName);
5313
+ try {
5314
+ await new Promise((resolve, reject) => {
5315
+ const tx = db.transaction(storeName, "readwrite");
5316
+ tx.objectStore(storeName).put(value, key);
5317
+ tx.oncomplete = () => resolve();
5318
+ tx.onabort = () => reject(tx.error);
5319
+ tx.onerror = () => reject(tx.error);
5320
+ });
5321
+ } finally {
5322
+ db.close();
5323
+ }
5324
+ };
5325
+ /** 读取一条记录,不存在时返回 undefined。 */
5326
+ var idbGet = async (dbName, storeName, key) => {
5327
+ const db = await openIndexedDB(dbName, storeName);
5328
+ try {
5329
+ return await new Promise((resolve, reject) => {
5330
+ const request = db.transaction(storeName, "readonly").objectStore(storeName).get(key);
5331
+ request.onsuccess = () => resolve(request.result);
5332
+ request.onerror = () => reject(request.error);
5333
+ });
5334
+ } finally {
5335
+ db.close();
5336
+ }
5337
+ };
5338
+ /** 删除一条记录。 */
5339
+ var idbDelete = async (dbName, storeName, key) => {
5340
+ const db = await openIndexedDB(dbName, storeName);
5341
+ try {
5342
+ await new Promise((resolve, reject) => {
5343
+ const tx = db.transaction(storeName, "readwrite");
5344
+ tx.objectStore(storeName).delete(key);
5345
+ tx.oncomplete = () => resolve();
5346
+ tx.onabort = () => reject(tx.error);
5347
+ tx.onerror = () => reject(tx.error);
5348
+ });
5349
+ } finally {
5350
+ db.close();
5149
5351
  }
5352
+ };
5353
+ //#endregion
5354
+ //#region packages/editor/src/services/history.ts
5355
+ /** 历史记录持久化快照的默认存储位置与结构版本。 */
5356
+ var DEFAULT_DB_NAME = "tmagic-editor";
5357
+ var DEFAULT_STORE_NAME = "history";
5358
+ var DEFAULT_KEY = "default";
5359
+ var PERSIST_VERSION = 1;
5360
+ var History = class extends BaseService {
5150
5361
  state = (0, vue.reactive)({
5151
5362
  pageSteps: {},
5152
5363
  pageId: void 0,
@@ -5186,6 +5397,53 @@
5186
5397
  this.state.dataSourceState = {};
5187
5398
  }
5188
5399
  /**
5400
+ * 为指定页面 / 页面片种入一条「初始基线」记录(如加载 DSL 时的「初始 / 加载」基线)。
5401
+ *
5402
+ * 该记录是一条 `opType: 'initial'` 的 {@link StepValue},作为页面历史栈 **index 0 的固定底线**:
5403
+ * - 它是一条真实入栈的 step(随栈一起持久化),但被钉为撤销/回滚的下限——cursor 永不低于它,
5404
+ * 因此不会被 undo / goto / revert 触达(详见 {@link undo} / {@link setCanUndoRedo});
5405
+ * - 历史面板把它过滤出分组列表(见 {@link getPageHistoryGroups}),改由底部「初始」行展示。
5406
+ *
5407
+ * 仅当目标页面栈为空时种入(保证 initial 一定位于 index 0);已存在 initial 底线时默认不重复种入,
5408
+ * 传 `force=true` 且栈为空时按新基线种入。
5409
+ */
5410
+ setPageMarker(pageId, options = {}) {
5411
+ if (pageId === void 0 || pageId === null || `${pageId}` === "") return null;
5412
+ const existing = this.getPageMarker(pageId);
5413
+ if (existing) return existing;
5414
+ const stack = getOrCreateStack(this.state.pageSteps, pageId);
5415
+ if (stack.getLength() > 0) return null;
5416
+ const marker = {
5417
+ uuid: (0, _tmagic_utils.guid)(),
5418
+ opType: "initial",
5419
+ diff: [],
5420
+ data: {
5421
+ name: options.name || "",
5422
+ id: pageId
5423
+ },
5424
+ selectedBefore: [],
5425
+ selectedAfter: [],
5426
+ modifiedNodeIds: /* @__PURE__ */ new Map(),
5427
+ historyDescription: options.description || "未修改的初始状态",
5428
+ timestamp: Date.now(),
5429
+ ...options.source ? { source: options.source } : {}
5430
+ };
5431
+ stack.pushElement(marker);
5432
+ if (`${pageId}` === `${this.state.pageId}`) this.setCanUndoRedo();
5433
+ this.emit("page-marker-change", marker);
5434
+ return marker;
5435
+ }
5436
+ /**
5437
+ * 读取指定页面(缺省当前活动页)的初始基线 step(页面栈 index 0 且 `opType: 'initial'`);
5438
+ * 不存在时返回 undefined。
5439
+ */
5440
+ getPageMarker(pageId) {
5441
+ const targetPageId = pageId ?? this.state.pageId;
5442
+ if (!targetPageId) return void 0;
5443
+ const first = this.state.pageSteps[targetPageId]?.getElementList()[0];
5444
+ return first?.opType === "initial" ? first : void 0;
5445
+ }
5446
+ /**
5189
5447
  * 把一条步骤推入指定页面的栈;不指定 pageId 时落到当前活动页。
5190
5448
  *
5191
5449
  * 跨页操作(例如 `moveToContainer` 把节点搬到其它页)必须显式传入 `pageId`,
@@ -5194,11 +5452,31 @@
5194
5452
  push(state, pageId) {
5195
5453
  const undoRedo = this.getUndoRedo(pageId);
5196
5454
  if (!undoRedo) return null;
5455
+ if (state.uuid === void 0) state.uuid = (0, _tmagic_utils.guid)();
5197
5456
  if (state.timestamp === void 0) state.timestamp = Date.now();
5198
5457
  undoRedo.pushElement(state);
5199
5458
  if (pageId === void 0 || `${pageId}` === `${this.state.pageId}`) this.emit("change", state);
5200
5459
  return state;
5201
5460
  }
5461
+ /** 读取指定页面(缺省当前活动页)历史栈当前游标所在的 step(cursor - 1);无则返回 null。 */
5462
+ getCurrentPageStep(pageId) {
5463
+ const targetPageId = pageId ?? this.state.pageId;
5464
+ if (!targetPageId) return null;
5465
+ return this.state.pageSteps[targetPageId]?.getCurrentElement() ?? null;
5466
+ }
5467
+ /**
5468
+ * 用 `state` 替换指定页面栈当前游标所在的 step(并丢弃其后的重做尾部),游标不变。
5469
+ * 用于「连续 set root 记录合并」等就地替换最新一条的场景;替换成功后按需刷新 / 通知。
5470
+ */
5471
+ replaceCurrentPageStep(state, pageId) {
5472
+ const undoRedo = this.getUndoRedo(pageId);
5473
+ if (!undoRedo) return null;
5474
+ if (state.uuid === void 0) state.uuid = (0, _tmagic_utils.guid)();
5475
+ if (state.timestamp === void 0) state.timestamp = Date.now();
5476
+ if (!undoRedo.replaceCurrentElement(state)) return null;
5477
+ this.emit("change", state);
5478
+ return state;
5479
+ }
5202
5480
  /**
5203
5481
  * 推入一条代码块变更记录(与页面/节点完全无关),按 `codeBlockId` 维度独立一份 UndoRedo 栈。
5204
5482
  *
@@ -5209,17 +5487,15 @@
5209
5487
  * - 不直接驱动 codeBlockService,调用方负责实际写回。
5210
5488
  */
5211
5489
  pushCodeBlock(codeBlockId, payload) {
5212
- if (!codeBlockId) return null;
5213
- const step = {
5214
- id: codeBlockId,
5215
- oldContent: payload.oldContent ? cloneDeep$1(payload.oldContent) : null,
5216
- newContent: payload.newContent ? cloneDeep$1(payload.newContent) : null,
5217
- changeRecords: payload.changeRecords?.length ? cloneDeep$1(payload.changeRecords) : void 0,
5490
+ const step = createStackStep(codeBlockId, {
5491
+ oldValue: payload.oldContent,
5492
+ newValue: payload.newContent,
5493
+ changeRecords: payload.changeRecords,
5218
5494
  historyDescription: payload.historyDescription,
5219
- source: payload.source,
5220
- timestamp: Date.now()
5221
- };
5222
- this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
5495
+ source: payload.source
5496
+ });
5497
+ if (!step) return null;
5498
+ getOrCreateStack(this.state.codeBlockState, codeBlockId).pushElement(step);
5223
5499
  this.emit("code-block-history-change", codeBlockId, step);
5224
5500
  return step;
5225
5501
  }
@@ -5228,17 +5504,15 @@
5228
5504
  * 行为同 pushCodeBlock(新增 oldSchema=null;删除 newSchema=null)。
5229
5505
  */
5230
5506
  pushDataSource(dataSourceId, payload) {
5231
- if (!dataSourceId) return null;
5232
- const step = {
5233
- id: dataSourceId,
5234
- oldSchema: payload.oldSchema ? cloneDeep$1(payload.oldSchema) : null,
5235
- newSchema: payload.newSchema ? cloneDeep$1(payload.newSchema) : null,
5236
- changeRecords: payload.changeRecords?.length ? cloneDeep$1(payload.changeRecords) : void 0,
5507
+ const step = createStackStep(dataSourceId, {
5508
+ oldValue: payload.oldSchema,
5509
+ newValue: payload.newSchema,
5510
+ changeRecords: payload.changeRecords,
5237
5511
  historyDescription: payload.historyDescription,
5238
- source: payload.source,
5239
- timestamp: Date.now()
5240
- };
5241
- this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
5512
+ source: payload.source
5513
+ });
5514
+ if (!step) return null;
5515
+ getOrCreateStack(this.state.dataSourceState, dataSourceId).pushElement(step);
5242
5516
  this.emit("data-source-history-change", dataSourceId, step);
5243
5517
  return step;
5244
5518
  }
@@ -5293,6 +5567,7 @@
5293
5567
  undo() {
5294
5568
  const undoRedo = this.getUndoRedo();
5295
5569
  if (!undoRedo) return null;
5570
+ if (undoRedo.getCursor() <= undoFloor(undoRedo)) return null;
5296
5571
  const state = undoRedo.undo();
5297
5572
  this.emit("change", state);
5298
5573
  return state;
@@ -5310,6 +5585,127 @@
5310
5585
  this.removeAllPlugins();
5311
5586
  }
5312
5587
  /**
5588
+ * 清空指定页面(缺省当前活动页)的历史记录栈。
5589
+ * 仅删除撤销/重做记录,不会改动当前 DSL;清空后该页将无法再撤销/重做之前的操作。
5590
+ */
5591
+ clearPage(pageId) {
5592
+ const targetPageId = pageId ?? this.state.pageId;
5593
+ if (!targetPageId) return;
5594
+ const marker = this.getPageMarker(targetPageId);
5595
+ this.state.pageSteps[targetPageId] = new UndoRedo();
5596
+ if (marker) this.setPageMarker(targetPageId, {
5597
+ name: marker.data?.name,
5598
+ description: marker.historyDescription,
5599
+ source: marker.source
5600
+ });
5601
+ if (`${targetPageId}` === `${this.state.pageId}`) {
5602
+ this.setCanUndoRedo();
5603
+ this.emit("clear-page", null);
5604
+ }
5605
+ }
5606
+ /**
5607
+ * 清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。
5608
+ * 仅删除撤销/重做记录,不会改动数据源本身。
5609
+ */
5610
+ clearDataSource(dataSourceId) {
5611
+ if (dataSourceId !== void 0) delete this.state.dataSourceState[dataSourceId];
5612
+ else this.state.dataSourceState = {};
5613
+ }
5614
+ /**
5615
+ * 清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。
5616
+ * 仅删除撤销/重做记录,不会改动代码块本身。
5617
+ */
5618
+ clearCodeBlock(codeBlockId) {
5619
+ if (codeBlockId !== void 0) delete this.state.codeBlockState[codeBlockId];
5620
+ else this.state.codeBlockState = {};
5621
+ }
5622
+ /**
5623
+ * 标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标为 `saved`。
5624
+ * 适用于「整体落库」场景;若只保存了其中一类,请改用更细粒度的
5625
+ * {@link markPageSaved} / {@link markCodeBlockSaved} / {@link markDataSourceSaved}。
5626
+ */
5627
+ markSaved() {
5628
+ Object.values(this.state.pageSteps).forEach(markStackSaved);
5629
+ Object.values(this.state.codeBlockState).forEach(markStackSaved);
5630
+ Object.values(this.state.dataSourceState).forEach(markStackSaved);
5631
+ this.emit("mark-saved", { kind: "all" });
5632
+ }
5633
+ /**
5634
+ * 标记指定页面(缺省为当前活动页)的历史栈当前记录为已保存。
5635
+ * 仅影响该页面自己的栈,不波及代码块 / 数据源 / 其它页面。
5636
+ */
5637
+ markPageSaved(pageId) {
5638
+ const targetPageId = pageId ?? this.state.pageId;
5639
+ if (!targetPageId) return;
5640
+ markStackSaved(this.state.pageSteps[targetPageId]);
5641
+ this.emit("mark-saved", {
5642
+ kind: "page",
5643
+ id: targetPageId
5644
+ });
5645
+ }
5646
+ /** 标记指定代码块的历史栈当前记录为已保存,仅影响该代码块自己的栈。 */
5647
+ markCodeBlockSaved(codeBlockId) {
5648
+ if (!codeBlockId) return;
5649
+ markStackSaved(this.state.codeBlockState[codeBlockId]);
5650
+ this.emit("mark-saved", {
5651
+ kind: "code-block",
5652
+ id: codeBlockId
5653
+ });
5654
+ }
5655
+ /** 标记指定数据源的历史栈当前记录为已保存,仅影响该数据源自己的栈。 */
5656
+ markDataSourceSaved(dataSourceId) {
5657
+ if (!dataSourceId) return;
5658
+ markStackSaved(this.state.dataSourceState[dataSourceId]);
5659
+ this.emit("mark-saved", {
5660
+ kind: "data-source",
5661
+ id: dataSourceId
5662
+ });
5663
+ }
5664
+ /**
5665
+ * 把当前内存中的全部历史栈(页面 / 代码块 / 数据源)序列化后写入本地 IndexedDB。
5666
+ *
5667
+ * - 每个 UndoRedo 栈连同其游标、容量一并保存,恢复后可继续 undo/redo;
5668
+ * - `key` 用于区分不同活动页 / 项目(同一 store 下可保存多份快照),缺省为 `default`;
5669
+ * - 返回写入成功的快照对象,便于调用方记录 savedAt 等信息;
5670
+ * - 不支持 IndexedDB 的环境(如 SSR)会 reject。
5671
+ */
5672
+ async saveToIndexedDB(options = {}) {
5673
+ const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY, appId } = options;
5674
+ const snapshot = {
5675
+ version: PERSIST_VERSION,
5676
+ pageId: this.state.pageId,
5677
+ pageSteps: serializeStacks(this.state.pageSteps),
5678
+ codeBlockState: serializeStacks(this.state.codeBlockState),
5679
+ dataSourceState: serializeStacks(this.state.dataSourceState),
5680
+ savedAt: Date.now()
5681
+ };
5682
+ await idbSet(this.resolveDbName(dbName, appId), storeName, key, (0, serialize_javascript.default)(snapshot));
5683
+ this.emit("save-to-indexed-db", snapshot);
5684
+ return snapshot;
5685
+ }
5686
+ /**
5687
+ * 从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
5688
+ *
5689
+ * - 读取到的每个栈都会经 {@link UndoRedo.fromSerialized} 还原(含游标),随后可直接 undo/redo;
5690
+ * - 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 pageId;
5691
+ * - 找不到对应记录时返回 null,且不改动当前状态;
5692
+ * - 不支持 IndexedDB 的环境(如 SSR)会 reject。
5693
+ */
5694
+ async restoreFromIndexedDB(options = {}) {
5695
+ const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY, appId } = options;
5696
+ const raw = await idbGet(this.resolveDbName(dbName, appId), storeName, key);
5697
+ if (!raw) return null;
5698
+ const snapshot = typeof raw === "string" ? getEditorConfig("parseDSL")(`(${raw})`) : raw;
5699
+ if (!snapshot) return null;
5700
+ this.state.pageSteps = deserializeStacks(snapshot.pageSteps);
5701
+ this.state.codeBlockState = deserializeStacks(snapshot.codeBlockState);
5702
+ this.state.dataSourceState = deserializeStacks(snapshot.dataSourceState);
5703
+ this.state.pageId = snapshot.pageId;
5704
+ this.setCanUndoRedo();
5705
+ this.emit("restore-from-indexed-db", snapshot);
5706
+ return snapshot;
5707
+ }
5708
+ /**
5313
5709
  * 取出当前活动页的历史步骤平铺列表(包含已应用 + 已撤销)。
5314
5710
  * 列表按时间正序,最早一步在最前面。
5315
5711
  * 通常 UI 应使用 `getPageHistoryGroups` 取已合并分组的版本;本方法仅为兼容/调试保留。
@@ -5340,8 +5736,7 @@
5340
5736
  if (!undoRedo) return [];
5341
5737
  const list = undoRedo.getElementList();
5342
5738
  if (!list.length) return [];
5343
- const cursor = undoRedo.getCursor();
5344
- return History.mergePageSteps(targetPageId, list, cursor);
5739
+ return mergePageSteps(targetPageId, list, undoRedo.getCursor()).filter((group) => group.opType !== "initial");
5345
5740
  }
5346
5741
  /**
5347
5742
  * 取出全部代码块的历史栈,按 codeBlockId 分组。
@@ -5357,7 +5752,7 @@
5357
5752
  const list = undoRedo.getElementList();
5358
5753
  if (!list.length) return;
5359
5754
  const cursor = undoRedo.getCursor();
5360
- groups.push(...History.mergeCodeBlockSteps(id, list, cursor));
5755
+ groups.push(...mergeStackSteps("code-block", id, list, cursor));
5361
5756
  });
5362
5757
  return groups;
5363
5758
  }
@@ -5407,6 +5802,44 @@
5407
5802
  }));
5408
5803
  }
5409
5804
  /**
5805
+ * 按历史记录 uuid 在指定页面(默认当前活动页)的栈中查找其索引。
5806
+ * 找不到时返回 -1。供「按 uuid 回滚」等需要把 uuid 映射回 index 的场景使用。
5807
+ */
5808
+ getPageStepIndexByUuid(uuid, pageId) {
5809
+ if (!uuid) return -1;
5810
+ return this.getPageStepList(pageId).findIndex((entry) => entry.step.uuid === uuid);
5811
+ }
5812
+ /**
5813
+ * 按历史记录 uuid 在全部代码块栈中查找其所属 codeBlockId 与索引。
5814
+ * 找不到时返回 null。
5815
+ */
5816
+ findCodeBlockStepLocationByUuid(uuid) {
5817
+ if (!uuid) return null;
5818
+ for (const id of Object.keys(this.state.codeBlockState)) {
5819
+ const index = this.getCodeBlockStepList(id).findIndex((entry) => entry.step.uuid === uuid);
5820
+ if (index >= 0) return {
5821
+ id,
5822
+ index
5823
+ };
5824
+ }
5825
+ return null;
5826
+ }
5827
+ /**
5828
+ * 按历史记录 uuid 在全部数据源栈中查找其所属 dataSourceId 与索引。
5829
+ * 找不到时返回 null。
5830
+ */
5831
+ findDataSourceStepLocationByUuid(uuid) {
5832
+ if (!uuid) return null;
5833
+ for (const id of Object.keys(this.state.dataSourceState)) {
5834
+ const index = this.getDataSourceStepList(id).findIndex((entry) => entry.step.uuid === uuid);
5835
+ if (index >= 0) return {
5836
+ id,
5837
+ index
5838
+ };
5839
+ }
5840
+ return null;
5841
+ }
5842
+ /**
5410
5843
  * 取出全部数据源的历史栈,按 dataSourceId 分组。同上。
5411
5844
  */
5412
5845
  getDataSourceHistoryGroups() {
@@ -5416,7 +5849,7 @@
5416
5849
  const list = undoRedo.getElementList();
5417
5850
  if (!list.length) return;
5418
5851
  const cursor = undoRedo.getCursor();
5419
- groups.push(...History.mergeDataSourceSteps(id, list, cursor));
5852
+ groups.push(...mergeStackSteps("data-source", id, list, cursor));
5420
5853
  });
5421
5854
  return groups;
5422
5855
  }
@@ -5432,35 +5865,22 @@
5432
5865
  if (!this.state.pageSteps[targetPageId]) this.state.pageSteps[targetPageId] = new UndoRedo();
5433
5866
  return this.state.pageSteps[targetPageId];
5434
5867
  }
5435
- setCanUndoRedo() {
5436
- const undoRedo = this.getUndoRedo();
5437
- this.state.canRedo = undoRedo?.canRedo() || false;
5438
- this.state.canUndo = undoRedo?.canUndo() || false;
5439
- }
5440
5868
  /**
5441
- * id 获取(或创建)指定代码块的 UndoRedo 栈。
5869
+ * 把基础 dbName 与当前 DSL(root app)的 id 拼成最终库名,实现不同应用历史隔离。
5870
+ * 取不到 app id(如尚未加载 DSL)时退回基础 dbName。
5442
5871
  */
5443
- getCodeBlockUndoRedo(codeBlockId) {
5444
- if (!this.state.codeBlockState[codeBlockId]) this.state.codeBlockState[codeBlockId] = new UndoRedo();
5445
- return this.state.codeBlockState[codeBlockId];
5872
+ resolveDbName(dbName, appId) {
5873
+ const resolvedAppId = appId ?? editor_default.get("root")?.id;
5874
+ return resolvedAppId ? `${dbName}-${resolvedAppId}` : dbName;
5446
5875
  }
5447
- /**
5448
- * id 获取(或创建)指定数据源的 UndoRedo 栈。
5449
- */
5450
- getDataSourceUndoRedo(dataSourceId) {
5451
- if (!this.state.dataSourceState[dataSourceId]) this.state.dataSourceState[dataSourceId] = new UndoRedo();
5452
- return this.state.dataSourceState[dataSourceId];
5876
+ setCanUndoRedo() {
5877
+ const undoRedo = this.getUndoRedo();
5878
+ this.state.canRedo = undoRedo?.canRedo() || false;
5879
+ this.state.canUndo = undoRedo ? undoRedo.getCursor() > undoFloor(undoRedo) : false;
5453
5880
  }
5454
5881
  };
5455
5882
  var history_default = new History();
5456
5883
  //#endregion
5457
- //#region packages/editor/src/utils/config.ts
5458
- var $TMAGIC_EDITOR = {};
5459
- var setEditorConfig = (option) => {
5460
- $TMAGIC_EDITOR = option;
5461
- };
5462
- var getEditorConfig = (key) => $TMAGIC_EDITOR[key];
5463
- //#endregion
5464
5884
  //#region packages/editor/src/services/storage.ts
5465
5885
  var Protocol = /* @__PURE__ */ function(Protocol) {
5466
5886
  Protocol["OBJECT"] = "object";
@@ -6010,6 +6430,40 @@
6010
6430
  aborted: false
6011
6431
  };
6012
6432
  };
6433
+ /**
6434
+ * 给「回滚」生成的新 step 用的简短描述生成器。
6435
+ * 与 UI 层 `describePageStep` 同义,但避免 service 反向依赖 layouts/,故放在此工具函数中。
6436
+ */
6437
+ var describeStepForRevert = (step) => {
6438
+ const items = step.diff ?? [];
6439
+ const withId = (node, label) => {
6440
+ const id = node?.id;
6441
+ if (id === void 0 || id === null || `${id}` === "") return label;
6442
+ return label ? `${label}(id: ${id})` : `id: ${id}`;
6443
+ };
6444
+ switch (step.opType) {
6445
+ case "add": {
6446
+ const count = items.length;
6447
+ const node = items[0]?.newSchema;
6448
+ const label = node?.name || node?.type || "";
6449
+ return `撤回新增 ${count} 个节点${count === 1 ? `(${withId(node, label)})` : ""}`;
6450
+ }
6451
+ case "remove": {
6452
+ const count = items.length;
6453
+ const node = items[0]?.oldSchema;
6454
+ const label = node?.name || node?.type || "";
6455
+ return `还原已删除的 ${count} 个节点${count === 1 ? `(${withId(node, label)})` : ""}`;
6456
+ }
6457
+ default:
6458
+ if (items.length === 1) {
6459
+ const { newSchema, oldSchema, changeRecords } = items[0];
6460
+ const target = withId(newSchema || oldSchema, newSchema?.name || newSchema?.type || oldSchema?.name || oldSchema?.type || "");
6461
+ const propPath = changeRecords?.[0]?.propPath;
6462
+ return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
6463
+ }
6464
+ return `还原 ${items.length} 个节点的修改`;
6465
+ }
6466
+ };
6013
6467
  //#endregion
6014
6468
  //#region packages/editor/src/utils/operator.ts
6015
6469
  /**
@@ -6078,35 +6532,15 @@
6078
6532
  //#endregion
6079
6533
  //#region packages/editor/src/services/editor.ts
6080
6534
  /**
6081
- * 给「回滚」生成的新 step 用的简短描述生成器。
6082
- * UI `describePageStep` 同义,但避免 service 反向依赖 layouts/,故在此本地实现。
6535
+ * 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
6536
+ * `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
6537
+ * 缺省(未传 / 空数组)才退化为整节点替换。
6083
6538
  */
6084
- var describeStepForRevert = (step) => {
6085
- switch (step.opType) {
6086
- case "add": {
6087
- const count = step.nodes?.length ?? 0;
6088
- const node = step.nodes?.[0];
6089
- const label = node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : "");
6090
- return `撤回新增 ${count} 个节点${count === 1 && label ? `(${label})` : ""}`;
6091
- }
6092
- case "remove": {
6093
- const count = step.removedItems?.length ?? 0;
6094
- const node = step.removedItems?.[0]?.node;
6095
- const label = node?.name || node?.type || (node?.id !== void 0 ? `${node.id}` : "");
6096
- return `还原已删除的 ${count} 个节点${count === 1 && label ? `(${label})` : ""}`;
6097
- }
6098
- default: {
6099
- const items = step.updatedItems ?? [];
6100
- if (items.length === 1) {
6101
- const { newNode, oldNode, changeRecords } = items[0];
6102
- const target = newNode?.name || newNode?.type || oldNode?.name || oldNode?.type || `${newNode?.id ?? ""}`;
6103
- const propPath = changeRecords?.[0]?.propPath;
6104
- return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
6105
- }
6106
- return `还原 ${items.length} 个节点的修改`;
6107
- }
6108
- }
6109
- };
6539
+ var buildUpdateDiff = (items) => items.map(({ oldNode, newNode, changeRecords }) => ({
6540
+ oldSchema: oldNode,
6541
+ newSchema: newNode,
6542
+ ...changeRecords?.length ? { changeRecords } : {}
6543
+ }));
6110
6544
  var Editor = class extends BaseService {
6111
6545
  state = (0, vue.reactive)({
6112
6546
  root: null,
@@ -6124,6 +6558,12 @@
6124
6558
  alwaysMultiSelect: false
6125
6559
  });
6126
6560
  selectionBeforeOp = null;
6561
+ /**
6562
+ * 最近一次 pushOpHistory 写入的历史记录 uuid。
6563
+ * 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
6564
+ * 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
6565
+ */
6566
+ lastPushedHistoryId = null;
6127
6567
  constructor() {
6128
6568
  super(canUsePluginMethods.async.map((methodName) => ({
6129
6569
  name: methodName,
@@ -6138,8 +6578,9 @@
6138
6578
  * 设置当前指点节点配置
6139
6579
  * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
6140
6580
  * @param value MNode
6581
+ * @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
6141
6582
  */
6142
- set(name, value) {
6583
+ set(name, value, options = {}) {
6143
6584
  const preValue = this.state[name];
6144
6585
  this.state[name] = value;
6145
6586
  if (name === "nodes" && Array.isArray(value)) this.set("node", value[0]);
@@ -6150,6 +6591,13 @@
6150
6591
  this.state.pageLength = getPageList(app).length || 0;
6151
6592
  this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
6152
6593
  this.state.stageLoading = this.state.pageLength !== 0;
6594
+ if (preValue && !isEmpty(preValue)) this.pushRootDiffHistory(preValue, app, options.historySource);
6595
+ else app.items?.forEach((pageNode) => {
6596
+ if (pageNode?.id !== void 0 && !history_default.getPageMarker(pageNode.id)) history_default.setPageMarker(pageNode.id, {
6597
+ name: pageNode.name,
6598
+ source: options.historySource
6599
+ });
6600
+ });
6153
6601
  } else {
6154
6602
  this.state.pageLength = 0;
6155
6603
  this.state.pageFragmentLength = 0;
@@ -6175,7 +6623,23 @@
6175
6623
  getNodeInfo(id, raw = true) {
6176
6624
  let root = this.get("root");
6177
6625
  if (raw) root = (0, vue.toRaw)(root);
6178
- return (0, _tmagic_utils.getNodeInfo)(id, root);
6626
+ if (!root) return {
6627
+ node: null,
6628
+ parent: null,
6629
+ page: null
6630
+ };
6631
+ if (id === root.id) return {
6632
+ node: root,
6633
+ parent: null,
6634
+ page: null
6635
+ };
6636
+ const pageIdStr = `${this.get("page")?.id || ""}`;
6637
+ const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
6638
+ if (currentPageNode && `${id}` !== pageIdStr) {
6639
+ const info = (0, _tmagic_utils.getNodeInfo)(id, currentPageNode);
6640
+ if (info.node) return info;
6641
+ }
6642
+ return (0, _tmagic_utils.getNodeInfo)(id, root, currentPageNode);
6179
6643
  }
6180
6644
  /**
6181
6645
  * 根据ID获取指点节点配置
@@ -6368,23 +6832,26 @@
6368
6832
  }
6369
6833
  if (!((0, _tmagic_utils.isPage)(newNodes[0]) || (0, _tmagic_utils.isPageFragment)(newNodes[0]))) {
6370
6834
  const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
6371
- if (!doNotPushHistory) this.pushOpHistory("add", {
6372
- extra: {
6373
- nodes: newNodes.map((n) => cloneDeep$1((0, vue.toRaw)(n))),
6374
- parentId: (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id,
6375
- indexMap: Object.fromEntries(newNodes.map((n) => {
6835
+ if (!doNotPushHistory) {
6836
+ const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get("root")).id;
6837
+ this.pushOpHistory("add", {
6838
+ diff: newNodes.map((n) => {
6376
6839
  const p = this.getParentById(n.id, false);
6377
- return [n.id, p ? getNodeIndex(n.id, p) : -1];
6378
- }))
6379
- },
6380
- pageData: {
6381
- name: pageForOp?.name || "",
6382
- id: pageForOp.id
6383
- },
6384
- historyDescription,
6385
- source: historySource
6386
- });
6387
- else this.selectionBeforeOp = null;
6840
+ const idx = p ? getNodeIndex(n.id, p) : -1;
6841
+ return {
6842
+ newSchema: cloneDeep$1((0, vue.toRaw)(n)),
6843
+ parentId,
6844
+ index: typeof idx === "number" ? idx : -1
6845
+ };
6846
+ }),
6847
+ pageData: {
6848
+ name: pageForOp?.name || "",
6849
+ id: pageForOp.id
6850
+ },
6851
+ historyDescription,
6852
+ source: historySource
6853
+ });
6854
+ } else this.selectionBeforeOp = null;
6388
6855
  }
6389
6856
  this.emit("add", newNodes);
6390
6857
  return Array.isArray(addNode) ? newNodes : newNodes[0];
@@ -6464,7 +6931,7 @@
6464
6931
  };
6465
6932
  const idx = getNodeIndex(curNode.id, parent);
6466
6933
  removedItems.push({
6467
- node: cloneDeep$1((0, vue.toRaw)(curNode)),
6934
+ oldSchema: cloneDeep$1((0, vue.toRaw)(curNode)),
6468
6935
  parentId: parent.id,
6469
6936
  index: typeof idx === "number" ? idx : -1
6470
6937
  });
@@ -6475,7 +6942,7 @@
6475
6942
  doNotSwitchPage
6476
6943
  })));
6477
6944
  if (removedItems.length > 0 && pageForOp) if (!doNotPushHistory) this.pushOpHistory("remove", {
6478
- extra: { removedItems },
6945
+ diff: removedItems,
6479
6946
  pageData: pageForOp,
6480
6947
  historyDescription,
6481
6948
  source: historySource
@@ -6483,7 +6950,7 @@
6483
6950
  else this.selectionBeforeOp = null;
6484
6951
  this.emit("remove", nodes);
6485
6952
  }
6486
- async doUpdate(config, { changeRecords = [] } = {}) {
6953
+ async doUpdate(config, { changeRecords = [], historySource } = {}) {
6487
6954
  const root = this.get("root");
6488
6955
  if (!root) throw new Error("root为空");
6489
6956
  if (!config?.id) throw new Error("没有配置或者配置缺少id值");
@@ -6494,7 +6961,7 @@
6494
6961
  newConfig = mergeWith(cloneDeep$1(node), newConfig, editorNodeMergeCustomizer);
6495
6962
  if (!newConfig.type) throw new Error("配置缺少type值");
6496
6963
  if (newConfig.type === _tmagic_core.NodeType.ROOT) {
6497
- this.set("root", newConfig);
6964
+ this.set("root", newConfig, { historySource });
6498
6965
  return {
6499
6966
  oldNode: node,
6500
6967
  newNode: newConfig,
@@ -6544,17 +7011,20 @@
6544
7011
  const nodes = Array.isArray(config) ? config : [config];
6545
7012
  const updateData = await Promise.all(nodes.map((node, index) => {
6546
7013
  const recordsForNode = changeRecordList ? changeRecordList[index] ?? [] : changeRecords ?? [];
6547
- return this.doUpdate(node, { changeRecords: recordsForNode });
7014
+ return this.doUpdate(node, {
7015
+ changeRecords: recordsForNode,
7016
+ historySource
7017
+ });
6548
7018
  }));
6549
7019
  if (updateData[0].oldNode?.type !== _tmagic_core.NodeType.ROOT) {
6550
7020
  if (this.get("nodes").length) if (!doNotPushHistory) {
6551
7021
  const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
6552
7022
  this.pushOpHistory("update", {
6553
- extra: { updatedItems: updateData.map((d) => ({
7023
+ diff: buildUpdateDiff(updateData.map((d) => ({
6554
7024
  oldNode: cloneDeep$1(d.oldNode),
6555
- newNode: cloneDeep$1((0, vue.toRaw)(d.newNode)),
7025
+ newNode: cloneDeep$1(d.newNode),
6556
7026
  changeRecords: d.changeRecords?.length ? cloneDeep$1(d.changeRecords) : void 0
6557
- })) },
7027
+ }))),
6558
7028
  pageData: {
6559
7029
  name: pageForOp?.name || "",
6560
7030
  id: pageForOp.id
@@ -6717,10 +7187,10 @@
6717
7187
  if (!doNotPushHistory) {
6718
7188
  const pageForOp = this.getNodeInfo(node.id, false).page;
6719
7189
  this.pushOpHistory("update", {
6720
- extra: { updatedItems: [{
7190
+ diff: buildUpdateDiff([{
6721
7191
  oldNode: oldParent,
6722
7192
  newNode: cloneDeep$1((0, vue.toRaw)(parent))
6723
- }] },
7193
+ }]),
6724
7194
  pageData: {
6725
7195
  name: pageForOp?.name || "",
6726
7196
  id: pageForOp.id
@@ -6794,7 +7264,7 @@
6794
7264
  id: target.id
6795
7265
  };
6796
7266
  this.pushOpHistory("update", {
6797
- extra: { updatedItems },
7267
+ diff: buildUpdateDiff(updatedItems),
6798
7268
  pageData: historyPage,
6799
7269
  historyDescription,
6800
7270
  source: historySource
@@ -6845,7 +7315,7 @@
6845
7315
  if (!doNotPushHistory) {
6846
7316
  const pageForOp = this.getNodeInfo(configs[0].id, false).page;
6847
7317
  this.pushOpHistory("update", {
6848
- extra: { updatedItems },
7318
+ diff: buildUpdateDiff(updatedItems),
6849
7319
  pageData: {
6850
7320
  name: pageForOp?.name || "",
6851
7321
  id: pageForOp.id
@@ -6861,6 +7331,49 @@
6861
7331
  });
6862
7332
  }
6863
7333
  /**
7334
+ * 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
7335
+ * 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link StepValue.uuid}),
7336
+ * 而非节点 / 节点数组。可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
7337
+ *
7338
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时返回 null。
7339
+ */
7340
+ /** 等价于 {@link add},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7341
+ async addAndGetHistoryId(addNode, parent, options = {}) {
7342
+ this.lastPushedHistoryId = null;
7343
+ await this.add(addNode, parent, options);
7344
+ return this.lastPushedHistoryId;
7345
+ }
7346
+ /** 等价于 {@link remove},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7347
+ async removeAndGetHistoryId(nodeOrNodeList, options = {}) {
7348
+ this.lastPushedHistoryId = null;
7349
+ await this.remove(nodeOrNodeList, options);
7350
+ return this.lastPushedHistoryId;
7351
+ }
7352
+ /** 等价于 {@link update},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7353
+ async updateAndGetHistoryId(config, data = {}) {
7354
+ this.lastPushedHistoryId = null;
7355
+ await this.update(config, data);
7356
+ return this.lastPushedHistoryId;
7357
+ }
7358
+ /** 等价于 {@link moveLayer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7359
+ async moveLayerAndGetHistoryId(offset, options = {}) {
7360
+ this.lastPushedHistoryId = null;
7361
+ await this.moveLayer(offset, options);
7362
+ return this.lastPushedHistoryId;
7363
+ }
7364
+ /** 等价于 {@link moveToContainer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7365
+ async moveToContainerAndGetHistoryId(config, targetId, options = {}) {
7366
+ this.lastPushedHistoryId = null;
7367
+ await this.moveToContainer(config, targetId, options);
7368
+ return this.lastPushedHistoryId;
7369
+ }
7370
+ /** 等价于 {@link dragTo},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
7371
+ async dragToAndGetHistoryId(config, targetParent, targetIndex, options = {}) {
7372
+ this.lastPushedHistoryId = null;
7373
+ await this.dragTo(config, targetParent, targetIndex, options);
7374
+ return this.lastPushedHistoryId;
7375
+ }
7376
+ /**
6864
7377
  * 撤销当前操作
6865
7378
  * @returns 被撤销的操作
6866
7379
  */
@@ -6898,9 +7411,10 @@
6898
7411
  const entry = history_default.getPageStepList()[index];
6899
7412
  if (!entry?.applied) return null;
6900
7413
  const { step } = entry;
7414
+ if (step.opType === "initial") return null;
6901
7415
  if (!this.get("root")) return null;
6902
7416
  if (step.opType === "update") {
6903
- const items = step.updatedItems ?? [];
7417
+ const items = step.diff ?? [];
6904
7418
  if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
6905
7419
  }
6906
7420
  let revertedStep = null;
@@ -6917,24 +7431,26 @@
6917
7431
  };
6918
7432
  try {
6919
7433
  switch (step.opType) {
6920
- case "add": {
6921
- const nodes = step.nodes ?? [];
6922
- for (const n of nodes) {
6923
- const existing = this.getNodeById(n.id, false);
7434
+ case "add":
7435
+ for (const { newSchema } of step.diff ?? []) {
7436
+ if (!newSchema) continue;
7437
+ const existing = this.getNodeById(newSchema.id, false);
6924
7438
  if (existing) await this.remove(existing, opts);
6925
7439
  }
6926
7440
  break;
6927
- }
6928
7441
  case "remove": {
6929
- const sorted = [...step.removedItems ?? []].sort((a, b) => a.index - b.index);
6930
- for (const { node, parentId } of sorted) {
7442
+ const sorted = [...step.diff ?? []].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
7443
+ for (const { oldSchema, parentId } of sorted) {
7444
+ if (!oldSchema || parentId === void 0) continue;
6931
7445
  const parent = this.getNodeById(parentId, false);
6932
- if (parent) await this.add([cloneDeep$1(node)], parent, opts);
7446
+ if (parent) await this.add([cloneDeep$1(oldSchema)], parent, opts);
6933
7447
  }
6934
7448
  break;
6935
7449
  }
6936
7450
  case "update": {
6937
- const configs = (step.updatedItems ?? []).map(({ oldNode, newNode, changeRecords }) => {
7451
+ const configs = (step.diff ?? []).filter((item) => item.oldSchema && item.newSchema).map(({ oldSchema, newSchema, changeRecords }) => {
7452
+ const oldNode = oldSchema;
7453
+ const newNode = newSchema;
6938
7454
  if (changeRecords?.length) {
6939
7455
  const patch = {
6940
7456
  id: newNode.id,
@@ -6964,6 +7480,19 @@
6964
7480
  return revertedStep;
6965
7481
  }
6966
7482
  /**
7483
+ * 通过历史记录 uuid 回滚当前页面的某条历史步骤,语义与 {@link revertPageStep} 完全一致,
7484
+ * 仅入参从 index 改为 uuid({@link StepValue.uuid})。uuid 不随栈内步骤增删而变化,
7485
+ * 更适合业务侧持有引用后再回滚(埋点、跨端同步等场景)。
7486
+ *
7487
+ * @param uuid 目标历史记录的 uuid,通常由 *AndGetHistoryId 方法返回
7488
+ * @returns 反向后产生的新 step;找不到对应 uuid / 未应用 / 反向失败时返回 null
7489
+ */
7490
+ async revertPageStepById(uuid) {
7491
+ const index = history_default.getPageStepIndexByUuid(uuid);
7492
+ if (index < 0) return null;
7493
+ return this.revertPageStep(index);
7494
+ }
7495
+ /**
6967
7496
  * 跳转当前页面历史栈到指定游标位置。
6968
7497
  *
6969
7498
  * `targetCursor` 与 `UndoRedo.getCursor()` 同义:表示"已应用步骤数量",
@@ -7039,19 +7568,83 @@
7039
7568
  if (this.selectionBeforeOp) return;
7040
7569
  this.selectionBeforeOp = this.get("nodes").map((n) => n.id);
7041
7570
  }
7042
- pushOpHistory(opType, { extra, pageData, historyDescription, source }) {
7571
+ /**
7572
+ * 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
7573
+ * - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
7574
+ * - 仅新 root 存在的页面 → 一条 `add`;
7575
+ * - 仅旧 root 存在的页面 → 一条 `remove`。
7576
+ *
7577
+ * 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
7578
+ * 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
7579
+ */
7580
+ pushRootDiffHistory(preRoot, nextRoot, source) {
7581
+ const prevPages = preRoot?.items || [];
7582
+ const nextPages = nextRoot?.items || [];
7583
+ const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
7584
+ const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
7585
+ const indexInItems = (root, id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
7586
+ nextPages.forEach((nextPage) => {
7587
+ const prevPage = prevMap.get(`${nextPage.id}`);
7588
+ if (!prevPage) this.pushPageDiffStep("add", nextPage, {
7589
+ newSchema: cloneDeep$1((0, vue.toRaw)(nextPage)),
7590
+ parentId: nextRoot.id,
7591
+ index: indexInItems(nextRoot, nextPage.id)
7592
+ }, source);
7593
+ else if (!isEqual((0, vue.toRaw)(prevPage), (0, vue.toRaw)(nextPage))) this.pushPageDiffStep("update", nextPage, {
7594
+ oldSchema: cloneDeep$1((0, vue.toRaw)(prevPage)),
7595
+ newSchema: cloneDeep$1((0, vue.toRaw)(nextPage))
7596
+ }, source);
7597
+ });
7598
+ prevPages.forEach((prevPage) => {
7599
+ if (!nextMap.has(`${prevPage.id}`)) this.pushPageDiffStep("remove", prevPage, {
7600
+ oldSchema: cloneDeep$1((0, vue.toRaw)(prevPage)),
7601
+ parentId: preRoot.id,
7602
+ index: indexInItems(preRoot, prevPage.id)
7603
+ }, source);
7604
+ });
7605
+ }
7606
+ /**
7607
+ * 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
7608
+ *
7609
+ * 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
7610
+ * 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
7611
+ * 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
7612
+ */
7613
+ pushPageDiffStep(opType, page, diffItem, source) {
7614
+ const step = {
7615
+ uuid: (0, _tmagic_utils.guid)(),
7616
+ data: {
7617
+ name: page.name || "",
7618
+ id: page.id
7619
+ },
7620
+ opType,
7621
+ selectedBefore: [],
7622
+ selectedAfter: [],
7623
+ modifiedNodeIds: /* @__PURE__ */ new Map(),
7624
+ diff: [diffItem],
7625
+ rootStep: true
7626
+ };
7627
+ if (source) step.source = source;
7628
+ const top = history_default.getCurrentPageStep(page.id);
7629
+ if (top?.rootStep && top.source === source) history_default.replaceCurrentPageStep(step, page.id);
7630
+ else history_default.push(step, page.id);
7631
+ }
7632
+ pushOpHistory(opType, { diff, pageData, historyDescription, source }) {
7043
7633
  const step = {
7634
+ uuid: (0, _tmagic_utils.guid)(),
7044
7635
  data: pageData,
7045
7636
  opType,
7046
7637
  selectedBefore: this.selectionBeforeOp ?? [],
7047
7638
  selectedAfter: this.get("nodes").map((n) => n.id),
7048
7639
  modifiedNodeIds: new Map(this.get("modifiedNodeIds")),
7049
- ...extra
7640
+ diff
7050
7641
  };
7051
7642
  if (historyDescription) step.historyDescription = historyDescription;
7052
7643
  if (source) step.source = source;
7053
- history_default.push(step, pageData.id);
7644
+ const historyId = history_default.push(step, pageData.id) ? step.uuid : null;
7645
+ this.lastPushedHistoryId = historyId;
7054
7646
  this.selectionBeforeOp = null;
7647
+ return historyId;
7055
7648
  }
7056
7649
  /**
7057
7650
  * 应用历史操作(撤销 / 重做)
@@ -7066,6 +7659,7 @@
7066
7659
  * @param reverse true = 撤销,false = 重做
7067
7660
  */
7068
7661
  async applyHistoryOp(step, reverse) {
7662
+ if (step.opType === "initial") return;
7069
7663
  const root = this.get("root");
7070
7664
  const stage = this.get("stage");
7071
7665
  if (!root) return;
@@ -7076,56 +7670,59 @@
7076
7670
  };
7077
7671
  switch (step.opType) {
7078
7672
  case "add": {
7079
- const nodes = step.nodes ?? [];
7080
- if (reverse) for (const n of nodes) {
7081
- const existing = this.getNodeById(n.id, false);
7673
+ const items = step.diff ?? [];
7674
+ if (reverse) for (const { newSchema } of items) {
7675
+ if (!newSchema) continue;
7676
+ const existing = this.getNodeById(newSchema.id, false);
7082
7677
  if (existing) await this.remove(existing, commonOpts);
7083
7678
  }
7084
7679
  else {
7085
- const parent = this.getNodeById(step.parentId, false);
7086
- if (parent) {
7087
- const sorted = [...nodes].sort((a, b) => (step.indexMap?.[a.id] ?? 0) - (step.indexMap?.[b.id] ?? 0));
7088
- for (const n of sorted) {
7089
- const idx = step.indexMap?.[n.id];
7090
- if (parent.items) {
7091
- if (typeof idx === "number" && idx >= 0 && idx < parent.items.length) parent.items.splice(idx, 0, cloneDeep$1(n));
7092
- else parent.items.push(cloneDeep$1(n));
7093
- await stage?.add({
7094
- config: cloneDeep$1(n),
7095
- parent: cloneDeep$1(parent),
7096
- parentId: parent.id,
7097
- root: cloneDeep$1(root)
7098
- });
7099
- }
7680
+ const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
7681
+ for (const { newSchema, parentId, index } of sorted) {
7682
+ if (!newSchema || parentId === void 0) continue;
7683
+ const parent = this.getNodeById(parentId, false);
7684
+ if (parent?.items) {
7685
+ if (typeof index === "number" && index >= 0 && index < parent.items.length) parent.items.splice(index, 0, cloneDeep$1(newSchema));
7686
+ else parent.items.push(cloneDeep$1(newSchema));
7687
+ await stage?.add({
7688
+ config: cloneDeep$1(newSchema),
7689
+ parent: cloneDeep$1(parent),
7690
+ parentId: parent.id,
7691
+ root: cloneDeep$1(root)
7692
+ });
7100
7693
  }
7101
7694
  }
7102
7695
  }
7103
7696
  break;
7104
7697
  }
7105
7698
  case "remove": {
7106
- const items = step.removedItems ?? [];
7699
+ const items = step.diff ?? [];
7107
7700
  if (reverse) {
7108
- const sorted = [...items].sort((a, b) => a.index - b.index);
7109
- for (const { node, parentId, index } of sorted) {
7701
+ const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
7702
+ for (const { oldSchema, parentId, index } of sorted) {
7703
+ if (!oldSchema || parentId === void 0) continue;
7110
7704
  const parent = this.getNodeById(parentId, false);
7111
7705
  if (parent?.items) {
7112
- parent.items.splice(index, 0, cloneDeep$1(node));
7706
+ parent.items.splice(index ?? parent.items.length, 0, cloneDeep$1(oldSchema));
7113
7707
  await stage?.add({
7114
- config: cloneDeep$1(node),
7708
+ config: cloneDeep$1(oldSchema),
7115
7709
  parent: cloneDeep$1(parent),
7116
7710
  parentId,
7117
7711
  root: cloneDeep$1(root)
7118
7712
  });
7119
7713
  }
7120
7714
  }
7121
- } else for (const { node } of items) {
7122
- const existing = this.getNodeById(node.id, false);
7715
+ } else for (const { oldSchema } of items) {
7716
+ if (!oldSchema) continue;
7717
+ const existing = this.getNodeById(oldSchema.id, false);
7123
7718
  if (existing) await this.remove(existing, commonOpts);
7124
7719
  }
7125
7720
  break;
7126
7721
  }
7127
7722
  case "update": {
7128
- const configs = (step.updatedItems ?? []).map(({ oldNode, newNode, changeRecords }) => {
7723
+ const configs = (step.diff ?? []).filter((item) => item.oldSchema && item.newSchema).map(({ oldSchema, newSchema, changeRecords }) => {
7724
+ const oldNode = oldSchema;
7725
+ const newNode = newSchema;
7129
7726
  if (changeRecords?.length) {
7130
7727
  const sourceForValues = reverse ? oldNode : newNode;
7131
7728
  const patch = {
@@ -7206,6 +7803,7 @@
7206
7803
  showPageListButton: true,
7207
7804
  hideSlideBar: false,
7208
7805
  sideBarItems: [],
7806
+ sideBarActiveTabName: "",
7209
7807
  navMenuRect: {
7210
7808
  left: 0,
7211
7809
  top: 0,
@@ -7238,7 +7836,9 @@
7238
7836
  }
7239
7837
  if (name === "showGuides") mask?.showGuides(value);
7240
7838
  if (name === "showRule") mask?.showRule(value);
7839
+ const preValue = state[name];
7241
7840
  state[name] = value;
7841
+ if (preValue !== value) this.emit("state-change", name, value, preValue);
7242
7842
  }
7243
7843
  get(name) {
7244
7844
  return state[name];
@@ -8461,7 +9061,7 @@
8461
9061
  key: 1,
8462
9062
  class: "menu-item-text"
8463
9063
  };
8464
- var _hoisted_2$28 = { class: "el-dropdown-link menubar-menu-button" };
9064
+ var _hoisted_2$30 = { class: "el-dropdown-link menubar-menu-button" };
8465
9065
  var ToolButton_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
8466
9066
  name: "MEditorToolButton",
8467
9067
  __name: "ToolButton",
@@ -8526,11 +9126,11 @@
8526
9126
  placement: "bottom-start",
8527
9127
  content: __props.data.tooltip
8528
9128
  }, {
8529
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
9129
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), (0, vue.mergeProps)({
8530
9130
  size: "small",
8531
9131
  link: "",
8532
9132
  disabled: disabled.value
8533
- }, (0, vue.createSlots)({ _: 2 }, [__props.data.icon ? {
9133
+ }, __props.data.buttonProps || {}), (0, vue.createSlots)({ _: 2 }, [__props.data.icon ? {
8534
9134
  name: "icon",
8535
9135
  fn: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: __props.data.icon }, null, 8, ["icon"])]),
8536
9136
  key: "0"
@@ -8538,15 +9138,15 @@
8538
9138
  name: "default",
8539
9139
  fn: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.data.text), 1)]),
8540
9140
  key: "1"
8541
- } : void 0]), 1032, ["disabled"])]),
9141
+ } : void 0]), 1040, ["disabled"])]),
8542
9142
  _: 1
8543
- }, 8, ["content"])) : ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicButton), {
9143
+ }, 8, ["content"])) : ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicButton), (0, vue.mergeProps)({
8544
9144
  key: 1,
8545
9145
  size: "small",
8546
9146
  link: "",
8547
9147
  disabled: disabled.value,
8548
9148
  title: __props.data.text
8549
- }, (0, vue.createSlots)({ _: 2 }, [__props.data.icon ? {
9149
+ }, __props.data.buttonProps || {}), (0, vue.createSlots)({ _: 2 }, [__props.data.icon ? {
8550
9150
  name: "icon",
8551
9151
  fn: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: __props.data.icon }, null, 8, ["icon"])]),
8552
9152
  key: "0"
@@ -8554,7 +9154,7 @@
8554
9154
  name: "default",
8555
9155
  fn: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.data.text), 1)]),
8556
9156
  key: "1"
8557
- } : void 0]), 1032, ["disabled", "title"]))], 64)) : __props.data.type === "dropdown" ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicDropdown), {
9157
+ } : void 0]), 1040, ["disabled", "title"]))], 64)) : __props.data.type === "dropdown" ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicDropdown), {
8558
9158
  key: 3,
8559
9159
  trigger: "click",
8560
9160
  disabled: disabled.value,
@@ -8575,7 +9175,7 @@
8575
9175
  }), 128))]),
8576
9176
  _: 1
8577
9177
  })) : (0, vue.createCommentVNode)("v-if", true)]),
8578
- default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("span", _hoisted_2$28, [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.data.text), 1), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicIcon), { class: "el-icon--right" }, {
9178
+ default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("span", _hoisted_2$30, [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.data.text), 1), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicIcon), { class: "el-icon--right" }, {
8579
9179
  default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_element_plus_icons_vue.ArrowDown))]),
8580
9180
  _: 1
8581
9181
  })])]),
@@ -8594,7 +9194,7 @@
8594
9194
  id: "m-editor-page-bar-add-icon",
8595
9195
  class: "m-editor-page-bar-item m-editor-page-bar-item-icon"
8596
9196
  };
8597
- var _hoisted_2$27 = {
9197
+ var _hoisted_2$29 = {
8598
9198
  key: 1,
8599
9199
  style: { "width": "21px" }
8600
9200
  };
@@ -8631,7 +9231,7 @@
8631
9231
  }
8632
9232
  } }, null, 8, ["data"])]),
8633
9233
  _: 1
8634
- })])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$27));
9234
+ })])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$29));
8635
9235
  };
8636
9236
  }
8637
9237
  });
@@ -8644,7 +9244,7 @@
8644
9244
  class: "m-editor-page-bar",
8645
9245
  ref: "pageBar"
8646
9246
  };
8647
- var _hoisted_2$26 = {
9247
+ var _hoisted_2$28 = {
8648
9248
  key: 0,
8649
9249
  class: "m-editor-page-bar-items",
8650
9250
  ref: "itemsContainer"
@@ -8736,7 +9336,7 @@
8736
9336
  return (_ctx, _cache) => {
8737
9337
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$68, [
8738
9338
  (0, vue.renderSlot)(_ctx.$slots, "prepend"),
8739
- __props.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$26, [(0, vue.renderSlot)(_ctx.$slots, "default")], 512)) : (0, vue.createCommentVNode)("v-if", true),
9339
+ __props.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$28, [(0, vue.renderSlot)(_ctx.$slots, "default")], 512)) : (0, vue.createCommentVNode)("v-if", true),
8740
9340
  canScroll.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
8741
9341
  key: 1,
8742
9342
  class: "m-editor-page-bar-item m-editor-page-bar-item-icon m-editor-page-bar-item-left-icon",
@@ -8761,8 +9361,8 @@
8761
9361
  id: "m-editor-page-bar-list-icon",
8762
9362
  class: "m-editor-page-bar-item m-editor-page-bar-item-icon"
8763
9363
  };
8764
- var _hoisted_2$25 = { class: "page-bar-popover-wrapper" };
8765
- var _hoisted_3$10 = { class: "page-bar-popover-inner" };
9364
+ var _hoisted_2$27 = { class: "page-bar-popover-wrapper" };
9365
+ var _hoisted_3$13 = { class: "page-bar-popover-inner" };
8766
9366
  var PageList_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
8767
9367
  name: "MEditorPageList",
8768
9368
  __name: "PageList",
@@ -8786,7 +9386,7 @@
8786
9386
  default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_element_plus_icons_vue.Files))]),
8787
9387
  _: 1
8788
9388
  })]),
8789
- default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_2$25, [(0, vue.createElementVNode)("div", _hoisted_3$10, [(0, vue.renderSlot)(_ctx.$slots, "page-list-popover", { list: __props.list }, () => [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.list, (item, index) => {
9389
+ default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_2$27, [(0, vue.createElementVNode)("div", _hoisted_3$13, [(0, vue.renderSlot)(_ctx.$slots, "page-list-popover", { list: __props.list }, () => [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.list, (item, index) => {
8790
9390
  return (0, vue.openBlock)(), (0, vue.createBlock)(ToolButton_default, {
8791
9391
  data: {
8792
9392
  type: "button",
@@ -8865,9 +9465,9 @@
8865
9465
  //#endregion
8866
9466
  //#region packages/editor/src/layouts/page-bar/PageBar.vue?vue&type=script&setup=true&lang.ts
8867
9467
  var _hoisted_1$65 = { class: "m-editor-page-bar-tabs" };
8868
- var _hoisted_2$24 = ["data-page-id", "onClick"];
8869
- var _hoisted_3$9 = { class: "m-editor-page-bar-title" };
8870
- var _hoisted_4$8 = ["title"];
9468
+ var _hoisted_2$26 = ["data-page-id", "onClick"];
9469
+ var _hoisted_3$12 = { class: "m-editor-page-bar-title" };
9470
+ var _hoisted_4$9 = ["title"];
8871
9471
  var PageBar_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
8872
9472
  name: "MEditorPageBar",
8873
9473
  __name: "PageBar",
@@ -8909,7 +9509,8 @@
8909
9509
  top: 0
8910
9510
  });
8911
9511
  };
8912
- const remove = (node) => {
9512
+ const remove = async (node) => {
9513
+ await _tmagic_design.tMagicMessageBox.confirm("确定删除该页面吗?");
8913
9514
  editorService.remove(node);
8914
9515
  };
8915
9516
  const pageBarScrollContainerRef = (0, vue.useTemplateRef)("pageBarScrollContainer");
@@ -8956,7 +9557,7 @@
8956
9557
  key: item.id,
8957
9558
  "data-page-id": item.id,
8958
9559
  onClick: ($event) => switchPage(item.id)
8959
- }, [(0, vue.createElementVNode)("div", _hoisted_3$9, [(0, vue.renderSlot)(_ctx.$slots, "page-bar-title", { page: item }, () => [(0, vue.createElementVNode)("span", { title: item.name }, (0, vue.toDisplayString)(item.name || item.id), 9, _hoisted_4$8)])]), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicPopover), {
9560
+ }, [(0, vue.createElementVNode)("div", _hoisted_3$12, [(0, vue.renderSlot)(_ctx.$slots, "page-bar-title", { page: item }, () => [(0, vue.createElementVNode)("span", { title: item.name }, (0, vue.toDisplayString)(item.name || item.id), 9, _hoisted_4$9)])]), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicPopover), {
8960
9561
  "popper-class": "page-bar-popover",
8961
9562
  placement: "top",
8962
9563
  trigger: "hover",
@@ -8976,10 +9577,11 @@
8976
9577
  type: "button",
8977
9578
  text: "删除",
8978
9579
  icon: (0, vue.unref)(_element_plus_icons_vue.Delete),
9580
+ buttonProps: { type: "danger" },
8979
9581
  handler: () => remove(item)
8980
9582
  } }, null, 8, ["data"])])])]),
8981
9583
  _: 2
8982
- }, 1024)], 10, _hoisted_2$24);
9584
+ }, 1024)], 10, _hoisted_2$26);
8983
9585
  }), 128))]),
8984
9586
  _: 3
8985
9587
  }, 8, ["page-bar-sort-options", "length"])]);
@@ -8992,7 +9594,7 @@
8992
9594
  //#endregion
8993
9595
  //#region packages/editor/src/layouts/AddPageBox.vue?vue&type=script&setup=true&lang.ts
8994
9596
  var _hoisted_1$64 = { class: "m-editor-empty-panel" };
8995
- var _hoisted_2$23 = { class: "m-editor-empty-content" };
9597
+ var _hoisted_2$25 = { class: "m-editor-empty-content" };
8996
9598
  var AddPageBox_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
8997
9599
  name: "MEditorAddPageBox",
8998
9600
  __name: "AddPageBox",
@@ -9009,7 +9611,7 @@
9009
9611
  });
9010
9612
  };
9011
9613
  return (_ctx, _cache) => {
9012
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$64, [(0, vue.createElementVNode)("div", _hoisted_2$23, [(0, vue.createElementVNode)("div", {
9614
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$64, [(0, vue.createElementVNode)("div", _hoisted_2$25, [(0, vue.createElementVNode)("div", {
9013
9615
  class: "m-editor-empty-button",
9014
9616
  onClick: _cache[0] || (_cache[0] = ($event) => clickHandler((0, vue.unref)(_tmagic_core.NodeType).PAGE))
9015
9617
  }, [(0, vue.createElementVNode)("div", null, [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(_element_plus_icons_vue.Plus) }, null, 8, ["icon"])]), _cache[2] || (_cache[2] = (0, vue.createElementVNode)("p", null, "新增页面", -1))]), !__props.disabledPageFragment ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
@@ -9026,7 +9628,7 @@
9026
9628
  //#endregion
9027
9629
  //#region packages/editor/src/layouts/CodeEditor.vue?vue&type=script&setup=true&lang.ts
9028
9630
  var _hoisted_1$63 = { class: "magic-code-editor" };
9029
- var _hoisted_2$22 = {
9631
+ var _hoisted_2$24 = {
9030
9632
  ref: "codeEditor",
9031
9633
  class: "magic-code-editor-content"
9032
9634
  };
@@ -9281,7 +9883,7 @@
9281
9883
  }, {
9282
9884
  default: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(_element_plus_icons_vue.FullScreen) }, null, 8, ["icon"])]),
9283
9885
  _: 1
9284
- })) : (0, vue.createCommentVNode)("v-if", true), (0, vue.createElementVNode)("div", _hoisted_2$22, null, 512)], 6)], 8, ["disabled"]))]);
9886
+ })) : (0, vue.createCommentVNode)("v-if", true), (0, vue.createElementVNode)("div", _hoisted_2$24, null, 512)], 6)], 8, ["disabled"]))]);
9285
9887
  };
9286
9888
  }
9287
9889
  });
@@ -9348,7 +9950,7 @@
9348
9950
  const saveCode = (value) => {
9349
9951
  try {
9350
9952
  const parseDSL = getEditorConfig("parseDSL");
9351
- editorService.set("root", parseDSL(value));
9953
+ editorService.set("root", parseDSL(value), { historySource: "root-code" });
9352
9954
  } catch (e) {
9353
9955
  console.error(e);
9354
9956
  }
@@ -9422,6 +10024,8 @@
9422
10024
  var Framework_default = Framework_vue_vue_type_script_setup_true_lang_default;
9423
10025
  //#endregion
9424
10026
  //#region packages/editor/src/layouts/history-list/composables.ts
10027
+ /** 合并组默认展开;仅当 expanded[key] === false 时为收起。 */
10028
+ var isHistoryGroupExpanded = (expanded, key) => expanded[key] !== false;
9425
10029
  /**
9426
10030
  * 历史记录面板共享逻辑:
9427
10031
  * - 暴露三类历史的聚合数据(页面 / 数据源 / 代码块);
@@ -9435,10 +10039,11 @@
9435
10039
  /**
9436
10040
  * 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
9437
10041
  * 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
10042
+ * 合并组默认展开;仅当值为 `false` 时表示收起。
9438
10043
  */
9439
10044
  const expanded = (0, vue.reactive)({});
9440
10045
  const toggleGroup = (key) => {
9441
- expanded[key] = !expanded[key];
10046
+ expanded[key] = expanded[key] === false;
9442
10047
  };
9443
10048
  const pageGroups = (0, vue.computed)(() => historyService.getPageHistoryGroups());
9444
10049
  const dataSourceGroups = (0, vue.computed)(() => historyService.getDataSourceHistoryGroups());
@@ -9500,6 +10105,7 @@
9500
10105
  "component-panel": "组件面板",
9501
10106
  props: "配置面板",
9502
10107
  code: "源码",
10108
+ "root-code": "DSL源码",
9503
10109
  "stage-contextmenu": "画布菜单",
9504
10110
  "tree-contextmenu": "树菜单",
9505
10111
  toolbar: "工具栏",
@@ -9507,6 +10113,8 @@
9507
10113
  rollback: "回滚",
9508
10114
  api: "接口",
9509
10115
  ai: "AI",
10116
+ initial: "初始值",
10117
+ sync: "同步",
9510
10118
  unknown: "未知"
9511
10119
  };
9512
10120
  /** 操作途径文案:用于历史面板展示「画布 / 树面板 / 配置面板…」标签。 */
@@ -9515,6 +10123,37 @@
9515
10123
  };
9516
10124
  /** 取一组历史步骤里最后一步(最近一次)的操作途径,用于组头部展示。 */
9517
10125
  var groupSource = (group) => group.steps[group.steps.length - 1]?.step.source;
10126
+ /**
10127
+ * 把一个历史分组(页面 / bucket)派生为 GroupRow 直接消费的视图模型 {@link HistoryRowGroup}。
10128
+ * 统一了原先 PageTab / Bucket 各自内联的 sub-steps 映射逻辑:描述、可差异、可回滚、时间、途径
10129
+ * 全部在此一次性算好,组件层只负责渲染。
10130
+ */
10131
+ var toRowGroup = (group, key, descriptor) => {
10132
+ const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor;
10133
+ const timestamp = groupTimestamp(group);
10134
+ return {
10135
+ key,
10136
+ applied: group.applied,
10137
+ isCurrent: Boolean(group.isCurrent),
10138
+ opType: group.opType,
10139
+ desc: describeGroup(group),
10140
+ source: groupSource(group),
10141
+ time: formatHistoryTime(timestamp),
10142
+ timeTitle: formatHistoryFullTime(timestamp),
10143
+ subSteps: group.steps.map((s) => ({
10144
+ index: s.index,
10145
+ applied: s.applied,
10146
+ isCurrent: s.isCurrent,
10147
+ saved: s.step.saved,
10148
+ desc: describeStep(s.step),
10149
+ diffable: isStepDiffable ? isStepDiffable(s.step) : false,
10150
+ revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
10151
+ source: s.step.source,
10152
+ time: formatHistoryTime(s.step.timestamp),
10153
+ timeTitle: formatHistoryFullTime(s.step.timestamp)
10154
+ }))
10155
+ };
10156
+ };
9518
10157
  var nameOf = (node) => node?.name || node?.type || `${node?.id ?? ""}`;
9519
10158
  /**
9520
10159
  * 默认描述里展示「名称 (id: xxx)」,便于区分同名实体。
@@ -9533,24 +10172,24 @@
9533
10172
  var describePageStep = (step) => {
9534
10173
  if (step.historyDescription) return step.historyDescription;
9535
10174
  const { opType } = step;
10175
+ const items = step.diff ?? [];
9536
10176
  if (opType === "add") {
9537
- const count = step.nodes?.length ?? 0;
9538
- const node = step.nodes?.[0];
10177
+ const count = items.length;
10178
+ const node = items[0]?.newSchema;
9539
10179
  return `新增 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ""}`;
9540
10180
  }
9541
10181
  if (opType === "remove") {
9542
- const count = step.removedItems?.length ?? 0;
9543
- const node = step.removedItems?.[0]?.node;
10182
+ const count = items.length;
10183
+ const node = items[0]?.oldSchema;
9544
10184
  return `删除 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ""}`;
9545
10185
  }
9546
- const updated = step.updatedItems ?? [];
9547
- if (!updated.length) return "修改节点";
9548
- if (updated.length === 1) {
9549
- const { newNode, changeRecords } = updated[0];
10186
+ if (!items.length) return "修改节点";
10187
+ if (items.length === 1) {
10188
+ const { newSchema, changeRecords } = items[0];
9550
10189
  const propPath = changeRecords?.[0]?.propPath;
9551
- return `修改 ${labelWithId(nameOf(newNode), newNode?.id)}${propPath ? ` · ${propPath}` : ""}`;
10190
+ return `修改 ${labelWithId(nameOf(newSchema), newSchema?.id)}${propPath ? ` · ${propPath}` : ""}`;
9552
10191
  }
9553
- return `修改 ${updated.length} 个节点`;
10192
+ return `修改 ${items.length} 个节点`;
9554
10193
  };
9555
10194
  /**
9556
10195
  * 合并组的展示文案:
@@ -9564,7 +10203,7 @@
9564
10203
  if (group.steps.length === 1) return describePageStep(group.steps[0].step);
9565
10204
  const paths = /* @__PURE__ */ new Set();
9566
10205
  group.steps.forEach((s) => {
9567
- s.step.updatedItems?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
10206
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
9568
10207
  });
9569
10208
  const pathList = Array.from(paths).slice(0, 3).join(", ");
9570
10209
  const target = labelWithId(group.targetName ?? (group.targetId !== void 0 ? `${group.targetId}` : "节点"), group.targetId);
@@ -9572,10 +10211,11 @@
9572
10211
  };
9573
10212
  var describeDataSourceStep = (step) => {
9574
10213
  if (step.historyDescription) return step.historyDescription;
9575
- if (step.oldSchema === null && step.newSchema) return `创建 ${labelWithId(step.newSchema.title, step.newSchema.id ?? step.id)}`;
9576
- if (step.newSchema === null && step.oldSchema) return `删除 ${labelWithId(step.oldSchema.title, step.oldSchema.id ?? step.id)}`;
9577
- const propPath = step.changeRecords?.[0]?.propPath;
9578
- const title = labelWithId(step.newSchema?.title || step.oldSchema?.title, step.id);
10214
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
10215
+ if (!oldSchema && newSchema) return `创建 ${labelWithId(newSchema.title, newSchema.id ?? step.id)}`;
10216
+ if (!newSchema && oldSchema) return `删除 ${labelWithId(oldSchema.title, oldSchema.id ?? step.id)}`;
10217
+ const propPath = changeRecords?.[0]?.propPath;
10218
+ const title = labelWithId(newSchema?.title || oldSchema?.title, step.id);
9579
10219
  return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
9580
10220
  };
9581
10221
  var describeDataSourceGroup = (group) => {
@@ -9584,18 +10224,19 @@
9584
10224
  if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
9585
10225
  const paths = /* @__PURE__ */ new Set();
9586
10226
  group.steps.forEach((s) => {
9587
- s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
10227
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
9588
10228
  });
9589
10229
  const pathList = Array.from(paths).slice(0, 3).join(", ");
9590
- const target = labelWithId(group.steps[group.steps.length - 1].step.newSchema?.title || group.steps[0].step.oldSchema?.title, group.id);
10230
+ const target = labelWithId(group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.title || group.steps[0].step.diff?.[0]?.oldSchema?.title, group.id);
9591
10231
  return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? "…" : ""}` : `修改 ${target}`;
9592
10232
  };
9593
10233
  var describeCodeBlockStep = (step) => {
9594
10234
  if (step.historyDescription) return step.historyDescription;
9595
- if (step.oldContent === null && step.newContent) return `创建 ${labelWithId(step.newContent.name, step.newContent.id ?? step.id)}`;
9596
- if (step.newContent === null && step.oldContent) return `删除 ${labelWithId(step.oldContent.name, step.oldContent.id ?? step.id)}`;
9597
- const propPath = step.changeRecords?.[0]?.propPath;
9598
- const title = labelWithId(step.newContent?.name || step.oldContent?.name, step.id);
10235
+ const { oldSchema: oldContent, newSchema: newContent, changeRecords } = step.diff?.[0] ?? {};
10236
+ if (!oldContent && newContent) return `创建 ${labelWithId(newContent.name, newContent.id ?? step.id)}`;
10237
+ if (!newContent && oldContent) return `删除 ${labelWithId(oldContent.name, oldContent.id ?? step.id)}`;
10238
+ const propPath = changeRecords?.[0]?.propPath;
10239
+ const title = labelWithId(newContent?.name || oldContent?.name, step.id);
9599
10240
  return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
9600
10241
  };
9601
10242
  var describeCodeBlockGroup = (group) => {
@@ -9604,10 +10245,10 @@
9604
10245
  if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
9605
10246
  const paths = /* @__PURE__ */ new Set();
9606
10247
  group.steps.forEach((s) => {
9607
- s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
10248
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
9608
10249
  });
9609
10250
  const pathList = Array.from(paths).slice(0, 3).join(", ");
9610
- const target = labelWithId(group.steps[group.steps.length - 1].step.newContent?.name || group.steps[0].step.oldContent?.name, group.id);
10251
+ const target = labelWithId(group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.name || group.steps[0].step.diff?.[0]?.oldSchema?.name, group.id);
9611
10252
  return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? "…" : ""}` : `修改 ${target}`;
9612
10253
  };
9613
10254
  /**
@@ -9618,136 +10259,176 @@
9618
10259
  */
9619
10260
  var isPageStepRevertable = (step) => {
9620
10261
  if (step.opType !== "update") return true;
9621
- const items = step.updatedItems ?? [];
10262
+ const items = step.diff ?? [];
9622
10263
  if (!items.length) return false;
9623
10264
  return items.every((item) => Boolean(item.changeRecords?.length));
9624
10265
  };
9625
10266
  /**
9626
10267
  * 数据源 step 是否支持「回滚」:
9627
- * - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
10268
+ * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
9628
10269
  * - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
9629
10270
  */
9630
10271
  var isDataSourceStepRevertable = (step) => {
9631
- if (step.oldSchema === null || step.newSchema === null) return true;
9632
- return Boolean(step.changeRecords?.length);
10272
+ const item = step.diff?.[0];
10273
+ if (!item?.oldSchema || !item?.newSchema) return true;
10274
+ return Boolean(item.changeRecords?.length);
9633
10275
  };
9634
10276
  /**
9635
10277
  * 代码块 step 是否支持「回滚」:
9636
- * - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
10278
+ * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
9637
10279
  * - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
9638
10280
  */
9639
10281
  var isCodeBlockStepRevertable = (step) => {
9640
- if (step.oldContent === null || step.newContent === null) return true;
9641
- return Boolean(step.changeRecords?.length);
10282
+ const item = step.diff?.[0];
10283
+ if (!item?.oldSchema || !item?.newSchema) return true;
10284
+ return Boolean(item.changeRecords?.length);
9642
10285
  };
9643
10286
  //#endregion
9644
10287
  //#region packages/editor/src/layouts/history-list/GroupRow.vue?vue&type=script&setup=true&lang.ts
9645
10288
  var _hoisted_1$61 = ["title"];
9646
- var _hoisted_2$21 = ["title"];
9647
- var _hoisted_3$8 = { class: "m-editor-history-list-item-desc" };
9648
- var _hoisted_4$7 = ["title"];
9649
- var _hoisted_5$3 = ["title"];
9650
- var _hoisted_6$3 = {
9651
- key: 2,
10289
+ var _hoisted_2$23 = ["title"];
10290
+ var _hoisted_3$11 = { class: "m-editor-history-list-item-desc" };
10291
+ var _hoisted_4$8 = {
10292
+ key: 0,
10293
+ class: "m-editor-history-list-item-saved",
10294
+ title: "该记录为最近一次保存的状态"
10295
+ };
10296
+ var _hoisted_5$3 = {
10297
+ key: 1,
10298
+ class: "m-editor-history-list-item-actions"
10299
+ };
10300
+ var _hoisted_6$3 = ["title"];
10301
+ var _hoisted_7$1 = ["title"];
10302
+ var _hoisted_8 = {
10303
+ key: 4,
9652
10304
  class: "m-editor-history-list-item-merge"
9653
10305
  };
9654
- var _hoisted_7$1 = {
10306
+ var _hoisted_9 = {
9655
10307
  key: 0,
9656
10308
  class: "m-editor-history-list-substeps"
9657
10309
  };
9658
- var _hoisted_8 = ["title"];
9659
- var _hoisted_9 = { class: "m-editor-history-list-item-index" };
9660
- var _hoisted_10 = { class: "m-editor-history-list-substep-desc" };
9661
- var _hoisted_11 = ["title"];
9662
- var _hoisted_12 = ["title"];
9663
- var _hoisted_13 = ["onClick"];
9664
- var _hoisted_14 = ["onClick"];
10310
+ var _hoisted_10 = ["title", "onClick"];
10311
+ var _hoisted_11 = { class: "m-editor-history-list-item-index" };
10312
+ var _hoisted_12 = { class: "m-editor-history-list-substep-desc" };
10313
+ var _hoisted_13 = {
10314
+ key: 0,
10315
+ class: "m-editor-history-list-item-saved",
10316
+ title: "该记录为最近一次保存的状态"
10317
+ };
10318
+ var _hoisted_14 = {
10319
+ key: 1,
10320
+ class: "m-editor-history-list-item-actions"
10321
+ };
9665
10322
  var _hoisted_15 = ["onClick"];
10323
+ var _hoisted_16 = ["onClick"];
10324
+ var _hoisted_17 = ["onClick"];
10325
+ var _hoisted_18 = ["title"];
10326
+ var _hoisted_19 = ["title"];
9666
10327
  var GroupRow_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
9667
10328
  name: "MEditorHistoryListGroupRow",
9668
10329
  __name: "GroupRow",
9669
10330
  props: {
9670
- groupKey: {},
9671
- applied: { type: Boolean },
9672
- merged: { type: Boolean },
9673
- opType: {},
9674
- desc: {},
9675
- source: {},
9676
- time: {},
9677
- timeTitle: {},
9678
- stepCount: {},
9679
- subSteps: {},
10331
+ group: {},
9680
10332
  expanded: { type: Boolean },
9681
- isCurrent: {
9682
- type: Boolean,
9683
- default: false
9684
- },
9685
10333
  gotoEnabled: {
9686
10334
  type: Boolean,
9687
10335
  default: true
10336
+ },
10337
+ selectEnabled: {
10338
+ type: Boolean,
10339
+ default: false
9688
10340
  }
9689
10341
  },
9690
10342
  emits: [
9691
10343
  "toggle",
9692
10344
  "goto",
9693
10345
  "diff-step",
9694
- "revert-step"
10346
+ "revert-step",
10347
+ "select"
9695
10348
  ],
9696
10349
  setup(__props, { emit: __emit }) {
9697
10350
  const props = __props;
9698
10351
  const emit = __emit;
10352
+ /** 子步数大于 1 即为合并组:决定是否展示合并标记与可展开的子步列表。 */
10353
+ const merged = (0, vue.computed)(() => props.group.subSteps.length > 1);
10354
+ /** 组内 step 总数,仅在合并组时显示为 "合并 N 步"。 */
10355
+ const stepCount = (0, vue.computed)(() => props.group.subSteps.length);
9699
10356
  /**
9700
- * 仅合并组头部可点击(切换展开 / 收起);
9701
- * 单步组的跳转改由头部的「回退」按钮触发,整行不再可点击。
10357
+ * 头部可点击的场景:
10358
+ * - 合并组:点击切换展开 / 收起;
10359
+ * - 开启 `selectEnabled`(页面 tab):点击选中对应节点。
10360
+ * 单步组的跳转仍由头部的「回到」按钮触发。
9702
10361
  */
9703
- const isHeadClickable = (0, vue.computed)(() => props.merged);
10362
+ const isHeadClickable = (0, vue.computed)(() => merged.value || props.selectEnabled);
9704
10363
  const headTitle = (0, vue.computed)(() => {
9705
- if (props.merged) return props.expanded ? "点击收起子步" : "点击展开子步";
9706
- if (props.isCurrent) return "当前所在记录";
10364
+ if (merged.value) {
10365
+ const expandHint = props.expanded ? "点击收起子步" : "点击展开子步";
10366
+ return props.selectEnabled ? `${expandHint}(并选中该节点)` : expandHint;
10367
+ }
10368
+ if (props.selectEnabled) return "点击选中该节点";
10369
+ if (props.group.isCurrent) return "当前所在记录";
9707
10370
  return "";
9708
10371
  });
9709
10372
  /**
9710
- * 头部点击行为:仅合并组切换展开 / 收起;单步组不再响应整行点击。
10373
+ * 头部点击行为:
10374
+ * - 开启 selectEnabled 时,发出 select(携带组内首步 index,上层据此选中节点);
10375
+ * - 合并组同时切换展开 / 收起。
9711
10376
  */
9712
10377
  const onHeadClick = () => {
9713
- if (props.merged) emit("toggle", props.groupKey);
10378
+ if (props.selectEnabled && props.group.subSteps.length) emit("select", props.group.subSteps[0].index);
10379
+ if (merged.value) emit("toggle", props.group.key);
9714
10380
  };
9715
10381
  const onGotoClick = (index) => {
9716
10382
  if (!props.gotoEnabled) return;
9717
10383
  emit("goto", index);
9718
10384
  };
10385
+ /** 点击子步行:开启 selectEnabled 时选中该子步对应的节点。 */
10386
+ const onSubStepClick = (index) => {
10387
+ if (!props.selectEnabled) return;
10388
+ emit("select", index);
10389
+ };
9719
10390
  const subStepTitle = (s) => {
10391
+ if (props.selectEnabled) return "点击选中该节点";
9720
10392
  if (s.isCurrent) return "当前所在记录";
9721
10393
  return "";
9722
10394
  };
10395
+ /**
10396
+ * 头部是否展示「已保存」标记:
10397
+ * - 单步组:取该唯一子步的 saved;
10398
+ * - 合并组:组内任一子步为已保存即在头部提示(具体落在哪一步可展开查看)。
10399
+ */
10400
+ const headSaved = (0, vue.computed)(() => merged.value ? props.group.subSteps.some((s) => s.saved) : Boolean(props.group.subSteps[0]?.saved));
9723
10401
  /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
9724
- const headDiffable = (0, vue.computed)(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
10402
+ const headDiffable = (0, vue.computed)(() => !merged.value && Boolean(props.group.subSteps[0]?.diffable));
9725
10403
  /** 单步组头部是否展示"回滚"入口:要求该唯一子步本身可回滚(已应用)。 */
9726
- const headRevertable = (0, vue.computed)(() => !props.merged && Boolean(props.subSteps[0]?.revertable));
10404
+ const headRevertable = (0, vue.computed)(() => !merged.value && Boolean(props.group.subSteps[0]?.revertable));
10405
+ /** 单步组头部是否展示"回到"入口:可跳转、非当前、且存在唯一子步。 */
10406
+ const canHeadGoto = (0, vue.computed)(() => !merged.value && props.gotoEnabled && !props.group.isCurrent && props.group.subSteps.length > 0);
9727
10407
  /**
9728
10408
  * 合并组展开后的子步渲染顺序:与外层分组列表保持一致——倒序展示(最新的子步在最上方)。
9729
10409
  * 外层 page tab / bucket 都已对 groups 做了 reverse,子步沿用同样的视觉规则更直观。
9730
10410
  * 注意:仅用于渲染,原 `subSteps` 保持时间正序,`headIndexLabel` 等基于首尾索引的展示语义不变。
9731
10411
  */
9732
- const subStepsDisplay = (0, vue.computed)(() => props.subSteps.slice().reverse());
10412
+ const subStepsDisplay = (0, vue.computed)(() => props.group.subSteps.slice().reverse());
9733
10413
  /**
9734
10414
  * 头部索引展示:
9735
- * - 单步组(merged=false):显示该唯一 step 的编号,如 `#5`;
10415
+ * - 单步组(非合并):显示该唯一 step 的编号,如 `#5`;
9736
10416
  * - 合并组:显示组内 step 的编号范围,如 `#3-#7`(首尾相同则退化为 `#5`)。
9737
10417
  *
9738
10418
  * 这里展示的是 step.index + 1(与子步列表 `#{{ s.index + 1 }}` 保持一致),从 1 起编号更符合直觉。
9739
10419
  */
9740
10420
  const headIndexLabel = (0, vue.computed)(() => {
9741
- const list = props.subSteps;
10421
+ const list = props.group.subSteps;
9742
10422
  if (!list.length) return "";
9743
10423
  const first = list[0].index + 1;
9744
10424
  const last = list[list.length - 1].index + 1;
9745
- if (!props.merged || first === last) return `#${first}`;
10425
+ if (!merged.value || first === last) return `#${first}`;
9746
10426
  return `#${first}-#${last}`;
9747
10427
  });
9748
10428
  const headIndexTitle = (0, vue.computed)(() => {
9749
- if (!props.merged) return `历史步骤编号 #${props.subSteps[0]?.index + 1}`;
9750
- return `合并了第 ${props.subSteps[0]?.index + 1} 至第 ${props.subSteps[props.subSteps.length - 1]?.index + 1} 共 ${props.subSteps.length} 条历史步骤`;
10429
+ const list = props.group.subSteps;
10430
+ if (!merged.value) return `历史步骤编号 #${list[0]?.index + 1}`;
10431
+ return `合并了第 ${list[0]?.index + 1} 至第 ${list[list.length - 1]?.index + 1} 共 ${list.length} 条历史步骤`;
9751
10432
  });
9752
10433
  const onDiffClick = (index) => {
9753
10434
  emit("diff-step", index);
@@ -9757,9 +10438,9 @@
9757
10438
  };
9758
10439
  return (_ctx, _cache) => {
9759
10440
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("li", { class: (0, vue.normalizeClass)(["m-editor-history-list-item m-editor-history-list-group", {
9760
- "is-undone": !__props.applied,
9761
- "is-merged": __props.merged,
9762
- "is-current": __props.isCurrent
10441
+ "is-undone": !__props.group.applied,
10442
+ "is-merged": merged.value,
10443
+ "is-current": __props.group.isCurrent
9763
10444
  }]) }, [(0, vue.createElementVNode)("div", {
9764
10445
  class: (0, vue.normalizeClass)(["m-editor-history-list-group-head", { "is-clickable": isHeadClickable.value }]),
9765
10446
  title: headTitle.value,
@@ -9768,82 +10449,90 @@
9768
10449
  (0, vue.createElementVNode)("span", {
9769
10450
  class: "m-editor-history-list-item-index",
9770
10451
  title: headIndexTitle.value
9771
- }, (0, vue.toDisplayString)(headIndexLabel.value), 9, _hoisted_2$21),
9772
- (0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["m-editor-history-list-item-op", `op-${__props.opType}`]) }, (0, vue.toDisplayString)((0, vue.unref)(opLabel)(__props.opType)), 3),
9773
- (0, vue.createElementVNode)("span", _hoisted_3$8, (0, vue.toDisplayString)(__props.desc), 1),
9774
- !__props.merged && (0, vue.unref)(sourceLabel)(__props.source) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9775
- key: 0,
10452
+ }, (0, vue.toDisplayString)(headIndexLabel.value), 9, _hoisted_2$23),
10453
+ (0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["m-editor-history-list-item-op", `op-${__props.group.opType}`]) }, (0, vue.toDisplayString)((0, vue.unref)(opLabel)(__props.group.opType)), 3),
10454
+ (0, vue.createElementVNode)("span", _hoisted_3$11, (0, vue.toDisplayString)(__props.group.desc), 1),
10455
+ headSaved.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_4$8, "已保存")) : (0, vue.createCommentVNode)("v-if", true),
10456
+ !merged.value && (headRevertable.value || headDiffable.value || canHeadGoto.value) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_5$3, [
10457
+ headRevertable.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10458
+ key: 0,
10459
+ class: "m-editor-history-list-item-revert",
10460
+ title: "将该步骤的修改作为一次新操作反向应用(不影响后续历史)",
10461
+ onClick: _cache[0] || (_cache[0] = (0, vue.withModifiers)(($event) => onRevertClick(__props.group.subSteps[0].index), ["stop"]))
10462
+ }, "回滚")) : (0, vue.createCommentVNode)("v-if", true),
10463
+ canHeadGoto.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10464
+ key: 1,
10465
+ class: "m-editor-history-list-item-goto",
10466
+ title: "回到该记录",
10467
+ onClick: _cache[1] || (_cache[1] = (0, vue.withModifiers)(($event) => onGotoClick(__props.group.subSteps[0].index), ["stop"]))
10468
+ }, "回到")) : (0, vue.createCommentVNode)("v-if", true),
10469
+ headDiffable.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10470
+ key: 2,
10471
+ class: "m-editor-history-list-item-diff",
10472
+ title: "查看修改差异",
10473
+ onClick: _cache[2] || (_cache[2] = (0, vue.withModifiers)(($event) => onDiffClick(__props.group.subSteps[0].index), ["stop"]))
10474
+ }, "查看差异")) : (0, vue.createCommentVNode)("v-if", true)
10475
+ ])) : (0, vue.createCommentVNode)("v-if", true),
10476
+ !merged.value && (0, vue.unref)(sourceLabel)(__props.group.source) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10477
+ key: 2,
9776
10478
  class: "m-editor-history-list-item-source",
9777
- title: `操作途径:${(0, vue.unref)(sourceLabel)(__props.source)}`
9778
- }, (0, vue.toDisplayString)((0, vue.unref)(sourceLabel)(__props.source)), 9, _hoisted_4$7)) : (0, vue.createCommentVNode)("v-if", true),
9779
- !__props.merged && __props.time ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9780
- key: 1,
9781
- class: "m-editor-history-list-item-time",
9782
- title: __props.timeTitle || __props.time
9783
- }, (0, vue.toDisplayString)(__props.time), 9, _hoisted_5$3)) : (0, vue.createCommentVNode)("v-if", true),
9784
- __props.merged ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_6$3, "合并 " + (0, vue.toDisplayString)(__props.stepCount) + " 步", 1)) : (0, vue.createCommentVNode)("v-if", true),
9785
- !__props.merged && headRevertable.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10479
+ title: `操作途径:${(0, vue.unref)(sourceLabel)(__props.group.source)}`
10480
+ }, (0, vue.toDisplayString)((0, vue.unref)(sourceLabel)(__props.group.source)), 9, _hoisted_6$3)) : (0, vue.createCommentVNode)("v-if", true),
10481
+ !merged.value && __props.group.time ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9786
10482
  key: 3,
9787
- class: "m-editor-history-list-item-revert",
9788
- title: "将该步骤的修改作为一次新操作反向应用(不影响后续历史)",
9789
- onClick: _cache[0] || (_cache[0] = (0, vue.withModifiers)(($event) => onRevertClick(__props.subSteps[0].index), ["stop"]))
9790
- }, "回滚")) : (0, vue.createCommentVNode)("v-if", true),
9791
- !__props.merged && __props.gotoEnabled && !__props.isCurrent && __props.subSteps.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9792
- key: 4,
9793
- class: "m-editor-history-list-item-goto",
9794
- title: "回到该记录",
9795
- onClick: _cache[1] || (_cache[1] = (0, vue.withModifiers)(($event) => onGotoClick(__props.subSteps[0].index), ["stop"]))
9796
- }, "回到")) : (0, vue.createCommentVNode)("v-if", true),
9797
- !__props.merged && headDiffable.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10483
+ class: "m-editor-history-list-item-time",
10484
+ title: __props.group.timeTitle || __props.group.time
10485
+ }, (0, vue.toDisplayString)(__props.group.time), 9, _hoisted_7$1)) : (0, vue.createCommentVNode)("v-if", true),
10486
+ merged.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_8, "合并 " + (0, vue.toDisplayString)(stepCount.value) + " 步", 1)) : (0, vue.createCommentVNode)("v-if", true),
10487
+ merged.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9798
10488
  key: 5,
9799
- class: "m-editor-history-list-item-diff",
9800
- title: "查看修改差异",
9801
- onClick: _cache[2] || (_cache[2] = (0, vue.withModifiers)(($event) => onDiffClick(__props.subSteps[0].index), ["stop"]))
9802
- }, "查看差异")) : (0, vue.createCommentVNode)("v-if", true),
9803
- __props.merged ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9804
- key: 6,
9805
10489
  class: (0, vue.normalizeClass)(["m-editor-history-list-group-toggle", { "is-expanded": __props.expanded }])
9806
10490
  }, "▾", 2)) : (0, vue.createCommentVNode)("v-if", true)
9807
- ], 10, _hoisted_1$61), __props.merged && __props.expanded ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("ul", _hoisted_7$1, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(subStepsDisplay.value, (s) => {
10491
+ ], 10, _hoisted_1$61), merged.value && __props.expanded ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("ul", _hoisted_9, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(subStepsDisplay.value, (s) => {
9808
10492
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("li", {
9809
10493
  key: s.index,
9810
10494
  class: (0, vue.normalizeClass)({
9811
10495
  "is-undone": !s.applied,
9812
- "is-current": s.isCurrent
10496
+ "is-current": s.isCurrent,
10497
+ "is-clickable": __props.selectEnabled
9813
10498
  }),
9814
- title: subStepTitle(s)
10499
+ title: subStepTitle(s),
10500
+ onClick: ($event) => onSubStepClick(s.index)
9815
10501
  }, [
9816
- (0, vue.createElementVNode)("span", _hoisted_9, "#" + (0, vue.toDisplayString)(s.index + 1), 1),
9817
- (0, vue.createElementVNode)("span", _hoisted_10, (0, vue.toDisplayString)(s.desc), 1),
10502
+ (0, vue.createElementVNode)("span", _hoisted_11, "#" + (0, vue.toDisplayString)(s.index + 1), 1),
10503
+ (0, vue.createElementVNode)("span", _hoisted_12, (0, vue.toDisplayString)(s.desc), 1),
10504
+ s.saved ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_13, "已保存")) : (0, vue.createCommentVNode)("v-if", true),
10505
+ s.revertable || s.diffable || __props.gotoEnabled && !s.isCurrent ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_14, [
10506
+ s.revertable ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10507
+ key: 0,
10508
+ class: "m-editor-history-list-item-revert",
10509
+ title: "将该步骤的修改作为一次新操作反向应用(不影响后续历史)",
10510
+ onClick: (0, vue.withModifiers)(($event) => onRevertClick(s.index), ["stop"])
10511
+ }, "回滚", 8, _hoisted_15)) : (0, vue.createCommentVNode)("v-if", true),
10512
+ __props.gotoEnabled && !s.isCurrent ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10513
+ key: 1,
10514
+ class: "m-editor-history-list-item-goto",
10515
+ title: "回到该记录",
10516
+ onClick: (0, vue.withModifiers)(($event) => onGotoClick(s.index), ["stop"])
10517
+ }, "回到", 8, _hoisted_16)) : (0, vue.createCommentVNode)("v-if", true),
10518
+ s.diffable ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10519
+ key: 2,
10520
+ class: "m-editor-history-list-item-diff",
10521
+ title: "查看修改差异",
10522
+ onClick: (0, vue.withModifiers)(($event) => onDiffClick(s.index), ["stop"])
10523
+ }, "查看差异", 8, _hoisted_17)) : (0, vue.createCommentVNode)("v-if", true)
10524
+ ])) : (0, vue.createCommentVNode)("v-if", true),
9818
10525
  (0, vue.unref)(sourceLabel)(s.source) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9819
- key: 0,
10526
+ key: 2,
9820
10527
  class: "m-editor-history-list-item-source",
9821
10528
  title: `操作途径:${(0, vue.unref)(sourceLabel)(s.source)}`
9822
- }, (0, vue.toDisplayString)((0, vue.unref)(sourceLabel)(s.source)), 9, _hoisted_11)) : (0, vue.createCommentVNode)("v-if", true),
10529
+ }, (0, vue.toDisplayString)((0, vue.unref)(sourceLabel)(s.source)), 9, _hoisted_18)) : (0, vue.createCommentVNode)("v-if", true),
9823
10530
  s.time ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9824
- key: 1,
10531
+ key: 3,
9825
10532
  class: "m-editor-history-list-item-time",
9826
10533
  title: s.timeTitle || s.time
9827
- }, (0, vue.toDisplayString)(s.time), 9, _hoisted_12)) : (0, vue.createCommentVNode)("v-if", true),
9828
- s.revertable ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9829
- key: 2,
9830
- class: "m-editor-history-list-item-revert",
9831
- title: "将该步骤的修改作为一次新操作反向应用(不影响后续历史)",
9832
- onClick: (0, vue.withModifiers)(($event) => onRevertClick(s.index), ["stop"])
9833
- }, "回滚", 8, _hoisted_13)) : (0, vue.createCommentVNode)("v-if", true),
9834
- __props.gotoEnabled && !s.isCurrent ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9835
- key: 3,
9836
- class: "m-editor-history-list-item-goto",
9837
- title: "回到该记录",
9838
- onClick: (0, vue.withModifiers)(($event) => onGotoClick(s.index), ["stop"])
9839
- }, "回到", 8, _hoisted_14)) : (0, vue.createCommentVNode)("v-if", true),
9840
- s.diffable ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9841
- key: 4,
9842
- class: "m-editor-history-list-item-diff",
9843
- title: "查看修改差异",
9844
- onClick: (0, vue.withModifiers)(($event) => onDiffClick(s.index), ["stop"])
9845
- }, "查看差异", 8, _hoisted_15)) : (0, vue.createCommentVNode)("v-if", true)
9846
- ], 10, _hoisted_8);
10534
+ }, (0, vue.toDisplayString)(s.time), 9, _hoisted_19)) : (0, vue.createCommentVNode)("v-if", true)
10535
+ ], 10, _hoisted_10);
9847
10536
  }), 128))])) : (0, vue.createCommentVNode)("v-if", true)], 2);
9848
10537
  };
9849
10538
  }
@@ -9854,6 +10543,12 @@
9854
10543
  //#endregion
9855
10544
  //#region packages/editor/src/layouts/history-list/InitialRow.vue?vue&type=script&setup=true&lang.ts
9856
10545
  var _hoisted_1$60 = ["title"];
10546
+ var _hoisted_2$22 = { class: "m-editor-history-list-item-desc" };
10547
+ var _hoisted_3$10 = {
10548
+ key: 0,
10549
+ class: "m-editor-history-list-item-actions"
10550
+ };
10551
+ var _hoisted_4$7 = ["title"];
9857
10552
  var InitialRow_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
9858
10553
  name: "MEditorHistoryListInitialRow",
9859
10554
  __name: "InitialRow",
@@ -9862,7 +10557,8 @@
9862
10557
  gotoEnabled: {
9863
10558
  type: Boolean,
9864
10559
  default: true
9865
- }
10560
+ },
10561
+ marker: { default: void 0 }
9866
10562
  },
9867
10563
  emits: ["goto-initial"],
9868
10564
  setup(__props, { emit: __emit }) {
@@ -9870,10 +10566,19 @@
9870
10566
  * 「初始状态」记录行:渲染于历史列表底部,作为整个栈的"零点"。
9871
10567
  * - 点击该行会把对应栈撤销到 cursor === 0(即没有任何已应用步骤),等同于回到所有修改之前。
9872
10568
  * - 当对应栈本身已处于 cursor === 0 时(isCurrent=true),用户已在初始状态,点击不再触发动作。
10569
+ * - 当上层传入 `marker`(设置 root 时为该页生成的「未修改的初始状态」标记)时,
10570
+ * 用标记的文案与时间渲染本行;标记不进入撤销/重做栈,仅作为该页基线展示。
9873
10571
  *
9874
10572
  * 该行不是真实 step,仅作为 UI 入口;上层负责把"点击"翻译为 `service.goto*(0)`。
9875
10573
  */
9876
10574
  const props = __props;
10575
+ const desc = (0, vue.computed)(() => props.marker?.historyDescription || "未修改的初始状态");
10576
+ const time = (0, vue.computed)(() => formatHistoryTime(props.marker?.timestamp));
10577
+ const timeTitle = (0, vue.computed)(() => formatHistoryFullTime(props.marker?.timestamp));
10578
+ const rowTitle = (0, vue.computed)(() => {
10579
+ const base = props.marker?.historyDescription || "未修改的初始状态";
10580
+ return props.isCurrent ? `当前已回到${base}` : `点击回到${base}`;
10581
+ });
9877
10582
  const emit = __emit;
9878
10583
  const onClick = () => {
9879
10584
  if (props.isCurrent) return;
@@ -9885,20 +10590,24 @@
9885
10590
  "is-current": __props.isCurrent,
9886
10591
  "is-clickable": !__props.isCurrent
9887
10592
  }]),
9888
- title: __props.isCurrent ? "当前已回到未修改的初始状态" : "点击回到未修改的初始状态"
10593
+ title: rowTitle.value
9889
10594
  }, [
9890
10595
  _cache[0] || (_cache[0] = (0, vue.createElementVNode)("span", {
9891
10596
  class: "m-editor-history-list-item-index",
9892
10597
  title: "历史步骤编号 #0(未修改的初始状态)"
9893
10598
  }, "#0", -1)),
9894
10599
  _cache[1] || (_cache[1] = (0, vue.createElementVNode)("span", { class: "m-editor-history-list-item-op op-initial" }, "初始", -1)),
9895
- _cache[2] || (_cache[2] = (0, vue.createElementVNode)("span", { class: "m-editor-history-list-item-desc" }, "未修改的初始状态", -1)),
9896
- __props.gotoEnabled && !__props.isCurrent ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
9897
- key: 0,
10600
+ (0, vue.createElementVNode)("span", _hoisted_2$22, (0, vue.toDisplayString)(desc.value), 1),
10601
+ __props.gotoEnabled && !__props.isCurrent ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_3$10, [(0, vue.createElementVNode)("span", {
9898
10602
  class: "m-editor-history-list-item-goto",
9899
10603
  title: "回到该记录",
9900
10604
  onClick: (0, vue.withModifiers)(onClick, ["stop"])
9901
- }, "回到")) : (0, vue.createCommentVNode)("v-if", true)
10605
+ }, "回到")])) : (0, vue.createCommentVNode)("v-if", true),
10606
+ time.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
10607
+ key: 1,
10608
+ class: "m-editor-history-list-item-time",
10609
+ title: timeTitle.value
10610
+ }, (0, vue.toDisplayString)(time.value), 9, _hoisted_4$7)) : (0, vue.createCommentVNode)("v-if", true)
9902
10611
  ], 10, _hoisted_1$60);
9903
10612
  };
9904
10613
  }
@@ -9909,30 +10618,17 @@
9909
10618
  //#endregion
9910
10619
  //#region packages/editor/src/layouts/history-list/Bucket.vue?vue&type=script&setup=true&lang.ts
9911
10620
  var _hoisted_1$59 = { class: "m-editor-history-list-bucket" };
9912
- var _hoisted_2$20 = { class: "m-editor-history-list-bucket-title" };
9913
- var _hoisted_3$7 = { class: "m-editor-history-list-bucket-count" };
10621
+ var _hoisted_2$21 = { class: "m-editor-history-list-bucket-title" };
10622
+ var _hoisted_3$9 = { class: "m-editor-history-list-bucket-count" };
9914
10623
  var _hoisted_4$6 = { class: "m-editor-history-list-ul" };
9915
10624
  var Bucket_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
9916
10625
  name: "MEditorHistoryListBucket",
9917
10626
  __name: "Bucket",
9918
10627
  props: {
9919
- title: {},
10628
+ config: {},
9920
10629
  bucketId: {},
9921
- prefix: {},
9922
- showInitial: {
9923
- type: Boolean,
9924
- default: true
9925
- },
9926
10630
  groups: {},
9927
- describeGroup: {},
9928
- describeStep: {},
9929
- isStepDiffable: {},
9930
- isStepRevertable: {},
9931
- expanded: {},
9932
- gotoEnabled: {
9933
- type: Boolean,
9934
- default: true
9935
- }
10631
+ expanded: {}
9936
10632
  },
9937
10633
  emits: [
9938
10634
  "toggle",
@@ -9943,65 +10639,42 @@
9943
10639
  ],
9944
10640
  setup(__props) {
9945
10641
  const props = __props;
10642
+ /**
10643
+ * 子项 / 折叠状态 key:`${prefix}-${bucketId}-${组内首步 index}`。
10644
+ * 以稳定的 step 索引(而非展示位置)标识分组,历史数据更新后已展开的分组状态仍能正确保持。
10645
+ */
10646
+ const rowKey = (group) => `${props.config.prefix}-${props.bucketId}-${group.steps[0]?.index}`;
10647
+ /** 把原始分组派生为 GroupRow 直接消费的视图模型。 */
10648
+ const toRow = (group) => toRowGroup(group, rowKey(group), props.config);
9946
10649
  /** 该 bucket 是否处于初始状态(栈 cursor=0),等价于全部 group 都未 applied。 */
9947
10650
  const isInitial = (0, vue.computed)(() => props.groups.length > 0 && props.groups.every((g) => !g.applied));
9948
10651
  return (_ctx, _cache) => {
9949
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$59, [(0, vue.createElementVNode)("div", _hoisted_2$20, [
9950
- (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(__props.title), 1),
10652
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$59, [(0, vue.createElementVNode)("div", _hoisted_2$21, [
10653
+ (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(__props.config.title), 1),
9951
10654
  (0, vue.createElementVNode)("code", null, (0, vue.toDisplayString)(String(__props.bucketId)), 1),
9952
- (0, vue.createElementVNode)("span", _hoisted_3$7, (0, vue.toDisplayString)(__props.groups.length) + " 组", 1)
10655
+ (0, vue.createElementVNode)("span", _hoisted_3$9, (0, vue.toDisplayString)(__props.groups.length) + " 组", 1)
9953
10656
  ]), (0, vue.createElementVNode)("ul", _hoisted_4$6, [
9954
10657
  ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.groups, (group) => {
9955
10658
  return (0, vue.openBlock)(), (0, vue.createBlock)(GroupRow_default, {
9956
- key: `${__props.prefix}-${__props.bucketId}-${group.steps[0]?.index}`,
9957
- "group-key": `${__props.prefix}-${__props.bucketId}-${group.steps[0]?.index}`,
9958
- applied: group.applied,
9959
- merged: group.steps.length > 1,
9960
- "op-type": group.opType,
9961
- desc: __props.describeGroup(group),
9962
- source: (0, vue.unref)(groupSource)(group),
9963
- time: (0, vue.unref)(formatHistoryTime)((0, vue.unref)(groupTimestamp)(group)),
9964
- "time-title": (0, vue.unref)(formatHistoryFullTime)((0, vue.unref)(groupTimestamp)(group)),
9965
- "step-count": group.steps.length,
9966
- "sub-steps": group.steps.map((s) => ({
9967
- index: s.index,
9968
- applied: s.applied,
9969
- isCurrent: s.isCurrent,
9970
- desc: __props.describeStep(s.step),
9971
- diffable: __props.isStepDiffable ? __props.isStepDiffable(s.step) : false,
9972
- revertable: s.applied && (__props.isStepRevertable ? __props.isStepRevertable(s.step) : true),
9973
- source: s.step.source,
9974
- time: (0, vue.unref)(formatHistoryTime)(s.step.timestamp),
9975
- timeTitle: (0, vue.unref)(formatHistoryFullTime)(s.step.timestamp)
9976
- })),
9977
- "is-current": group.isCurrent,
9978
- expanded: !!__props.expanded[`${__props.prefix}-${__props.bucketId}-${group.steps[0]?.index}`],
9979
- "goto-enabled": __props.gotoEnabled,
10659
+ key: rowKey(group),
10660
+ group: toRow(group),
10661
+ expanded: (0, vue.unref)(isHistoryGroupExpanded)(__props.expanded, rowKey(group)),
10662
+ "goto-enabled": __props.config.gotoEnabled,
9980
10663
  onToggle: _cache[0] || (_cache[0] = (key) => _ctx.$emit("toggle", key)),
9981
10664
  onGoto: _cache[1] || (_cache[1] = (index) => _ctx.$emit("goto", __props.bucketId, index)),
9982
10665
  onDiffStep: _cache[2] || (_cache[2] = (index) => _ctx.$emit("diff-step", __props.bucketId, index)),
9983
10666
  onRevertStep: _cache[3] || (_cache[3] = (index) => _ctx.$emit("revert-step", __props.bucketId, index))
9984
10667
  }, null, 8, [
9985
- "group-key",
9986
- "applied",
9987
- "merged",
9988
- "op-type",
9989
- "desc",
9990
- "source",
9991
- "time",
9992
- "time-title",
9993
- "step-count",
9994
- "sub-steps",
9995
- "is-current",
10668
+ "group",
9996
10669
  "expanded",
9997
10670
  "goto-enabled"
9998
10671
  ]);
9999
10672
  }), 128)),
10000
- (0, vue.createCommentVNode)("\n 初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。\n 当 bucket 内所有 group 都未 applied 时即为当前位置。\n showInitial=false 时不展示(用于没有\"撤销到初始状态\"语义的自定义历史,如业务模块历史)。\n "),
10001
- __props.showInitial !== false ? ((0, vue.openBlock)(), (0, vue.createBlock)(InitialRow_default, {
10673
+ (0, vue.createCommentVNode)("\n 初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。\n 当 bucket 内所有 group 都未 applied 时即为当前位置。\n config.showInitial=false 时不展示(用于没有\"撤销到初始状态\"语义的自定义历史,如业务模块历史)。\n "),
10674
+ __props.config.showInitial !== false ? ((0, vue.openBlock)(), (0, vue.createBlock)(InitialRow_default, {
10002
10675
  key: 0,
10003
10676
  "is-current": isInitial.value,
10004
- "goto-enabled": __props.gotoEnabled,
10677
+ "goto-enabled": __props.config.gotoEnabled,
10005
10678
  onGotoInitial: _cache[4] || (_cache[4] = ($event) => _ctx.$emit("goto-initial", __props.bucketId))
10006
10679
  }, null, 8, ["is-current", "goto-enabled"])) : (0, vue.createCommentVNode)("v-if", true)
10007
10680
  ])]);
@@ -10017,69 +10690,52 @@
10017
10690
  key: 0,
10018
10691
  class: "m-editor-history-list-empty"
10019
10692
  };
10693
+ var _hoisted_2$20 = { class: "m-editor-history-list-toolbar" };
10694
+ var _hoisted_3$8 = ["title"];
10020
10695
  var BucketTab_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
10021
10696
  name: "MEditorHistoryListBucketTab",
10022
10697
  __name: "BucketTab",
10023
10698
  props: {
10024
- title: {},
10025
- prefix: {},
10699
+ config: {},
10026
10700
  buckets: {},
10027
- describeGroup: {},
10028
- describeStep: {},
10029
- isStepDiffable: {},
10030
- isStepRevertable: {},
10031
- expanded: {},
10032
- gotoEnabled: {
10033
- type: Boolean,
10034
- default: true
10035
- }
10701
+ expanded: {}
10036
10702
  },
10037
10703
  emits: [
10038
10704
  "toggle",
10039
10705
  "goto",
10040
10706
  "goto-initial",
10041
10707
  "diff-step",
10042
- "revert-step"
10708
+ "revert-step",
10709
+ "clear"
10043
10710
  ],
10044
10711
  setup(__props) {
10045
10712
  return (_ctx, _cache) => {
10046
- return !__props.buckets.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$58, "暂无操作记录")) : ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicScrollbar), {
10047
- key: 1,
10048
- "max-height": "360px"
10049
- }, {
10713
+ return !__props.buckets.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$58, "暂无操作记录")) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [(0, vue.createElementVNode)("div", _hoisted_2$20, [(0, vue.createElementVNode)("span", {
10714
+ class: "m-editor-history-list-clear",
10715
+ title: `清空${__props.config.title}的历史记录`,
10716
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("clear"))
10717
+ }, "清空", 8, _hoisted_3$8)]), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicScrollbar), { "max-height": "360px" }, {
10050
10718
  default: (0, vue.withCtx)(() => [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.buckets, (bucket) => {
10051
10719
  return (0, vue.openBlock)(), (0, vue.createBlock)(Bucket_default, {
10052
- key: `${__props.prefix}-${bucket.id}`,
10053
- title: __props.title,
10720
+ key: `${__props.config.prefix}-${bucket.id}`,
10721
+ config: __props.config,
10054
10722
  "bucket-id": bucket.id,
10055
- prefix: __props.prefix,
10056
10723
  groups: bucket.groups,
10057
- "describe-group": __props.describeGroup,
10058
- "describe-step": __props.describeStep,
10059
- "is-step-diffable": __props.isStepDiffable,
10060
- "is-step-revertable": __props.isStepRevertable,
10061
10724
  expanded: __props.expanded,
10062
- "goto-enabled": __props.gotoEnabled,
10063
- onToggle: _cache[0] || (_cache[0] = (key) => _ctx.$emit("toggle", key)),
10064
- onGoto: _cache[1] || (_cache[1] = (id, index) => _ctx.$emit("goto", id, index)),
10065
- onGotoInitial: _cache[2] || (_cache[2] = (id) => _ctx.$emit("goto-initial", id)),
10066
- onDiffStep: _cache[3] || (_cache[3] = (id, index) => _ctx.$emit("diff-step", id, index)),
10067
- onRevertStep: _cache[4] || (_cache[4] = (id, index) => _ctx.$emit("revert-step", id, index))
10725
+ onToggle: _cache[1] || (_cache[1] = (key) => _ctx.$emit("toggle", key)),
10726
+ onGoto: _cache[2] || (_cache[2] = (id, index) => _ctx.$emit("goto", id, index)),
10727
+ onGotoInitial: _cache[3] || (_cache[3] = (id) => _ctx.$emit("goto-initial", id)),
10728
+ onDiffStep: _cache[4] || (_cache[4] = (id, index) => _ctx.$emit("diff-step", id, index)),
10729
+ onRevertStep: _cache[5] || (_cache[5] = (id, index) => _ctx.$emit("revert-step", id, index))
10068
10730
  }, null, 8, [
10069
- "title",
10731
+ "config",
10070
10732
  "bucket-id",
10071
- "prefix",
10072
10733
  "groups",
10073
- "describe-group",
10074
- "describe-step",
10075
- "is-step-diffable",
10076
- "is-step-revertable",
10077
- "expanded",
10078
- "goto-enabled"
10734
+ "expanded"
10079
10735
  ]);
10080
10736
  }), 128))]),
10081
10737
  _: 1
10082
- }));
10738
+ })], 64));
10083
10739
  };
10084
10740
  }
10085
10741
  });
@@ -10184,7 +10840,7 @@
10184
10840
  switch (props.category) {
10185
10841
  case "node":
10186
10842
  if (!props.type) return [];
10187
- return removeStyleDisplayConfig(await propsService.getPropsConfig(props.type));
10843
+ return removeStyleDisplayConfig(await propsService.getPropsConfig(props.type, { node: props.value }));
10188
10844
  case "data-source": return dataSourceService.getFormConfig(props.type || "base");
10189
10845
  case "code-block": return getCodeBlockFormConfig({
10190
10846
  paramColConfig: codeBlockService.getParamsColConfig(),
@@ -10277,7 +10933,7 @@
10277
10933
  key: 0,
10278
10934
  class: "m-editor-history-diff-dialog-notice"
10279
10935
  };
10280
- var _hoisted_3$6 = { class: "m-editor-history-diff-dialog-header" };
10936
+ var _hoisted_3$7 = { class: "m-editor-history-diff-dialog-header" };
10281
10937
  var _hoisted_4$5 = { class: "m-editor-history-diff-dialog-target" };
10282
10938
  var _hoisted_5$2 = { class: "m-editor-history-diff-dialog-controls" };
10283
10939
  var _hoisted_6$2 = { class: "m-editor-history-diff-dialog-legend" };
@@ -10292,6 +10948,7 @@
10292
10948
  extendState: {},
10293
10949
  loadConfig: {},
10294
10950
  width: { default: "900px" },
10951
+ isConfirm: { type: Boolean },
10295
10952
  onConfirm: {},
10296
10953
  selfDiffFieldTypes: {}
10297
10954
  },
@@ -10346,9 +11003,12 @@
10346
11003
  if (mode.value !== "current" || !payload.value) return false;
10347
11004
  return isEqual(payload.value.value, payload.value.currentValue);
10348
11005
  });
11006
+ /** confirm() 的 resolve,仅在「等待用户确认回滚」期间存在 */
11007
+ let confirmResolve = null;
10349
11008
  const onConfirmClick = () => {
10350
- const cb = props.onConfirm;
10351
- cb?.();
11009
+ props.onConfirm?.();
11010
+ confirmResolve?.(true);
11011
+ confirmResolve = null;
10352
11012
  visible.value = false;
10353
11013
  };
10354
11014
  const targetText = (0, vue.computed)(() => {
@@ -10370,21 +11030,41 @@
10370
11030
  viewMode.value = "form";
10371
11031
  visible.value = true;
10372
11032
  };
11033
+ /**
11034
+ * 以 Promise 形式打开确认回滚弹窗:
11035
+ * - 用户点击「确定回滚」时 resolve(true);
11036
+ * - 取消 / 关闭 / 按 Esc 等其他方式关闭弹窗时 resolve(false)。
11037
+ *
11038
+ * 同一时刻只允许一个待确认流程,重复调用会先 resolve(false) 掉上一个。
11039
+ */
11040
+ const confirm = (p) => {
11041
+ confirmResolve?.(false);
11042
+ confirmResolve = null;
11043
+ return new Promise((resolve) => {
11044
+ confirmResolve = resolve;
11045
+ open(p);
11046
+ });
11047
+ };
10373
11048
  const close = () => {
10374
11049
  visible.value = false;
10375
11050
  };
10376
11051
  (0, vue.watch)(visible, (v) => {
10377
- if (!v) payload.value = null;
11052
+ if (!v) {
11053
+ payload.value = null;
11054
+ confirmResolve?.(false);
11055
+ confirmResolve = null;
11056
+ }
10378
11057
  });
10379
11058
  const onClose = () => {
10380
11059
  emit("close");
10381
11060
  };
10382
11061
  __expose({
10383
11062
  open,
11063
+ confirm,
10384
11064
  close
10385
11065
  });
10386
11066
  return (_ctx, _cache) => {
10387
- return (0, vue.openBlock)(), (0, vue.createBlock)(vue.Teleport, { to: "body" }, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicDialog), {
11067
+ return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicDialog), {
10388
11068
  modelValue: visible.value,
10389
11069
  "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => visible.value = $event),
10390
11070
  class: "m-editor-history-diff-dialog",
@@ -10395,7 +11075,7 @@
10395
11075
  width: __props.width,
10396
11076
  onClose
10397
11077
  }, {
10398
- footer: (0, vue.withCtx)(() => [__props.onConfirm ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
11078
+ footer: (0, vue.withCtx)(() => [__props.isConfirm ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
10399
11079
  size: "small",
10400
11080
  onClick: _cache[2] || (_cache[2] = ($event) => visible.value = false)
10401
11081
  }, {
@@ -10416,9 +11096,9 @@
10416
11096
  default: (0, vue.withCtx)(() => [..._cache[12] || (_cache[12] = [(0, vue.createTextVNode)("关闭", -1)])]),
10417
11097
  _: 1
10418
11098
  }))]),
10419
- default: (0, vue.withCtx)(() => [payload.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$57, [
11099
+ default: (0, vue.withCtx)(() => [payload.value && visible.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$57, [
10420
11100
  __props.onConfirm ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$19, "仅回滚有差异的字段")) : (0, vue.createCommentVNode)("v-if", true),
10421
- (0, vue.createElementVNode)("div", _hoisted_3$6, [(0, vue.createElementVNode)("span", _hoisted_4$5, (0, vue.toDisplayString)(targetText.value), 1), (0, vue.createElementVNode)("div", _hoisted_5$2, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicRadioGroup), {
11101
+ (0, vue.createElementVNode)("div", _hoisted_3$7, [(0, vue.createElementVNode)("span", _hoisted_4$5, (0, vue.toDisplayString)(targetText.value), 1), (0, vue.createElementVNode)("div", _hoisted_5$2, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicRadioGroup), {
10422
11102
  modelValue: viewMode.value,
10423
11103
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => viewMode.value = $event),
10424
11104
  size: "small",
@@ -10504,7 +11184,7 @@
10504
11184
  "modelValue",
10505
11185
  "title",
10506
11186
  "width"
10507
- ])]);
11187
+ ]);
10508
11188
  };
10509
11189
  }
10510
11190
  });
@@ -10517,99 +11197,86 @@
10517
11197
  key: 0,
10518
11198
  class: "m-editor-history-list-empty"
10519
11199
  };
10520
- var _hoisted_2$18 = { class: "m-editor-history-list-ul" };
11200
+ var _hoisted_2$18 = {
11201
+ key: 0,
11202
+ class: "m-editor-history-list-toolbar"
11203
+ };
11204
+ var _hoisted_3$6 = { class: "m-editor-history-list-ul" };
10521
11205
  var PageTab_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
10522
11206
  name: "MEditorHistoryListPageTab",
10523
11207
  __name: "PageTab",
10524
11208
  props: {
10525
11209
  list: {},
10526
- expanded: {}
11210
+ expanded: {},
11211
+ marker: {}
10527
11212
  },
10528
11213
  emits: [
10529
11214
  "toggle",
10530
11215
  "goto",
10531
11216
  "goto-initial",
10532
11217
  "diff-step",
10533
- "revert-step"
11218
+ "revert-step",
11219
+ "select",
11220
+ "clear"
10534
11221
  ],
10535
11222
  setup(__props) {
10536
11223
  const props = __props;
10537
11224
  /**
10538
11225
  * 当前 step 是否可查看差异:
10539
11226
  * - 仅 update 操作;
10540
- * - 单节点更新(updatedItems.length === 1),且 oldNode / newNode 都存在。
11227
+ * - 单节点更新(diff.length === 1),且 oldSchema / newSchema 都存在。
10541
11228
  * 多节点更新难以选定单一对比目标,统一不展示差异入口。
10542
11229
  */
10543
11230
  const isPageStepDiffable = (step) => {
10544
11231
  if (step.opType !== "update") return false;
10545
- const items = step.updatedItems ?? [];
11232
+ const items = step.diff ?? [];
10546
11233
  if (items.length !== 1) return false;
10547
- return Boolean(items[0]?.oldNode && items[0]?.newNode);
11234
+ return Boolean(items[0]?.oldSchema && items[0]?.newSchema);
10548
11235
  };
11236
+ /** 页面历史的描述 / 可操作性判定集合,注入给统一的 `toRowGroup`。 */
11237
+ const descriptor = {
11238
+ describeGroup: describePageGroup,
11239
+ describeStep: describePageStep,
11240
+ isStepDiffable: isPageStepDiffable,
11241
+ isStepRevertable: isPageStepRevertable
11242
+ };
11243
+ const rowKey = (group) => `pg-${group.steps[0]?.index}`;
11244
+ const toRow = (group) => toRowGroup(group, rowKey(group), descriptor);
10549
11245
  /**
10550
11246
  * 是否处于"初始状态"——即对应页面历史栈 cursor===0:
10551
- * 当 list 中所有 group 的 applied 都为 false 时即为该状态。
10552
- * 没有任何 group 的情况由外层"暂无操作记录"分支兜底,本计算可以不考虑。
11247
+ * 当 list 中所有 group 的 applied 都为 false 时即为该状态(空列表 `every` 返回 true,
11248
+ * 即仅有 marker、无任何操作记录时也视为处于初始状态)。
10553
11249
  */
10554
- const isInitial = (0, vue.computed)(() => props.list.length > 0 && props.list.every((g) => !g.applied));
11250
+ const isInitial = (0, vue.computed)(() => props.list.every((g) => !g.applied));
10555
11251
  return (_ctx, _cache) => {
10556
- return !__props.list.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$56, "暂无操作记录")) : ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(_tmagic_design.TMagicScrollbar), {
10557
- key: 1,
10558
- "max-height": "360px"
10559
- }, {
10560
- default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("ul", _hoisted_2$18, [
11252
+ return !__props.list.length && !__props.marker ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$56, "暂无操作记录")) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [__props.list.length ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$18, [(0, vue.createElementVNode)("span", {
11253
+ class: "m-editor-history-list-clear",
11254
+ title: "清空当前页面的历史记录",
11255
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("clear"))
11256
+ }, "清空")])) : (0, vue.createCommentVNode)("v-if", true), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicScrollbar), { "max-height": "360px" }, {
11257
+ default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("ul", _hoisted_3$6, [
10561
11258
  ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.list, (group) => {
10562
11259
  return (0, vue.openBlock)(), (0, vue.createBlock)(GroupRow_default, {
10563
- key: `pg-${group.steps[0]?.index}`,
10564
- "group-key": `pg-${group.steps[0]?.index}`,
10565
- applied: group.applied,
10566
- merged: group.steps.length > 1,
10567
- "op-type": group.opType,
10568
- desc: (0, vue.unref)(describePageGroup)(group),
10569
- source: (0, vue.unref)(groupSource)(group),
10570
- time: (0, vue.unref)(formatHistoryTime)((0, vue.unref)(groupTimestamp)(group)),
10571
- "time-title": (0, vue.unref)(formatHistoryFullTime)((0, vue.unref)(groupTimestamp)(group)),
10572
- "step-count": group.steps.length,
10573
- "sub-steps": group.steps.map((s) => ({
10574
- index: s.index,
10575
- applied: s.applied,
10576
- isCurrent: s.isCurrent,
10577
- desc: (0, vue.unref)(describePageStep)(s.step),
10578
- diffable: isPageStepDiffable(s.step),
10579
- revertable: s.applied && (0, vue.unref)(isPageStepRevertable)(s.step),
10580
- source: s.step.source,
10581
- time: (0, vue.unref)(formatHistoryTime)(s.step.timestamp),
10582
- timeTitle: (0, vue.unref)(formatHistoryFullTime)(s.step.timestamp)
10583
- })),
10584
- "is-current": group.isCurrent,
10585
- expanded: !!__props.expanded[`pg-${group.steps[0]?.index}`],
10586
- onToggle: _cache[0] || (_cache[0] = (key) => _ctx.$emit("toggle", key)),
10587
- onGoto: _cache[1] || (_cache[1] = (index) => _ctx.$emit("goto", index)),
10588
- onDiffStep: _cache[2] || (_cache[2] = (index) => _ctx.$emit("diff-step", index)),
10589
- onRevertStep: _cache[3] || (_cache[3] = (index) => _ctx.$emit("revert-step", index))
10590
- }, null, 8, [
10591
- "group-key",
10592
- "applied",
10593
- "merged",
10594
- "op-type",
10595
- "desc",
10596
- "source",
10597
- "time",
10598
- "time-title",
10599
- "step-count",
10600
- "sub-steps",
10601
- "is-current",
10602
- "expanded"
10603
- ]);
11260
+ key: rowKey(group),
11261
+ group: toRow(group),
11262
+ expanded: (0, vue.unref)(isHistoryGroupExpanded)(__props.expanded, rowKey(group)),
11263
+ "select-enabled": true,
11264
+ onToggle: _cache[1] || (_cache[1] = (key) => _ctx.$emit("toggle", key)),
11265
+ onGoto: _cache[2] || (_cache[2] = (index) => _ctx.$emit("goto", index)),
11266
+ onDiffStep: _cache[3] || (_cache[3] = (index) => _ctx.$emit("diff-step", index)),
11267
+ onRevertStep: _cache[4] || (_cache[4] = (index) => _ctx.$emit("revert-step", index)),
11268
+ onSelect: _cache[5] || (_cache[5] = (index) => _ctx.$emit("select", index))
11269
+ }, null, 8, ["group", "expanded"]);
10604
11270
  }), 128)),
10605
- (0, vue.createCommentVNode)("\n 初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),\n 作为\"未修改\"零点。当所有 group 都未 applied 时它即为当前位置。\n "),
11271
+ (0, vue.createCommentVNode)("\n 初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),\n 作为\"未修改\"零点。当所有 group 都未 applied 时它即为当前位置。\n 设置 root 时生成的「未修改的初始状态」标记(marker)会作为该行的文案与时间来源。\n "),
10606
11272
  (0, vue.createVNode)(InitialRow_default, {
10607
11273
  "is-current": isInitial.value,
10608
- onGotoInitial: _cache[4] || (_cache[4] = ($event) => _ctx.$emit("goto-initial"))
10609
- }, null, 8, ["is-current"])
11274
+ marker: __props.marker,
11275
+ onGotoInitial: _cache[6] || (_cache[6] = ($event) => _ctx.$emit("goto-initial"))
11276
+ }, null, 8, ["is-current", "marker"])
10610
11277
  ])]),
10611
11278
  _: 1
10612
- }));
11279
+ })], 64));
10613
11280
  };
10614
11281
  }
10615
11282
  });
@@ -10619,6 +11286,12 @@
10619
11286
  //#endregion
10620
11287
  //#region packages/editor/src/layouts/history-list/HistoryListPanel.vue?vue&type=script&setup=true&lang.ts
10621
11288
  var _hoisted_1$55 = { class: "m-editor-history-list" };
11289
+ /**
11290
+ * 构造差异弹窗入参:仅 update(前后值都存在)可对比。
11291
+ * - 页面(无 id):在全部分组中按 index 定位 step,目标 id 取自快照;
11292
+ * - 数据源 / 代码块(带 id):先匹配分组 id 再按 index 定位。
11293
+ * 无可对比内容(多节点 / add / remove)或定位不到时返回 null。
11294
+ */
10622
11295
  var HistoryListPanel_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
10623
11296
  name: "MEditorHistoryListPanel",
10624
11297
  __name: "HistoryListPanel",
@@ -10647,7 +11320,10 @@
10647
11320
  const ClockIcon = (0, vue.markRaw)(_element_plus_icons_vue.Clock);
10648
11321
  const CloseIcon = (0, vue.markRaw)(_element_plus_icons_vue.Close);
10649
11322
  const activeTab = (0, vue.ref)("page");
10650
- /** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
11323
+ /**
11324
+ * 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。
11325
+ * 点击面板以外区域的自动收起由 TMagicPopover 通过 v-model:visible 回写完成。
11326
+ */
10651
11327
  const visible = (0, vue.ref)(false);
10652
11328
  const tabPaneComponent = (0, _tmagic_design.getDesignConfig)("components")?.tabPane;
10653
11329
  /**
@@ -10657,7 +11333,7 @@
10657
11333
  const extraTabs = (0, vue.inject)("historyListExtraTabs", []);
10658
11334
  /** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
10659
11335
  const resolveTabLabel = (tab) => typeof tab.label === "function" ? tab.label() : tab.label;
10660
- const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
11336
+ const { editorService, dataSourceService, codeBlockService, historyService, propsService, stageOverlayService } = useServices();
10661
11337
  /**
10662
11338
  * 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
10663
11339
  * 禁用后对应的历史记录 tab 不再展示。若当前激活的 tab 恰好被禁用,则回退到「页面」tab。
@@ -10674,15 +11350,60 @@
10674
11350
  */
10675
11351
  const extendFormState = (0, vue.inject)("extendFormState", void 0);
10676
11352
  const { expanded, toggleGroup, pageGroups, dataSourceGroups, codeBlockGroups, pageGroupsDisplay, dataSourceGroupsByTarget, codeBlockGroupsByTarget } = useHistoryList();
11353
+ /**
11354
+ * 当前活动页的「加载/初始」标记记录(设置 root 时生成),透传给 PageTab 的底部初始行展示。
11355
+ * 基于 historyService 的 reactive state 派生,活动页切换或标记写入后自动刷新。
11356
+ */
11357
+ const pageMarker = (0, vue.computed)(() => historyService.getPageMarker());
10677
11358
  /** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
10678
- const isDataSourceStepDiffable = (step) => Boolean(step.oldSchema && step.newSchema);
11359
+ const isDataSourceStepDiffable = (step) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
10679
11360
  /** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
10680
- const isCodeBlockStepDiffable = (step) => Boolean(step.oldContent && step.newContent);
11361
+ const isCodeBlockStepDiffable = (step) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
11362
+ /**
11363
+ * 数据源 / 代码块两类 bucket 历史的整体渲染配置:把 title / prefix 与各自的描述、
11364
+ * 可差异、可回滚判定收敛为单一对象整体注入 BucketTab,组件内部按需读取。
11365
+ */
11366
+ const dataSourceConfig = {
11367
+ title: "数据源",
11368
+ prefix: "ds",
11369
+ describeGroup: describeDataSourceGroup,
11370
+ describeStep: describeDataSourceStep,
11371
+ isStepDiffable: isDataSourceStepDiffable,
11372
+ isStepRevertable: isDataSourceStepRevertable
11373
+ };
11374
+ const codeBlockConfig = {
11375
+ title: "代码块",
11376
+ prefix: "cb",
11377
+ describeGroup: describeCodeBlockGroup,
11378
+ describeStep: describeCodeBlockStep,
11379
+ isStepDiffable: isCodeBlockStepDiffable,
11380
+ isStepRevertable: isCodeBlockStepRevertable
11381
+ };
10681
11382
  /** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
10682
11383
  const indexToCursor = (index) => index + 1;
10683
11384
  const onPageGoto = (index) => {
10684
11385
  editorService.gotoPageStep(indexToCursor(index));
10685
11386
  };
11387
+ /**
11388
+ * 点击页面历史记录行:选中该记录对应的画布节点。
11389
+ * - 从目标 step 的 diff 中取节点 id(优先 newSchema,回退 oldSchema),按出现顺序找到第一个当前仍存在的节点;
11390
+ * - 与图层树点击选中一致:editorService.select + 画布 / overlay 画布 select 三者联动;
11391
+ * - 该 step 涉及的节点都已不存在(如删除记录、被撤销的新增)时给出提示,不做选中。
11392
+ */
11393
+ const onPageSelect = async (index) => {
11394
+ const step = historyService.getPageStepList()[index]?.step;
11395
+ if (!step) return;
11396
+ const targetId = (step.diff ?? []).map((item) => item.newSchema?.id ?? item.oldSchema?.id).find((id) => id !== void 0 && id !== null && editorService.getNodeById(id, false));
11397
+ if (targetId === void 0 || targetId === null) {
11398
+ _tmagic_design.tMagicMessage.warning("该记录对应的节点已不存在,无法选中");
11399
+ return;
11400
+ }
11401
+ const node = editorService.getNodeById(targetId, false);
11402
+ if (!node) return;
11403
+ await editorService.select(node);
11404
+ editorService.get("stage")?.select(targetId);
11405
+ stageOverlayService.get("stage")?.select(targetId);
11406
+ };
10686
11407
  const onDataSourceGoto = (id, index) => {
10687
11408
  dataSourceService.goto(id, indexToCursor(index));
10688
11409
  };
@@ -10703,71 +11424,53 @@
10703
11424
  codeBlockService.goto(id, 0);
10704
11425
  };
10705
11426
  const diffDialogRef = (0, vue.useTemplateRef)("diffDialog");
11427
+ const confirmDialogRef = (0, vue.useTemplateRef)("confirmDialog");
10706
11428
  /**
10707
- * 构造页面 step 的差异弹窗入参:仅 update 单节点修改可对比,传入旧/新节点。
10708
- * 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
10709
- * `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
10710
- * 无可对比内容(如多节点 / add / remove)时返回 null。
11429
+ * 三类历史(页面 / 数据源 / 代码块)差异弹窗入参的构造差异,收敛为一份配置:
11430
+ * 仅「分组来源、当前值读取、类型 / 展示名提取」不同,定位 step、校验前后值、组装 payload 的流程共用。
10711
11431
  */
10712
- const buildPageDiffPayload = (index) => {
10713
- const groups = historyService.getPageHistoryGroups();
10714
- for (const group of groups) {
10715
- const entry = group.steps.find((s) => s.index === index);
10716
- if (!entry) continue;
10717
- const item = entry.step.updatedItems?.[0];
10718
- if (!item?.oldNode || !item?.newNode) return null;
10719
- const type = item.newNode.type || item.oldNode.type || "";
10720
- const nodeId = item.newNode.id ?? item.oldNode.id;
10721
- const currentNode = nodeId !== void 0 ? editorService.getNodeById(nodeId) : null;
11432
+ const buildDiffPayload = (source, index, id) => {
11433
+ for (const group of source.groups()) {
11434
+ if (id !== void 0 && group.id !== id) continue;
11435
+ const step = group.steps.find((s) => s.index === index)?.step;
11436
+ if (!step) continue;
11437
+ const oldSchema = step.diff?.[0]?.oldSchema;
11438
+ const newSchema = step.diff?.[0]?.newSchema;
11439
+ if (!oldSchema || !newSchema) return null;
11440
+ const targetId = id ?? newSchema.id ?? oldSchema.id;
11441
+ const type = source.resolveType?.(newSchema, oldSchema);
10722
11442
  return {
10723
- category: "node",
10724
- type,
10725
- lastValue: item.oldNode,
10726
- value: item.newNode,
10727
- currentValue: currentNode || null,
10728
- targetLabel: item.newNode.name || item.oldNode.name || type,
10729
- id: nodeId
11443
+ category: source.category,
11444
+ ...type !== void 0 ? { type } : {},
11445
+ lastValue: oldSchema,
11446
+ value: newSchema,
11447
+ currentValue: (targetId !== void 0 ? source.getCurrent(targetId) : null) || null,
11448
+ targetLabel: source.resolveLabel(newSchema, oldSchema, targetId),
11449
+ id: targetId
10730
11450
  };
10731
11451
  }
10732
11452
  return null;
10733
11453
  };
10734
- /**
10735
- * 在指定分组列表中按 id / index 查找命中的 step,命中后交由 build 构造差异弹窗入参。
10736
- * 用于统一数据源、代码块两类历史的查找逻辑。
10737
- */
10738
- const findGroupStep = (groups, id, index, build) => {
10739
- for (const group of groups) {
10740
- if (group.id !== id) continue;
10741
- const entry = group.steps.find((s) => s.index === index);
10742
- if (!entry) continue;
10743
- return build(entry.step);
10744
- }
10745
- return null;
10746
- };
10747
- const buildDataSourceDiffPayload = (id, index) => findGroupStep(historyService.getDataSourceHistoryGroups(), id, index, ({ oldSchema, newSchema }) => {
10748
- if (!oldSchema || !newSchema) return null;
10749
- const currentSchema = dataSourceService.getDataSourceById(`${id}`);
10750
- return {
10751
- category: "data-source",
10752
- type: newSchema.type || oldSchema.type || "base",
10753
- lastValue: oldSchema,
10754
- value: newSchema,
10755
- currentValue: currentSchema || null,
10756
- targetLabel: newSchema.title || oldSchema.title || `${id}`,
10757
- id
10758
- };
10759
- });
10760
- const buildCodeBlockDiffPayload = (id, index) => findGroupStep(historyService.getCodeBlockHistoryGroups(), id, index, ({ oldContent, newContent }) => {
10761
- if (!oldContent || !newContent) return null;
10762
- return {
10763
- category: "code-block",
10764
- lastValue: oldContent,
10765
- value: newContent,
10766
- currentValue: codeBlockService.getCodeContentById(id) || null,
10767
- targetLabel: newContent.name || oldContent.name || `${id}`,
10768
- id
10769
- };
10770
- });
11454
+ const buildPageDiffPayload = (index) => buildDiffPayload({
11455
+ category: "node",
11456
+ groups: () => historyService.getPageHistoryGroups(),
11457
+ getCurrent: (id) => editorService.getNodeById(id),
11458
+ resolveType: (n, o) => n.type || o.type || "",
11459
+ resolveLabel: (n, o) => n.name || o.name || n.type || o.type || ""
11460
+ }, index);
11461
+ const buildDataSourceDiffPayload = (id, index) => buildDiffPayload({
11462
+ category: "data-source",
11463
+ groups: () => historyService.getDataSourceHistoryGroups(),
11464
+ getCurrent: (id) => dataSourceService.getDataSourceById(`${id}`),
11465
+ resolveType: (n, o) => n.type || o.type || "base",
11466
+ resolveLabel: (n, o, id) => n.title || o.title || `${id}`
11467
+ }, index, id);
11468
+ const buildCodeBlockDiffPayload = (id, index) => buildDiffPayload({
11469
+ category: "code-block",
11470
+ groups: () => historyService.getCodeBlockHistoryGroups(),
11471
+ getCurrent: (id) => codeBlockService.getCodeContentById(id),
11472
+ resolveLabel: (n, o, id) => n.name || o.name || `${id}`
11473
+ }, index, id);
10771
11474
  const onPageDiff = (index) => {
10772
11475
  const payload = buildPageDiffPayload(index);
10773
11476
  if (payload) diffDialogRef.value?.open(payload);
@@ -10780,173 +11483,259 @@
10780
11483
  const payload = buildCodeBlockDiffPayload(id, index);
10781
11484
  if (payload) diffDialogRef.value?.open(payload);
10782
11485
  };
10783
- const onConfirmRevert = (0, vue.shallowRef)();
10784
11486
  /**
10785
- * 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
11487
+ * 「回滚」统一入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
10786
11488
  * 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
10787
11489
  *
10788
- * 交互:先弹出该步骤的差异弹窗供用户确认,点击「确定回滚」后再真正执行回滚;
10789
- * 对没有可对比内容的步骤(如 add / remove / 多节点更新)则直接回滚。
11490
+ * 交互:
11491
+ * - 可差异对比的步骤(单节点 / 单实体 update):弹出差异弹窗供用户确认,点「确定回滚」再执行;
11492
+ * - 无法对比的步骤(add / remove / 多节点更新,payload 为 null):弹出普通二次确认框,确认后执行。
11493
+ *
11494
+ * 页面 / 数据源 / 代码块三类回滚仅「差异入参构造」与「实际 revert 调用」不同,
11495
+ * 由调用方分别传入 payload 与 revert,公共的弹窗 / 确认流程在此收敛。
11496
+ */
11497
+ const runRevert = (payload) => {
11498
+ if (payload && confirmDialogRef.value) return confirmDialogRef.value.confirm(payload);
11499
+ return confirmRevert();
11500
+ };
11501
+ /**
11502
+ * 回滚前置校验:若该历史步骤回滚所依赖的目标数据已被删除,则无法回滚。
11503
+ * - update(把旧值写回):被修改的目标必须仍存在;
11504
+ * - 页面 remove(还原被删节点):被删节点的原父容器必须仍存在,否则无处插回;
11505
+ * add(回滚即删除)即使目标已不在,也已达成「删除」目的,不视为失败。
11506
+ *
11507
+ * 命中时弹出「回滚失败」提示并返回 true,调用方据此中止本次回滚。
10790
11508
  */
11509
+ const isPageRevertTargetMissing = (index) => {
11510
+ const step = historyService.getPageStepList()[index]?.step;
11511
+ if (!step) return false;
11512
+ if (step.opType === "update") return (step.diff ?? []).some((item) => {
11513
+ const id = item.newSchema?.id ?? item.oldSchema?.id;
11514
+ return id !== void 0 && !editorService.getNodeById(id, false);
11515
+ });
11516
+ if (step.opType === "remove") return (step.diff ?? []).some((item) => item.parentId !== void 0 && !editorService.getNodeById(item.parentId, false));
11517
+ return false;
11518
+ };
11519
+ /** 数据源 update 步骤回滚时,对应数据源必须仍存在(已删除则无处写回旧值)。 */
11520
+ const isDataSourceRevertTargetMissing = (id, index) => {
11521
+ const step = historyService.getDataSourceStepList(id)[index]?.step;
11522
+ return Boolean(step && step.opType === "update" && !dataSourceService.getDataSourceById(`${id}`));
11523
+ };
11524
+ /** 代码块 update 步骤回滚时,对应代码块必须仍存在(已删除则无处写回旧值)。 */
11525
+ const isCodeBlockRevertTargetMissing = (id, index) => {
11526
+ const step = historyService.getCodeBlockStepList(id)[index]?.step;
11527
+ return Boolean(step && step.opType === "update" && !codeBlockService.getCodeContentById(id));
11528
+ };
11529
+ /** 目标数据已被删除、无法回滚时的统一提示。 */
11530
+ const showRevertTargetMissing = () => {
11531
+ _tmagic_design.tMagicMessage.error("回滚失败:该记录对应的数据已被删除");
11532
+ };
10791
11533
  const onPageRevert = (index) => {
10792
- const payload = buildPageDiffPayload(index);
10793
- onConfirmRevert.value = () => editorService.revertPageStep(index);
10794
- if (payload) diffDialogRef.value?.open({ ...payload });
10795
- else onConfirmRevert.value();
11534
+ if (isPageRevertTargetMissing(index)) {
11535
+ showRevertTargetMissing();
11536
+ return Promise.resolve(null);
11537
+ }
11538
+ return runRevert(buildPageDiffPayload(index)).then((result) => result ? editorService.revertPageStep(index) : null);
10796
11539
  };
10797
11540
  const onDataSourceRevert = (id, index) => {
10798
- const payload = buildDataSourceDiffPayload(id, index);
10799
- onConfirmRevert.value = () => dataSourceService.revert(id, index);
10800
- if (payload) diffDialogRef.value?.open({ ...payload });
10801
- else onConfirmRevert.value();
11541
+ if (isDataSourceRevertTargetMissing(id, index)) {
11542
+ showRevertTargetMissing();
11543
+ return Promise.resolve(null);
11544
+ }
11545
+ return runRevert(buildDataSourceDiffPayload(id, index)).then((result) => result ? dataSourceService.revert(id, index) : null);
10802
11546
  };
10803
11547
  const onCodeBlockRevert = (id, index) => {
10804
- const payload = buildCodeBlockDiffPayload(id, index);
10805
- onConfirmRevert.value = () => codeBlockService.revert(id, index);
10806
- if (payload) diffDialogRef.value?.open({ ...payload });
10807
- else onConfirmRevert.value();
11548
+ if (isCodeBlockRevertTargetMissing(id, index)) {
11549
+ showRevertTargetMissing();
11550
+ return Promise.resolve(null);
11551
+ }
11552
+ return runRevert(buildCodeBlockDiffPayload(id, index)).then((result) => result ? codeBlockService.revert(id, index) : null);
11553
+ };
11554
+ /**
11555
+ * 「回滚」二次确认:新增 / 删除 / 多节点更新等无法做差异对比的步骤,
11556
+ * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
11557
+ * 用户取消时返回 false,调用方据此中止回滚。
11558
+ */
11559
+ const confirmRevert = () => confirmDialog("确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。");
11560
+ /**
11561
+ * 通用二次确认弹窗:清空历史 / 无法差异对比的回滚等会改变状态的操作,先弹出确认框,
11562
+ * 用户点击「确定」返回 true,取消(confirm reject)时返回 false 并静默忽略。
11563
+ */
11564
+ const confirmDialog = async (message) => {
11565
+ try {
11566
+ await _tmagic_design.tMagicMessageBox.confirm(message, "提示", {
11567
+ confirmButtonText: "确定",
11568
+ cancelButtonText: "取消",
11569
+ type: "warning"
11570
+ });
11571
+ return true;
11572
+ } catch (e) {
11573
+ return false;
11574
+ }
11575
+ };
11576
+ /**
11577
+ * 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
11578
+ * 使本地持久化的那份与内存保持一致——即「连同本地保存的一并删除」。
11579
+ * 不支持 IndexedDB 或写入失败时静默忽略(内存清空已生效)。
11580
+ */
11581
+ const syncIndexedDB = async () => {
11582
+ try {
11583
+ await historyService.saveToIndexedDB();
11584
+ } catch (e) {}
11585
+ };
11586
+ const onPageClear = async () => {
11587
+ if (await confirmDialog("确定清空当前页面的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。")) {
11588
+ historyService.clearPage();
11589
+ await syncIndexedDB();
11590
+ }
10808
11591
  };
10809
- const onDiffDialogClose = () => {
10810
- onConfirmRevert.value = void 0;
11592
+ const onDataSourceClear = async () => {
11593
+ if (await confirmDialog("确定清空数据源的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。")) {
11594
+ historyService.clearDataSource();
11595
+ await syncIndexedDB();
11596
+ }
11597
+ };
11598
+ const onCodeBlockClear = async () => {
11599
+ if (await confirmDialog("确定清空代码块的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。")) {
11600
+ historyService.clearCodeBlock();
11601
+ await syncIndexedDB();
11602
+ }
10811
11603
  };
10812
11604
  return (_ctx, _cache) => {
10813
- return (0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, null, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicPopover), {
10814
- "popper-class": "m-editor-history-list-popover",
10815
- placement: "bottom",
10816
- trigger: "click",
10817
- visible: visible.value,
10818
- width: 660
10819
- }, {
10820
- reference: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTooltip), {
10821
- effect: "dark",
11605
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, null, [
11606
+ (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicPopover), {
11607
+ "popper-class": "m-editor-history-list-popover",
10822
11608
  placement: "bottom",
10823
- content: "历史记录"
11609
+ trigger: "click",
11610
+ visible: visible.value,
11611
+ "onUpdate:visible": _cache[3] || (_cache[3] = ($event) => visible.value = $event),
11612
+ width: 660
10824
11613
  }, {
10825
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
10826
- size: "small",
10827
- link: "",
10828
- onClick: _cache[2] || (_cache[2] = ($event) => visible.value = !visible.value)
11614
+ reference: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTooltip), {
11615
+ effect: "dark",
11616
+ placement: "bottom",
11617
+ content: "历史记录"
10829
11618
  }, {
10830
- icon: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(ClockIcon) }, null, 8, ["icon"])]),
11619
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
11620
+ size: "small",
11621
+ link: "",
11622
+ onClick: _cache[2] || (_cache[2] = ($event) => visible.value = !visible.value)
11623
+ }, {
11624
+ icon: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(ClockIcon) }, null, 8, ["icon"])]),
11625
+ _: 1
11626
+ })]),
10831
11627
  _: 1
10832
11628
  })]),
10833
- _: 1
10834
- })]),
10835
- default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_1$55, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTooltip), {
10836
- effect: "dark",
10837
- placement: "top",
10838
- content: "关闭"
10839
- }, {
10840
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
10841
- class: "m-editor-history-list-close",
10842
- size: "small",
10843
- link: "",
10844
- onClick: _cache[0] || (_cache[0] = ($event) => visible.value = false)
11629
+ default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_1$55, [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTooltip), {
11630
+ effect: "dark",
11631
+ placement: "top",
11632
+ content: "关闭"
10845
11633
  }, {
10846
- icon: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(CloseIcon) }, null, 8, ["icon"])]),
10847
- _: 1
10848
- })]),
10849
- _: 1
10850
- }), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTabs), {
10851
- modelValue: activeTab.value,
10852
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => activeTab.value = $event),
10853
- class: "m-editor-history-list-tabs"
10854
- }, {
10855
- default: (0, vue.withCtx)(() => [
10856
- ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.guardReactiveProps)((0, vue.unref)(tabPaneComponent)?.props({
10857
- name: "page",
10858
- label: `页面 (${(0, vue.unref)(pageGroups).length})`
10859
- }) || {})), {
10860
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)(PageTab_default, {
10861
- list: (0, vue.unref)(pageGroupsDisplay),
10862
- expanded: (0, vue.unref)(expanded),
10863
- onToggle: (0, vue.unref)(toggleGroup),
10864
- onGoto: onPageGoto,
10865
- onGotoInitial: onPageGotoInitial,
10866
- onDiffStep: onPageDiff,
10867
- onRevertStep: onPageRevert
10868
- }, null, 8, [
10869
- "list",
10870
- "expanded",
10871
- "onToggle"
10872
- ])]),
10873
- _: 1
10874
- }, 16)),
10875
- !disabledDataSource.value ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.mergeProps)({ key: 0 }, (0, vue.unref)(tabPaneComponent)?.props({
10876
- name: "data-source",
10877
- label: `数据源 (${(0, vue.unref)(dataSourceGroups).length})`
10878
- }) || {})), {
10879
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)(BucketTab_default, {
10880
- title: "数据源",
10881
- prefix: "ds",
10882
- buckets: (0, vue.unref)(dataSourceGroupsByTarget),
10883
- expanded: (0, vue.unref)(expanded),
10884
- "describe-group": (0, vue.unref)(describeDataSourceGroup),
10885
- "describe-step": (0, vue.unref)(describeDataSourceStep),
10886
- "is-step-diffable": isDataSourceStepDiffable,
10887
- "is-step-revertable": (0, vue.unref)(isDataSourceStepRevertable),
10888
- onToggle: (0, vue.unref)(toggleGroup),
10889
- onGoto: onDataSourceGoto,
10890
- onGotoInitial: onDataSourceGotoInitial,
10891
- onDiffStep: onDataSourceDiff,
10892
- onRevertStep: onDataSourceRevert
10893
- }, null, 8, [
10894
- "buckets",
10895
- "expanded",
10896
- "describe-group",
10897
- "describe-step",
10898
- "is-step-revertable",
10899
- "onToggle"
10900
- ])]),
10901
- _: 1
10902
- }, 16)) : (0, vue.createCommentVNode)("v-if", true),
10903
- !disabledCodeBlock.value ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.mergeProps)({ key: 1 }, (0, vue.unref)(tabPaneComponent)?.props({
10904
- name: "code-block",
10905
- label: `代码块 (${(0, vue.unref)(codeBlockGroups).length})`
10906
- }) || {})), {
10907
- default: (0, vue.withCtx)(() => [(0, vue.createVNode)(BucketTab_default, {
10908
- title: "代码块",
10909
- prefix: "cb",
10910
- buckets: (0, vue.unref)(codeBlockGroupsByTarget),
10911
- expanded: (0, vue.unref)(expanded),
10912
- "describe-group": (0, vue.unref)(describeCodeBlockGroup),
10913
- "describe-step": (0, vue.unref)(describeCodeBlockStep),
10914
- "is-step-diffable": isCodeBlockStepDiffable,
10915
- "is-step-revertable": (0, vue.unref)(isCodeBlockStepRevertable),
10916
- onToggle: (0, vue.unref)(toggleGroup),
10917
- onGoto: onCodeBlockGoto,
10918
- onGotoInitial: onCodeBlockGotoInitial,
10919
- onDiffStep: onCodeBlockDiff,
10920
- onRevertStep: onCodeBlockRevert
10921
- }, null, 8, [
10922
- "buckets",
10923
- "expanded",
10924
- "describe-group",
10925
- "describe-step",
10926
- "is-step-revertable",
10927
- "onToggle"
10928
- ])]),
11634
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicButton), {
11635
+ class: "m-editor-history-list-close",
11636
+ size: "small",
11637
+ link: "",
11638
+ onClick: _cache[0] || (_cache[0] = ($event) => visible.value = false)
11639
+ }, {
11640
+ icon: (0, vue.withCtx)(() => [(0, vue.createVNode)(Icon_default, { icon: (0, vue.unref)(CloseIcon) }, null, 8, ["icon"])]),
10929
11641
  _: 1
10930
- }, 16)) : (0, vue.createCommentVNode)("v-if", true),
10931
- ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)((0, vue.unref)(extraTabs), (tab) => {
10932
- return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.mergeProps)({ key: tab.name }, { ref_for: true }, (0, vue.unref)(tabPaneComponent)?.props({
10933
- name: tab.name,
10934
- label: resolveTabLabel(tab)
10935
- }) || {}), {
10936
- default: (0, vue.withCtx)(() => [((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)(tab.component), (0, vue.mergeProps)({ ref_for: true }, tab.props || {}, (0, vue.toHandlers)(tab.listeners || {})), null, 16))]),
10937
- _: 2
10938
- }, 1040);
10939
- }), 128))
10940
- ]),
11642
+ })]),
11643
+ _: 1
11644
+ }), (0, vue.createVNode)((0, vue.unref)(_tmagic_design.TMagicTabs), {
11645
+ modelValue: activeTab.value,
11646
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => activeTab.value = $event),
11647
+ class: "m-editor-history-list-tabs"
11648
+ }, {
11649
+ default: (0, vue.withCtx)(() => [
11650
+ ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.guardReactiveProps)((0, vue.unref)(tabPaneComponent)?.props({
11651
+ name: "page",
11652
+ label: `页面 (${(0, vue.unref)(pageGroups).length})`
11653
+ }) || {})), {
11654
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)(PageTab_default, {
11655
+ list: (0, vue.unref)(pageGroupsDisplay),
11656
+ expanded: (0, vue.unref)(expanded),
11657
+ marker: pageMarker.value,
11658
+ onToggle: (0, vue.unref)(toggleGroup),
11659
+ onGoto: onPageGoto,
11660
+ onGotoInitial: onPageGotoInitial,
11661
+ onDiffStep: onPageDiff,
11662
+ onRevertStep: onPageRevert,
11663
+ onSelect: onPageSelect,
11664
+ onClear: onPageClear
11665
+ }, null, 8, [
11666
+ "list",
11667
+ "expanded",
11668
+ "marker",
11669
+ "onToggle"
11670
+ ])]),
11671
+ _: 1
11672
+ }, 16)),
11673
+ !disabledDataSource.value ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.mergeProps)({ key: 0 }, (0, vue.unref)(tabPaneComponent)?.props({
11674
+ name: "data-source",
11675
+ label: `数据源 (${(0, vue.unref)(dataSourceGroups).length})`
11676
+ }) || {})), {
11677
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)(BucketTab_default, {
11678
+ config: dataSourceConfig,
11679
+ buckets: (0, vue.unref)(dataSourceGroupsByTarget),
11680
+ expanded: (0, vue.unref)(expanded),
11681
+ onToggle: (0, vue.unref)(toggleGroup),
11682
+ onGoto: onDataSourceGoto,
11683
+ onGotoInitial: onDataSourceGotoInitial,
11684
+ onDiffStep: onDataSourceDiff,
11685
+ onRevertStep: onDataSourceRevert,
11686
+ onClear: onDataSourceClear
11687
+ }, null, 8, [
11688
+ "buckets",
11689
+ "expanded",
11690
+ "onToggle"
11691
+ ])]),
11692
+ _: 1
11693
+ }, 16)) : (0, vue.createCommentVNode)("v-if", true),
11694
+ !disabledCodeBlock.value ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.normalizeProps)((0, vue.mergeProps)({ key: 1 }, (0, vue.unref)(tabPaneComponent)?.props({
11695
+ name: "code-block",
11696
+ label: `代码块 (${(0, vue.unref)(codeBlockGroups).length})`
11697
+ }) || {})), {
11698
+ default: (0, vue.withCtx)(() => [(0, vue.createVNode)(BucketTab_default, {
11699
+ config: codeBlockConfig,
11700
+ buckets: (0, vue.unref)(codeBlockGroupsByTarget),
11701
+ expanded: (0, vue.unref)(expanded),
11702
+ onToggle: (0, vue.unref)(toggleGroup),
11703
+ onGoto: onCodeBlockGoto,
11704
+ onGotoInitial: onCodeBlockGotoInitial,
11705
+ onDiffStep: onCodeBlockDiff,
11706
+ onRevertStep: onCodeBlockRevert,
11707
+ onClear: onCodeBlockClear
11708
+ }, null, 8, [
11709
+ "buckets",
11710
+ "expanded",
11711
+ "onToggle"
11712
+ ])]),
11713
+ _: 1
11714
+ }, 16)) : (0, vue.createCommentVNode)("v-if", true),
11715
+ ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)((0, vue.unref)(extraTabs), (tab) => {
11716
+ return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(tabPaneComponent)?.component || "el-tab-pane"), (0, vue.mergeProps)({ key: tab.name }, { ref_for: true }, (0, vue.unref)(tabPaneComponent)?.props({
11717
+ name: tab.name,
11718
+ label: resolveTabLabel(tab)
11719
+ }) || {}), {
11720
+ default: (0, vue.withCtx)(() => [((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)(tab.component), (0, vue.mergeProps)({ ref_for: true }, tab.props || {}, (0, vue.toHandlers)(tab.listeners || {})), null, 16))]),
11721
+ _: 2
11722
+ }, 1040);
11723
+ }), 128))
11724
+ ]),
11725
+ _: 1
11726
+ }, 8, ["modelValue"])])]),
10941
11727
  _: 1
10942
- }, 8, ["modelValue"])])]),
10943
- _: 1
10944
- }, 8, ["visible"]), (0, vue.createVNode)(HistoryDiffDialog_default, {
10945
- ref: "diffDialog",
10946
- "extend-state": (0, vue.unref)(extendFormState),
10947
- "on-confirm": onConfirmRevert.value,
10948
- onClose: onDiffDialogClose
10949
- }, null, 8, ["extend-state", "on-confirm"])], 64);
11728
+ }, 8, ["visible"]),
11729
+ (0, vue.createVNode)(HistoryDiffDialog_default, {
11730
+ ref: "diffDialog",
11731
+ "extend-state": (0, vue.unref)(extendFormState)
11732
+ }, null, 8, ["extend-state"]),
11733
+ (0, vue.createVNode)(HistoryDiffDialog_default, {
11734
+ ref: "confirmDialog",
11735
+ "is-confirm": true,
11736
+ "extend-state": (0, vue.unref)(extendFormState)
11737
+ }, null, 8, ["extend-state"])
11738
+ ], 64);
10950
11739
  };
10951
11740
  }
10952
11741
  });
@@ -14242,7 +15031,11 @@
14242
15031
  });
14243
15032
  }
14244
15033
  }, { immediate: true });
14245
- const activeTabName = (0, vue.ref)(props.data?.status);
15034
+ const activeTabName = (0, vue.computed)({
15035
+ get: () => uiService.get("sideBarActiveTabName"),
15036
+ set: (value) => uiService.set("sideBarActiveTabName", value)
15037
+ });
15038
+ uiService.set("sideBarActiveTabName", props.data?.status || "");
14246
15039
  const getItemConfig = (data) => {
14247
15040
  const map = {
14248
15041
  [SideItemKey.COMPONENT_LIST]: {
@@ -15436,17 +16229,6 @@
15436
16229
  ],
15437
16230
  sync: ["setCodeDslByIdSync"]
15438
16231
  };
15439
- /**
15440
- * 「回滚」生成的新 step 简短描述。仅 service 层使用。
15441
- */
15442
- var describeRevertCodeBlockStep = (step) => {
15443
- const { oldContent, newContent, changeRecords, id } = step;
15444
- if (oldContent === null && newContent) return `撤回新增 ${newContent.name || newContent.id || id}`;
15445
- if (oldContent && newContent === null) return `还原已删除的 ${oldContent.name || oldContent.id || id}`;
15446
- const name = newContent?.name || oldContent?.name || `${id}`;
15447
- const propPath = changeRecords?.[0]?.propPath;
15448
- return propPath ? `还原 ${name} · ${propPath}` : `还原 ${name}`;
15449
- };
15450
16232
  var CodeBlock = class extends BaseService {
15451
16233
  state = (0, vue.reactive)({
15452
16234
  codeDsl: null,
@@ -15455,6 +16237,16 @@
15455
16237
  undeletableList: [],
15456
16238
  paramsColConfig: void 0
15457
16239
  });
16240
+ /**
16241
+ * 最近一次写入历史栈的代码块历史记录 uuid(单条写入场景:新增 / 更新)。
16242
+ * 供 setCodeDslById(Sync)AndGetHistoryId 取回,普通方法不读取它。
16243
+ */
16244
+ lastPushedHistoryId = null;
16245
+ /**
16246
+ * deleteCodeDslByIds 一次删除多个代码块时,按写入顺序收集的历史记录 uuid 列表。
16247
+ * 在 deleteCodeDslByIds 入口处重置,供 deleteCodeDslByIdsAndGetHistoryId 取回。
16248
+ */
16249
+ lastDeletedHistoryIds = [];
15458
16250
  constructor() {
15459
16251
  super([...canUsePluginMethods$4.async.map((methodName) => ({
15460
16252
  name: methodName,
@@ -15537,13 +16329,13 @@
15537
16329
  ...codeConfigProcessed
15538
16330
  };
15539
16331
  const newContent = cloneDeep$1(codeDsl[id]);
15540
- if (!doNotPushHistory) history_default.pushCodeBlock(id, {
16332
+ if (!doNotPushHistory) this.lastPushedHistoryId = history_default.pushCodeBlock(id, {
15541
16333
  oldContent,
15542
16334
  newContent,
15543
16335
  changeRecords,
15544
16336
  historyDescription,
15545
16337
  source: historySource
15546
- });
16338
+ })?.uuid ?? null;
15547
16339
  this.emit("addOrUpdate", id, codeDsl[id]);
15548
16340
  }
15549
16341
  /**
@@ -15626,18 +16418,50 @@
15626
16418
  async deleteCodeDslByIds(codeIds, { doNotPushHistory = false, historyDescription, historySource } = {}) {
15627
16419
  const currentDsl = await this.getCodeDsl();
15628
16420
  if (!currentDsl) return;
16421
+ this.lastDeletedHistoryIds = [];
15629
16422
  codeIds.forEach((id) => {
15630
16423
  const oldContent = currentDsl[id] ? cloneDeep$1(currentDsl[id]) : null;
15631
16424
  delete currentDsl[id];
15632
- if (oldContent && !doNotPushHistory) history_default.pushCodeBlock(id, {
15633
- oldContent,
15634
- newContent: null,
15635
- historyDescription,
15636
- source: historySource
15637
- });
16425
+ if (oldContent && !doNotPushHistory) {
16426
+ const uuid = history_default.pushCodeBlock(id, {
16427
+ oldContent,
16428
+ newContent: null,
16429
+ historyDescription,
16430
+ source: historySource
16431
+ })?.uuid;
16432
+ if (uuid) this.lastDeletedHistoryIds.push(uuid);
16433
+ }
15638
16434
  this.emit("remove", id);
15639
16435
  });
15640
16436
  }
16437
+ /**
16438
+ * 下列 *AndGetHistoryId 方法与对应的写入方法行为完全一致,
16439
+ * 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link CodeBlockStepValue.uuid}),
16440
+ * 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
16441
+ *
16442
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时:单条写入返回 null,批量删除返回空数组。
16443
+ */
16444
+ /** 等价于 {@link setCodeDslById},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
16445
+ async setCodeDslByIdAndGetHistoryId(id, codeConfig, options = {}) {
16446
+ this.lastPushedHistoryId = null;
16447
+ await this.setCodeDslById(id, codeConfig, options);
16448
+ return this.lastPushedHistoryId;
16449
+ }
16450
+ /** 等价于 {@link setCodeDslByIdSync},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
16451
+ setCodeDslByIdSyncAndGetHistoryId(id, codeConfig, force = true, options = {}) {
16452
+ this.lastPushedHistoryId = null;
16453
+ this.setCodeDslByIdSync(id, codeConfig, force, options);
16454
+ return this.lastPushedHistoryId;
16455
+ }
16456
+ /**
16457
+ * 等价于 {@link deleteCodeDslByIds},但返回本次写入的全部历史记录 uuid(按删除顺序)。
16458
+ * 一次删除多个代码块会产生多条历史记录,因此返回数组;未写入任何历史时返回空数组。
16459
+ */
16460
+ async deleteCodeDslByIdsAndGetHistoryId(codeIds, options = {}) {
16461
+ this.lastDeletedHistoryIds = [];
16462
+ await this.deleteCodeDslByIds(codeIds, options);
16463
+ return [...this.lastDeletedHistoryIds];
16464
+ }
15641
16465
  setParamsColConfig(config) {
15642
16466
  this.state.paramsColConfig = config;
15643
16467
  }
@@ -15713,11 +16537,25 @@
15713
16537
  async revert(id, index) {
15714
16538
  const entry = history_default.getCodeBlockStepList(id)[index];
15715
16539
  if (!entry?.applied) return null;
15716
- if (entry.step.oldContent && entry.step.newContent && !entry.step.changeRecords?.length) return null;
15717
- const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
16540
+ const { oldSchema, newSchema, changeRecords } = entry.step.diff?.[0] ?? {};
16541
+ if (oldSchema && newSchema && !changeRecords?.length) return null;
16542
+ const description = `回滚 #${index + 1}: ${describeRevertStep(entry.step.id, entry.step.diff?.[0], (s) => s.name)}`;
15718
16543
  return await this.applyRevertStep(entry.step, description);
15719
16544
  }
15720
16545
  /**
16546
+ * 通过历史记录 uuid 回滚某条代码块历史步骤,语义同 {@link revert},
16547
+ * 仅无需调用方再传 codeBlockId 与 index:内部会按 uuid({@link CodeBlockStepValue.uuid})
16548
+ * 在全部代码块栈中定位对应步骤后再回滚。
16549
+ *
16550
+ * @param uuid 目标历史记录的 uuid,通常由 {@link setCodeDslByIdAndGetHistoryId} 等方法返回
16551
+ * @returns 反向后产生的新 step;找不到对应 uuid / 未应用时返回 null
16552
+ */
16553
+ async revertById(uuid) {
16554
+ const location = history_default.findCodeBlockStepLocationByUuid(uuid);
16555
+ if (!location) return null;
16556
+ return await this.revert(location.id, location.index);
16557
+ }
16558
+ /**
15721
16559
  * 生成代码块唯一id
15722
16560
  * @returns {Id} 代码块唯一id
15723
16561
  */
@@ -15782,22 +16620,23 @@
15782
16620
  * 差异仅在于通过公开的 setCodeDslByIdSync / deleteCodeDslByIds 触发 push。
15783
16621
  */
15784
16622
  async applyRevertStep(step, historyDescription) {
15785
- const { id, oldContent, newContent, changeRecords } = step;
15786
- if (oldContent === null && newContent) {
16623
+ const { id } = step;
16624
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
16625
+ if (!oldSchema && newSchema) {
15787
16626
  await this.deleteCodeDslByIds([id], {
15788
16627
  historyDescription,
15789
16628
  historySource: "rollback"
15790
16629
  });
15791
16630
  return history_default.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
15792
16631
  }
15793
- if (oldContent && newContent === null) {
15794
- this.setCodeDslByIdSync(id, cloneDeep$1(oldContent), true, {
16632
+ if (oldSchema && !newSchema) {
16633
+ this.setCodeDslByIdSync(id, cloneDeep$1(oldSchema), true, {
15795
16634
  historyDescription,
15796
16635
  historySource: "rollback"
15797
16636
  });
15798
16637
  return history_default.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
15799
16638
  }
15800
- if (!oldContent || !newContent) return null;
16639
+ if (!oldSchema || !newSchema) return null;
15801
16640
  if (changeRecords?.length) {
15802
16641
  const current = this.getCodeContentById(id);
15803
16642
  if (!current) return null;
@@ -15808,17 +16647,17 @@
15808
16647
  fallbackToFullReplace = true;
15809
16648
  break;
15810
16649
  }
15811
- const value = cloneDeep$1((0, _tmagic_utils.getValueByKeyPath)(record.propPath, oldContent));
16650
+ const value = cloneDeep$1((0, _tmagic_utils.getValueByKeyPath)(record.propPath, oldSchema));
15812
16651
  (0, _tmagic_utils.setValueByKeyPath)(record.propPath, value, patched);
15813
16652
  }
15814
- this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep$1(oldContent) : patched, true, {
16653
+ this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep$1(oldSchema) : patched, true, {
15815
16654
  changeRecords,
15816
16655
  historyDescription,
15817
16656
  historySource: "rollback"
15818
16657
  });
15819
16658
  return history_default.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
15820
16659
  }
15821
- this.setCodeDslByIdSync(id, cloneDeep$1(oldContent), true, {
16660
+ this.setCodeDslByIdSync(id, cloneDeep$1(oldSchema), true, {
15822
16661
  historyDescription,
15823
16662
  historySource: "rollback"
15824
16663
  });
@@ -15839,19 +16678,20 @@
15839
16678
  * @param reverse true=撤销,false=重做
15840
16679
  */
15841
16680
  async applyHistoryStep(step, reverse) {
15842
- const { id, oldContent, newContent, changeRecords } = step;
15843
- if (oldContent === null && newContent) {
16681
+ const { id } = step;
16682
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
16683
+ if (!oldSchema && newSchema) {
15844
16684
  if (reverse) await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
15845
- else this.setCodeDslByIdSync(id, cloneDeep$1(newContent), true, { doNotPushHistory: true });
16685
+ else this.setCodeDslByIdSync(id, cloneDeep$1(newSchema), true, { doNotPushHistory: true });
15846
16686
  return;
15847
16687
  }
15848
- if (oldContent && newContent === null) {
15849
- if (reverse) this.setCodeDslByIdSync(id, cloneDeep$1(oldContent), true, { doNotPushHistory: true });
16688
+ if (oldSchema && !newSchema) {
16689
+ if (reverse) this.setCodeDslByIdSync(id, cloneDeep$1(oldSchema), true, { doNotPushHistory: true });
15850
16690
  else await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
15851
16691
  return;
15852
16692
  }
15853
- if (!oldContent || !newContent) return;
15854
- const sourceForValues = reverse ? oldContent : newContent;
16693
+ if (!oldSchema || !newSchema) return;
16694
+ const sourceForValues = reverse ? oldSchema : newSchema;
15855
16695
  if (changeRecords?.length) {
15856
16696
  const current = this.getCodeContentById(id);
15857
16697
  if (!current) return;
@@ -15920,18 +16760,6 @@
15920
16760
  "createId"
15921
16761
  ]
15922
16762
  };
15923
- /**
15924
- * 「回滚」生成的新 step 简短描述。
15925
- * 仅在 service 层使用,避免依赖 UI 层 composables。
15926
- */
15927
- var describeRevertDataSourceStep = (step) => {
15928
- const { oldSchema, newSchema, changeRecords, id } = step;
15929
- if (oldSchema === null && newSchema) return `撤回新增 ${newSchema.title || newSchema.id || id}`;
15930
- if (oldSchema && newSchema === null) return `还原已删除的 ${oldSchema.title || oldSchema.id || id}`;
15931
- const title = newSchema?.title || oldSchema?.title || `${id}`;
15932
- const propPath = changeRecords?.[0]?.propPath;
15933
- return propPath ? `还原 ${title} · ${propPath}` : `还原 ${title}`;
15934
- };
15935
16763
  var DataSource = class extends BaseService {
15936
16764
  state = (0, vue.reactive)({
15937
16765
  datasourceTypeList: [],
@@ -15942,6 +16770,12 @@
15942
16770
  events: {},
15943
16771
  methods: {}
15944
16772
  });
16773
+ /**
16774
+ * 最近一次写入历史栈的数据源历史记录 uuid。
16775
+ * 供 *AndGetHistoryId 系列方法在调用 add / update / remove 后取回本次产生的历史记录 id;
16776
+ * 普通方法不读取它,调用前由 *AndGetHistoryId 重置为 null。
16777
+ */
16778
+ lastPushedHistoryId = null;
15945
16779
  constructor() {
15946
16780
  super(canUsePluginMethods$3.sync.map((methodName) => ({
15947
16781
  name: methodName,
@@ -15991,12 +16825,12 @@
15991
16825
  id: config.id && !this.getDataSourceById(config.id) ? config.id : this.createId()
15992
16826
  };
15993
16827
  this.get("dataSources").push(newConfig);
15994
- if (!doNotPushHistory) history_default.pushDataSource(newConfig.id, {
16828
+ if (!doNotPushHistory) this.lastPushedHistoryId = history_default.pushDataSource(newConfig.id, {
15995
16829
  oldSchema: null,
15996
16830
  newSchema: newConfig,
15997
16831
  historyDescription,
15998
16832
  source: historySource
15999
- });
16833
+ })?.uuid ?? null;
16000
16834
  this.emit("add", newConfig);
16001
16835
  return newConfig;
16002
16836
  }
@@ -16014,13 +16848,13 @@
16014
16848
  const oldConfig = dataSources[index];
16015
16849
  const newConfig = cloneDeep$1(config);
16016
16850
  dataSources[index] = newConfig;
16017
- if (!doNotPushHistory) history_default.pushDataSource(newConfig.id, {
16851
+ if (!doNotPushHistory) this.lastPushedHistoryId = history_default.pushDataSource(newConfig.id, {
16018
16852
  oldSchema: oldConfig ? cloneDeep$1(oldConfig) : null,
16019
16853
  newSchema: newConfig,
16020
16854
  changeRecords,
16021
16855
  historyDescription,
16022
16856
  source: historySource
16023
- });
16857
+ })?.uuid ?? null;
16024
16858
  this.emit("update", newConfig, {
16025
16859
  oldConfig,
16026
16860
  changeRecords
@@ -16039,15 +16873,40 @@
16039
16873
  const index = dataSources.findIndex((ds) => ds.id === id);
16040
16874
  const oldConfig = index !== -1 ? dataSources[index] : null;
16041
16875
  dataSources.splice(index, 1);
16042
- if (oldConfig && !doNotPushHistory) history_default.pushDataSource(id, {
16876
+ if (oldConfig && !doNotPushHistory) this.lastPushedHistoryId = history_default.pushDataSource(id, {
16043
16877
  oldSchema: cloneDeep$1(oldConfig),
16044
16878
  newSchema: null,
16045
16879
  historyDescription,
16046
16880
  source: historySource
16047
- });
16881
+ })?.uuid ?? null;
16048
16882
  this.emit("remove", id);
16049
16883
  }
16050
16884
  /**
16885
+ * 下列 *AndGetHistoryId 方法与对应的 add / update / remove 行为完全一致,
16886
+ * 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link DataSourceStepValue.uuid}),
16887
+ * 而非数据源配置。可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
16888
+ *
16889
+ * 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时返回 null。
16890
+ */
16891
+ /** 等价于 {@link add},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
16892
+ addAndGetHistoryId(config, options = {}) {
16893
+ this.lastPushedHistoryId = null;
16894
+ this.add(config, options);
16895
+ return this.lastPushedHistoryId;
16896
+ }
16897
+ /** 等价于 {@link update},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
16898
+ updateAndGetHistoryId(config, options = {}) {
16899
+ this.lastPushedHistoryId = null;
16900
+ this.update(config, options);
16901
+ return this.lastPushedHistoryId;
16902
+ }
16903
+ /** 等价于 {@link remove},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
16904
+ removeAndGetHistoryId(id, options = {}) {
16905
+ this.lastPushedHistoryId = null;
16906
+ this.remove(id, options);
16907
+ return this.lastPushedHistoryId;
16908
+ }
16909
+ /**
16051
16910
  * 撤销指定数据源的最近一次变更。
16052
16911
  *
16053
16912
  * 内部走 add / update / remove,因此会自动触发 dataSourceService 的事件,
@@ -16115,10 +16974,24 @@
16115
16974
  revert(id, index) {
16116
16975
  const entry = history_default.getDataSourceStepList(id)[index];
16117
16976
  if (!entry?.applied) return null;
16118
- if (entry.step.oldSchema && entry.step.newSchema && !entry.step.changeRecords?.length) return null;
16119
- const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
16977
+ const { oldSchema, newSchema, changeRecords } = entry.step.diff?.[0] ?? {};
16978
+ if (oldSchema && newSchema && !changeRecords?.length) return null;
16979
+ const description = `回滚 #${index + 1}: ${describeRevertStep(entry.step.id, entry.step.diff?.[0], (s) => s.title)}`;
16120
16980
  return this.applyRevertStep(entry.step, description);
16121
16981
  }
16982
+ /**
16983
+ * 通过历史记录 uuid 回滚某条数据源历史步骤,语义同 {@link revert},
16984
+ * 仅无需调用方再传 dataSourceId 与 index:内部会按 uuid({@link DataSourceStepValue.uuid})
16985
+ * 在全部数据源栈中定位对应步骤后再回滚。
16986
+ *
16987
+ * @param uuid 目标历史记录的 uuid,通常由 {@link addAndGetHistoryId} 等方法返回
16988
+ * @returns 反向后产生的新 step;找不到对应 uuid / 未应用时返回 null
16989
+ */
16990
+ revertById(uuid) {
16991
+ const location = history_default.findDataSourceStepLocationByUuid(uuid);
16992
+ if (!location) return null;
16993
+ return this.revert(location.id, location.index);
16994
+ }
16122
16995
  createId() {
16123
16996
  return `ds_${(0, _tmagic_utils.guid)()}`;
16124
16997
  }
@@ -16177,15 +17050,16 @@
16177
17050
  * 同构,差异仅在于走对应的公共 add / update / remove 而不是带 doNotPushHistory 的版本。
16178
17051
  */
16179
17052
  applyRevertStep(step, historyDescription) {
16180
- const { id, oldSchema, newSchema, changeRecords } = step;
16181
- if (oldSchema === null && newSchema) {
17053
+ const { id } = step;
17054
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
17055
+ if (!oldSchema && newSchema) {
16182
17056
  this.remove(`${id}`, {
16183
17057
  historyDescription,
16184
17058
  historySource: "rollback"
16185
17059
  });
16186
17060
  return history_default.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
16187
17061
  }
16188
- if (oldSchema && newSchema === null) {
17062
+ if (oldSchema && !newSchema) {
16189
17063
  this.add(cloneDeep$1(oldSchema), {
16190
17064
  historyDescription,
16191
17065
  historySource: "rollback"
@@ -16234,13 +17108,14 @@
16234
17108
  * @param reverse true=撤销,false=重做
16235
17109
  */
16236
17110
  applyHistoryStep(step, reverse) {
16237
- const { id, oldSchema, newSchema, changeRecords } = step;
16238
- if (oldSchema === null && newSchema) {
17111
+ const { id } = step;
17112
+ const { oldSchema, newSchema, changeRecords } = step.diff?.[0] ?? {};
17113
+ if (!oldSchema && newSchema) {
16239
17114
  if (reverse) this.remove(`${id}`, { doNotPushHistory: true });
16240
17115
  else this.add(cloneDeep$1(newSchema), { doNotPushHistory: true });
16241
17116
  return;
16242
17117
  }
16243
- if (oldSchema && newSchema === null) {
17118
+ if (oldSchema && !newSchema) {
16244
17119
  if (reverse) this.add(cloneDeep$1(oldSchema), { doNotPushHistory: true });
16245
17120
  else this.remove(`${id}`, { doNotPushHistory: true });
16246
17121
  return;
@@ -16891,7 +17766,7 @@
16891
17766
  //#region packages/editor/src/initService.ts
16892
17767
  var initServiceState = (props, { editorService, historyService, componentListService, propsService, eventsService, uiService, codeBlockService, keybindingService, dataSourceService, depService }) => {
16893
17768
  (0, vue.watch)(() => props.modelValue, (modelValue) => {
16894
- editorService.set("root", modelValue || null);
17769
+ editorService.set("root", modelValue || null, { historySource: "initial" });
16895
17770
  }, { immediate: true });
16896
17771
  (0, vue.watch)(() => props.disabledMultiSelect, (disabledMultiSelect) => {
16897
17772
  editorService.set("disabledMultiSelect", disabledMultiSelect || false);
@@ -22166,6 +23041,7 @@
22166
23041
  exports.H_GUIDE_LINE_STORAGE_KEY = H_GUIDE_LINE_STORAGE_KEY;
22167
23042
  exports.HistoryDiffDialog = HistoryDiffDialog_default;
22168
23043
  exports.HistoryListBucket = Bucket_default;
23044
+ exports.HistoryListBucketTab = BucketTab_default;
22169
23045
  exports.Icon = Icon_default;
22170
23046
  exports.IdleTask = IdleTask;
22171
23047
  exports.KeyBindingCommand = KeyBindingCommand;
@@ -22215,17 +23091,24 @@
22215
23091
  exports.classifyDragSources = classifyDragSources;
22216
23092
  exports.codeBlockService = codeBlock_default;
22217
23093
  exports.collectRelatedNodes = collectRelatedNodes;
23094
+ exports.createStackStep = createStackStep;
22218
23095
  exports.dataSourceService = dataSource_default;
22219
23096
  exports.debug = debug;
22220
23097
  exports.default = plugin_default;
22221
23098
  exports.defaultIsExpandable = defaultIsExpandable;
22222
23099
  exports.depService = dep_default;
23100
+ exports.describeRevertStep = describeRevertStep;
23101
+ exports.describeStepForRevert = describeStepForRevert;
23102
+ exports.deserializeStacks = deserializeStacks;
22223
23103
  Object.defineProperty(exports, "designPlugin", {
22224
23104
  enumerable: true,
22225
23105
  get: function() {
22226
23106
  return _tmagic_design.default;
22227
23107
  }
22228
23108
  });
23109
+ exports.detectPageTargetId = detectPageTargetId;
23110
+ exports.detectPageTargetName = detectPageTargetName;
23111
+ exports.detectStackOpType = detectStackOpType;
22229
23112
  exports.displayTabConfig = displayTabConfig;
22230
23113
  exports.editorNodeMergeCustomizer = editorNodeMergeCustomizer;
22231
23114
  exports.editorService = editor_default;
@@ -22256,21 +23139,31 @@
22256
23139
  exports.getGuideLineFromCache = getGuideLineFromCache;
22257
23140
  exports.getInitPositionStyle = getInitPositionStyle;
22258
23141
  exports.getNodeIndex = getNodeIndex;
23142
+ exports.getOrCreateStack = getOrCreateStack;
22259
23143
  exports.getPageFragmentList = getPageFragmentList;
22260
23144
  exports.getPageList = getPageList;
22261
23145
  exports.getPageNameList = getPageNameList;
22262
23146
  exports.getPositionInContainer = getPositionInContainer;
22263
23147
  exports.getRelativeStyle = getRelativeStyle;
22264
23148
  exports.historyService = history_default;
23149
+ exports.idbDelete = idbDelete;
23150
+ exports.idbGet = idbGet;
23151
+ exports.idbSet = idbSet;
22265
23152
  exports.info = info;
22266
23153
  exports.isIncludeDataSource = isIncludeDataSource;
23154
+ exports.isIndexedDBSupported = isIndexedDBSupported;
22267
23155
  exports.loadMonaco = monaco_editor_default;
22268
23156
  exports.log = log;
23157
+ exports.markStackSaved = markStackSaved;
23158
+ exports.mergePageSteps = mergePageSteps;
23159
+ exports.mergeStackSteps = mergeStackSteps;
22269
23160
  exports.moveItemsInContainer = moveItemsInContainer;
22270
23161
  exports.numberOptions = numberOptions;
23162
+ exports.openIndexedDB = openIndexedDB;
22271
23163
  exports.propsService = props_default;
22272
23164
  exports.resolveSelectedNode = resolveSelectedNode;
22273
23165
  exports.serializeConfig = serializeConfig;
23166
+ exports.serializeStacks = serializeStacks;
22274
23167
  exports.setChildrenLayout = setChildrenLayout;
22275
23168
  exports.setEditorConfig = setEditorConfig;
22276
23169
  exports.setLayout = setLayout;
@@ -22285,6 +23178,7 @@
22285
23178
  });
22286
23179
  exports.toggleFixedPosition = toggleFixedPosition;
22287
23180
  exports.uiService = ui_default;
23181
+ exports.undoFloor = undoFloor;
22288
23182
  exports.updateStatus = updateStatus;
22289
23183
  exports.useCodeBlockEdit = useCodeBlockEdit;
22290
23184
  exports.useEditorContentHeight = useEditorContentHeight;