@tmagic/editor 1.8.0-beta.1 → 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 (119) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +20 -2
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
  4. package/dist/es/components/CompareForm.js +5 -0
  5. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +171 -0
  6. package/dist/es/editorProps.js +2 -0
  7. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
  8. package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +2 -5
  9. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
  10. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
  11. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  12. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +2 -2
  13. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
  14. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
  15. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
  16. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
  17. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  18. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
  19. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
  20. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
  21. package/dist/es/hooks/use-code-block-edit.js +2 -2
  22. package/dist/es/hooks/use-stage.js +17 -8
  23. package/dist/es/index.js +5 -1
  24. package/dist/es/initService.js +6 -6
  25. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +6 -5
  26. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
  27. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
  28. package/dist/es/layouts/history-list/Bucket.js +5 -0
  29. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +103 -0
  30. package/dist/es/layouts/history-list/BucketTab.js +5 -0
  31. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +73 -0
  32. package/dist/es/layouts/history-list/GroupRow.js +5 -0
  33. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +198 -0
  34. package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
  35. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +245 -0
  36. package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
  37. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +340 -0
  38. package/dist/es/layouts/history-list/InitialRow.js +5 -0
  39. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +54 -0
  40. package/dist/es/layouts/history-list/PageTab.js +5 -0
  41. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +105 -0
  42. package/dist/es/layouts/history-list/composables.js +172 -0
  43. package/dist/es/layouts/sidebar/layer/use-node-status.js +1 -1
  44. package/dist/es/services/BaseService.js +31 -68
  45. package/dist/es/services/codeBlock.js +205 -8
  46. package/dist/es/services/componentList.js +2 -2
  47. package/dist/es/services/dataSource.js +217 -8
  48. package/dist/es/services/dep.js +21 -9
  49. package/dist/es/services/editor.js +350 -160
  50. package/dist/es/services/events.js +2 -2
  51. package/dist/es/services/history.js +410 -12
  52. package/dist/es/services/keybinding.js +2 -2
  53. package/dist/es/services/props.js +8 -8
  54. package/dist/es/services/stageOverlay.js +2 -2
  55. package/dist/es/services/storage.js +2 -2
  56. package/dist/es/services/ui.js +2 -2
  57. package/dist/es/style.css +379 -1
  58. package/dist/es/utils/code-block.js +122 -0
  59. package/dist/es/utils/content-menu.js +4 -5
  60. package/dist/es/utils/dep/worker.js +1 -1
  61. package/dist/es/utils/editor.js +2 -2
  62. package/dist/es/utils/undo-redo.js +20 -0
  63. package/dist/style.css +379 -1
  64. package/dist/tmagic-editor.umd.cjs +3331 -787
  65. package/package.json +7 -7
  66. package/src/Editor.vue +15 -0
  67. package/src/components/CodeBlockEditor.vue +20 -109
  68. package/src/components/CodeParams.vue +6 -0
  69. package/src/components/CompareForm.vue +238 -0
  70. package/src/editorProps.ts +9 -0
  71. package/src/fields/Code.vue +19 -3
  72. package/src/fields/CodeLink.vue +2 -5
  73. package/src/fields/CodeSelect.vue +17 -1
  74. package/src/fields/CodeSelectCol.vue +30 -2
  75. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
  76. package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
  77. package/src/fields/DataSourceFields.vue +12 -3
  78. package/src/fields/DataSourceMethodSelect.vue +4 -1
  79. package/src/fields/DataSourceMethods.vue +14 -4
  80. package/src/fields/DataSourceMocks.vue +13 -3
  81. package/src/fields/DataSourceSelect.vue +4 -1
  82. package/src/fields/EventSelect.vue +57 -8
  83. package/src/fields/KeyValue.vue +17 -4
  84. package/src/fields/UISelect.vue +5 -2
  85. package/src/hooks/use-code-block-edit.ts +5 -2
  86. package/src/hooks/use-stage.ts +24 -7
  87. package/src/index.ts +3 -0
  88. package/src/layouts/CodeEditor.vue +17 -5
  89. package/src/layouts/Framework.vue +10 -5
  90. package/src/layouts/NavMenu.vue +9 -0
  91. package/src/layouts/history-list/Bucket.vue +124 -0
  92. package/src/layouts/history-list/BucketTab.vue +77 -0
  93. package/src/layouts/history-list/GroupRow.vue +242 -0
  94. package/src/layouts/history-list/HistoryDiffDialog.vue +231 -0
  95. package/src/layouts/history-list/HistoryListPanel.vue +380 -0
  96. package/src/layouts/history-list/InitialRow.vue +52 -0
  97. package/src/layouts/history-list/PageTab.vue +108 -0
  98. package/src/layouts/history-list/composables.ts +226 -0
  99. package/src/layouts/sidebar/layer/use-node-status.ts +9 -6
  100. package/src/services/BaseService.ts +35 -86
  101. package/src/services/codeBlock.ts +256 -5
  102. package/src/services/dataSource.ts +257 -6
  103. package/src/services/dep.ts +32 -9
  104. package/src/services/editor.ts +454 -171
  105. package/src/services/history.ts +455 -8
  106. package/src/services/props.ts +2 -2
  107. package/src/theme/history-list-panel.scss +462 -0
  108. package/src/theme/theme.scss +1 -0
  109. package/src/type.ts +269 -6
  110. package/src/utils/code-block.ts +150 -0
  111. package/src/utils/content-menu.ts +8 -5
  112. package/src/utils/dep/worker.ts +5 -3
  113. package/src/utils/index.ts +1 -0
  114. package/src/utils/undo-redo.ts +23 -0
  115. package/types/index.d.ts +2274 -819
  116. package/dist/es/utils/compose.js +0 -44
  117. package/dist/es/utils/editor-history.js +0 -100
  118. package/src/utils/compose.ts +0 -52
  119. package/src/utils/editor-history.ts +0 -138
@@ -19,6 +19,7 @@ import { NodeType } from '@tmagic/core';
19
19
  import { useServices } from '@editor/hooks/use-services';
20
20
  import { ColumnLayout, MenuBarData, MenuButton, MenuComponent, MenuItem } from '@editor/type';
21
21
 
22
+ import HistoryListPanel from './history-list/HistoryListPanel.vue';
22
23
  import NavMenuColumn from './NavMenuColumn.vue';
23
24
 
24
25
  defineOptions({
@@ -103,6 +104,14 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
103
104
  handler: () => editorService.redo(),
104
105
  });
105
106
  break;
107
+ case 'history-list':
108
+ // 历史记录面板:以 component 形式挂入,自带 popover;点击 nav 上的图标弹出。
109
+ config.push({
110
+ type: 'component',
111
+ className: 'history-list',
112
+ component: markRaw(HistoryListPanel),
113
+ });
114
+ break;
106
115
  case 'zoom-in':
107
116
  config.push({
108
117
  type: 'button',
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <div class="m-editor-history-list-bucket">
3
+ <div class="m-editor-history-list-bucket-title">
4
+ <span>{{ title }}</span>
5
+ <code>{{ String(bucketId) }}</code>
6
+ <span class="m-editor-history-list-bucket-count">{{ groups.length }} 组</span>
7
+ </div>
8
+
9
+ <ul class="m-editor-history-list-ul">
10
+ <GroupRow
11
+ v-for="group in groups"
12
+ :key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
13
+ :group-key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
14
+ :applied="group.applied"
15
+ :merged="group.steps.length > 1"
16
+ :op-type="group.opType"
17
+ :desc="describeGroup(group)"
18
+ :time="formatHistoryTime(groupTimestamp(group))"
19
+ :time-title="formatHistoryFullTime(groupTimestamp(group))"
20
+ :step-count="group.steps.length"
21
+ :sub-steps="
22
+ group.steps.map((s: any) => ({
23
+ index: s.index,
24
+ applied: s.applied,
25
+ isCurrent: s.isCurrent,
26
+ desc: describeStep(s.step),
27
+ diffable: isStepDiffable ? isStepDiffable(s.step) : false,
28
+ revertable: s.applied,
29
+ time: formatHistoryTime(s.step.timestamp),
30
+ timeTitle: formatHistoryFullTime(s.step.timestamp),
31
+ }))
32
+ "
33
+ :is-current="group.isCurrent"
34
+ :expanded="!!expanded[`${prefix}-${bucketId}-${group.steps[0]?.index}`]"
35
+ :goto-enabled="gotoEnabled"
36
+ @toggle="(key: string) => $emit('toggle', key)"
37
+ @goto="(index: number) => $emit('goto', bucketId, index)"
38
+ @diff-step="(index: number) => $emit('diff-step', bucketId, index)"
39
+ @revert-step="(index: number) => $emit('revert-step', bucketId, index)"
40
+ />
41
+ <!--
42
+ 初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
43
+ 当 bucket 内所有 group 都未 applied 时即为当前位置。
44
+ showInitial=false 时不展示(用于没有"撤销到初始状态"语义的自定义历史,如业务模块历史)。
45
+ -->
46
+ <InitialRow
47
+ v-if="showInitial !== false"
48
+ :is-current="isInitial"
49
+ :goto-enabled="gotoEnabled"
50
+ @goto-initial="$emit('goto-initial', bucketId)"
51
+ />
52
+ </ul>
53
+ </div>
54
+ </template>
55
+
56
+ <script lang="ts" setup>
57
+ import { computed } from 'vue';
58
+
59
+ import type { HistoryOpType } from '@editor/type';
60
+
61
+ import { formatHistoryFullTime, formatHistoryTime, groupTimestamp } from './composables';
62
+ import GroupRow from './GroupRow.vue';
63
+ import InitialRow from './InitialRow.vue';
64
+
65
+ defineOptions({
66
+ name: 'MEditorHistoryListBucket',
67
+ });
68
+
69
+ const props = withDefaults(
70
+ defineProps<{
71
+ /** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
72
+ title: string;
73
+ /** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
74
+ bucketId: string | number;
75
+ /**
76
+ * 子项 key 的命名空间前缀:内置 `ds` 表示数据源,`cb` 表示代码块;
77
+ * 业务方复用 Bucket 时可传入自定义前缀(如 `mod`)。与上层折叠状态 key 保持一致。
78
+ */
79
+ prefix: string;
80
+ /** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false 关闭。 */
81
+ showInitial?: boolean;
82
+ /** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
83
+ groups: {
84
+ applied: boolean;
85
+ isCurrent?: boolean;
86
+ opType: HistoryOpType;
87
+ steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
88
+ }[];
89
+ /** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
90
+ describeGroup: (_group: any) => string;
91
+ /** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
92
+ describeStep: (_step: any) => string;
93
+ /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
94
+ isStepDiffable?: (_step: any) => boolean;
95
+ /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
96
+ expanded: Record<string, boolean>;
97
+ /** 是否支持「跳转到该记录」(goto)。默认 true。 */
98
+ gotoEnabled?: boolean;
99
+ }>(),
100
+ {
101
+ showInitial: true,
102
+ gotoEnabled: true,
103
+ },
104
+ );
105
+
106
+ defineEmits<{
107
+ /** 透传子组件 GroupRow 的 toggle,由上层 panel 更新 expanded。 */
108
+ (_e: 'toggle', _key: string): void;
109
+ /**
110
+ * 透传子组件 GroupRow 的 goto,并附带当前 bucket 对应的 id(dataSourceId / codeBlockId),
111
+ * 上层据此调用对应 service.goto(id, targetCursor)。
112
+ */
113
+ (_e: 'goto', _bucketId: string | number, _index: number): void;
114
+ /** 用户点击初始项希望该 bucket 回到未修改状态;携带 bucketId 用于上层路由到正确的 service。 */
115
+ (_e: 'goto-initial', _bucketId: string | number): void;
116
+ /** 用户点击"查看差异",携带 bucketId 与 step 索引。 */
117
+ (_e: 'diff-step', _bucketId: string | number, _index: number): void;
118
+ /** 用户点击"回滚"按钮,携带 bucketId 与 step 索引,类 git revert。 */
119
+ (_e: 'revert-step', _bucketId: string | number, _index: number): void;
120
+ }>();
121
+
122
+ /** 该 bucket 是否处于初始状态(栈 cursor=0),等价于全部 group 都未 applied。 */
123
+ const isInitial = computed(() => props.groups.length > 0 && props.groups.every((g) => !g.applied));
124
+ </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
3
+ <TMagicScrollbar v-else max-height="360px">
4
+ <Bucket
5
+ v-for="bucket in buckets"
6
+ :key="`${prefix}-${bucket.id}`"
7
+ :title="title"
8
+ :bucket-id="bucket.id"
9
+ :prefix="prefix"
10
+ :groups="bucket.groups"
11
+ :describe-group="describeGroup"
12
+ :describe-step="describeStep"
13
+ :is-step-diffable="isStepDiffable"
14
+ :expanded="expanded"
15
+ :goto-enabled="gotoEnabled"
16
+ @toggle="(key: string) => $emit('toggle', key)"
17
+ @goto="(id: string | number, index: number) => $emit('goto', id, index)"
18
+ @goto-initial="(id: string | number) => $emit('goto-initial', id)"
19
+ @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
20
+ @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
21
+ />
22
+ </TMagicScrollbar>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ import { TMagicScrollbar } from '@tmagic/design';
27
+
28
+ import Bucket from './Bucket.vue';
29
+
30
+ defineOptions({
31
+ name: 'MEditorHistoryListBucketTab',
32
+ });
33
+
34
+ withDefaults(
35
+ defineProps<{
36
+ /** bucket 头部展示的标题,例如 "数据源" / "代码块"。 */
37
+ title: string;
38
+ /** 子项 key 的命名空间前缀(`ds` 数据源 / `cb` 代码块),与上层折叠状态 key 保持一致。 */
39
+ prefix: string;
40
+ /**
41
+ * 已按目标 id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
42
+ * 空数组时显示空态。
43
+ */
44
+ buckets: { id: string | number; groups: any[] }[];
45
+ /** 组级描述文案生成器,由父组件按业务类型注入。 */
46
+ describeGroup: (_group: any) => string;
47
+ /** 单步描述文案生成器,由父组件按业务类型注入。 */
48
+ describeStep: (_step: any) => string;
49
+ /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
50
+ isStepDiffable: (_step: any) => boolean;
51
+ /**
52
+ * 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
53
+ * 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
54
+ * 这样历史数据更新后已展开的分组状态仍能正确保持。
55
+ */
56
+ expanded: Record<string, boolean>;
57
+ /** 是否支持「跳转到该记录」(goto),透传给 Bucket。默认 true。 */
58
+ gotoEnabled?: boolean;
59
+ }>(),
60
+ {
61
+ gotoEnabled: true,
62
+ },
63
+ );
64
+
65
+ defineEmits<{
66
+ /** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
67
+ (_e: 'toggle', _key: string): void;
68
+ /** 透传 Bucket 的 goto 事件,携带目标 id 与目标 step 索引。 */
69
+ (_e: 'goto', _targetId: string | number, _index: number): void;
70
+ /** 透传 Bucket 的 goto-initial 事件,携带目标 id(回到该目标未修改时的状态)。 */
71
+ (_e: 'goto-initial', _targetId: string | number): void;
72
+ /** 透传 Bucket 的 diff-step 事件,携带目标 id 与 step 索引。 */
73
+ (_e: 'diff-step', _targetId: string | number, _index: number): void;
74
+ /** 透传 Bucket 的 revert-step 事件,携带目标 id 与 step 索引(类 git revert)。 */
75
+ (_e: 'revert-step', _targetId: string | number, _index: number): void;
76
+ }>();
77
+ </script>
@@ -0,0 +1,242 @@
1
+ <template>
2
+ <li
3
+ class="m-editor-history-list-item m-editor-history-list-group"
4
+ :class="{ 'is-undone': !applied, 'is-merged': merged, 'is-current': isCurrent }"
5
+ >
6
+ <div
7
+ class="m-editor-history-list-group-head"
8
+ :class="{ 'is-clickable': isHeadClickable }"
9
+ :title="headTitle"
10
+ @click="onHeadClick"
11
+ >
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>
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>
19
+
20
+ <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
26
+ >
27
+ <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
33
+ >
34
+ <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
40
+ >
41
+ <span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
42
+ </div>
43
+
44
+ <ul v-if="merged && expanded" class="m-editor-history-list-substeps">
45
+ <li
46
+ v-for="s in subStepsDisplay"
47
+ :key="s.index"
48
+ :class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent }"
49
+ :title="subStepTitle(s)"
50
+ >
51
+ <span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
52
+ <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>
54
+ <span
55
+ v-if="s.revertable"
56
+ class="m-editor-history-list-item-revert"
57
+ title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
58
+ @click.stop="onRevertClick(s.index)"
59
+ >回滚</span
60
+ >
61
+ <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
74
+ >
75
+ </li>
76
+ </ul>
77
+ </li>
78
+ </template>
79
+
80
+ <script lang="ts" setup>
81
+ import { computed } from 'vue';
82
+
83
+ import type { HistoryOpType } from '@editor/type';
84
+
85
+ import { opLabel } from './composables';
86
+
87
+ defineOptions({
88
+ name: 'MEditorHistoryListGroupRow',
89
+ });
90
+
91
+ const props = withDefaults(
92
+ 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
+ expanded: boolean;
125
+ /** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
126
+ isCurrent?: boolean;
127
+ /**
128
+ * 是否支持「跳转到该记录」(goto)。默认 true。
129
+ * 为 false 时:单步组头部与子步条目都不再可点击跳转、也不会触发 goto 事件,
130
+ * 仅保留合并组头部的展开 / 收起能力,以及查看差异、回滚等其它入口。
131
+ */
132
+ gotoEnabled?: boolean;
133
+ }>(),
134
+ {
135
+ isCurrent: false,
136
+ gotoEnabled: true,
137
+ },
138
+ );
139
+
140
+ const emit = defineEmits<{
141
+ /**
142
+ * 用户点击合并组头部时触发,携带 groupKey;上层用其切换 expanded 状态。
143
+ * 对单步组(非合并)头部点击不会发该事件——因为单步组没有"展开"的概念。
144
+ */
145
+ (_e: 'toggle', _key: string): void;
146
+ /**
147
+ * 用户希望跳转到该记录时触发,携带"目标 step 在所属栈中的索引"——上层据此计算目标 cursor (= index + 1)。
148
+ * 触发场景:
149
+ * - 单步组(merged=false)头部:取该唯一 step 的 index;
150
+ * - 子步条目:取该子步的 index。
151
+ * 合并组头部不再触发 goto,避免与展开/收起冲突;用户应展开后点具体子步精准跳转。
152
+ * 当前所在的步骤(isCurrent)始终不会触发 goto。
153
+ */
154
+ (_e: 'goto', _index: number): void;
155
+ /**
156
+ * 用户希望查看该 step 的修改差异(旧值 vs 新值)。
157
+ * 只在 step 满足"前后值都存在"(如 update / 数据源、代码块的 update)时由父级标记 `diffable=true`。
158
+ * payload 为该 step 在所属栈中的索引,由上层根据 index 取 step 内容并展示对比。
159
+ */
160
+ (_e: 'diff-step', _index: number): void;
161
+ /**
162
+ * 用户希望「回滚」该 step——把它的修改作为一次新操作反向应用(类 git revert)。
163
+ * payload 为该 step 在所属栈中的索引。仅在单步组头部(headRevertable)或合并组的可回滚子步上触发。
164
+ */
165
+ (_e: 'revert-step', _index: number): void;
166
+ }>();
167
+
168
+ /**
169
+ * 仅合并组头部可点击(切换展开 / 收起);
170
+ * 单步组的跳转改由头部的「回退」按钮触发,整行不再可点击。
171
+ */
172
+ const isHeadClickable = computed(() => props.merged);
173
+
174
+ const headTitle = computed(() => {
175
+ if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
176
+ if (props.isCurrent) return '当前所在记录';
177
+ return '';
178
+ });
179
+
180
+ /**
181
+ * 头部点击行为:仅合并组切换展开 / 收起;单步组不再响应整行点击。
182
+ */
183
+ const onHeadClick = () => {
184
+ if (props.merged) {
185
+ emit('toggle', props.groupKey);
186
+ }
187
+ };
188
+
189
+ const onGotoClick = (index: number) => {
190
+ if (!props.gotoEnabled) return;
191
+ emit('goto', index);
192
+ };
193
+
194
+ const subStepTitle = (s: { isCurrent?: boolean }) => {
195
+ if (s.isCurrent) return '当前所在记录';
196
+ return '';
197
+ };
198
+
199
+ /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
200
+ const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
201
+
202
+ /** 单步组头部是否展示"回滚"入口:要求该唯一子步本身可回滚(已应用)。 */
203
+ const headRevertable = computed(() => !props.merged && Boolean(props.subSteps[0]?.revertable));
204
+
205
+ /**
206
+ * 合并组展开后的子步渲染顺序:与外层分组列表保持一致——倒序展示(最新的子步在最上方)。
207
+ * 外层 page tab / bucket 都已对 groups 做了 reverse,子步沿用同样的视觉规则更直观。
208
+ * 注意:仅用于渲染,原 `subSteps` 保持时间正序,`headIndexLabel` 等基于首尾索引的展示语义不变。
209
+ */
210
+ const subStepsDisplay = computed(() => props.subSteps.slice().reverse());
211
+
212
+ /**
213
+ * 头部索引展示:
214
+ * - 单步组(merged=false):显示该唯一 step 的编号,如 `#5`;
215
+ * - 合并组:显示组内 step 的编号范围,如 `#3-#7`(首尾相同则退化为 `#5`)。
216
+ *
217
+ * 这里展示的是 step.index + 1(与子步列表 `#{{ s.index + 1 }}` 保持一致),从 1 起编号更符合直觉。
218
+ */
219
+ const headIndexLabel = computed(() => {
220
+ const list = props.subSteps;
221
+ if (!list.length) return '';
222
+ const first = list[0].index + 1;
223
+ const last = list[list.length - 1].index + 1;
224
+ if (!props.merged || first === last) return `#${first}`;
225
+ return `#${first}-#${last}`;
226
+ });
227
+
228
+ 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} 条历史步骤`;
233
+ });
234
+
235
+ const onDiffClick = (index: number) => {
236
+ emit('diff-step', index);
237
+ };
238
+
239
+ const onRevertClick = (index: number) => {
240
+ emit('revert-step', index);
241
+ };
242
+ </script>