@tmagic/editor 1.8.0-beta.6 → 1.8.0-beta.8

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 (42) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +10 -6
  2. package/dist/es/index.js +6 -4
  3. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +2 -2
  4. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +38 -26
  5. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +5 -1
  6. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +163 -310
  7. package/dist/es/layouts/history-list/composables.js +47 -128
  8. package/dist/es/layouts/history-list/useHistoryList.js +56 -0
  9. package/dist/es/layouts/history-list/useHistoryRevert.js +304 -0
  10. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +2 -2
  11. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +3 -3
  12. package/dist/es/services/codeBlock.js +32 -28
  13. package/dist/es/services/dataSource.js +43 -34
  14. package/dist/es/services/editor.js +31 -26
  15. package/dist/es/services/history.js +268 -384
  16. package/dist/es/style.css +12 -0
  17. package/dist/es/utils/editor.js +2 -2
  18. package/dist/es/utils/history.js +35 -47
  19. package/dist/style.css +12 -0
  20. package/dist/tmagic-editor.umd.cjs +3808 -3090
  21. package/package.json +7 -7
  22. package/src/components/CompareForm.vue +14 -6
  23. package/src/index.ts +2 -0
  24. package/src/layouts/NavMenu.vue +2 -2
  25. package/src/layouts/history-list/GroupRow.vue +10 -0
  26. package/src/layouts/history-list/HistoryDiffDialog.vue +6 -1
  27. package/src/layouts/history-list/HistoryListPanel.vue +60 -270
  28. package/src/layouts/history-list/PageTab.vue +4 -4
  29. package/src/layouts/history-list/composables.ts +82 -180
  30. package/src/layouts/history-list/useHistoryList.ts +60 -0
  31. package/src/layouts/history-list/useHistoryRevert.ts +400 -0
  32. package/src/layouts/sidebar/Sidebar.vue +2 -2
  33. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +5 -4
  34. package/src/services/codeBlock.ts +30 -29
  35. package/src/services/dataSource.ts +37 -37
  36. package/src/services/editor.ts +32 -29
  37. package/src/services/history.ts +340 -431
  38. package/src/theme/history-list-panel.scss +14 -0
  39. package/src/type.ts +179 -100
  40. package/src/utils/editor.ts +2 -2
  41. package/src/utils/history.ts +52 -74
  42. package/types/index.d.ts +435 -309
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.6",
2
+ "version": "1.8.0-beta.8",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -58,11 +58,11 @@
58
58
  "moveable": "^0.53.0",
59
59
  "serialize-javascript": "^7.0.0",
60
60
  "sortablejs": "^1.15.6",
61
- "@tmagic/design": "1.8.0-beta.6",
62
- "@tmagic/stage": "1.8.0-beta.6",
63
- "@tmagic/form": "1.8.0-beta.6",
64
- "@tmagic/table": "1.8.0-beta.6",
65
- "@tmagic/utils": "1.8.0-beta.6"
61
+ "@tmagic/design": "1.8.0-beta.8",
62
+ "@tmagic/form": "1.8.0-beta.8",
63
+ "@tmagic/utils": "1.8.0-beta.8",
64
+ "@tmagic/stage": "1.8.0-beta.8",
65
+ "@tmagic/table": "1.8.0-beta.8"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -76,7 +76,7 @@
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vue": "^3.5.34",
79
- "@tmagic/core": "1.8.0-beta.6"
79
+ "@tmagic/core": "1.8.0-beta.8"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -17,14 +17,14 @@
17
17
  </template>
18
18
 
19
19
  <script lang="ts" setup>
