@tmagic/editor 1.8.0-beta.3 → 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 (92) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +15 -1
  2. package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
  3. package/dist/es/hooks/use-code-block-edit.js +6 -3
  4. package/dist/es/hooks/use-data-source-edit.js +5 -2
  5. package/dist/es/hooks/use-stage.js +6 -3
  6. package/dist/es/index.js +6 -3
  7. package/dist/es/initService.js +1 -1
  8. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
  9. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -51
  11. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -36
  12. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +152 -93
  13. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +36 -10
  14. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +334 -208
  15. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
  16. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -57
  17. package/dist/es/layouts/history-list/composables.js +118 -25
  18. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
  19. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  20. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
  22. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
  23. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  24. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  25. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  26. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  27. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
  28. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
  29. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
  30. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
  31. package/dist/es/services/codeBlock.js +105 -42
  32. package/dist/es/services/dataSource.js +85 -35
  33. package/dist/es/services/editor.js +317 -122
  34. package/dist/es/services/history.js +270 -204
  35. package/dist/es/services/keybinding.js +3 -3
  36. package/dist/es/services/ui.js +3 -0
  37. package/dist/es/style.css +65 -10
  38. package/dist/es/utils/content-menu.js +17 -9
  39. package/dist/es/utils/editor.js +35 -1
  40. package/dist/es/utils/history.js +223 -0
  41. package/dist/es/utils/indexed-db.js +86 -0
  42. package/dist/es/utils/undo-redo.js +60 -1
  43. package/dist/style.css +65 -10
  44. package/dist/tmagic-editor.umd.cjs +2047 -954
  45. package/package.json +7 -7
  46. package/src/components/CompareForm.vue +21 -1
  47. package/src/components/ToolButton.vue +2 -2
  48. package/src/hooks/use-code-block-edit.ts +4 -3
  49. package/src/hooks/use-data-source-edit.ts +2 -2
  50. package/src/hooks/use-stage.ts +3 -3
  51. package/src/index.ts +1 -0
  52. package/src/initService.ts +1 -1
  53. package/src/layouts/Framework.vue +1 -1
  54. package/src/layouts/NavMenu.vue +1 -1
  55. package/src/layouts/history-list/Bucket.vue +34 -67
  56. package/src/layouts/history-list/BucketTab.vue +46 -51
  57. package/src/layouts/history-list/GroupRow.vue +150 -98
  58. package/src/layouts/history-list/HistoryDiffDialog.vue +96 -69
  59. package/src/layouts/history-list/HistoryListPanel.vue +298 -111
  60. package/src/layouts/history-list/InitialRow.vue +25 -9
  61. package/src/layouts/history-list/PageTab.vue +58 -48
  62. package/src/layouts/history-list/composables.ts +210 -29
  63. package/src/layouts/page-bar/PageBar.vue +4 -2
  64. package/src/layouts/props-panel/PropsPanel.vue +5 -1
  65. package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
  66. package/src/layouts/sidebar/Sidebar.vue +6 -1
  67. package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
  68. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  69. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  70. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
  71. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  72. package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
  73. package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
  74. package/src/layouts/workspace/viewer/Stage.vue +1 -1
  75. package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
  76. package/src/services/codeBlock.ts +129 -35
  77. package/src/services/dataSource.ts +105 -34
  78. package/src/services/editor.ts +470 -157
  79. package/src/services/history.ts +312 -203
  80. package/src/services/keybinding.ts +3 -3
  81. package/src/services/ui.ts +7 -0
  82. package/src/theme/history-list-panel.scss +87 -6
  83. package/src/theme/page-bar.scss +0 -4
  84. package/src/theme/props-panel.scss +3 -3
  85. package/src/type.ts +221 -61
  86. package/src/utils/content-menu.ts +18 -9
  87. package/src/utils/editor.ts +41 -1
  88. package/src/utils/history.ts +298 -0
  89. package/src/utils/index.ts +2 -0
  90. package/src/utils/indexed-db.ts +122 -0
  91. package/src/utils/undo-redo.ts +88 -0
  92. package/types/index.d.ts +871 -308
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <li
3
3
  class="m-editor-history-list-item m-editor-history-list-group"
4
- :class="{ 'is-undone': !applied, 'is-merged': merged, 'is-current': isCurrent }"
4
+ :class="{ 'is-undone': !group.applied, 'is-merged': merged, 'is-current': group.isCurrent }"
5
5
  >
6
6
  <div
7
7
  class="m-editor-history-list-group-head"
@@ -10,34 +10,53 @@
10
10
  @click="onHeadClick"
11
11
  >
12
12
  <span class="m-editor-history-list-item-index" :title="headIndexTitle">{{ headIndexLabel }}</span>
13
- <span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
14
- <span class="m-editor-history-list-item-desc">{{ desc }}</span>
13
+ <span class="m-editor-history-list-item-op" :class="`op-${group.opType}`">{{ opLabel(group.opType) }}</span>
14
+ <span class="m-editor-history-list-item-desc">{{ group.desc }}</span>
15
15
 
16
- <span v-if="time" class="m-editor-history-list-item-time" :title="timeTitle || time">{{ time }}</span>
17
-
18
- <span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
16
+ <span v-if="headSaved" class="m-editor-history-list-item-saved" title="该记录为最近一次保存的状态">已保存</span>
19
17
 
20
18
  <span
21
- v-if="!merged && headRevertable"
22
- class="m-editor-history-list-item-revert"
23
- title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
24
- @click.stop="onRevertClick(subSteps[0].index)"
25
- >回滚</span
19
+ v-if="!merged && (headRevertable || headDiffable || canHeadGoto)"
20
+ class="m-editor-history-list-item-actions"
26
21
  >
22
+ <span
23
+ v-if="headRevertable"
24
+ class="m-editor-history-list-item-revert"
25
+ title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
26
+ @click.stop="onRevertClick(group.subSteps[0].index)"
27
+ >回滚</span
28
+ >
29
+ <span
30
+ v-if="canHeadGoto"
31
+ class="m-editor-history-list-item-goto"
32
+ title="回到该记录"
33
+ @click.stop="onGotoClick(group.subSteps[0].index)"
34
+ >回到</span
35
+ >
36
+ <span
37
+ v-if="headDiffable"
38
+ class="m-editor-history-list-item-diff"
39
+ title="查看修改差异"
40
+ @click.stop="onDiffClick(group.subSteps[0].index)"
41
+ >查看差异</span
42
+ >
43
+ </span>
44
+
27
45
  <span
28
- v-if="!merged && gotoEnabled && !isCurrent && subSteps.length"
29
- class="m-editor-history-list-item-goto"
30
- title="回到该记录"
31
- @click.stop="onGotoClick(subSteps[0].index)"
32
- >回到</span
46
+ v-if="!merged && sourceLabel(group.source)"
47
+ class="m-editor-history-list-item-source"
48
+ :title="`操作途径:${sourceLabel(group.source)}`"
49
+ >{{ sourceLabel(group.source) }}</span
33
50
  >
51
+
34
52
  <span
35
- v-if="!merged && headDiffable"
36
- class="m-editor-history-list-item-diff"
37
- title="查看修改差异"
38
- @click.stop="onDiffClick(subSteps[0].index)"
39
- >查看差异</span
53
+ v-if="!merged && group.time"
54
+ class="m-editor-history-list-item-time"
55
+ :title="group.timeTitle || group.time"
56
+ >{{ group.time }}</span
40
57
  >
58
+
59
+ <span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
41
60
  <span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
42
61
  </div>
43
62
 
@@ -45,33 +64,46 @@
45
64
  <li
46
65
  v-for="s in subStepsDisplay"
47
66
  :key="s.index"
48
- :class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent }"
67
+ :class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': selectEnabled }"
49
68
  :title="subStepTitle(s)"
69
+ @click="onSubStepClick(s.index)"
50
70
  >
51
71
  <span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
52
72
  <span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
53
- <span v-if="s.time" class="m-editor-history-list-item-time" :title="s.timeTitle || s.time">{{ s.time }}</span>
73
+ <span v-if="s.saved" class="m-editor-history-list-item-saved" title="该记录为最近一次保存的状态">已保存</span>
54
74
  <span
