@tmagic/editor 1.8.0-beta.2 → 1.8.0-beta.3

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 (45) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +9 -0
  2. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +32 -28
  3. package/dist/es/editorProps.js +2 -0
  4. package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +2 -5
  5. package/dist/es/hooks/use-stage.js +2 -1
  6. package/dist/es/index.js +3 -1
  7. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +2 -5
  8. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +33 -14
  9. package/dist/es/layouts/history-list/BucketTab.js +5 -0
  10. package/dist/es/layouts/history-list/{CodeBlockTab.vue_vue_type_script_setup_true_lang.js → BucketTab.vue_vue_type_script_setup_true_lang.js} +27 -16
  11. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +71 -53
  12. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +78 -28
  13. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +136 -66
  14. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +15 -9
  15. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +12 -6
  16. package/dist/es/layouts/history-list/composables.js +20 -2
  17. package/dist/es/services/history.js +5 -2
  18. package/dist/es/style.css +44 -12
  19. package/dist/style.css +44 -12
  20. package/dist/tmagic-editor.umd.cjs +479 -326
  21. package/package.json +7 -7
  22. package/src/Editor.vue +8 -0
  23. package/src/components/CompareForm.vue +32 -19
  24. package/src/editorProps.ts +7 -0
  25. package/src/fields/CodeLink.vue +2 -5
  26. package/src/hooks/use-stage.ts +1 -0
  27. package/src/index.ts +2 -0
  28. package/src/layouts/CodeEditor.vue +2 -5
  29. package/src/layouts/history-list/Bucket.vue +53 -28
  30. package/src/layouts/history-list/BucketTab.vue +77 -0
  31. package/src/layouts/history-list/GroupRow.vue +95 -60
  32. package/src/layouts/history-list/HistoryDiffDialog.vue +51 -32
  33. package/src/layouts/history-list/HistoryListPanel.vue +161 -52
  34. package/src/layouts/history-list/InitialRow.vue +17 -6
  35. package/src/layouts/history-list/PageTab.vue +20 -6
  36. package/src/layouts/history-list/composables.ts +32 -1
  37. package/src/services/history.ts +3 -0
  38. package/src/theme/history-list-panel.scss +52 -13
  39. package/src/type.ts +90 -0
  40. package/types/index.d.ts +295 -135
  41. package/dist/es/layouts/history-list/CodeBlockTab.js +0 -5
  42. package/dist/es/layouts/history-list/DataSourceTab.js +0 -5
  43. package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +0 -62
  44. package/src/layouts/history-list/CodeBlockTab.vue +0 -61
  45. package/src/layouts/history-list/DataSourceTab.vue +0 -61
@@ -32,12 +32,18 @@
32
32
  </component>
33
33
 
34
34
  <component
35
+ v-if="!disabledDataSource"
35
36
  :is="tabPaneComponent?.component || 'el-tab-pane'"
36
37
  v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
37
38
  >
38
- <DataSourceTab
39
+ <BucketTab
40
+ title="数据源"
41
+ prefix="ds"
39
42
  :buckets="dataSourceGroupsByTarget"
40
43
  :expanded="expanded"
44
+ :describe-group="describeDataSourceGroup"
45
+ :describe-step="describeDataSourceStep"
46
+ :is-step-diffable="isDataSourceStepDiffable"
41
47
  @toggle="toggleGroup"
42
48
  @goto="onDataSourceGoto"
43
49
  @goto-initial="onDataSourceGotoInitial"
@@ -47,12 +53,18 @@
47
53
  </component>
48
54
 
49
55
  <component
56
+ v-if="!disabledCodeBlock"
50
57
  :is="tabPaneComponent?.component || 'el-tab-pane'"
51
58
  v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
52
59
  >
53
- <CodeBlockTab
60
+ <BucketTab
61
+ title="代码块"
62
+ prefix="cb"
54
63
  :buckets="codeBlockGroupsByTarget"
55
64
  :expanded="expanded"
65
+ :describe-group="describeCodeBlockGroup"
66
+ :describe-step="describeCodeBlockStep"
67
+ :is-step-diffable="isCodeBlockStepDiffable"
56
68
  @toggle="toggleGroup"
57
69
  @goto="onCodeBlockGoto"
58
70
  @goto-initial="onCodeBlockGotoInitial"
@@ -60,6 +72,15 @@
60
72
  @revert-step="onCodeBlockRevert"
61
73
  />
62
74
  </component>
75
+
76
+ <component
77
+ v-for="tab in extraTabs"
78
+ :key="tab.name"
79
+ :is="tabPaneComponent?.component || 'el-tab-pane'"
80
+ v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
81
+ >
82
+ <component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
83
+ </component>
63
84
  </TMagicTabs>
64
85
  </div>
65
86
 
@@ -74,7 +95,12 @@
74
95
  </template>
75
96
  </TMagicPopover>
76
97
 
77
- <HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" />
98
+ <HistoryDiffDialog
99
+ ref="diffDialog"
100
+ :extend-state="extendFormState"
101
+ :on-confirm="onConfirmRevert"
102
+ @close="onDiffDialogClose"
103
+ />
78
104
  </template>
79
105
 
80
106
  <script lang="ts" setup>
@@ -95,10 +121,11 @@
95
121
  * 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
96
122
  * 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
97
123
  *
98
- * 各 tab 的内容拆分为独立的 SFCPageTab / DataSourceTab / CodeBlockTab),
124
+ * 各 tab 的内容拆分为独立的 SFC:页面用 PageTab,数据源 / 代码块复用通用的 BucketTab
125
+ * (通过 title / prefix / describe* / isStepDiffable 注入差异)。
99
126
  * 共享的描述生成与折叠状态在 composables.ts 中维护。
100
127
  */
101
- import { inject, markRaw, ref, useTemplateRef } from 'vue';
128
+ import { computed, inject, markRaw, ref, shallowRef, useTemplateRef, watch } from 'vue';
102
129
  import { Clock, Close } from '@element-plus/icons-vue';
103
130
 
104
131
  import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
@@ -106,10 +133,16 @@ import type { FormState } from '@tmagic/form';
106
133
 
107
134
  import MIcon from '@editor/components/Icon.vue';
108
135
  import { useServices } from '@editor/hooks/use-services';
136
+ import type { CodeBlockStepValue, DataSourceStepValue, DiffDialogPayload, HistoryListExtraTab } from '@editor/type';
109
137
 
110
- import CodeBlockTab from './CodeBlockTab.vue';
111
- import { useHistoryList } from './composables';
112
- import DataSourceTab from './DataSourceTab.vue';
138
+ import BucketTab from './BucketTab.vue';
139
+ import {
140
+ describeCodeBlockGroup,
141
+ describeCodeBlockStep,
142
+ describeDataSourceGroup,
143
+ describeDataSourceStep,
144
+ useHistoryList,
145
+ } from './composables';
113
146
  import HistoryDiffDialog from './HistoryDiffDialog.vue';
114
147
  import PageTab from './PageTab.vue';
115
148
 
@@ -119,14 +152,36 @@ defineOptions({
119
152
 
120
153
  const ClockIcon = markRaw(Clock);
121
154
  const CloseIcon = markRaw(Close);
122
- const activeTab = ref<'page' | 'data-source' | 'code-block'>('page');
155
+ const activeTab = ref<string>('page');
123
156
 
124
157
  /** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
125
158
  const visible = ref(false);
126
159
 
127
160
  const tabPaneComponent = getDesignConfig('components')?.tabPane;
128
161
 
129
- const { editorService, dataSourceService, codeBlockService, historyService } = useServices();
162
+ /**
163
+ * 业务方自定义的扩展 tab,由 Editor 顶层通过 `historyListExtraTabs` 注入。
164
+ * 追加在内置「页面 / 数据源 / 代码块」三个 tab 之后,未提供时为空数组。
165
+ */
166
+ const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
167
+
168
+ /** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
169
+ const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
170
+
171
+ const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
172
+
173
+ /**
174
+ * 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
175
+ * 禁用后对应的历史记录 tab 不再展示。若当前激活的 tab 恰好被禁用,则回退到「页面」tab。
176
+ */
177
+ const disabledDataSource = computed(() => propsService.getDisabledDataSource());
178
+ const disabledCodeBlock = computed(() => propsService.getDisabledCodeBlock());
179
+
180
+ watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
181
+ if ((activeTab.value === 'data-source' && dsDisabled) || (activeTab.value === 'code-block' && cbDisabled)) {
182
+ activeTab.value = 'page';
183
+ }
184
+ });
130
185
 