20
- import { computed, inject, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
20
+ import { computed, inject, provide, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
21
21
  import { isEqual } from 'lodash-es';
22
22
 
23
23
  import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
24
24
  import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
25
25
 
26
26
  import { useServices } from '@editor/hooks/use-services';
27
- import type { CompareCategory, CompareFormLoadConfig } from '@editor/type';
27
+ import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
28
28
  import { getCodeBlockFormConfig } from '@editor/utils/code-block';
29
29
 
30
30
  defineOptions({
@@ -67,15 +67,19 @@ const props = withDefaults(
67
67
  * `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。返回的 config 直接用于对比展示。
68
68
  */
69
69
  loadConfig?: CompareFormLoadConfig;
70
+ /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
71
+ services: Services;
70
72
  }>(),
71
73
  {
72
74
  category: 'node',
73
75
  labelWidth: '120px',
76
+ services: () => useServices(),
74
77
  },
75
78
  );
76
79
 
77
- const { propsService, dataSourceService, codeBlockService, editorService } = useServices();
78
- const services = useServices();
80
+ const { propsService, dataSourceService, codeBlockService, editorService } = props.services;
81
+
82
+ provide('services', props.services);
79
83
 
80
84
  const config = ref<FormConfig>([]);
81
85
 
@@ -185,7 +189,11 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
185
189
  );
186
190
  }
187
191
  case 'data-source': {
188
- return dataSourceService.getFormConfig(props.type || 'base');
192
+ const config = dataSourceService.getFormConfig(props.type || 'base');
193
+ // 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
194
+ // 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
195
+ // 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
196
+ return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
189
197
  }
190
198
  case 'code-block': {
191
199
  return getCodeBlockFormConfig({
@@ -242,7 +250,7 @@ const stage = computed(() => editorService.get('stage'));
242
250
  watchEffect(() => {
243
251
  if (formRef.value) {
244
252
  formRef.value.formState.stage = stage.value;
245
- formRef.value.formState.services = services;
253
+ formRef.value.formState.services = props.services;
246
254
  }
247
255
  });
248
256
 
package/src/index.ts CHANGED
@@ -74,6 +74,8 @@ export { default as CompareForm } from './components/CompareForm.vue';
74
74
  export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
75
75
  export { default as HistoryListBucketTab } from './layouts/history-list/BucketTab.vue';
76
76
  export { default as HistoryDiffDialog } from './layouts/history-list/HistoryDiffDialog.vue';
77
+ export { confirmHistoryAction } from './layouts/history-list/composables';
78
+ export { useHistoryRevert } from './layouts/history-list/useHistoryRevert';
77
79
  export { default as FloatingBox } from './components/FloatingBox.vue';
78
80
  export { default as Tree } from './components/Tree.vue';
79
81
  export { default as TreeNode } from './components/TreeNode.vue';
@@ -90,7 +90,7 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
90
90
  className: 'undo',
91
91
  icon: markRaw(Back),
92
92
  tooltip: `后退(${ctrl}+z)`,
93
- disabled: () => !historyService.state.canUndo,
93
+ disabled: () => !historyService.canUndo('page', editorService.get('page')?.id),
94
94
  handler: () => editorService.undo(),
95
95
  });
96
96
  break;
@@ -100,7 +100,7 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
100
100
  className: 'redo',
101
101
  icon: markRaw(Right),
102
102
  tooltip: `前进(${ctrl}+Shift+z)`,
103
- disabled: () => !historyService.state.canRedo,
103
+ disabled: () => !historyService.canRedo('page', editorService.get('page')?.id),
104
104
  handler: () => editorService.redo(),
105
105
  });
106
106
  break;
@@ -49,6 +49,13 @@
49
49
  >{{ sourceLabel(group.source) }}</span
50
50
  >
51
51
 
52
+ <span
53
+ v-if="!merged && group.operator"
54
+ class="m-editor-history-list-item-operator"
55
+ :title="`操作人:${group.operator}`"
56
+ >{{ group.operator }}</span
57
+ >
58
+
52
59
  <span
53
60
  v-if="!merged && group.time"
54
61
  class="m-editor-history-list-item-time"
@@ -103,6 +110,9 @@
103
110
  :title="`操作途径:${sourceLabel(s.source)}`"
104
111
  >{{ sourceLabel(s.source) }}</span
105
112
  >
113
+ <span v-if="s.operator" class="m-editor-history-list-item-operator" :title="`操作人:${s.operator}`">{{
114
+ s.operator
115
+ }}</span>
106
116
  <span v-if="s.time" class="m-editor-history-list-item-time" :title="s.timeTitle || s.time">{{ s.time }}</span>
107
117
  </li>
108
118
  </ul>
@@ -46,6 +46,7 @@
46
46
  :extend-state="extendState"
47
47
  :load-config="loadConfig"
48
48
  :self-diff-field-types="selfDiffFieldTypes"
49
+ :services="props.services"
49
50
  height="70vh"
50
51
  />
51
52
 
@@ -79,8 +80,9 @@ import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagic
79
80
  import type { FormState } from '@tmagic/form';
80
81
 
81
82
  import CompareForm from '@editor/components/CompareForm.vue';
83
+ import { useServices } from '@editor/hooks/use-services';
82
84
  import CodeEditor from '@editor/layouts/CodeEditor.vue';
83
- import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload } from '@editor/type';
85
+ import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload, Services } from '@editor/type';
84
86
 
85
87
  defineOptions({
86
88
  name: 'MEditorHistoryDiffDialog',
@@ -88,6 +90,8 @@ defineOptions({
88
90
 
89
91
  const props = withDefaults(
90
92
  defineProps<{
93
+ /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
94
+ services: Services;
91
95
  /**
92
96
  * 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
93
97
  * 透传给 CompareForm,从而让差异对比时表单 item 中依赖业务上下文的
@@ -105,6 +109,7 @@ const props = withDefaults(
105
109
  selfDiffFieldTypes?: string[];
106
110
  }>(),
107
111
  {
112
+ services: () => useServices(),
108
113
  width: '900px',
109
114
  },
110
115
  );
@@ -18,7 +18,7 @@
18
18
  <TMagicTabs v-model="activeTab" class="m-editor-history-list-tabs">
19
19
  <component
20
20
  :is="tabPaneComponent?.component || 'el-tab-pane'"
21
- v-bind="tabPaneComponent?.props({ name: 'page', label: `页面 (${pageGroups.length})` }) || {}"
21
+ v-bind="tabPaneComponent?.props({ name: 'page', label: `${pageName} (${pageGroups.length})` }) || {}"
22
22
  >
23
23
  <PageTab
24
24
  :list="pageGroupsDisplay"
@@ -37,7 +37,10 @@
37
37
  <component
38
38
  v-if="!disabledDataSource"
39
39
  :is="tabPaneComponent?.component || 'el-tab-pane'"
40
- v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
40
+ v-bind="
41
+ tabPaneComponent?.props({ name: 'data-source', label: `${dataSourceName} (${dataSourceGroups.length})` }) ||
42
+ {}
43
+ "
41
44
  >
42
45
  <BucketTab
43
46
  :config="dataSourceConfig"
@@ -55,7 +58,9 @@
55
58
  <component
56
59
  v-if="!disabledCodeBlock"
57
60
  :is="tabPaneComponent?.component || 'el-tab-pane'"
58
- v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
61
+ v-bind="
62
+ tabPaneComponent?.props({ name: 'code-block', label: `${codeBlockName} (${codeBlockGroups.length})` }) || {}
63
+ "
59
64
  >
60
65
  <BucketTab
61
66
  :config="codeBlockConfig"
@@ -91,9 +96,6 @@
91
96
  </TMagicTooltip>
92
97
  </template>
93
98
  </TMagicPopover>
94
-
95
- <HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" />
96
- <HistoryDiffDialog ref="confirmDialog" :is-confirm="true" :extend-state="extendFormState" />
97
99
  </template>
98
100
 
99
101
  <script lang="ts" setup>
@@ -118,42 +120,27 @@
118
120
  * (通过 title / prefix / describe* / isStepDiffable 注入差异)。
119
121
  * 共享的描述生成与折叠状态在 composables.ts 中维护。
120
122
  */
121
- import { computed, inject, markRaw, ref, useTemplateRef, watch } from 'vue';
123
+ import { computed, inject, markRaw, ref, watch } from 'vue';
122
124
  import { Clock, Close } from '@element-plus/icons-vue';
123
125
 
124
- import {
125
- getDesignConfig,
126
- TMagicButton,
127
- tMagicMessage,
128
- tMagicMessageBox,
129
- TMagicPopover,
130
- TMagicTabs,
131
- TMagicTooltip,
132
- } from '@tmagic/design';
126
+ import { getDesignConfig, TMagicButton, tMagicMessage, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
133
127
  import type { FormState } from '@tmagic/form';
134
128
 
135
129
  import MIcon from '@editor/components/Icon.vue';
136
130
  import { useServices } from '@editor/hooks/use-services';
137
131
  import type {
132
+ BaseStepValue,
138
133
  CodeBlockStepValue,
139
134
  DataSourceStepValue,
140
- DiffDialogPayload,
141
135
  HistoryBucketConfig,
142
136
  HistoryListExtraTab,
143
137
  } from '@editor/type';
144
138
 
145
139
  import BucketTab from './BucketTab.vue';
146
- import {
147
- describeCodeBlockGroup,
148
- describeCodeBlockStep,
149
- describeDataSourceGroup,
150
- describeDataSourceStep,
151
- isCodeBlockStepRevertable,
152
- isDataSourceStepRevertable,
153
- useHistoryList,
154
- } from './composables';
155
- import HistoryDiffDialog from './HistoryDiffDialog.vue';
140
+ import { confirmHistoryAction, describeStep, isSingleDiffStepRevertable } from './composables';
156
141
  import PageTab from './PageTab.vue';
142
+ import { useHistoryList } from './useHistoryList';
143
+ import { useHistoryRevert } from './useHistoryRevert';
157
144
 
158
145
  defineOptions({
159
146
  name: 'MEditorHistoryListPanel',
@@ -180,8 +167,9 @@ const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
180
167
  /** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
181
168
  const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
182
169
 
170
+ const services = useServices();
183
171
  const { editorService, dataSourceService, codeBlockService, historyService, propsService, stageOverlayService } =
184
- useServices();
172
+ services;
185
173
 
186
174
  /**
187
175
  * 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
@@ -221,37 +209,42 @@ const {
221
209
  * 当前活动页的「加载/初始」标记记录(设置 root 时生成),透传给 PageTab 的底部初始行展示。
222
210
  * 基于 historyService 的 reactive state 派生,活动页切换或标记写入后自动刷新。
223
211
  */
224
- const pageMarker = computed(() => historyService.getPageMarker());
212
+ const pageMarker = computed(() => historyService.getMarker('page', editorService.get('page')?.id));
225
213
 
226
- /** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
227
- const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
228
- Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
214
+ /**
215
+ * 各历史类型的展示名称(页面 / 数据源 / 代码块),由 historyService.state.stepNames 配置,
216
+ * 业务方可通过 historyService.setStepName / registerStepType 覆盖。基于 reactive state 派生,配置变更后自动刷新。
217
+ */
218
+ const pageName = computed(() => historyService.getStepName('page'));
219
+ const dataSourceName = computed(() => historyService.getStepName('dataSource'));
220
+ const codeBlockName = computed(() => historyService.getStepName('codeBlock'));
229
221
 
230
222
  /** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
231
- const isCodeBlockStepDiffable = (step: CodeBlockStepValue) =>
232
- Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
223
+ const isStepDiffable = (step: BaseStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
233
224
 
234
225
  /**
235
226
  * 数据源 / 代码块两类 bucket 历史的整体渲染配置:把 title / prefix 与各自的描述、
236
227
  * 可差异、可回滚判定收敛为单一对象整体注入 BucketTab,组件内部按需读取。
228
+ * title / 描述回退名取自可配置的展示名称,故用 computed 使其随 stepNames 变更刷新。
237
229
  */
238
- const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = {
239
- title: '数据源',
230
+ // 数据源/代码块不做相邻合并,每组恒为单步,省略 describeGroup,由 toRowGroup 回退到 describeStep。
231
+ const dataSourceConfig = computed<HistoryBucketConfig<DataSourceStepValue>>(() => ({
232
+ title: dataSourceName.value,
240
233
  prefix: 'ds',
241
- describeGroup: describeDataSourceGroup,
242
- describeStep: describeDataSourceStep,
243
- isStepDiffable: isDataSourceStepDiffable,
244
- isStepRevertable: isDataSourceStepRevertable,
245
- };
246
-
247
- const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = {
248
- title: '代码块',
234
+ describeStep: (step: DataSourceStepValue): string =>
235
+ describeStep(step, (schema) => schema?.title, dataSourceName.value),
236
+ isStepDiffable,
237
+ isStepRevertable: isSingleDiffStepRevertable,
238
+ }));
239
+
240
+ const codeBlockConfig = computed<HistoryBucketConfig<CodeBlockStepValue>>(() => ({
241
+ title: codeBlockName.value,
249
242
  prefix: 'cb',
250
- describeGroup: describeCodeBlockGroup,
251
- describeStep: describeCodeBlockStep,
252
- isStepDiffable: isCodeBlockStepDiffable,
253
- isStepRevertable: isCodeBlockStepRevertable,
254
- };
243
+ describeStep: (step: CodeBlockStepValue): string =>
244
+ describeStep(step, (content) => content?.name, codeBlockName.value),
245
+ isStepDiffable,
246
+ isStepRevertable: isSingleDiffStepRevertable,
247
+ }));
255
248
 
256
249
  /** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
257
250
  const indexToCursor = (index: number) => index + 1;
@@ -267,7 +260,7 @@ const onPageGoto = (index: number) => {
267
260
  * - 该 step 涉及的节点都已不存在(如删除记录、被撤销的新增)时给出提示,不做选中。
268
261
  */
269
262
  const onPageSelect = async (index: number) => {
270
- const step = historyService.getPageStepList()[index]?.step;
263
+ const step = historyService.getStepList('page', editorService.get('page')?.id)[index]?.step;
271
264
  if (!step) return;
272
265
  const targetId = (step.diff ?? [])
273
266
  .map((item) => item.newSchema?.id ?? item.oldSchema?.id)
@@ -307,222 +300,13 @@ const onCodeBlockGotoInitial = (id: string | number) => {
307
300
  codeBlockService.goto(id, 0);
308
301
  };
309
302
 
310
- const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
311
- const confirmDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('confirmDialog');
312
-
313
- /**
314
- * 三类历史(页面 / 数据源 / 代码块)差异弹窗入参的构造差异,收敛为一份配置:
315
- * 仅「分组来源、当前值读取、类型 / 展示名提取」不同,定位 step、校验前后值、组装 payload 的流程共用。
316
- */
317
- interface DiffPayloadSource {
318
- /** 表单类别:节点 / 数据源 / 代码块。 */
319
- category: DiffDialogPayload['category'];
320
- /** 该类别按时间正序的历史分组列表(含已撤销)。 */
321
- groups: () => { id?: string | number; steps: { index: number; step: { diff?: any[] } }[] }[];
322
- /** 读取目标当前实际值,用于「与当前对比」;不存在时返回空即禁用对比。 */
323
- getCurrent: (_id: string | number) => Record<string, any> | null | undefined;
324
- /** 由新/旧快照提取展示名(含各自的兜底,如节点回退 type、数据源 / 代码块回退 id)。 */
325
- resolveLabel: (_newSchema: Record<string, any>, _oldSchema: Record<string, any>, _id: string | number) => string;
326
- /** 由新/旧快照提取类型;代码块无 type 字段则不传。 */
327
- resolveType?: (_newSchema: Record<string, any>, _oldSchema: Record<string, any>) => string;
328
- }
329
-
330
- /**
331
- * 构造差异弹窗入参:仅 update(前后值都存在)可对比。
332
- * - 页面(无 id):在全部分组中按 index 定位 step,目标 id 取自快照;
333
- * - 数据源 / 代码块(带 id):先匹配分组 id 再按 index 定位。
334
- * 无可对比内容(多节点 / add / remove)或定位不到时返回 null。
335
- */
336
- const buildDiffPayload = (source: DiffPayloadSource, index: number, id?: string | number): DiffDialogPayload | null => {
337
- for (const group of source.groups()) {
338
- if (id !== undefined && group.id !== id) continue;
339
- const step = group.steps.find((s) => s.index === index)?.step;
340
- if (!step) continue;
341
- const oldSchema = step.diff?.[0]?.oldSchema as Record<string, any> | undefined;
342
- const newSchema = step.diff?.[0]?.newSchema as Record<string, any> | undefined;
343
- if (!oldSchema || !newSchema) return null;
344
- const targetId = id ?? newSchema.id ?? oldSchema.id;
345
- const type = source.resolveType?.(newSchema, oldSchema);
346
- return {
347
- category: source.category,
348
- ...(type !== undefined ? { type } : {}),
349
- lastValue: oldSchema,
350
- value: newSchema,
351
- currentValue: (targetId !== undefined ? source.getCurrent(targetId) : null) || null,
352
- targetLabel: source.resolveLabel(newSchema, oldSchema, targetId),
353
- id: targetId,
354
- };
355
- }
356
- return null;
357
- };
358
-
359
- const buildPageDiffPayload = (index: number): DiffDialogPayload | null =>
360
- buildDiffPayload(
361
- {
362
- category: 'node',
363
- groups: () => historyService.getPageHistoryGroups(),
364
- getCurrent: (id) => editorService.getNodeById(id) as Record<string, any> | null,
365
- resolveType: (n, o) => n.type || o.type || '',
366
- resolveLabel: (n, o) => n.name || o.name || n.type || o.type || '',
367
- },
368
- index,
369
- );
370
-
371
- const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
372
- buildDiffPayload(
373
- {
374
- category: 'data-source',
375
- groups: () => historyService.getDataSourceHistoryGroups(),
376
- getCurrent: (id) => dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
377
- resolveType: (n, o) => n.type || o.type || 'base',
378
- resolveLabel: (n, o, id) => n.title || o.title || `${id}`,
379
- },
380
- index,
381
- id,
382
- );
383
-
384
- const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
385
- buildDiffPayload(
386
- {
387
- category: 'code-block',
388
- groups: () => historyService.getCodeBlockHistoryGroups(),
389
- getCurrent: (id) => codeBlockService.getCodeContentById(id) as Record<string, any> | null,
390
- resolveLabel: (n, o, id) => n.name || o.name || `${id}`,
391
- },
392
- index,
393
- id,
394
- );
395
-
396
- const onPageDiff = (index: number) => {
397
- const payload = buildPageDiffPayload(index);
398
- if (payload) diffDialogRef.value?.open(payload);
399
- };
400
-
401
- const onDataSourceDiff = (id: string | number, index: number) => {
402
- const payload = buildDataSourceDiffPayload(id, index);
403
- if (payload) diffDialogRef.value?.open(payload);
404
- };
405
-
406
- const onCodeBlockDiff = (id: string | number, index: number) => {
407
- const payload = buildCodeBlockDiffPayload(id, index);
408
- if (payload) diffDialogRef.value?.open(payload);
409
- };
410
-
411
- /**
412
- * 「回滚」统一入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
413
- * 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
414
- *
415
- * 交互:
416
- * - 可差异对比的步骤(单节点 / 单实体 update):弹出差异弹窗供用户确认,点「确定回滚」再执行;
417
- * - 无法对比的步骤(add / remove / 多节点更新,payload 为 null):弹出普通二次确认框,确认后执行。
418
- *
419
- * 页面 / 数据源 / 代码块三类回滚仅「差异入参构造」与「实际 revert 调用」不同,
420
- * 由调用方分别传入 payload 与 revert,公共的弹窗 / 确认流程在此收敛。
421
- */
422
- const runRevert = (payload: DiffDialogPayload | null): Promise<boolean> => {
423
- if (payload && confirmDialogRef.value) {
424
- return confirmDialogRef.value.confirm(payload);
425
- }
426
- return confirmRevert();
427
- };
428
-
429
- /**
430
- * 回滚前置校验:若该历史步骤回滚所依赖的目标数据已被删除,则无法回滚。
431
- * - update(把旧值写回):被修改的目标必须仍存在;
432
- * - 页面 remove(还原被删节点):被删节点的原父容器必须仍存在,否则无处插回;
433
- * add(回滚即删除)即使目标已不在,也已达成「删除」目的,不视为失败。
434
- *
435
- * 命中时弹出「回滚失败」提示并返回 true,调用方据此中止本次回滚。
436
- */
437
- const isPageRevertTargetMissing = (index: number): boolean => {
438
- const step = historyService.getPageStepList()[index]?.step;
439
- if (!step) return false;
440
- if (step.opType === 'update') {
441
- return (step.diff ?? []).some((item) => {
442
- const id = item.newSchema?.id ?? item.oldSchema?.id;
443
- return id !== undefined && !editorService.getNodeById(id, false);
444
- });
445
- }
446
- if (step.opType === 'remove') {
447
- return (step.diff ?? []).some(
448
- (item) => item.parentId !== undefined && !editorService.getNodeById(item.parentId, false),
449
- );
450
- }
451
- return false;
452
- };
453
-
454
- /** 数据源 update 步骤回滚时,对应数据源必须仍存在(已删除则无处写回旧值)。 */
455
- const isDataSourceRevertTargetMissing = (id: string | number, index: number): boolean => {
456
- const step = historyService.getDataSourceStepList(id)[index]?.step;
457
- return Boolean(step && step.opType === 'update' && !dataSourceService.getDataSourceById(`${id}`));
458
- };
459
-
460
- /** 代码块 update 步骤回滚时,对应代码块必须仍存在(已删除则无处写回旧值)。 */
461
- const isCodeBlockRevertTargetMissing = (id: string | number, index: number): boolean => {
462
- const step = historyService.getCodeBlockStepList(id)[index]?.step;
463
- return Boolean(step && step.opType === 'update' && !codeBlockService.getCodeContentById(id));
464
- };
465
-
466
- /** 目标数据已被删除、无法回滚时的统一提示。 */
467
- const showRevertTargetMissing = () => {
468
- tMagicMessage.error('回滚失败:该记录对应的数据已被删除');
469
- };
470
-
471
- const onPageRevert = (index: number) => {
472
- if (isPageRevertTargetMissing(index)) {
473
- showRevertTargetMissing();
474
- return Promise.resolve(null);
475
- }
476
- return runRevert(buildPageDiffPayload(index)).then((result) => (result ? editorService.revertPageStep(index) : null));
477
- };
478
-
479
- const onDataSourceRevert = (id: string | number, index: number) => {
480
- if (isDataSourceRevertTargetMissing(id, index)) {
481
- showRevertTargetMissing();
482
- return Promise.resolve(null);
483
- }
484
- return runRevert(buildDataSourceDiffPayload(id, index)).then((result) =>
485
- result ? dataSourceService.revert(id, index) : null,
486
- );
487
- };
488
-
489
- const onCodeBlockRevert = (id: string | number, index: number) => {
490
- if (isCodeBlockRevertTargetMissing(id, index)) {
491
- showRevertTargetMissing();
492
- return Promise.resolve(null);
493
- }
494
- return runRevert(buildCodeBlockDiffPayload(id, index)).then((result) =>
495
- result ? codeBlockService.revert(id, index) : null,
496
- );
497
- };
498
-
499
- /**
500
- * 「回滚」二次确认:新增 / 删除 / 多节点更新等无法做差异对比的步骤,
501
- * 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
502
- * 用户取消时返回 false,调用方据此中止回滚。
503
- */
504
- const confirmRevert = (): Promise<boolean> =>
505
- confirmDialog(
506
- '确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。',
507
- );
508
-
509
303
  /**
510
- * 通用二次确认弹窗:清空历史 / 无法差异对比的回滚等会改变状态的操作,先弹出确认框,
511
- * 用户点击「确定」返回 true,取消(confirm reject)时返回 false 并静默忽略。
304
+ * 「单步回滚」与「查看差异」的完整逻辑收敛到 useHistoryRevert,面板与业务方共用:
305
+ * 二者均由 useHistoryRevert 内部按需动态挂载 HistoryDiffDialog,
306
+ * 业务方亦可直接 import useHistoryRevert(services) 调用,无需自行挂载任何弹窗。
512
307
  */
513
- const confirmDialog = async (message: string): Promise<boolean> => {
514
- try {
515
- await tMagicMessageBox.confirm(message, '提示', {
516
- confirmButtonText: '确定',
517
- cancelButtonText: '取消',
518
- type: 'warning',
519
- });
520
- return true;
521
- // eslint-disable-next-line no-unused-vars
522
- } catch (e) {
523
- return false;
524
- }
525
- };
308
+ const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
309
+ useHistoryRevert(services, { extendState: extendFormState });
526
310
 
527
311
  /**
528
312
  * 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
@@ -540,27 +324,33 @@ const syncIndexedDB = async () => {
540
324
 
541
325
  const onPageClear = async () => {
542
326
  if (
543
- await confirmDialog('确定清空当前页面的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
327
+ await confirmHistoryAction(
328
+ '确定清空当前页面的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。',
329
+ )
544
330
  ) {
545
- historyService.clearPage();
331
+ historyService.clear('page', editorService.get('page')?.id);
546
332
  await syncIndexedDB();
547
333
  }
548
334
  };
549
335
 
550
336
  const onDataSourceClear = async () => {
551
337
  if (
552
- await confirmDialog('确定清空数据源的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
338
+ await confirmHistoryAction(
339
+ '确定清空数据源的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。',
340
+ )
553
341
  ) {
554
- historyService.clearDataSource();
342
+ historyService.clear('dataSource');
555
343
  await syncIndexedDB();
556
344
  }
557
345
  };
558
346
 
559
347
  const onCodeBlockClear = async () => {
560
348
  if (
561
- await confirmDialog('确定清空代码块的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
349
+ await confirmHistoryAction(
350
+ '确定清空代码块的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。',
351
+ )
562
352
  ) {
563
- historyService.clearCodeBlock();
353
+ historyService.clear('codeBlock');
564
354
  await syncIndexedDB();
565
355
  }
566
356
  };
@@ -34,7 +34,7 @@ import { computed } from 'vue';
34
34
 
35
35
  import { TMagicScrollbar } from '@tmagic/design';
36
36
 
37
- import type { HistoryRowDescriptor, PageHistoryGroup, StepValue } from '@editor/type';
37
+ import type { HistoryGroup, HistoryRowDescriptor, StepValue } from '@editor/type';
38
38
 
39
39
  import type { HistoryRowGroup } from './composables';
40
40
  import {
@@ -53,7 +53,7 @@ defineOptions({
53
53
 
54
54
  const props = defineProps<{
55
55
  /** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
56
- list: PageHistoryGroup[];
56
+ list: HistoryGroup<StepValue>[];
57
57
  /**
58
58
  * 共享的折叠状态表(key -> 是否展开,缺省或 true 为展开、false 为收起),由顶层 panel 统一维护。
59
59
  * 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
@@ -105,9 +105,9 @@ const descriptor: HistoryRowDescriptor<StepValue> = {
105
105
  isStepRevertable: isPageStepRevertable,
106
106
  };
107
107
 
108
- const rowKey = (group: PageHistoryGroup) => `pg-${group.steps[0]?.index}`;
108
+ const rowKey = (group: HistoryGroup<StepValue>) => `pg-${group.steps[0]?.index}`;
109
109
 
110
- const toRow = (group: PageHistoryGroup): HistoryRowGroup => toRowGroup(group, rowKey(group), descriptor);
110
+ const toRow = (group: HistoryGroup<StepValue>): HistoryRowGroup => toRowGroup(group, rowKey(group), descriptor);
111
111
 
112
112
  /**
113
113
  * 是否处于"初始状态"——即对应页面历史栈 cursor===0: