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

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 (66) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +15 -1
  2. package/dist/es/hooks/use-code-block-edit.js +6 -3
  3. package/dist/es/hooks/use-data-source-edit.js +5 -2
  4. package/dist/es/hooks/use-stage.js +6 -3
  5. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  6. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +6 -2
  7. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +3 -0
  8. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +42 -29
  9. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +4 -2
  10. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  11. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +5 -2
  12. package/dist/es/layouts/history-list/composables.js +53 -1
  13. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  14. package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
  16. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  17. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  18. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  19. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  20. package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
  21. package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
  22. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
  23. package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
  24. package/dist/es/services/codeBlock.js +25 -10
  25. package/dist/es/services/dataSource.js +24 -10
  26. package/dist/es/services/editor.js +98 -46
  27. package/dist/es/services/history.js +2 -0
  28. package/dist/es/services/keybinding.js +3 -3
  29. package/dist/es/style.css +16 -4
  30. package/dist/es/utils/content-menu.js +17 -9
  31. package/dist/style.css +16 -4
  32. package/dist/tmagic-editor.umd.cjs +343 -144
  33. package/package.json +7 -7
  34. package/src/components/CompareForm.vue +19 -1
  35. package/src/hooks/use-code-block-edit.ts +4 -3
  36. package/src/hooks/use-data-source-edit.ts +2 -2
  37. package/src/hooks/use-stage.ts +3 -3
  38. package/src/layouts/NavMenu.vue +1 -1
  39. package/src/layouts/history-list/Bucket.vue +6 -2
  40. package/src/layouts/history-list/BucketTab.vue +3 -0
  41. package/src/layouts/history-list/GroupRow.vue +20 -3
  42. package/src/layouts/history-list/HistoryDiffDialog.vue +2 -1
  43. package/src/layouts/history-list/HistoryListPanel.vue +4 -0
  44. package/src/layouts/history-list/PageTab.vue +5 -1
  45. package/src/layouts/history-list/composables.ts +60 -0
  46. package/src/layouts/props-panel/PropsPanel.vue +5 -1
  47. package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
  48. package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
  49. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  50. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  51. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
  52. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  53. package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
  54. package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
  55. package/src/layouts/workspace/viewer/Stage.vue +1 -1
  56. package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
  57. package/src/services/codeBlock.ts +33 -9
  58. package/src/services/dataSource.ts +30 -8
  59. package/src/services/editor.ts +111 -34
  60. package/src/services/history.ts +7 -0
  61. package/src/services/keybinding.ts +3 -3
  62. package/src/theme/history-list-panel.scss +15 -1
  63. package/src/theme/props-panel.scss +3 -3
  64. package/src/type.ts +58 -2
  65. package/src/utils/content-menu.ts +18 -9
  66. package/types/index.d.ts +96 -25
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.3",
2
+ "version": "1.8.0-beta.4",
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.3",
62
- "@tmagic/form": "1.8.0-beta.3",
63
- "@tmagic/stage": "1.8.0-beta.3",
64
- "@tmagic/utils": "1.8.0-beta.3",
65
- "@tmagic/table": "1.8.0-beta.3"
61
+ "@tmagic/design": "1.8.0-beta.4",
62
+ "@tmagic/form": "1.8.0-beta.4",
63
+ "@tmagic/stage": "1.8.0-beta.4",
64
+ "@tmagic/table": "1.8.0-beta.4",
65
+ "@tmagic/utils": "1.8.0-beta.4"
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.3"
79
+ "@tmagic/core": "1.8.0-beta.4"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -152,6 +152,24 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
152
152
  return !isEqual(curValue, lastValue);
153
153
  };
154
154
 