131
186
  /**
132
187
  * 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
@@ -149,6 +204,12 @@ const {
149
204
  codeBlockGroupsByTarget,
150
205
  } = useHistoryList();
151
206
 
207
+ /** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
208
+ const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
209
+
210
+ /** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
211
+ const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
212
+
152
213
  /** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
153
214
  const indexToCursor = (index: number) => index + 1;
154
215
 
@@ -180,40 +241,25 @@ const onCodeBlockGotoInitial = (id: string | number) => {
180
241
  codeBlockService.goto(id, 0);
181
242
  };
182
243
 
183
- /**
184
- * 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
185
- * 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
186
- */
187
- const onPageRevert = (index: number) => {
188
- editorService.revertPageStep(index);
189
- };
190
-
191
- const onDataSourceRevert = (id: string | number, index: number) => {
192
- dataSourceService.revert(id, index);
193
- };
194
-
195
- const onCodeBlockRevert = (id: string | number, index: number) => {
196
- codeBlockService.revert(id, index);
197
- };
198
-
199
244
  const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
200
245
 
201
246
  /**
202
- * 页面 step 差异:仅 update 单节点修改可对比,传入旧/新节点。
247
+ * 构造页面 step 的差异弹窗入参:仅 update 单节点修改可对比,传入旧/新节点。
203
248
  * 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
204
249
  * `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
250
+ * 无可对比内容(如多节点 / add / remove)时返回 null。
205
251
  */