55
- v-if="s.revertable"
56
- class="m-editor-history-list-item-revert"
57
- title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
58
- @click.stop="onRevertClick(s.index)"
59
- >回滚</span
75
+ v-if="s.revertable || s.diffable || (gotoEnabled && !s.isCurrent)"
76
+ class="m-editor-history-list-item-actions"
60
77
  >
78
+ <span
79
+ v-if="s.revertable"
80
+ class="m-editor-history-list-item-revert"
81
+ title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
82
+ @click.stop="onRevertClick(s.index)"
83
+ >回滚</span
84
+ >
85
+ <span
86
+ v-if="gotoEnabled && !s.isCurrent"
87
+ class="m-editor-history-list-item-goto"
88
+ title="回到该记录"
89
+ @click.stop="onGotoClick(s.index)"
90
+ >回到</span
91
+ >
92
+ <span
93
+ v-if="s.diffable"
94
+ class="m-editor-history-list-item-diff"
95
+ title="查看修改差异"
96
+ @click.stop="onDiffClick(s.index)"
97
+ >查看差异</span
98
+ >
99
+ </span>
61
100
  <span
62
- v-if="gotoEnabled && !s.isCurrent"
63
- class="m-editor-history-list-item-goto"
64
- title="回到该记录"
65
- @click.stop="onGotoClick(s.index)"
66
- >回到</span
67
- >
68
- <span
69
- v-if="s.diffable"
70
- class="m-editor-history-list-item-diff"
71
- title="查看修改差异"
72
- @click.stop="onDiffClick(s.index)"
73
- >查看差异</span
101
+ v-if="sourceLabel(s.source)"
102
+ class="m-editor-history-list-item-source"
103
+ :title="`操作途径:${sourceLabel(s.source)}`"
104
+ >{{ sourceLabel(s.source) }}</span
74
105
  >
106
+ <span v-if="s.time" class="m-editor-history-list-item-time" :title="s.timeTitle || s.time">{{ s.time }}</span>
75
107
  </li>
76
108
  </ul>
77
109
  </li>
@@ -80,9 +112,8 @@
80
112
  <script lang="ts" setup>
81
113
  import { computed } from 'vue';
82
114
 
83
- import type { HistoryOpType } from '@editor/type';
84
-
85
- import { opLabel } from './composables';
115
+ import type { HistoryRowGroup, HistoryRowStep } from './composables';
116
+ import { opLabel, sourceLabel } from './composables';
86
117
 