155
+ const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
156
+ formConfig.map((item) => {
157
+ if (!('type' in item)) return item;
158
+ if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
159
+
160
+ return {
161
+ ...item,
162
+ items: item.items.map((tabPane) => {
163
+ if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
164
+
165
+ return {
166
+ ...tabPane,
167
+ display: true,
168
+ };
169
+ }),
170
+ };
171
+ });
172
+
155
173
  /**
156
174
  * 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
157
175
  * 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
@@ -162,7 +180,7 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
162
180
  if (!props.type) {
163
181
  return [];
164
182
  }
165
- return await propsService.getPropsConfig(props.type);
183
+ return removeStyleDisplayConfig(await propsService.getPropsConfig(props.type));
166
184
  }
167
185
  case 'data-source': {
168
186
  return dataSourceService.getFormConfig(props.type || 'base');
@@ -6,7 +6,7 @@ import { tMagicMessage } from '@tmagic/design';
6
6
  import type { ContainerChangeEventData } from '@tmagic/form';
7
7
 
8
8
  import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
9
- import type { Services } from '@editor/type';
9
+ import type { HistoryOpSource, Services } from '@editor/type';
10
10
 
11
11
  export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService']) => {
12
12
  const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
@@ -58,8 +58,8 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
58
58
  };
59
59
 
60
60
  // 删除代码块
61
- const deleteCode = async (key: string) => {
62
- codeBlockService.deleteCodeDslByIds([key]);
61
+ const deleteCode = async (key: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
62
+ codeBlockService.deleteCodeDslByIds([key], { historySource });
63
63
  };
64
64
 
65
65
  const submitCodeBlockHandler = async (values: CodeBlockContent, eventData?: ContainerChangeEventData) => {
@@ -67,6 +67,7 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
67
67
 
68
68
  await codeBlockService.setCodeDslById(codeId.value, values, {
69
69
  changeRecords: eventData?.changeRecords,
70
+ historySource: 'props',
70
71
  });
71
72
 
72
73
  codeBlockEditorRef.value?.hide();
@@ -27,9 +27,9 @@ export const useDataSourceEdit = (dataSourceService: Services['dataSourceService
27
27
 
28
28
  const submitDataSourceHandler = (value: DataSourceSchema, eventData: ContainerChangeEventData) => {
29
29
  if (value.id) {
30
- dataSourceService.update(value, { changeRecords: eventData.changeRecords });
30
+ dataSourceService.update(value, { changeRecords: eventData.changeRecords, historySource: 'props' });
31
31
  } else {
32
- dataSourceService.add(value);
32
+ dataSourceService.add(value, { historySource: 'props' });
33
33
  }
34
34
 
35
35
  editDialog.value?.hide();
@@ -130,16 +130,16 @@ export const useStage = (stageOptions: StageOptions) => {
130
130
  });
131
131
  if (configs.length === 0) return;
132
132
 
133
- editorService.update(configs, { changeRecordList });
133
+ editorService.update(configs, { changeRecordList, historySource: 'stage' });
134
134
  });
135
135
 
136
136
  stage.on('sort', (ev: SortEventData) => {
137
- editorService.sort(ev.src, ev.dist);
137
+ editorService.sort(ev.src, ev.dist, { historySource: 'stage' });
138
138
  });
139
139
 
140
140
  stage.on('remove', (ev: RemoveEventData) => {
141
141
  const nodes = ev.data.map(({ el }) => editorService.getNodeById(getIdFromEl()(el) || ''));
142
- editorService.remove(nodes.filter((node) => Boolean(node)) as MNode[]);
142
+ editorService.remove(nodes.filter((node) => Boolean(node)) as MNode[], { historySource: 'stage' });
143
143
  });
144
144
 
145
145
  stage.on('select-parent', () => {
@@ -80,7 +80,7 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
80
80
  disabled: () => editorService.get('node')?.type === NodeType.PAGE,
81
81
  handler: () => {
82
82
  const node = editorService.get('node');
83
- node && editorService.remove(node);
83
+ node && editorService.remove(node, { historySource: 'toolbar' });
84
84
  },
85
85
  });
86
86
  break;
@@ -15,6 +15,7 @@
15
15
  :merged="group.steps.length > 1"
16
16
  :op-type="group.opType"
17
17
  :desc="describeGroup(group)"
18
+ :source="groupSource(group)"
18
19
  :time="formatHistoryTime(groupTimestamp(group))"
19
20
  :time-title="formatHistoryFullTime(groupTimestamp(group))"
20
21
  :step-count="group.steps.length"
@@ -25,7 +26,8 @@
25
26
  isCurrent: s.isCurrent,
26
27
  desc: describeStep(s.step),
27
28
  diffable: isStepDiffable ? isStepDiffable(s.step) : false,
28
- revertable: s.applied,
29
+ revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
30
+ source: s.step.source,
29
31
  time: formatHistoryTime(s.step.timestamp),
30
32
  timeTitle: formatHistoryFullTime(s.step.timestamp),
31
33
  }))
@@ -58,7 +60,7 @@ import { computed } from 'vue';
58
60
 
59
61
  import type { HistoryOpType } from '@editor/type';
60
62
 
61
- import { formatHistoryFullTime, formatHistoryTime, groupTimestamp } from './composables';
63
+ import { formatHistoryFullTime, formatHistoryTime, groupSource, groupTimestamp } from './composables';
62
64
  import GroupRow from './GroupRow.vue';
63
65
  import InitialRow from './InitialRow.vue';
64
66
 
@@ -92,6 +94,8 @@ const props = withDefaults(
92
94
  describeStep: (_step: any) => string;
93
95
  /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
94
96
  isStepDiffable?: (_step: any) => boolean;
97
+ /** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
98
+ isStepRevertable?: (_step: any) => boolean;
95
99
  /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
96
100
  expanded: Record<string, boolean>;
97
101
  /** 是否支持「跳转到该记录」(goto)。默认 true。 */
@@ -11,6 +11,7 @@
11
11
  :describe-group="describeGroup"
12
12
  :describe-step="describeStep"
13
13
  :is-step-diffable="isStepDiffable"
14
+ :is-step-revertable="isStepRevertable"
14
15
  :expanded="expanded"
15
16
  :goto-enabled="gotoEnabled"
16
17
  @toggle="(key: string) => $emit('toggle', key)"
@@ -48,6 +49,8 @@ withDefaults(
48
49
  describeStep: (_step: any) => string;
49
50
  /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
50
51
  isStepDiffable: (_step: any) => boolean;
52
+ /** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
53
+ isStepRevertable?: (_step: any) => boolean;
51
54
  /**
52
55
  * 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
53
56
  * 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
@@ -13,7 +13,14 @@
13
13
  <span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
14
14
  <span class="m-editor-history-list-item-desc">{{ desc }}</span>
15
15
 
16
- <span v-if="time" class="m-editor-history-list-item-time" :title="timeTitle || time">{{ time }}</span>
16
+ <span
17
+ v-if="!merged && sourceLabel(source)"
18
+ class="m-editor-history-list-item-source"
19
+ :title="`操作途径:${sourceLabel(source)}`"
20
+ >{{ sourceLabel(source) }}</span
21
+ >
22
+
23
+ <span v-if="!merged && time" class="m-editor-history-list-item-time" :title="timeTitle || time">{{ time }}</span>
17
24
 
18
25
  <span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
19
26
 
@@ -50,6 +57,12 @@
50
57
  >
51
58
  <span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
52
59
  <span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
60
+ <span
61
+ v-if="sourceLabel(s.source)"
62
+ class="m-editor-history-list-item-source"
63
+ :title="`操作途径:${sourceLabel(s.source)}`"
64
+ >{{ sourceLabel(s.source) }}</span
65
+ >
53
66
  <span v-if="s.time" class="m-editor-history-list-item-time" :title="s.timeTitle || s.time">{{ s.time }}</span>
54
67
  <span
55
68
  v-if="s.revertable"
@@ -80,9 +93,9 @@
80
93
  <script lang="ts" setup>
81
94
  import { computed } from 'vue';
82
95
 
83
- import type { HistoryOpType } from '@editor/type';
96
+ import type { HistoryOpSource, HistoryOpType } from '@editor/type';
84
97
 
85
- import { opLabel } from './composables';
98
+ import { opLabel, sourceLabel } from './composables';
86
99
 
87
100
  defineOptions({
88
101
  name: 'MEditorHistoryListGroupRow',
@@ -100,6 +113,8 @@ const props = withDefaults(
100
113
  opType: HistoryOpType;
101
114
  /** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
102
115
  desc: string;
116
+ /** 组的操作途径(一般取组内最近一步),用于头部展示「画布 / 树面板 / 配置面板…」标签。 */
117
+ source?: HistoryOpSource;
103
118
  /** 组头部展示的时间文案(一般为组内最近一步的时间),为空时不渲染。 */
104
119
  time?: string;
105
120
  /** 组头部时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
@@ -115,6 +130,8 @@ const props = withDefaults(
115
130
  diffable?: boolean;
116
131
  /** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
117
132
  revertable?: boolean;
133
+ /** 该子步的操作途径,用于展示「画布 / 树面板 / 配置面板…」标签。 */
134
+ source?: HistoryOpSource;
118
135
  /** 该子步的时间文案,为空时不渲染。 */
119
136
  time?: string;
120
137
  /** 该子步时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
@@ -193,7 +193,8 @@ const targetText = computed(() => {
193
193
  'data-source': '数据源',
194
194
  'code-block': '代码块',
195
195
  };
196
- const prefix = categoryText[payload.value.category] || '';
196
+ const { category } = payload.value;
197
+ const prefix = category ? categoryText[category] : '';
197
198
  const label = payload.value.targetLabel || payload.value.type || '';
198
199
  const { id } = payload.value;
199
200
  const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
@@ -44,6 +44,7 @@
44
44
  :describe-group="describeDataSourceGroup"
45
45
  :describe-step="describeDataSourceStep"
46
46
  :is-step-diffable="isDataSourceStepDiffable"
47
+ :is-step-revertable="isDataSourceStepRevertable"
47
48
  @toggle="toggleGroup"
48
49
  @goto="onDataSourceGoto"
49
50
  @goto-initial="onDataSourceGotoInitial"
@@ -65,6 +66,7 @@
65
66
  :describe-group="describeCodeBlockGroup"
66
67
  :describe-step="describeCodeBlockStep"
67
68
  :is-step-diffable="isCodeBlockStepDiffable"
69
+ :is-step-revertable="isCodeBlockStepRevertable"
68
70
  @toggle="toggleGroup"
69
71
  @goto="onCodeBlockGoto"
70
72
  @goto-initial="onCodeBlockGotoInitial"
@@ -141,6 +143,8 @@ import {
141
143
  describeCodeBlockStep,
142
144
  describeDataSourceGroup,
143
145
  describeDataSourceStep,
146
+ isCodeBlockStepRevertable,
147
+ isDataSourceStepRevertable,
144
148
  useHistoryList,
145
149
  } from './composables';
146
150
  import HistoryDiffDialog from './HistoryDiffDialog.vue';
@@ -10,6 +10,7 @@
10
10
  :merged="group.steps.length > 1"
11
11
  :op-type="group.opType"
12
12
  :desc="describePageGroup(group)"
13
+ :source="groupSource(group)"
13
14
  :time="formatHistoryTime(groupTimestamp(group))"
14
15
  :time-title="formatHistoryFullTime(groupTimestamp(group))"
15
16
  :step-count="group.steps.length"
@@ -20,7 +21,8 @@
20
21
  isCurrent: s.isCurrent,
21
22
  desc: describePageStep(s.step),
22
23
  diffable: isPageStepDiffable(s.step),
23
- revertable: s.applied,
24
+ revertable: s.applied && isPageStepRevertable(s.step),
25
+ source: s.step.source,
24
26
  time: formatHistoryTime(s.step.timestamp),
25
27
  timeTitle: formatHistoryFullTime(s.step.timestamp),
26
28
  }))
@@ -53,7 +55,9 @@ import {
53
55
  describePageStep,
54
56
  formatHistoryFullTime,
55
57
  formatHistoryTime,
58
+ groupSource,
56
59
  groupTimestamp,
60
+ isPageStepRevertable,
57
61
  } from './composables';
58
62
  import GroupRow from './GroupRow.vue';
59
63
  import InitialRow from './InitialRow.vue';
@@ -8,6 +8,7 @@ import type {
8
8
  CodeBlockStepValue,
9
9
  DataSourceHistoryGroup,
10
10
  DataSourceStepValue,
11
+ HistoryOpSource,
11
12
  HistoryOpType,
12
13
  PageHistoryGroup,
13
14
  StepValue,
@@ -107,6 +108,32 @@ export const opLabel = (op: HistoryOpType) => {
107
108
  }
108
109
  };
109
110
 
111
+ /** 内置操作途径的中文文案;自定义来源直接回显原值,未知 / 缺省返回空串(UI 据此不渲染)。 */
112
+ const HISTORY_SOURCE_LABELS: Record<string, string> = {
113
+ stage: '画布',
114
+ tree: '树面板',
115
+ 'component-panel': '组件面板',
116
+ props: '配置面板',
117
+ code: '源码',
118
+ 'stage-contextmenu': '画布菜单',
119
+ 'tree-contextmenu': '树菜单',
120
+ toolbar: '工具栏',
121
+ shortcut: '快捷键',
122
+ rollback: '回滚',
123
+ api: '接口',
124
+ ai: 'AI',
125
+ unknown: '未知',
126
+ };
127
+
128
+ /** 操作途径文案:用于历史面板展示「画布 / 树面板 / 配置面板…」标签。 */
129
+ export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
130
+ return HISTORY_SOURCE_LABELS[source] ?? `${source}`;
131
+ };
132
+
133
+ /** 取一组历史步骤里最后一步(最近一次)的操作途径,用于组头部展示。 */
134
+ export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
135
+ group.steps[group.steps.length - 1]?.step.source;
136
+
110
137
  const nameOf = (node: { name?: string; id?: string | number; type?: string }) =>