206
- const onPageDiff = (index: number) => {
252
+ const buildPageDiffPayload = (index: number): DiffDialogPayload | null => {
207
253
  const groups = historyService.getPageHistoryGroups();
208
254
  for (const group of groups) {
209
255
  const entry = group.steps.find((s) => s.index === index);
210
256
  if (!entry) continue;
211
257
  const item = entry.step.updatedItems?.[0];
212
- if (!item?.oldNode || !item?.newNode) return;
258
+ if (!item?.oldNode || !item?.newNode) return null;
213
259
  const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
214
260
  const nodeId = item.newNode.id ?? item.oldNode.id;
215
261
  const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
216
- diffDialogRef.value?.open({
262
+ return {
217
263
  category: 'node',
218
264
  type,
219
265
  lastValue: item.oldNode as Record<string, any>,
@@ -221,21 +267,35 @@ const onPageDiff = (index: number) => {
221
267
  currentValue: (currentNode as Record<string, any>) || null,
222
268
  targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
223
269
  id: nodeId,
224
- });
225
- return;
270
+ };
226
271
  }
272
+ return null;
227
273
  };
228
274
 
229
- const onDataSourceDiff = (id: string | number, index: number) => {
230
- const groups = historyService.getDataSourceHistoryGroups();
275
+ /**
276
+ * 在指定分组列表中按 id / index 查找命中的 step,命中后交由 build 构造差异弹窗入参。
277
+ * 用于统一数据源、代码块两类历史的查找逻辑。
278
+ */
279
+ const findGroupStep = <G extends { id: string | number; steps: { index: number; step: any }[] }>(
280
+ groups: G[],
281
+ id: string | number,
282
+ index: number,
283
+ build: (_step: G['steps'][number]['step']) => DiffDialogPayload | null,
284
+ ): DiffDialogPayload | null => {
231
285
  for (const group of groups) {
232
286
  if (group.id !== id) continue;
233
287
  const entry = group.steps.find((s) => s.index === index);
234
288
  if (!entry) continue;
235
- const { oldSchema, newSchema } = entry.step;
236
- if (!oldSchema || !newSchema) return;
289
+ return build(entry.step);
290
+ }
291
+ return null;
292
+ };
293
+
294
+ const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
295
+ findGroupStep(historyService.getDataSourceHistoryGroups(), id, index, ({ oldSchema, newSchema }) => {
296
+ if (!oldSchema || !newSchema) return null;
237
297
  const currentSchema = dataSourceService.getDataSourceById(`${id}`);
238
- diffDialogRef.value?.open({
298
+ return {
239
299
  category: 'data-source',
240
300
  type: newSchema.type || oldSchema.type || 'base',
241
301
  lastValue: oldSchema as Record<string, any>,
@@ -243,29 +303,78 @@ const onDataSourceDiff = (id: string | number, index: number) => {
243
303
  currentValue: (currentSchema as Record<string, any>) || null,
244
304
  targetLabel: newSchema.title || oldSchema.title || `${id}`,
245
305
  id,
246
- });
247
- return;
248
- }
249
- };
306
+ };
307
+ });
250
308
 
251
- const onCodeBlockDiff = (id: string | number, index: number) => {
252
- const groups = historyService.getCodeBlockHistoryGroups();
253
- for (const group of groups) {
254
- if (group.id !== id) continue;
255
- const entry = group.steps.find((s) => s.index === index);
256
- if (!entry) continue;
257
- const { oldContent, newContent } = entry.step;
258
- if (!oldContent || !newContent) return;
309
+ const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
310
+ findGroupStep(historyService.getCodeBlockHistoryGroups(), id, index, ({ oldContent, newContent }) => {
311
+ if (!oldContent || !newContent) return null;
259
312
  const currentContent = codeBlockService.getCodeContentById(id);
260
- diffDialogRef.value?.open({
313
+ return {
261
314
  category: 'code-block',
262
315
  lastValue: oldContent as Record<string, any>,
263
316
  value: newContent as Record<string, any>,
264
317
  currentValue: (currentContent as Record<string, any>) || null,
265
318
  targetLabel: newContent.name || oldContent.name || `${id}`,
266
319
  id,
267
- });
268
- return;
320
+ };
321
+ });
322
+
323
+ const onPageDiff = (index: number) => {
324
+ const payload = buildPageDiffPayload(index);
325
+ if (payload) diffDialogRef.value?.open(payload);
326
+ };
327
+
328
+ const onDataSourceDiff = (id: string | number, index: number) => {
329
+ const payload = buildDataSourceDiffPayload(id, index);
330
+ if (payload) diffDialogRef.value?.open(payload);
331
+ };
332
+
333
+ const onCodeBlockDiff = (id: string | number, index: number) => {
334
+ const payload = buildCodeBlockDiffPayload(id, index);
335
+ if (payload) diffDialogRef.value?.open(payload);
336
+ };
337
+
338
+ const onConfirmRevert = shallowRef();
339
+
340
+ /**
341
+ * 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
342
+ * 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
343
+ *
344
+ * 交互:先弹出该步骤的差异弹窗供用户确认,点击「确定回滚」后再真正执行回滚;
345
+ * 对没有可对比内容的步骤(如 add / remove / 多节点更新)则直接回滚。
346
+ */
347
+ const onPageRevert = (index: number) => {
348
+ const payload = buildPageDiffPayload(index);
349
+ onConfirmRevert.value = () => editorService.revertPageStep(index);
350
+ if (payload) {
351
+ diffDialogRef.value?.open({ ...payload });
352
+ } else {
353
+ onConfirmRevert.value();
269
354
  }
270
355
  };
356
+
357
+ const onDataSourceRevert = (id: string | number, index: number) => {
358
+ const payload = buildDataSourceDiffPayload(id, index);
359
+ onConfirmRevert.value = () => dataSourceService.revert(id, index);
360
+ if (payload) {
361
+ diffDialogRef.value?.open({ ...payload });
362
+ } else {
363
+ onConfirmRevert.value();
364
+ }
365
+ };
366
+
367
+ const onCodeBlockRevert = (id: string | number, index: number) => {
368
+ const payload = buildCodeBlockDiffPayload(id, index);
369
+ onConfirmRevert.value = () => codeBlockService.revert(id, index);
370
+ if (payload) {
371
+ diffDialogRef.value?.open({ ...payload });
372
+ } else {
373
+ onConfirmRevert.value();
374
+ }
375
+ };
376
+
377
+ const onDiffDialogClose = () => {
378
+ onConfirmRevert.value = undefined;
379
+ };
271
380
  </script>
@@ -3,12 +3,17 @@
3
3
  class="m-editor-history-list-item m-editor-history-list-initial"
4
4
  :class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }"
5
5
  :title="isCurrent ? '当前已回到未修改的初始状态' : '点击回到未修改的初始状态'"
6
- @click="onClick"
7
6
  >
8
7
  <span class="m-editor-history-list-item-index" title="历史步骤编号 #0(未修改的初始状态)">#0</span>
9
8
  <span class="m-editor-history-list-item-op op-initial">初始</span>
10
9
  <span class="m-editor-history-list-item-desc">未修改的初始状态</span>
11
- <span v-if="isCurrent" class="m-editor-history-list-item-current">当前</span>
10
+ <span
11
+ v-if="gotoEnabled && !isCurrent"
12
+ class="m-editor-history-list-item-goto"
13
+ title="回到该记录"
14
+ @click.stop="onClick"
15
+ >回到</span
16
+ >
12
17
  </li>
13
18
  </template>
14
19
 
@@ -24,10 +29,16 @@ defineOptions({
24
29
  name: 'MEditorHistoryListInitialRow',
25
30
  });
26
31
 
27
- const props = defineProps<{
28
- /** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
29
- isCurrent: boolean;
30
- }>();
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ /** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
35
+ isCurrent: boolean;
36
+ gotoEnabled?: boolean;
37
+ }>(),
38
+ {
39
+ gotoEnabled: true,
40
+ },
41
+ );
31
42
 
32
43
  const emit = defineEmits<{
33
44
  /** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */
@@ -3,13 +3,15 @@
3
3
  <TMagicScrollbar v-else max-height="360px">
4
4
  <ul class="m-editor-history-list-ul">
5
5
  <GroupRow
6
- v-for="(group, gIdx) in list"
7
- :key="`pg-${gIdx}`"
8
- :group-key="`pg-${gIdx}`"
6
+ v-for="group in list"
7
+ :key="`pg-${group.steps[0]?.index}`"
8
+ :group-key="`pg-${group.steps[0]?.index}`"
9
9
  :applied="group.applied"
10
10
  :merged="group.steps.length > 1"
11
11
  :op-type="group.opType"
12
12
  :desc="describePageGroup(group)"
13
+ :time="formatHistoryTime(groupTimestamp(group))"
14
+ :time-title="formatHistoryFullTime(groupTimestamp(group))"
13
15
  :step-count="group.steps.length"
14
16
  :sub-steps="
15
17
  group.steps.map((s) => ({
@@ -19,10 +21,12 @@
19
21
  desc: describePageStep(s.step),
20
22
  diffable: isPageStepDiffable(s.step),
21
23
  revertable: s.applied,
24
+ time: formatHistoryTime(s.step.timestamp),
25
+ timeTitle: formatHistoryFullTime(s.step.timestamp),
22
26
  }))
23
27
  "
24
28
  :is-current="group.isCurrent"
25
- :expanded="!!expanded[`pg-${gIdx}`]"
29
+ :expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
26
30
  @toggle="(key: string) => $emit('toggle', key)"
27
31
  @goto="(index: number) => $emit('goto', index)"
28
32
  @diff-step="(index: number) => $emit('diff-step', index)"
@@ -44,7 +48,13 @@ import { TMagicScrollbar } from '@tmagic/design';
44
48
 
45
49
  import type { PageHistoryGroup, StepValue } from '@editor/type';
46
50
 
47
- import { describePageGroup, describePageStep } from './composables';
51
+ import {
52
+ describePageGroup,
53
+ describePageStep,
54
+ formatHistoryFullTime,
55
+ formatHistoryTime,
56
+ groupTimestamp,
57
+ } from './composables';
48
58
  import GroupRow from './GroupRow.vue';
49
59
  import InitialRow from './InitialRow.vue';
50
60
 
@@ -55,7 +65,11 @@ defineOptions({
55
65
  const props = defineProps<{
56
66
  /** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
57
67
  list: PageHistoryGroup[];
58
- /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `pg-${idx}` 作为 key。 */
68
+ /**
69
+ * 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
70
+ * 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
71
+ * 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
72
+ */
59
73
  expanded: Record<string, boolean>;
60
74
  }>();
61
75
 
@@ -1,5 +1,7 @@
1
1
  import { computed, reactive } from 'vue';
2
2
 
3
+ import { datetimeFormatter } from '@tmagic/form';
4
+
3
5
  import { useServices } from '@editor/hooks/use-services';
4
6
  import type {
5
7
  CodeBlockHistoryGroup,
@@ -22,7 +24,10 @@ import type {
22
24
  export const useHistoryList = () => {
23
25
  const { historyService } = useServices();
24
26
 
25
- /** 折叠状态:key 形如 `pg-${groupIdx}` / `ds-${id}-${groupIdx}` / `cb-${id}-${groupIdx}`。 */
27
+ /**
28
+ * 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
29
+ * 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
30
+ */
26
31
  const expanded = reactive<Record<string, boolean>>({});
27
32
  const toggleGroup = (key: string) => {
28
33
  expanded[key] = !expanded[key];
@@ -64,6 +69,32 @@ export const useHistoryList = () => {
64
69
  };
65
70
  };
66
71
 
72
+ /**
73
+ * 历史面板的时间展示:
74
+ * - 当天的记录只显示 `HH:mm:ss`;
75
+ * - 跨天的记录显示 `MM-DD HH:mm:ss`。
76
+ * 无时间戳(旧数据 / 未写入)时返回空串,UI 据此不渲染时间。
77
+ */
78
+ export const formatHistoryTime = (timestamp?: number): string => {
79
+ if (!timestamp) return '';
80
+ const isToday =
81
+ datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD') ===
82
+ (datetimeFormatter(new Date(), '', 'YYYY-MM-DD') as string);
83
+ return `${
84
+ isToday
85
+ ? datetimeFormatter(new Date(timestamp), '', 'HH:mm:ss')
86
+ : datetimeFormatter(new Date(timestamp), '', 'MM-DD HH:mm:ss')
87
+ }`;
88
+ };
89
+
90
+ /** 完整时间(含年份与秒),用于 title 悬浮提示。无时间戳时返回空串。 */
91
+ export const formatHistoryFullTime = (timestamp?: number): string =>
92
+ timestamp ? `${datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD HH:mm:ss')}` : '';
93
+
94
+ /** 取一组历史步骤里最后一步(最近一次)的时间戳,用于组头部展示。 */
95
+ export const groupTimestamp = (group: { steps: { step: { timestamp?: number } }[] }): number | undefined =>
96
+ group.steps[group.steps.length - 1]?.step.timestamp;
97
+
67
98
  export const opLabel = (op: HistoryOpType) => {
68
99
  switch (op) {
69
100
  case 'add':
@@ -254,6 +254,7 @@ class History extends BaseService {
254
254
  public push(state: StepValue, pageId?: Id): StepValue | null {
255
255
  const undoRedo = this.getUndoRedo(pageId);
256
256
  if (!undoRedo) return null;
257
+ if (state.timestamp === undefined) state.timestamp = Date.now();
257
258
  undoRedo.pushElement(state);
258
259
  // 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
259
260
  if (pageId === undefined || `${pageId}` === `${this.state.pageId}`) {
@@ -289,6 +290,7 @@ class History extends BaseService {
289
290
  newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
290
291
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
291
292
  historyDescription: payload.historyDescription,
293
+ timestamp: Date.now(),
292
294
  };
293
295
 
294
296
  this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
@@ -318,6 +320,7 @@ class History extends BaseService {
318
320
  newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
319
321
  changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
320
322
  historyDescription: payload.historyDescription,
323
+ timestamp: Date.now(),
321
324
  };
322
325
 
323
326
  this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
@@ -125,19 +125,19 @@
125
125
  &.is-merged {
126
126
  margin: 4px 0;
127
127
  padding: 4px 8px 6px;
128
- background-color: rgba(144, 105, 219, 0.06);
129
- border: 1px solid rgba(144, 105, 219, 0.18);
130
- border-left: 3px solid #9069db;
128
+ background-color: rgba(47, 84, 235, 0.06);
129
+ border: 1px solid rgba(47, 84, 235, 0.18);
130
+ border-left: 3px solid #2f54eb;
131
131
  border-radius: 4px;
132
132
 
133
133
  // 卡片本体已经有背景色,hover 状态以更深的同色提示交互
134
134
  &:hover {
135
- background-color: rgba(144, 105, 219, 0.1);
135
+ background-color: rgba(47, 84, 235, 0.1);
136
136
  }
137
137
 
138
138
  .m-editor-history-list-group-head {
139
139
  font-weight: 600;
140
- color: #5b3fa5;
140
+ color: #1d39c4;
141
141
  }
142
142
 
143
143
  // 已撤销态:整张卡片去色
@@ -169,7 +169,7 @@
169
169
  margin: 6px 0 0 6px;
170
170
  padding: 0;
171
171
  list-style: none;
172
- border-left: 1px dashed rgba(144, 105, 219, 0.45);
172
+ border-left: 1px dashed rgba(47, 84, 235, 0.45);
173
173
 
174
174
  li {
175
175
  display: flex;
@@ -185,7 +185,7 @@
185
185
  cursor: pointer;
186
186
 
187
187
  &:hover {
188
- background-color: rgba(144, 105, 219, 0.1);
188
+ background-color: rgba(47, 84, 235, 0.1);
189
189
  }
190
190
  }
191
191
 
@@ -222,6 +222,16 @@
222
222
  white-space: nowrap;
223
223
  }
224
224
 
225
+ // 操作时间:弱化展示,紧贴在描述之后、各操作按钮之前。
226
+ .m-editor-history-list-item-time {
227
+ flex: 0 0 auto;
228
+ color: #a8abb2;
229
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
230
+ font-size: 11px;
231
+ font-weight: 400; // 防止被合并组头部的粗体继承
232
+ white-space: nowrap;
233
+ }
234
+
225
235
  .m-editor-history-list-item-op {
226
236
  flex: 0 0 auto;
227
237
  padding: 0 6px;
@@ -240,7 +250,7 @@
240
250
  }
241
251
 
242
252
  &.op-update {
243
- background-color: #409eff;
253
+ background-color: #e6a23c;
244
254
  }
245
255
 
246
256
  &.op-initial {
@@ -279,7 +289,7 @@
279
289
  font-size: 10px;
280
290
  line-height: 16px;
281
291
  color: #fff;
282
- background-color: #9069db;
292
+ background-color: #2f54eb;
283
293
  font-weight: 500;
284
294
  letter-spacing: 0.2px;
285
295
  }
@@ -300,21 +310,39 @@
300
310
  }
301
311
  }
302
312
 
313
+ // 「跳转」按钮:将历史游标移动到该 step,替代原先点击整行跳转的交互。
314
+ // 使用与组卡片一致的紫色色系,与「查看差异」「回滚」区分开。
315
+ .m-editor-history-list-item-goto {
316
+ flex: 0 0 auto;
317
+ padding: 0 6px;
318
+ border-radius: 2px;
319
+ font-size: 10px;
320
+ line-height: 16px;
321
+ color: #606266;
322
+ background-color: rgba(96, 98, 102, 0.1);
323
+ cursor: pointer;
324
+ user-select: none;
325
+
326
+ &:hover {
327
+ background-color: rgba(96, 98, 102, 0.18);
328
+ }
329
+ }
330
+
303
331
  // 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。
304
- // 使用与「查看差异」不同的色调(橙黄),用来区分"可逆操作"与"只读对比"。
332
+ // 使用红色色调,强调其为"破坏性/可逆操作",与「查看差异」「跳转」区分开。
305
333
  .m-editor-history-list-item-revert {
306
334
  flex: 0 0 auto;
307
335
  padding: 0 6px;
308
336
  border-radius: 2px;
309
337
  font-size: 10px;
310
338
  line-height: 16px;
311
- color: #e6a23c;
312
- background-color: rgba(230, 162, 60, 0.12);
339
+ color: #f56c6c;
340
+ background-color: rgba(245, 108, 108, 0.12);
313
341
  cursor: pointer;
314
342
  user-select: none;
315
343
 
316
344
  &:hover {
317
- background-color: rgba(230, 162, 60, 0.25);
345
+ background-color: rgba(245, 108, 108, 0.25);
318
346
  }
319
347
  }
320
348
 
@@ -369,6 +397,17 @@
369
397
  flex-direction: column;
370
398
  }
371
399
 
400
+ .m-editor-history-diff-dialog-notice {
401
+ margin-bottom: 8px;
402
+ padding: 8px 12px;
403
+ background-color: #fdf6ec;
404
+ border: 1px solid #faecd8;
405
+ border-radius: 4px;
406
+ color: #e6a23c;
407
+ font-size: 13px;
408
+ line-height: 1.5;
409
+ }
410
+
372
411
  .m-editor-history-diff-dialog-header {
373
412
  display: flex;
374
413
  align-items: center;