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

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 +11 -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 +167 -0
  6. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
  7. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
  8. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
  9. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  10. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +2 -2
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
  12. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
  13. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
  14. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
  15. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  16. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
  17. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
  18. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
  19. package/dist/es/hooks/use-code-block-edit.js +2 -2
  20. package/dist/es/hooks/use-stage.js +15 -7
  21. package/dist/es/index.js +3 -1
  22. package/dist/es/initService.js +6 -6
  23. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
  24. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
  25. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
  26. package/dist/es/layouts/history-list/Bucket.js +5 -0
  27. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +84 -0
  28. package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
  29. package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
  30. package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
  31. package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +62 -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 +180 -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 +195 -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 +270 -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 +48 -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 +99 -0
  42. package/dist/es/layouts/history-list/composables.js +154 -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 +407 -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 +347 -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 +347 -1
  64. package/dist/tmagic-editor.umd.cjs +3165 -774
  65. package/package.json +7 -7
  66. package/src/Editor.vue +7 -0
  67. package/src/components/CodeBlockEditor.vue +20 -109
  68. package/src/components/CodeParams.vue +6 -0
  69. package/src/components/CompareForm.vue +225 -0
  70. package/src/editorProps.ts +2 -0
  71. package/src/fields/Code.vue +19 -3
  72. package/src/fields/CodeSelect.vue +17 -1
  73. package/src/fields/CodeSelectCol.vue +30 -2
  74. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
  75. package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
  76. package/src/fields/DataSourceFields.vue +12 -3
  77. package/src/fields/DataSourceMethodSelect.vue +4 -1
  78. package/src/fields/DataSourceMethods.vue +14 -4
  79. package/src/fields/DataSourceMocks.vue +13 -3
  80. package/src/fields/DataSourceSelect.vue +4 -1
  81. package/src/fields/EventSelect.vue +57 -8
  82. package/src/fields/KeyValue.vue +17 -4
  83. package/src/fields/UISelect.vue +5 -2
  84. package/src/hooks/use-code-block-edit.ts +5 -2
  85. package/src/hooks/use-stage.ts +23 -7
  86. package/src/index.ts +1 -0
  87. package/src/layouts/CodeEditor.vue +15 -0
  88. package/src/layouts/Framework.vue +10 -5
  89. package/src/layouts/NavMenu.vue +9 -0
  90. package/src/layouts/history-list/Bucket.vue +99 -0
  91. package/src/layouts/history-list/CodeBlockTab.vue +61 -0
  92. package/src/layouts/history-list/DataSourceTab.vue +61 -0
  93. package/src/layouts/history-list/GroupRow.vue +207 -0
  94. package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
  95. package/src/layouts/history-list/HistoryListPanel.vue +271 -0
  96. package/src/layouts/history-list/InitialRow.vue +41 -0
  97. package/src/layouts/history-list/PageTab.vue +94 -0
  98. package/src/layouts/history-list/composables.ts +195 -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 +452 -8
  106. package/src/services/props.ts +2 -2
  107. package/src/theme/history-list-panel.scss +423 -0
  108. package/src/theme/theme.scss +1 -0
  109. package/src/type.ts +179 -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 +2105 -810
  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
@@ -0,0 +1,61 @@
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="`cb-${bucket.id}`"
7
+ title="代码块"
8
+ :bucket-id="bucket.id"
9
+ prefix="cb"
10
+ :groups="bucket.groups"
11
+ :describe-group="describeCodeBlockGroup"
12
+ :describe-step="describeCodeBlockStep"
13
+ :is-step-diffable="isCodeBlockStepDiffable"
14
+ :expanded="expanded"
15
+ @toggle="(key: string) => $emit('toggle', key)"
16
+ @goto="(id: string | number, index: number) => $emit('goto', id, index)"
17
+ @goto-initial="(id: string | number) => $emit('goto-initial', id)"
18
+ @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
19
+ @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
20
+ />
21
+ </TMagicScrollbar>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import { TMagicScrollbar } from '@tmagic/design';
26
+
27
+ import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type';
28
+
29
+ import Bucket from './Bucket.vue';
30
+ import { describeCodeBlockGroup, describeCodeBlockStep } from './composables';
31
+
32
+ defineOptions({
33
+ name: 'MEditorHistoryListCodeBlockTab',
34
+ });
35
+
36
+ defineProps<{
37
+ /**
38
+ * 已按 codeBlock.id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
39
+ * 空数组时显示空态。
40
+ */
41
+ buckets: { id: string | number; groups: CodeBlockHistoryGroup[] }[];
42
+ /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `cb-${id}-${idx}` 作为 key。 */
43
+ expanded: Record<string, boolean>;
44
+ }>();
45
+
46
+ defineEmits<{
47
+ /** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
48
+ (_e: 'toggle', _key: string): void;
49
+ /** 透传 Bucket 的 goto 事件,携带 codeBlock id 与目标 step 索引。 */
50
+ (_e: 'goto', _codeBlockId: string | number, _index: number): void;
51
+ /** 透传 Bucket 的 goto-initial 事件,携带 codeBlock id(回到该代码块未修改时的状态)。 */
52
+ (_e: 'goto-initial', _codeBlockId: string | number): void;
53
+ /** 透传 Bucket 的 diff-step 事件,携带 codeBlock id 与 step 索引。 */
54
+ (_e: 'diff-step', _codeBlockId: string | number, _index: number): void;
55
+ /** 透传 Bucket 的 revert-step 事件,携带 codeBlock id 与 step 索引(类 git revert)。 */
56
+ (_e: 'revert-step', _codeBlockId: string | number, _index: number): void;
57
+ }>();
58
+
59
+ /** 仅 update(前后 content 都存在)时可查看差异。 */
60
+ const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
61
+ </script>
@@ -0,0 +1,61 @@
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="`ds-${bucket.id}`"
7
+ title="数据源"
8
+ :bucket-id="bucket.id"
9
+ prefix="ds"
10
+ :groups="bucket.groups"
11
+ :describe-group="describeDataSourceGroup"
12
+ :describe-step="describeDataSourceStep"
13
+ :is-step-diffable="isDataSourceStepDiffable"
14
+ :expanded="expanded"
15
+ @toggle="(key: string) => $emit('toggle', key)"
16
+ @goto="(id: string | number, index: number) => $emit('goto', id, index)"
17
+ @goto-initial="(id: string | number) => $emit('goto-initial', id)"
18
+ @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
19
+ @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
20
+ />
21
+ </TMagicScrollbar>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import { TMagicScrollbar } from '@tmagic/design';
26
+
27
+ import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type';
28
+
29
+ import Bucket from './Bucket.vue';
30
+ import { describeDataSourceGroup, describeDataSourceStep } from './composables';
31
+
32
+ defineOptions({
33
+ name: 'MEditorHistoryListDataSourceTab',
34
+ });
35
+
36
+ defineProps<{
37
+ /**
38
+ * 已按 dataSource.id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
39
+ * 空数组时显示空态。
40
+ */
41
+ buckets: { id: string | number; groups: DataSourceHistoryGroup[] }[];
42
+ /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `ds-${id}-${idx}` 作为 key。 */
43
+ expanded: Record<string, boolean>;
44
+ }>();
45
+
46
+ defineEmits<{
47
+ /** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
48
+ (_e: 'toggle', _key: string): void;
49
+ /** 透传 Bucket 的 goto 事件,携带 dataSource id 与目标 step 索引。 */
50
+ (_e: 'goto', _dataSourceId: string | number, _index: number): void;
51
+ /** 透传 Bucket 的 goto-initial 事件,携带 dataSource id(回到该数据源未修改时的状态)。 */
52
+ (_e: 'goto-initial', _dataSourceId: string | number): void;
53
+ /** 透传 Bucket 的 diff-step 事件,携带 dataSource id 与 step 索引。 */
54
+ (_e: 'diff-step', _dataSourceId: string | number, _index: number): void;
55
+ /** 透传 Bucket 的 revert-step 事件,携带 dataSource id 与 step 索引(类 git revert)。 */
56
+ (_e: 'revert-step', _dataSourceId: string | number, _index: number): void;
57
+ }>();
58
+
59
+ /** 仅 update(前后 schema 都存在)时可查看差异。 */
60
+ const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
61
+ </script>
@@ -0,0 +1,207 @@
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
+ <span v-if="isCurrent" class="m-editor-history-list-item-current">当前</span>
16
+ <span
17
+ v-if="!merged && headDiffable"
18
+ class="m-editor-history-list-item-diff"
19
+ title="查看修改差异"
20
+ @click.stop="onDiffClick(subSteps[0].index)"
21
+ >查看差异</span
22
+ >
23
+ <span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
24
+ <span
25
+ v-if="!merged && headRevertable"
26
+ class="m-editor-history-list-item-revert"
27
+ title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
28
+ @click.stop="onRevertClick(subSteps[0].index)"
29
+ >回滚</span
30
+ >
31
+ <span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
32
+ </div>
33
+
34
+ <ul v-if="merged && expanded" class="m-editor-history-list-substeps">
35
+ <li
36
+ v-for="s in subStepsDisplay"
37
+ :key="s.index"
38
+ :class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': !s.isCurrent }"
39
+ :title="s.isCurrent ? '当前所在记录' : '点击跳转到该记录'"
40
+ @click="onSubStepClick(s)"
41
+ >
42
+ <span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
43
+ <span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
44
+ <span v-if="s.isCurrent" class="m-editor-history-list-item-current">当前</span>
45
+ <span
46
+ v-if="s.diffable"
47
+ class="m-editor-history-list-item-diff"
48
+ title="查看修改差异"
49
+ @click.stop="onDiffClick(s.index)"
50
+ >查看差异</span
51
+ >
52
+ <span
53
+ v-if="s.revertable"
54
+ class="m-editor-history-list-item-revert"
55
+ title="将该步骤的修改作为一次新操作反向应用(不影响后续历史)"
56
+ @click.stop="onRevertClick(s.index)"
57
+ >回滚</span
58
+ >
59
+ </li>
60
+ </ul>
61
+ </li>
62
+ </template>
63
+
64
+ <script lang="ts" setup>
65
+ import { computed } from 'vue';
66
+
67
+ import type { HistoryOpType } from '@editor/type';
68
+
69
+ import { opLabel } from './composables';
70
+
71
+ defineOptions({
72
+ name: 'MEditorHistoryListGroupRow',
73
+ });
74
+
75
+ const props = defineProps<{
76
+ /** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */
77
+ groupKey: string;
78
+ /** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
79
+ applied: boolean;
80
+ /** 是否为合并组(即组内 step 数大于 1,由多次连续操作合并而来)。决定是否展示合并标记与可展开的子步列表。 */
81
+ merged: boolean;
82
+ /** 操作类型:`add` / `remove` / `update`,用于决定操作徽标的颜色和文案。 */
83
+ opType: HistoryOpType;
84
+ /** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
85
+ desc: string;
86
+ /** 组内的 step 总数,仅在 merged 为 true 时显示为 "合并 N 步"。 */
87
+ stepCount: number;
88
+ /** 子步列表,用于在展开状态下逐条展示每个 step 的索引、应用状态与描述文案。 */
89
+ subSteps: {
90
+ index: number;
91
+ applied: boolean;
92
+ desc: string;
93
+ isCurrent?: boolean;
94
+ diffable?: boolean;
95
+ /** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
96
+ revertable?: boolean;
97
+ }[];
98
+ /** 当前组是否处于展开状态。仅在 merged 为 true 时生效,控制子步列表是否渲染。 */
99
+ expanded: boolean;
100
+ /** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
101
+ isCurrent?: boolean;
102
+ }>();
103
+
104
+ const emit = defineEmits<{
105
+ /**
106
+ * 用户点击合并组头部时触发,携带 groupKey;上层用其切换 expanded 状态。
107
+ * 对单步组(非合并)头部点击不会发该事件——因为单步组没有"展开"的概念。
108
+ */
109
+ (_e: 'toggle', _key: string): void;
110
+ /**
111
+ * 用户希望跳转到该记录时触发,携带"目标 step 在所属栈中的索引"——上层据此计算目标 cursor (= index + 1)。
112
+ * 触发场景:
113
+ * - 单步组(merged=false)头部:取该唯一 step 的 index;
114
+ * - 子步条目:取该子步的 index。
115
+ * 合并组头部不再触发 goto,避免与展开/收起冲突;用户应展开后点具体子步精准跳转。
116
+ * 当前所在的步骤(isCurrent)始终不会触发 goto。
117
+ */
118
+ (_e: 'goto', _index: number): void;
119
+ /**
120
+ * 用户希望查看该 step 的修改差异(旧值 vs 新值)。
121
+ * 只在 step 满足"前后值都存在"(如 update / 数据源、代码块的 update)时由父级标记 `diffable=true`。
122
+ * payload 为该 step 在所属栈中的索引,由上层根据 index 取 step 内容并展示对比。
123
+ */
124
+ (_e: 'diff-step', _index: number): void;
125
+ /**
126
+ * 用户希望「回滚」该 step——把它的修改作为一次新操作反向应用(类 git revert)。
127
+ * payload 为该 step 在所属栈中的索引。仅在单步组头部(headRevertable)或合并组的可回滚子步上触发。
128
+ */
129
+ (_e: 'revert-step', _index: number): void;
130
+ }>();
131
+
132
+ /** 单步组:头部可点击 goto;合并组:头部可点击切换展开。当前组(isCurrent)的单步组头部不可点击。 */
133
+ const isHeadClickable = computed(() => {
134
+ if (props.merged) return true;
135
+ return !props.isCurrent;
136
+ });
137
+
138
+ const headTitle = computed(() => {
139
+ if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
140
+ if (props.isCurrent) return '当前所在记录';
141
+ return '点击跳转到该记录';
142
+ });
143
+
144
+ /**
145
+ * 头部点击行为分流:
146
+ * - 合并组:仅切换展开 / 收起,不触发 goto;
147
+ * - 单步组:跳转到该唯一步骤;当前组忽略点击。
148
+ */
149
+ const onHeadClick = () => {
150
+ if (props.merged) {
151
+ emit('toggle', props.groupKey);
152
+ return;
153
+ }
154
+ if (props.isCurrent) return;
155
+ if (!props.subSteps.length) return;
156
+ emit('goto', props.subSteps[0].index);
157
+ };
158
+
159
+ const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
160
+ if (s.isCurrent) return;
161
+ emit('goto', s.index);
162
+ };
163
+
164
+ /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
165
+ const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
166
+
167
+ /** 单步组头部是否展示"回滚"入口:要求该唯一子步本身可回滚(已应用)。 */
168
+ const headRevertable = computed(() => !props.merged && Boolean(props.subSteps[0]?.revertable));
169
+
170
+ /**
171
+ * 合并组展开后的子步渲染顺序:与外层分组列表保持一致——倒序展示(最新的子步在最上方)。
172
+ * 外层 page tab / bucket 都已对 groups 做了 reverse,子步沿用同样的视觉规则更直观。
173
+ * 注意:仅用于渲染,原 `subSteps` 保持时间正序,`headIndexLabel` 等基于首尾索引的展示语义不变。
174
+ */
175
+ const subStepsDisplay = computed(() => props.subSteps.slice().reverse());
176
+
177
+ /**
178
+ * 头部索引展示:
179
+ * - 单步组(merged=false):显示该唯一 step 的编号,如 `#5`;
180
+ * - 合并组:显示组内 step 的编号范围,如 `#3-#7`(首尾相同则退化为 `#5`)。
181
+ *
182
+ * 这里展示的是 step.index + 1(与子步列表 `#{{ s.index + 1 }}` 保持一致),从 1 起编号更符合直觉。
183
+ */
184
+ const headIndexLabel = computed(() => {
185
+ const list = props.subSteps;
186
+ if (!list.length) return '';
187
+ const first = list[0].index + 1;
188
+ const last = list[list.length - 1].index + 1;
189
+ if (!props.merged || first === last) return `#${first}`;
190
+ return `#${first}-#${last}`;
191
+ });
192
+
193
+ const headIndexTitle = computed(() => {
194
+ if (!props.merged) return `历史步骤编号 #${props.subSteps[0]?.index + 1}`;
195
+ return `合并了第 ${props.subSteps[0]?.index + 1} 至第 ${
196
+ props.subSteps[props.subSteps.length - 1]?.index + 1
197
+ } 共 ${props.subSteps.length} 条历史步骤`;
198
+ });
199
+
200
+ const onDiffClick = (index: number) => {
201
+ emit('diff-step', index);
202
+ };
203
+
204
+ const onRevertClick = (index: number) => {
205
+ emit('revert-step', index);
206
+ };
207
+ </script>
@@ -0,0 +1,212 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <TMagicDialog
4
+ v-model="visible"
5
+ class="m-editor-history-diff-dialog"
6
+ title="查看修改差异"
7
+ width="900px"
8
+ top="5vh"
9
+ destroy-on-close
10
+ append-to-body
11
+ >
12
+ <div v-if="payload" class="m-editor-history-diff-dialog-body">
13
+ <div class="m-editor-history-diff-dialog-header">
14
+ <span class="m-editor-history-diff-dialog-target">{{ targetText }}</span>
15
+ <div class="m-editor-history-diff-dialog-controls">
16
+ <TMagicRadioGroup v-model="viewMode" size="small" class="m-editor-history-diff-dialog-view">
17
+ <TMagicRadioButton value="form">表单对比</TMagicRadioButton>
18
+ <TMagicRadioButton value="code">源码对比</TMagicRadioButton>
19
+ </TMagicRadioGroup>
20
+
21
+ <TMagicRadioGroup v-model="mode" size="small" class="m-editor-history-diff-dialog-mode">
22
+ <TMagicRadioButton value="before">与修改前对比</TMagicRadioButton>
23
+ <TMagicRadioButton value="current" :disabled="!hasCurrent">与当前对比</TMagicRadioButton>
24
+ </TMagicRadioGroup>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="m-editor-history-diff-dialog-legend">
29
+ <TMagicTag size="small" type="danger">{{ leftLabel }}</TMagicTag>
30
+ <span class="m-editor-history-diff-dialog-arrow">→</span>
31
+ <TMagicTag size="small" type="success">{{ rightLabel }}</TMagicTag>
32
+ <span v-if="mode === 'current' && isSameAsCurrent" class="m-editor-history-diff-dialog-tip">
33
+ 当前值与该步修改后一致,无差异
34
+ </span>
35
+ </div>
36
+
37
+ <CompareForm
38
+ v-if="viewMode === 'form'"
39
+ :category="payload.category"
40
+ :type="payload.type"
41
+ :data-source-type="payload.dataSourceType"
42
+ :value="rightValue"
43
+ :last-value="leftValue"
44
+ :extend-state="extendState"
45
+ height="70vh"
46
+ />
47
+
48
+ <CodeEditor
49
+ v-else
50
+ type="diff"
51
+ language="json"
52
+ :init-values="leftValue"
53
+ :modified-values="rightValue"
54
+ :options="codeDiffOptions"
55
+ disabled-full-screen
56
+ height="70vh"
57
+ />
58
+ </div>
59
+
60
+ <template #footer>
61
+ <TMagicButton size="small" @click="visible = false">关闭</TMagicButton>
62
+ </template>
63
+ </TMagicDialog>
64
+ </Teleport>
65
+ </template>
66
+
67
+ <script lang="ts" setup>
68
+ import { computed, ref, watch } from 'vue';
69
+ import { isEqual } from 'lodash-es';
70
+
71
+ import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagicTag } from '@tmagic/design';
72
+ import type { FormState } from '@tmagic/form';
73
+
74
+ import CompareForm, { type CompareCategory } from '@editor/components/CompareForm.vue';
75
+ import CodeEditor from '@editor/layouts/CodeEditor.vue';
76
+
77
+ defineOptions({
78
+ name: 'MEditorHistoryDiffDialog',
79
+ });
80
+
81
+ defineProps<{
82
+ /**
83
+ * 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
84
+ * 透传给 CompareForm,从而让差异对比时表单 item 中依赖业务上下文的
85
+ * `display` / `disabled` 等 filterFunction 正常工作。
86
+ */
87
+ extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
88
+ }>();
89
+
90
+ /** 差异对话框的入参 */
91
+ export interface DiffDialogPayload {
92
+ /** 表单类别 */
93
+ category: CompareCategory;
94
+ /** 节点类型 / 数据源类型 */
95
+ type?: string;
96
+ /** 代码块场景下的数据源类型 */
97
+ dataSourceType?: string;
98
+ /** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
99
+ lastValue: Record<string, any>;
100
+ /** 该 step 修改后的值(newNode / newSchema / newContent) */
101
+ value: Record<string, any>;
102
+ /** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
103
+ currentValue?: Record<string, any> | null;
104
+ /** 用于标题展示的目标名称 */
105
+ targetLabel?: string;
106
+ /** 用于标题展示的目标 id */
107
+ id?: string | number;
108
+ }
109
+
110
+ /**
111
+ * 差异对比模式:
112
+ * - before:该步骤修改前 vs 该步骤修改后(默认行为,体现这一步带来的变化)
113
+ * - current:该步骤修改后 vs 当前最新值(用于查看「该步骤之后是否还被改过」)
114
+ */
115
+ type DiffMode = 'before' | 'current';
116
+
117
+ /**
118
+ * 展示形态:
119
+ * - form:以属性表单形式逐字段对比(默认,可读性更好)
120
+ * - code:以 JSON 源码形式做整体 diff(贴近"看原始数据差异",可看到表单未覆盖的字段)
121
+ */
122
+ type ViewMode = 'form' | 'code';
123
+
124
+ const visible = ref(false);
125
+ const payload = ref<DiffDialogPayload | null>(null);
126
+ const mode = ref<DiffMode>('before');
127
+ const viewMode = ref<ViewMode>('form');
128
+
129
+ /**
130
+ * 源码对比始终只读,关闭小地图,强制左右并排(side-by-side)展示。
131
+ *
132
+ * monaco diff 编辑器在宽度低于 `renderSideBySideInlineBreakpoint`(默认 900px)时
133
+ * 会自动退化为 inline(上下/单栏)视图。本弹窗宽度约 900px,去掉内边距后编辑器实际
134
+ * 宽度小于该阈值,会被切到 inline。这里通过 `useInlineViewWhenSpaceIsLimited: false`
135
+ * 关闭该自动降级,确保始终保持左右两栏对比。
136
+ */
137
+ const codeDiffOptions = {
138
+ readOnly: true,
139
+ tabSize: 2,
140
+ minimap: { enabled: false },
141
+ renderSideBySide: true,
142
+ useInlineViewWhenSpaceIsLimited: false,
143
+ scrollBeyondLastLine: false,
144
+ hideUnchangedRegions: {
145
+ enabled: true,
146
+ },
147
+ };
148
+
149
+ const hasCurrent = computed(() => payload.value?.currentValue !== undefined && payload.value?.currentValue !== null);
150
+
151
+ /** 左侧(旧/参照)值 */
152
+ const leftValue = computed<Record<string, any>>(() => {
153
+ if (!payload.value) return {};
154
+ if (mode.value === 'current') return payload.value.value;
155
+ return payload.value.lastValue;
156
+ });
157
+
158
+ /** 右侧(新/对比)值 */
159
+ const rightValue = computed<Record<string, any>>(() => {
160
+ if (!payload.value) return {};
161
+ if (mode.value === 'current') return payload.value.currentValue || {};
162
+ return payload.value.value;
163
+ });
164
+
165
+ const leftLabel = computed(() => (mode.value === 'current' ? '该步修改后' : '修改前'));
166
+ const rightLabel = computed(() => (mode.value === 'current' ? '当前' : '修改后'));
167
+
168
+ /** 「与当前对比」模式下,若当前值与该步修改后值相等,则展示提示 */
169
+ const isSameAsCurrent = computed(() => {
170
+ if (mode.value !== 'current' || !payload.value) return false;
171
+ return isEqual(payload.value.value, payload.value.currentValue);
172
+ });
173
+
174
+ const targetText = computed(() => {
175
+ if (!payload.value) return '';
176
+ const categoryText: Record<CompareCategory, string> = {
177
+ node: '节点',
178
+ 'data-source': '数据源',
179
+ 'code-block': '代码块',
180
+ };
181
+ const prefix = categoryText[payload.value.category] || '';
182
+ const label = payload.value.targetLabel || payload.value.type || '';
183
+ const { id } = payload.value;
184
+ const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
185
+ return [prefix, labelWithId].filter(Boolean).join(':');
186
+ });
187
+
188
+ const open = (p: DiffDialogPayload) => {
189
+ payload.value = p;
190
+ // 每次打开按需重置默认模式:有当前值时优先「与当前对比」更贴近"看现在差什么",否则回退到默认
191
+ mode.value = 'before';
192
+ // 默认回到表单对比形态,避免残留上一次选择的源码模式
193
+ viewMode.value = 'form';
194
+ visible.value = true;
195
+ };
196
+
197
+ const close = () => {
198
+ visible.value = false;
199
+ };
200
+
201
+ // 关闭后清理 payload,避免下一次打开时残留旧值闪现
202
+ watch(visible, (v) => {
203
+ if (!v) {
204
+ payload.value = null;
205
+ }
206
+ });
207
+
208
+ defineExpose({
209
+ open,
210
+ close,
211
+ });
212
+ </script>