111
138
  node?.name || node?.type || `${node?.id ?? ''}`;
112
139
 
@@ -224,3 +251,36 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
224
251
  const target = labelWithId(rawName, group.id);
225
252
  return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
226
253
  };
254
+
255
+ /**
256
+ * 页面 step 是否支持「回滚」(类 git revert):
257
+ * - 新增 / 删除:不依赖 changeRecords,反向应用即删除 / 加回,始终可回滚;
258
+ * - 更新:必须每个被更新节点都带有 changeRecords,才支持按 propPath 局部反向 patch。
259
+ * 缺失 changeRecords 的更新只能整节点替换,会冲掉该节点后续的无关变更,因此不支持回滚。
260
+ */
261
+ export const isPageStepRevertable = (step: StepValue): boolean => {
262
+ if (step.opType !== 'update') return true;
263
+ const items = step.updatedItems ?? [];
264
+ if (!items.length) return false;
265
+ return items.every((item) => Boolean(item.changeRecords?.length));
266
+ };
267
+
268
+ /**
269
+ * 数据源 step 是否支持「回滚」:
270
+ * - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
271
+ * - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
272
+ */
273
+ export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
274
+ if (step.oldSchema === null || step.newSchema === null) return true;
275
+ return Boolean(step.changeRecords?.length);
276
+ };
277
+
278
+ /**
279
+ * 代码块 step 是否支持「回滚」:
280
+ * - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
281
+ * - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
282
+ */
283
+ export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
284
+ if (step.oldContent === null || step.newContent === null) return true;
285
+ return Boolean(step.changeRecords?.length);
286
+ };
@@ -151,7 +151,11 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
151
151
  });
152
152
  }
153
153
 
154
- editorService.update(newValue, { changeRecords: eventData?.changeRecords });
154
+ // 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
155
+ // 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
156
+ const historySource = eventData ? 'props' : 'code';
157
+
158
+ editorService.update(newValue, { changeRecords: eventData?.changeRecords, historySource });
155
159
  } catch (e: any) {
156
160
  emit('submit-error', e);
157
161
  }
@@ -94,11 +94,15 @@ let clientX: number;
94
94
  let clientY: number;
95
95
 
96
96
  const appendComponent = ({ text, type, data = {} }: ComponentItem): void => {
97
- editorService.add({
98
- name: text,
99
- type,
100
- ...data,
101
- });
97
+ editorService.add(
98
+ {
99
+ name: text,
100
+ type,
101
+ ...data,
102
+ },
103
+ undefined,
104
+ { historySource: 'component-panel' },
105
+ );
102
106
  };
103
107
 
104
108
  const dragstartHandler = ({ text, type, data = {} }: ComponentItem, e: DragEvent) => {
@@ -24,7 +24,7 @@
24
24
  <Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.key}`)"></Icon>
25
25
  </TMagicTooltip>
26
26
  <TMagicTooltip v-if="data.type === 'code' && editable" effect="dark" content="删除" placement="bottom">
27
- <Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`)"></Icon>
27
+ <Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`, { historySource: 'tree' })"></Icon>
28
28
  </TMagicTooltip>
29
29
  <slot name="code-block-panel-tool" :id="data.key" :data="data"></slot>
30
30
  </template>
@@ -44,7 +44,7 @@ import Tree from '@editor/components/Tree.vue';
44
44
  import { useFilter } from '@editor/hooks/use-filter';
45
45
  import { useNodeStatus } from '@editor/hooks/use-node-status';
46
46
  import { useServices } from '@editor/hooks/use-services';
47
- import { type CodeBlockListSlots, CodeDeleteErrorType, type TreeNodeData } from '@editor/type';
47
+ import { type CodeBlockListSlots, CodeDeleteErrorType, HistoryOpSource, type TreeNodeData } from '@editor/type';
48
48
 
49
49
  defineSlots<CodeBlockListSlots>();
50
50
 
@@ -60,7 +60,7 @@ const props = defineProps<{
60
60
 
61
61
  const emit = defineEmits<{
62
62
  edit: [id: string];
63
- remove: [id: string];
63
+ remove: [id: string, { historySource?: HistoryOpSource }];
64
64
  'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
65
65
  }>();
66
66
 
@@ -142,7 +142,7 @@ const editCode = (id: string) => {
142
142
  emit('edit', id);
143
143
  };
144
144
 
145
- const deleteCode = async (id: string) => {
145
+ const deleteCode = async (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
146
146
  const currentCode = codeList.value.find((codeItem) => codeItem.id === id);
147
147
  const existBinds = Boolean(currentCode?.items?.length);
148
148
  const undeleteableList = codeBlockService.getUndeletableList() || [];
@@ -154,7 +154,7 @@ const deleteCode = async (id: string) => {
154
154
  });
155
155
 
156
156
  // 无绑定关系,且不在不可删除列表中
157
- emit('remove', id);
157
+ emit('remove', id, { historySource });
158
158
  } else {
159
159
  if (typeof props.customError === 'function') {
160
160
  props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
@@ -122,7 +122,7 @@ const {
122
122
  menuData: contentMenuData,
123
123
  contentMenuHideHandler,
124
124
  } = useContentMenu((id: string) => {
125
- codeBlockListRef.value?.deleteCode(id);
125
+ codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
126
126
  });
127
127
  const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
128
128
  </script>
@@ -41,7 +41,7 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
41
41
 
42
42
  const newCodeId = await codeBlockService.getUniqueId();
43
43
 
44
- codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock));
44
+ codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock), { historySource: 'tree-contextmenu' });
45
45
  },
46
46
  },
47
47
  {
@@ -129,7 +129,7 @@ const removeHandler = async (id: string) => {
129
129
  type: 'warning',
130
130
  });
131
131
 
132
- dataSourceService.remove(id);
132
+ dataSourceService.remove(id, { historySource: 'tree-contextmenu' });
133
133
  };
134
134
 
135
135
  const dataSourceListRef = useTemplateRef<InstanceType<typeof DataSourceList>>('dataSourceList');
@@ -39,7 +39,7 @@ export const useContentMenu = () => {
39
39
  return;
40
40
  }
41
41
 
42
- dataSourceService.add(cloneDeep(ds));
42
+ dataSourceService.add(cloneDeep(ds), { historySource: 'tree-contextmenu' });
43
43
  },
44
44
  },
45
45
  {
@@ -41,11 +41,15 @@ const createMenuItems = (group: ComponentGroup): MenuButton[] =>
41
41
  type: 'button',
42
42
  icon: component.icon,
43
43
  handler: () => {
44
- editorService.add({
45
- name: component.text,
46
- type: component.type,
47
- ...(component.data || {}),
48
- });
44
+ editorService.add(
45
+ {
46
+ name: component.text,
47
+ type: component.type,
48
+ ...(component.data || {}),
49
+ },
50
+ undefined,
51
+ { historySource: 'tree-contextmenu' },
52
+ );
49
53
  },
50
54
  }));
51
55
 
@@ -57,9 +61,13 @@ const getSubMenuData = computed<MenuButton[]>(() => {
57
61
  type: 'button',
58
62
  icon: Files,
59
63
  handler: () => {
60
- editorService.add({
61
- type: 'tab-pane',
62
- });
64
+ editorService.add(
65
+ {
66
+ type: 'tab-pane',
67
+ },
68
+ undefined,
69
+ { historySource: 'tree-contextmenu' },
70
+ );
63
71
  },
64
72
  },
65
73
  ];
@@ -106,9 +114,9 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
106
114
  items: getSubMenuData.value,
107
115
  },
108
116
  useCopyMenu(),
109
- usePasteMenu(),
110
- useDeleteMenu(),
111
- useMoveToMenu(services),
117
+ usePasteMenu('tree-contextmenu'),
118
+ useDeleteMenu('tree-contextmenu'),
119
+ useMoveToMenu(services, 'tree-contextmenu'),
112
120
  ...props.layerContentMenu,
113
121
  ],
114
122
  'layer',
@@ -25,9 +25,12 @@ const props = defineProps<{
25
25
  const { editorService } = useServices();
26
26
 
27
27
  const setNodeVisible = (visible: boolean) => {
28
- editorService.update({
29
- id: props.data.id,
30
- visible,
31
- });
28
+ editorService.update(
29
+ {
30
+ id: props.data.id,
31
+ visible,
32
+ },
33
+ { historySource: 'tree' },
34
+ );
32
35
  };
33
36
  </script>
@@ -380,7 +380,7 @@ const dropHandler = async (e: DragEvent) => {
380
380
 
381
381
  config.data.inputEvent = e;
382
382
 
383
- editorService.add(config.data, parent);
383
+ editorService.add(config.data, parent, { historySource: 'component-panel' });
384
384
  }
385
385
  };
386
386
  </script>
@@ -49,11 +49,11 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
49
49
  display: () => canCenter.value,
50
50
  handler: () => {
51
51
  if (!nodes.value) return;
52
- editorService.alignCenter(nodes.value);
52
+ editorService.alignCenter(nodes.value, { historySource: 'stage-contextmenu' });
53
53
  },
54
54
  },
55
55
  useCopyMenu(),
56
- usePasteMenu(menuRef),
56
+ usePasteMenu('stage-contextmenu', menuRef),
57
57
  {
58
58
  type: 'divider',
59
59
  direction: 'horizontal',
@@ -68,7 +68,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
68
68
  icon: markRaw(Top),
69
69
  display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
70
70
  handler: () => {
71
- editorService.moveLayer(1);
71
+ editorService.moveLayer(1, { historySource: 'stage-contextmenu' });
72
72
  },
73
73
  },
74
74
  {
@@ -77,7 +77,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
77
77
  icon: markRaw(Bottom),
78
78
  display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
79
79
  handler: () => {
80
- editorService.moveLayer(-1);
80
+ editorService.moveLayer(-1, { historySource: 'stage-contextmenu' });
81
81
  },
82
82
  },
83
83
  {
@@ -86,7 +86,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
86
86
  icon: markRaw(Top),
87
87
  display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
88
88
  handler: () => {
89
- editorService.moveLayer(LayerOffset.TOP);
89
+ editorService.moveLayer(LayerOffset.TOP, { historySource: 'stage-contextmenu' });
90
90
  },
91
91
  },
92
92
  {
@@ -95,16 +95,16 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
95
95
  icon: markRaw(Bottom),
96
96
  display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
97
97
  handler: () => {
98
- editorService.moveLayer(LayerOffset.BOTTOM);
98
+ editorService.moveLayer(LayerOffset.BOTTOM, { historySource: 'stage-contextmenu' });
99
99
  },
100
100
  },
101
- useMoveToMenu(services),
101
+ useMoveToMenu(services, 'stage-contextmenu'),
102
102
  {
103
103
  type: 'divider',
104
104
  direction: 'horizontal',
105
105
  display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
106
106
  },
107
- useDeleteMenu(),
107
+ useDeleteMenu('stage-contextmenu'),
108
108
  {
109
109
  type: 'divider',
110
110
  direction: 'horizontal',