87
118
  defineOptions({
88
119
  name: 'MEditorHistoryListGroupRow',
@@ -90,63 +121,42 @@ defineOptions({
90
121
 
91
122
  const props = withDefaults(
92
123
  defineProps<{
93
- /** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${首步 index}` / `ds-${id}-${首步 index}` / `cb-${id}-${首步 index}`,以稳定的 step 索引标识分组。 */
94
- groupKey: string;
95
- /** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
96
- applied: boolean;
97
- /** 是否为合并组(即组内 step 数大于 1,由多次连续操作合并而来)。决定是否展示合并标记与可展开的子步列表。 */
98
- merged: boolean;
99
- /** 操作类型:`add` / `remove` / `update`,用于决定操作徽标的颜色和文案。 */
100
- opType: HistoryOpType;
101
- /** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
102
- desc: string;
103
- /** 组头部展示的时间文案(一般为组内最近一步的时间),为空时不渲染。 */
104
- time?: string;
105
- /** 组头部时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
106
- timeTitle?: string;
107
- /** 组内的 step 总数,仅在 merged 为 true 时显示为 "合并 N 步"。 */
108
- stepCount: number;
109
- /** 子步列表,用于在展开状态下逐条展示每个 step 的索引、应用状态与描述文案。 */
110
- subSteps: {
111
- index: number;
112
- applied: boolean;
113
- desc: string;
114
- isCurrent?: boolean;
115
- diffable?: boolean;
116
- /** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
117
- revertable?: boolean;
118
- /** 该子步的时间文案,为空时不渲染。 */
119
- time?: string;
120
- /** 该子步时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
121
- timeTitle?: string;
122
- }[];
123
- /** 当前组是否处于展开状态。仅在 merged 为 true 时生效,控制子步列表是否渲染。 */
124
+ /**
125
+ * 该组的视图模型(由 `toRowGroup` 统一派生):包含 key、应用状态、操作类型、描述、
126
+ * 来源 / 时间等头部信息以及子步列表。原先散落的十余个扁平 props 收敛于此单一对象。
127
+ */
128
+ group: HistoryRowGroup;
129
+ /** 当前组是否处于展开状态(合并组默认展开)。仅在合并组(子步数 > 1)时生效,控制子步列表是否渲染。 */
124
130
  expanded: boolean;
125
- /** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
126
- isCurrent?: boolean;
127
131
  /**
128
132
  * 是否支持「跳转到该记录」(goto)。默认 true。
129
133
  * 为 false 时:单步组头部与子步条目都不再可点击跳转、也不会触发 goto 事件,
130
134
  * 仅保留合并组头部的展开 / 收起能力,以及查看差异、回滚等其它入口。
131
135
  */
132
136
  gotoEnabled?: boolean;
137
+ /**
138
+ * 是否支持「点击记录选中对应节点」。默认 false(仅页面 tab 启用,数据源 / 代码块无节点概念)。
139
+ * 为 true 时:点击单步组头部、子步条目或合并组头部都会发出 `select` 事件(携带对应 step 索引),
140
+ * 由上层解析出节点 id 并在画布选中;合并组头部同时保留展开 / 收起能力。
141
+ */
142
+ selectEnabled?: boolean;
133
143
  }>(),
134
144
  {
135
- isCurrent: false,
136
145
  gotoEnabled: true,
146
+ selectEnabled: false,
137
147
  },
138
148
  );
139
149
 
140
150
  const emit = defineEmits<{
141
151
  /**
142
- * 用户点击合并组头部时触发,携带 groupKey;上层用其切换 expanded 状态。
152
+ * 用户点击合并组头部时触发,携带 group.key;上层用其切换 expanded 状态。
143
153
  * 对单步组(非合并)头部点击不会发该事件——因为单步组没有"展开"的概念。
144
154
  */
145
155
  (_e: 'toggle', _key: string): void;
146
156
  /**
147
157
  * 用户希望跳转到该记录时触发,携带"目标 step 在所属栈中的索引"——上层据此计算目标 cursor (= index + 1)。
148
158
  * 触发场景:
149
- * - 单步组(merged=false)头部:取该唯一 step 的 index;
159
+ * - 单步组(非合并)头部:取该唯一 step 的 index;
150
160
  * - 子步条目:取该子步的 index。
151
161
  * 合并组头部不再触发 goto,避免与展开/收起冲突;用户应展开后点具体子步精准跳转。
152
162
  * 当前所在的步骤(isCurrent)始终不会触发 goto。
@@ -154,7 +164,7 @@ const emit = defineEmits<{
154
164
  (_e: 'goto', _index: number): void;
155
165
  /**
156
166
  * 用户希望查看该 step 的修改差异(旧值 vs 新值)。
157
- * 只在 step 满足"前后值都存在"(如 update / 数据源、代码块的 update)时由父级标记 `diffable=true`。
167
+ * 只在 step 满足"前后值都存在"(如 update / 数据源、代码块的 update)时由 `toRowGroup` 标记 `diffable=true`。
158
168
  * payload 为该 step 在所属栈中的索引,由上层根据 index 取 step 内容并展示对比。
159
169
  */
160
170
  (_e: 'diff-step', _index: number): void;
@@ -163,26 +173,48 @@ const emit = defineEmits<{
163
173
  * payload 为该 step 在所属栈中的索引。仅在单步组头部(headRevertable)或合并组的可回滚子步上触发。
164
174
  */
165
175
  (_e: 'revert-step', _index: number): void;
176
+ /**
177
+ * 用户希望「选中该记录对应的节点」。payload 为该 step 在所属栈中的索引,
178
+ * 由上层据 index 取出 step、解析出节点 id 并在画布选中。仅在 `selectEnabled` 为 true 时触发。
179
+ */
180
+ (_e: 'select', _index: number): void;
166
181
  }>();
167
182
 
183
+ /** 子步数大于 1 即为合并组:决定是否展示合并标记与可展开的子步列表。 */
184
+ const merged = computed(() => props.group.subSteps.length > 1);
185
+
186
+ /** 组内 step 总数,仅在合并组时显示为 "合并 N 步"。 */
187
+ const stepCount = computed(() => props.group.subSteps.length);
188
+
168
189
  /**
169
- * 仅合并组头部可点击(切换展开 / 收起);
170
- * 单步组的跳转改由头部的「回退」按钮触发,整行不再可点击。
190
+ * 头部可点击的场景:
191
+ * - 合并组:点击切换展开 / 收起;
192
+ * - 开启 `selectEnabled`(页面 tab):点击选中对应节点。
193
+ * 单步组的跳转仍由头部的「回到」按钮触发。
171
194
  */
172
- const isHeadClickable = computed(() => props.merged);
195
+ const isHeadClickable = computed(() => merged.value || props.selectEnabled);
173
196
 
174
197
  const headTitle = computed(() => {
175
- if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
176
- if (props.isCurrent) return '当前所在记录';
198
+ if (merged.value) {
199
+ const expandHint = props.expanded ? '点击收起子步' : '点击展开子步';
200
+ return props.selectEnabled ? `${expandHint}(并选中该节点)` : expandHint;
201
+ }
202
+ if (props.selectEnabled) return '点击选中该节点';
203
+ if (props.group.isCurrent) return '当前所在记录';
177
204
  return '';
178
205
  });
179
206
 
180
207
  /**
181
- * 头部点击行为:仅合并组切换展开 / 收起;单步组不再响应整行点击。
208
+ * 头部点击行为:
209
+ * - 开启 selectEnabled 时,发出 select(携带组内首步 index,上层据此选中节点);
210
+ * - 合并组同时切换展开 / 收起。
182
211
  */
183
212
  const onHeadClick = () => {
184
- if (props.merged) {
185
- emit('toggle', props.groupKey);
213
+ if (props.selectEnabled && props.group.subSteps.length) {
214
+ emit('select', props.group.subSteps[0].index);
215
+ }
216
+ if (merged.value) {
217
+ emit('toggle', props.group.key);
186
218
  }
187
219
  };
188
220
 
@@ -191,45 +223,65 @@ const onGotoClick = (index: number) => {
191
223
  emit('goto', index);
192
224
  };
193
225
 
226
+ /** 点击子步行:开启 selectEnabled 时选中该子步对应的节点。 */
227
+ const onSubStepClick = (index: number) => {
228
+ if (!props.selectEnabled) return;
229
+ emit('select', index);
230
+ };
231
+
194
232
  const subStepTitle = (s: { isCurrent?: boolean }) => {
233
+ if (props.selectEnabled) return '点击选中该节点';
195
234
  if (s.isCurrent) return '当前所在记录';
196
235
  return '';
197
236
  };
198
237
 
238
+ /**
239
+ * 头部是否展示「已保存」标记:
240
+ * - 单步组:取该唯一子步的 saved;
241
+ * - 合并组:组内任一子步为已保存即在头部提示(具体落在哪一步可展开查看)。
242
+ */
243
+ const headSaved = computed(() =>
244
+ merged.value ? props.group.subSteps.some((s) => s.saved) : Boolean(props.group.subSteps[0]?.saved),
245
+ );
246
+
199
247
  /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
200
- const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
248
+ const headDiffable = computed(() => !merged.value && Boolean(props.group.subSteps[0]?.diffable));
201
249
 
202
250
  /** 单步组头部是否展示"回滚"入口:要求该唯一子步本身可回滚(已应用)。 */
203
- const headRevertable = computed(() => !props.merged && Boolean(props.subSteps[0]?.revertable));
251
+ const headRevertable = computed(() => !merged.value && Boolean(props.group.subSteps[0]?.revertable));
252
+
253
+ /** 单步组头部是否展示"回到"入口:可跳转、非当前、且存在唯一子步。 */
254
+ const canHeadGoto = computed(
255
+ () => !merged.value && props.gotoEnabled && !props.group.isCurrent && props.group.subSteps.length > 0,
256
+ );
204
257
 
205
258
  /**
206
259
  * 合并组展开后的子步渲染顺序:与外层分组列表保持一致——倒序展示(最新的子步在最上方)。
207
260
  * 外层 page tab / bucket 都已对 groups 做了 reverse,子步沿用同样的视觉规则更直观。
208
261
  * 注意:仅用于渲染,原 `subSteps` 保持时间正序,`headIndexLabel` 等基于首尾索引的展示语义不变。
209
262
  */
210
- const subStepsDisplay = computed(() => props.subSteps.slice().reverse());
263
+ const subStepsDisplay = computed<HistoryRowStep[]>(() => props.group.subSteps.slice().reverse());
211
264
 
212
265
  /**
213
266
  * 头部索引展示:
214
- * - 单步组(merged=false):显示该唯一 step 的编号,如 `#5`;
267
+ * - 单步组(非合并):显示该唯一 step 的编号,如 `#5`;
215
268
  * - 合并组:显示组内 step 的编号范围,如 `#3-#7`(首尾相同则退化为 `#5`)。
216
269
  *
217
270
  * 这里展示的是 step.index + 1(与子步列表 `#{{ s.index + 1 }}` 保持一致),从 1 起编号更符合直觉。
218
271
  */
219
272
  const headIndexLabel = computed(() => {
220
- const list = props.subSteps;
273
+ const list = props.group.subSteps;
221
274
  if (!list.length) return '';
222
275
  const first = list[0].index + 1;
223
276
  const last = list[list.length - 1].index + 1;
224
- if (!props.merged || first === last) return `#${first}`;
277
+ if (!merged.value || first === last) return `#${first}`;
225
278
  return `#${first}-#${last}`;
226
279
  });
227
280
 
228
281
  const headIndexTitle = computed(() => {
229
- if (!props.merged) return `历史步骤编号 #${props.subSteps[0]?.index + 1}`;
230
- return `合并了第 ${props.subSteps[0]?.index + 1} 至第 ${
231
- props.subSteps[props.subSteps.length - 1]?.index + 1
232
- } 共 ${props.subSteps.length} 条历史步骤`;
282
+ const list = props.group.subSteps;
283
+ if (!merged.value) return `历史步骤编号 #${list[0]?.index + 1}`;
284
+ return `合并了第 ${list[0]?.index + 1} 至第 ${list[list.length - 1]?.index + 1} 共 ${list.length} 条历史步骤`;
233
285
  });
234
286
 
235
287
  const onDiffClick = (index: number) => {
@@ -1,76 +1,74 @@
1
1
  <template>
2
- <Teleport to="body">
3
- <TMagicDialog
4
- v-model="visible"
5
- class="m-editor-history-diff-dialog"
6
- :title="dialogTitle"
7
- top="5vh"
8
- destroy-on-close
9
- append-to-body
10
- :width="width"
11
- @close="onClose"
12
- >
13
- <div v-if="payload" class="m-editor-history-diff-dialog-body">
14
- <div v-if="onConfirm" class="m-editor-history-diff-dialog-notice">仅回滚有差异的字段</div>
15
-
16
- <div class="m-editor-history-diff-dialog-header">
17
- <span class="m-editor-history-diff-dialog-target">{{ targetText }}</span>
18
- <div class="m-editor-history-diff-dialog-controls">
19
- <TMagicRadioGroup v-model="viewMode" size="small" class="m-editor-history-diff-dialog-view">
20
- <TMagicRadioButton value="form">表单对比</TMagicRadioButton>
21
- <TMagicRadioButton value="code">源码对比</TMagicRadioButton>
22
- </TMagicRadioGroup>
23
-
24
- <TMagicRadioGroup v-model="mode" size="small" class="m-editor-history-diff-dialog-mode">
25
- <TMagicRadioButton value="before">与修改前对比</TMagicRadioButton>
26
- <TMagicRadioButton value="current" :disabled="!hasCurrent">与当前对比</TMagicRadioButton>
27
- </TMagicRadioGroup>
28
- </div>
29
- </div>
2
+ <TMagicDialog
3
+ v-model="visible"
4
+ class="m-editor-history-diff-dialog"
5
+ :title="dialogTitle"
6
+ top="5vh"
7
+ destroy-on-close
8
+ append-to-body
9
+ :width="width"
10
+ @close="onClose"
11
+ >
12
+ <div v-if="payload && visible" class="m-editor-history-diff-dialog-body">
13
+ <div v-if="onConfirm" class="m-editor-history-diff-dialog-notice">仅回滚有差异的字段</div>
14
+
15
+ <div class="m-editor-history-diff-dialog-header">
16
+ <span class="m-editor-history-diff-dialog-target">{{ targetText }}</span>
17
+ <div class="m-editor-history-diff-dialog-controls">
18
+ <TMagicRadioGroup v-model="viewMode" size="small" class="m-editor-history-diff-dialog-view">
19
+ <TMagicRadioButton value="form">表单对比</TMagicRadioButton>
20
+ <TMagicRadioButton value="code">源码对比</TMagicRadioButton>
21
+ </TMagicRadioGroup>
30
22
 
31
- <div class="m-editor-history-diff-dialog-legend">
32
- <TMagicTag size="small" type="danger">{{ leftLabel }}</TMagicTag>
33
- <span class="m-editor-history-diff-dialog-arrow">→</span>
34
- <TMagicTag size="small" type="success">{{ rightLabel }}</TMagicTag>
35
- <span v-if="mode === 'current' && isSameAsCurrent" class="m-editor-history-diff-dialog-tip">
36
- 当前值与该步修改后一致,无差异
37
- </span>
23
+ <TMagicRadioGroup v-model="mode" size="small" class="m-editor-history-diff-dialog-mode">
24
+ <TMagicRadioButton value="before">与修改前对比</TMagicRadioButton>
25
+ <TMagicRadioButton value="current" :disabled="!hasCurrent">与当前对比</TMagicRadioButton>
26
+ </TMagicRadioGroup>
38
27
  </div>
28
+ </div>
39
29
 
40
- <CompareForm
41
- v-if="viewMode === 'form'"
42
- :category="payload.category"
43
- :type="payload.type"
44
- :data-source-type="payload.dataSourceType"
45
- :value="rightValue"
46
- :last-value="leftValue"
47
- :extend-state="extendState"
48
- :load-config="loadConfig"
49
- :self-diff-field-types="selfDiffFieldTypes"
50
- height="70vh"
51
- />
52
-
53
- <CodeEditor
54
- v-else
55
- type="diff"
56
- language="json"
57
- :init-values="leftValue"
58
- :modified-values="rightValue"
59
- :options="codeDiffOptions"
60
- disabled-full-screen
61
- height="70vh"
62
- />
30
+ <div class="m-editor-history-diff-dialog-legend">
31
+ <TMagicTag size="small" type="danger">{{ leftLabel }}</TMagicTag>
32
+ <span class="m-editor-history-diff-dialog-arrow">→</span>
33
+ <TMagicTag size="small" type="success">{{ rightLabel }}</TMagicTag>
34
+ <span v-if="mode === 'current' && isSameAsCurrent" class="m-editor-history-diff-dialog-tip">
35
+ 当前值与该步修改后一致,无差异
36
+ </span>
63
37
  </div>
64
38
 
65
- <template #footer>
66
- <template v-if="onConfirm">
67
- <TMagicButton size="small" @click="visible = false">取消</TMagicButton>
68
- <TMagicButton size="small" type="primary" @click="onConfirmClick">确定回滚</TMagicButton>
69
- </template>
70
- <TMagicButton v-else size="small" @click="visible = false">关闭</TMagicButton>
39
+ <CompareForm
40
+ v-if="viewMode === 'form'"
41
+ :category="payload.category"
42
+ :type="payload.type"
43
+ :data-source-type="payload.dataSourceType"
44
+ :value="rightValue"
45
+ :last-value="leftValue"
46
+ :extend-state="extendState"
47
+ :load-config="loadConfig"
48
+ :self-diff-field-types="selfDiffFieldTypes"
49
+ height="70vh"
50
+ />
51
+
52
+ <CodeEditor
53
+ v-else
54
+ type="diff"
55
+ language="json"
56
+ :init-values="leftValue"
57
+ :modified-values="rightValue"
58
+ :options="codeDiffOptions"
59
+ disabled-full-screen
60
+ height="70vh"
61
+ />
62
+ </div>
63
+
64
+ <template #footer>
65
+ <template v-if="isConfirm">
66
+ <TMagicButton size="small" @click="visible = false">取消</TMagicButton>
67
+ <TMagicButton size="small" type="primary" @click="onConfirmClick">确定回滚</TMagicButton>
71
68
  </template>
72
- </TMagicDialog>
73
- </Teleport>
69
+ <TMagicButton v-else size="small" @click="visible = false">关闭</TMagicButton>
70
+ </template>
71
+ </TMagicDialog>
74
72
  </template>
75
73
 
76
74
  <script lang="ts" setup>
@@ -102,6 +100,7 @@ const props = withDefaults(
102
100
  */
103
101
  loadConfig?: CompareFormLoadConfig;
104
102
  width?: string;
103
+ isConfirm?: boolean;
105
104
  onConfirm?: () => void;
106
105
  selfDiffFieldTypes?: string[];
107
106
  }>(),
@@ -178,10 +177,15 @@ const isSameAsCurrent = computed(() => {
178
177
  return isEqual(payload.value.value, payload.value.currentValue);
179
178
  });
180
179
 
180
+ /** confirm() 的 resolve,仅在「等待用户确认回滚」期间存在 */
181
+ let confirmResolve: ((_value: boolean) => void) | null = null;
182
+
181
183
  const onConfirmClick = () => {
182
- const cb = props.onConfirm;
184
+ props.onConfirm?.();
183
185
 
184
- cb?.();
186
+ // 用户确认回滚:resolve(true),并清空以避免随后 visible=false 再 resolve(false)
187
+ confirmResolve?.(true);
188
+ confirmResolve = null;
185
189
 
186
190
  visible.value = false;
187
191
  };
@@ -193,7 +197,8 @@ const targetText = computed(() => {
193
197
  'data-source': '数据源',
194
198
  'code-block': '代码块',
195
199
  };
196
- const prefix = categoryText[payload.value.category] || '';
200
+ const { category } = payload.value;
201
+ const prefix = category ? categoryText[category] : '';
197
202
  const label = payload.value.targetLabel || payload.value.type || '';
198
203
  const { id } = payload.value;
199
204
  const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
@@ -209,6 +214,24 @@ const open = (p: DiffDialogPayload) => {
209
214
  visible.value = true;
210
215
  };
211
216
 
217
+ /**
218
+ * 以 Promise 形式打开确认回滚弹窗:
219
+ * - 用户点击「确定回滚」时 resolve(true);
220
+ * - 取消 / 关闭 / 按 Esc 等其他方式关闭弹窗时 resolve(false)。
221
+ *
222
+ * 同一时刻只允许一个待确认流程,重复调用会先 resolve(false) 掉上一个。
223
+ */
224
+ const confirm = (p: DiffDialogPayload): Promise<boolean> => {
225
+ // 终止上一个未完成的确认流程,避免悬挂的 Promise
226
+ confirmResolve?.(false);
227
+ confirmResolve = null;
228
+
229
+ return new Promise<boolean>((resolve) => {
230
+ confirmResolve = resolve;
231
+ open(p);
232
+ });
233
+ };
234
+
212
235
  const close = () => {
213
236
  visible.value = false;
214
237
  };
@@ -217,6 +240,9 @@ const close = () => {
217
240
  watch(visible, (v) => {
218
241
  if (!v) {
219
242
  payload.value = null;
243
+ // 非「确定回滚」方式关闭(取消 / Esc / 点遮罩等)时,resolve(false)
244
+ confirmResolve?.(false);
245
+ confirmResolve = null;
220
246
  }
221
247
  });
222
248
 
@@ -226,6 +252,7 @@ const onClose = () => {
226
252
 
227
253
  defineExpose({
228
254
  open,
255
+ confirm,
229
256
  close,
230
257
  });
231
258
  </script>