@tmagic/editor 1.8.0-manmanyu.11 → 1.8.0-manmanyu.13

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.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-manmanyu.11",
2
+ "version": "1.8.0-manmanyu.13",
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-manmanyu.11",
62
- "@tmagic/form": "1.8.0-manmanyu.11",
63
- "@tmagic/stage": "1.8.0-manmanyu.11",
64
- "@tmagic/utils": "1.8.0-manmanyu.11",
65
- "@tmagic/table": "1.8.0-manmanyu.11"
61
+ "@tmagic/utils": "1.8.0-manmanyu.13",
62
+ "@tmagic/form": "1.8.0-manmanyu.13",
63
+ "@tmagic/table": "1.8.0-manmanyu.13",
64
+ "@tmagic/stage": "1.8.0-manmanyu.13",
65
+ "@tmagic/design": "1.8.0-manmanyu.13"
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.38",
79
- "@tmagic/core": "1.8.0-manmanyu.11"
79
+ "@tmagic/core": "1.8.0-manmanyu.13"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -24,7 +24,6 @@ import { isEqual } from 'lodash-es';
24
24
  import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
25
25
  import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
26
26
 
27
- import { useServices } from '@editor/hooks/use-services';
28
27
  import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
29
28
  import { getCodeBlockFormConfig } from '@editor/utils/code-block';
30
29
 
@@ -76,18 +75,15 @@ const props = withDefaults(
76
75
  */
77
76
  loadConfig?: CompareFormLoadConfig;
78
77
  /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
79
- services: Services;
78
+ services?: Services;
80
79
  }>(),
81
80
  {
82
81
  category: 'node',
83
82
  labelWidth: '120px',
84
- services: () => useServices(),
85
83
  extendState: (state: FormState) => state,
86
84
  },
87
85
  );
88
86
 
89
- const { propsService, dataSourceService, codeBlockService, editorService } = props.services;
90
-
91
87
  provide('services', props.services);
92
88
 
93
89
  const config = ref<FormConfig>([]);
@@ -192,17 +188,21 @@ const mergedExtendState = (state: FormState) => {
192
188
  * 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
193
189
  */
194
190
  const defaultLoadConfig = async (): Promise<FormConfig> => {
191
+ if (!props.services) {
192
+ return [];
193
+ }
194
+
195
195
  switch (props.category) {
196
196
  case 'node': {
197
197
  if (!props.type) {
198
198
  return [];
199
199
  }
200
200
  return removeStyleDisplayConfig(
201
- await propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
201
+ await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
202
202
  );
203
203
  }
204
204
  case 'data-source': {
205
- const config = dataSourceService.getFormConfig(props.type || 'base');
205
+ const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
206
206
  // 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
207
207
  // 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
208
208
  // 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
@@ -210,7 +210,7 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
210
210
  }
211
211
  case 'code-block': {
212
212
  return getCodeBlockFormConfig({
213
- paramColConfig: codeBlockService.getParamsColConfig(),
213
+ paramColConfig: props.services.codeBlockService.getParamsColConfig(),
214
214
  // 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比场景下 props.dataSourceType
215
215
  // 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
216
216
  isDataSource: () => Boolean(props.dataSourceType),
@@ -258,11 +258,9 @@ const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
258
258
  * - services:整个 useServices() 返回的服务集合;
259
259
  * - stage:当前 editorService.get('stage') 的最新值。
260
260
  */
261
- const stage = computed(() => editorService.get('stage'));
262
-
263
261
  watchEffect(() => {
264
- if (formRef.value) {
265
- formRef.value.formState.stage = stage.value;
262
+ if (formRef.value && props.services) {
263
+ formRef.value.formState.stage = props.services.editorService.get('stage');
266
264
  formRef.value.formState.services = props.services;
267
265
  }
268
266
  });
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
3
3
  <template v-else>
4
- <div class="m-editor-history-list-toolbar">
4
+ <div v-if="config.showClear !== false" class="m-editor-history-list-toolbar">
5
5
  <span class="m-editor-history-list-clear" :title="`清空${config.title}的历史记录`" @click="$emit('clear')"
6
6
  >清空</span
7
7
  >
@@ -38,7 +38,7 @@ defineOptions({
38
38
 
39
39
  defineProps<{
40
40
  /**
41
- * 该类历史的整体渲染配置(title / prefix / describe* / isStep* / showInitial / gotoEnabled),
41
+ * 该类历史的整体渲染配置(title / prefix / describe* / isStep* / showInitial / gotoEnabled / showClear),
42
42
  * 由父组件按业务类型注入并整体透传给 Bucket,避免逐项透传多个 props。
43
43
  */
44
44
  config: HistoryBucketConfig<T>;
@@ -21,7 +21,7 @@
21
21
  </TMagicRadioGroup>
22
22
 
23
23
  <TMagicRadioGroup v-model="mode" size="small" class="m-editor-history-diff-dialog-mode">
24
- <TMagicRadioButton value="before">与修改前对比</TMagicRadioButton>
24
+ <TMagicRadioButton value="before" :disabled="!hasValue">与修改前对比</TMagicRadioButton>
25
25
  <TMagicRadioButton value="current" :disabled="!hasCurrent">与当前对比</TMagicRadioButton>
26
26
  </TMagicRadioGroup>
27
27
  </div>
@@ -81,7 +81,6 @@ import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagic
81
81
  import type { FormState } from '@tmagic/form';
82
82
 
83
83
  import CompareForm from '@editor/components/CompareForm.vue';
84
- import { useServices } from '@editor/hooks/use-services';
85
84
  import CodeEditor from '@editor/layouts/CodeEditor.vue';
86
85
  import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload, Services } from '@editor/type';
87
86
 
@@ -92,7 +91,7 @@ defineOptions({
92
91
  const props = withDefaults(
93
92
  defineProps<{
94
93
  /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
95
- services: Services;
94
+ services?: Services;
96
95
  /**
97
96
  * 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
98
97
  * 透传给 CompareForm,从而让差异对比时表单 item 中依赖业务上下文的
@@ -111,7 +110,6 @@ const props = withDefaults(
111
110
  compareFormState?: FormState;
112
111
  }>(),
113
112
  {
114
- services: () => useServices(),
115
113
  width: '900px',
116
114
  },
117
115
  );
@@ -161,6 +159,23 @@ const dialogTitle = computed(() => (props.onConfirm ? '确认回滚' : '查看
161
159
 
162
160
  const hasCurrent = computed(() => payload.value?.currentValue !== undefined && payload.value?.currentValue !== null);
163
161
 
162
+ /** 是否存在该步「修改后的值」:不存在(如仅删除)时「与修改前对比」无意义,置灰禁用。 */
163
+ const hasValue = computed(() => payload.value?.value !== undefined && payload.value?.value !== null);
164
+
165
+ /** 指定模式当前是否可用:before 依赖「修改后的值」,current 依赖「当前值」。 */
166
+ const isModeAvailable = (m: DiffMode): boolean => (m === 'current' ? hasCurrent.value : hasValue.value);
167
+
168
+ /**
169
+ * 计算 open 时的初始对比模式:
170
+ * - 调用方通过 payload.mode 指定且该模式可用时,优先使用指定模式;
171
+ * - 否则没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;
172
+ * - 其余情况默认「与修改前对比」。
173
+ */
174
+ const resolveInitialMode = (specified?: DiffMode): DiffMode => {
175
+ if (specified && isModeAvailable(specified)) return specified;
176
+ return !hasValue.value && hasCurrent.value ? 'current' : 'before';
177
+ };
178
+
164
179
  /** 左侧(旧/参照)值 */
165
180
  const leftValue = computed<Record<string, any>>(() => {
166
181
  if (!payload.value) return {};
@@ -214,8 +229,9 @@ const targetText = computed(() => {
214
229
 
215
230
  const open = (p: DiffDialogPayload) => {
216
231
  payload.value = p;
217
- // 每次打开按需重置默认模式:有当前值时优先「与当前对比」更贴近"看现在差什么",否则回退到默认
218
- mode.value = 'before';
232
+ // 每次打开按 payload 重置对比模式(hasValue / hasCurrent 依赖 payload,需先赋值):
233
+ // 优先使用 payload.mode 指定的模式;不可用或未指定时按数据自动推断。
234
+ mode.value = resolveInitialMode(p.mode);
219
235
  // 默认回到表单对比形态,避免残留上一次选择的源码模式
220
236
  viewMode.value = 'form';
221
237
  visible.value = true;
@@ -313,10 +313,10 @@ const onCodeBlockGotoInitial = (id: string | number) => {
313
313
  /**
314
314
  * 「单步回滚」与「查看差异」的完整逻辑收敛到 useHistoryRevert,面板与业务方共用:
315
315
  * 二者均由 useHistoryRevert 内部按需动态挂载 HistoryDiffDialog,
316
- * 业务方亦可直接 import useHistoryRevert(services) 调用,无需自行挂载任何弹窗。
316
+ * 业务方亦可直接 import useHistoryRevert(options, services) 调用,无需自行挂载任何弹窗。
317
317
  */
318
318
  const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
319
- useHistoryRevert(services, { extendState: extendFormState, getPropsPanelFormState });
319
+ useHistoryRevert({ extendState: extendFormState, getPropsPanelFormState }, services);
320
320
 
321
321
  /**
322
322
  * 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
@@ -76,7 +76,7 @@ interface MountedDiffDialog {
76
76
  const mountHistoryDiffDialog = async (
77
77
  options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
78
78
  CustomDiffFormOptions & {
79
- services: Services;
79
+ services?: Services;
80
80
  isConfirm?: boolean;
81
81
  onClose?: () => void;
82
82
  },
@@ -94,6 +94,7 @@ const mountHistoryDiffDialog = async (
94
94
  loadConfig: options.loadConfig,
95
95
  selfDiffFieldTypes: options.selfDiffFieldTypes,
96
96
  compareFormState: options.compareFormState,
97
+ width: options.width,
97
98
  onClose: options.onClose,
98
99
  });
99
100
  if (options.appContext) {
@@ -125,7 +126,7 @@ const confirmRevertWithDiffDialog = async (
125
126
  payload: DiffDialogPayload,
126
127
  options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
127
128
  CustomDiffFormOptions & {
128
- services: Services;
129
+ services?: Services;
129
130
  },
130
131
  ): Promise<boolean> => {
131
132
  const { instance, destroy } = await mountHistoryDiffDialog({
@@ -147,7 +148,7 @@ const viewHistoryDiffDialog = async (
147
148
  payload: DiffDialogPayload,
148
149
  options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
149
150
  CustomDiffFormOptions & {
150
- services: Services;
151
+ services?: Services;
151
152
  },
152
153
  ): Promise<void> => {
153
154
  // onClose 在用户关闭弹窗时才触发,此时 handle.destroy 早已赋值。
@@ -184,16 +185,15 @@ const viewHistoryDiffDialog = async (
184
185
  * ```ts
185
186
  * import { useHistoryRevert } from '@tmagic/editor';
186
187
  *
187
- * const { onPageRevert, onPageDiff } = useHistoryRevert(editorRef.value); // editorRef.value 即 Editor 暴露的 services
188
+ * const { onPageRevert, onPageDiff } = useHistoryRevert({}, editorRef.value); // editorRef.value 即 Editor 暴露的 services
188
189
  * await onPageRevert(index); // 弹出差异 / 二次确认弹窗后回滚
189
190
  * onPageDiff(index); // 弹出只读差异弹窗查看前后值差异
190
191
  * ```
191
192
  */
192
- export const useHistoryRevert = (services: Services, options: UseHistoryRevertOptions = {}) => {
193
- const { editorService, dataSourceService, codeBlockService, historyService } = services;
193
+ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
194
194
  // 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
195
195
  const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
196
- const { extendState, getPropsPanelFormState } = options;
196
+ const { extendState, getPropsPanelFormState, dialogWidth } = options;
197
197
 
198
198
  /** 目标数据已被删除、无法回滚时的统一提示。 */
199
199
  const showRevertTargetMissing = () => {
@@ -217,7 +217,13 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
217
217
  */
218
218
  const runRevert = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<boolean> => {
219
219
  if (payload) {
220
- return confirmRevertWithDiffDialog(payload, { appContext, extendState, services, ...extra });
220
+ return confirmRevertWithDiffDialog(payload, {
221
+ appContext,
222
+ extendState,
223
+ services,
224
+ ...extra,
225
+ width: extra?.width ?? dialogWidth,
226
+ });
221
227
  }
222
228
  return confirmRevert();
223
229
  };
@@ -226,8 +232,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
226
232
  buildDiffPayload(
227
233
  {
228
234
  category: 'node',
229
- groups: () => historyService.getHistoryGroups('page', editorService.get('page')?.id),
230
- getCurrent: (id) => editorService.getNodeById(id) as Record<string, any> | null,
235
+ groups: () => services?.historyService.getHistoryGroups('page', services?.editorService.get('page')?.id) ?? [],
236
+ getCurrent: (id) => services?.editorService.getNodeById(id) as Record<string, any> | null,
231
237
  resolveType: (n, o) => n.type || o.type || '',
232
238
  resolveLabel: (n, o) => n.name || o.name || n.type || o.type || '',
233
239
  },
@@ -238,8 +244,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
238
244
  buildDiffPayload(
239
245
  {
240
246
  category: 'data-source',
241
- groups: () => historyService.getHistoryGroups('dataSource'),
242
- getCurrent: (id) => dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
247
+ groups: () => services?.historyService.getHistoryGroups('dataSource') ?? [],
248
+ getCurrent: (id) => services?.dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
243
249
  resolveType: (n, o) => n.type || o.type || 'base',
244
250
  resolveLabel: (n, o, id) => n.title || o.title || `${id}`,
245
251
  },
@@ -251,8 +257,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
251
257
  buildDiffPayload(
252
258
  {
253
259
  category: 'code-block',
254
- groups: () => historyService.getHistoryGroups('codeBlock'),
255
- getCurrent: (id) => codeBlockService.getCodeContentById(id) as Record<string, any> | null,
260
+ groups: () => services?.historyService.getHistoryGroups('codeBlock') ?? [],
261
+ getCurrent: (id) => services?.codeBlockService.getCodeContentById(id) as Record<string, any> | null,
256
262
  resolveLabel: (n, o, id) => n.name || o.name || `${id}`,
257
263
  },
258
264
  index,
@@ -266,17 +272,17 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
266
272
  * add(回滚即删除)即使目标已不在,也已达成「删除」目的,不视为失败。
267
273
  */
268
274
  const isPageRevertTargetMissing = (index: number): boolean => {
269
- const step = historyService.getStepList('page', editorService.get('page')?.id)[index]?.step;
275
+ const step = services?.historyService.getStepList('page', services?.editorService.get('page')?.id)[index]?.step;
270
276
  if (!step) return false;
271
277
  if (step.opType === 'update') {
272
278
  return (step.diff ?? []).some((item) => {
273
279
  const id = item.newSchema?.id ?? item.oldSchema?.id;
274
- return id !== undefined && !editorService.getNodeById(id, false);
280
+ return id !== undefined && !services?.editorService.getNodeById(id, false);
275
281
  });
276
282
  }
277
283
  if (step.opType === 'remove') {
278
284
  return (step.diff ?? []).some(
279
- (item) => item.parentId !== undefined && !editorService.getNodeById(item.parentId, false),
285
+ (item) => item.parentId !== undefined && !services?.editorService.getNodeById(item.parentId, false),
280
286
  );
281
287
  }
282
288
  return false;
@@ -284,14 +290,14 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
284
290
 
285
291
  /** 数据源 update 步骤回滚时,对应数据源必须仍存在(已删除则无处写回旧值)。 */
286
292
  const isDataSourceRevertTargetMissing = (id: string | number, index: number): boolean => {
287
- const step = historyService.getStepList('dataSource', id)[index]?.step;
288
- return Boolean(step?.opType === 'update' && !dataSourceService.getDataSourceById(`${id}`));
293
+ const step = services?.historyService.getStepList('dataSource', id)[index]?.step;
294
+ return Boolean(step?.opType === 'update' && !services?.dataSourceService.getDataSourceById(`${id}`));
289
295
  };
290
296
 
291
297
  /** 代码块 update 步骤回滚时,对应代码块必须仍存在(已删除则无处写回旧值)。 */
292
298
  const isCodeBlockRevertTargetMissing = (id: string | number, index: number): boolean => {
293
- const step = historyService.getStepList('codeBlock', id)[index]?.step;
294
- return Boolean(step?.opType === 'update' && !codeBlockService.getCodeContentById(id));
299
+ const step = services?.historyService.getStepList('codeBlock', id)[index]?.step;
300
+ return Boolean(step?.opType === 'update' && !services?.codeBlockService.getCodeContentById(id));
295
301
  };
296
302
 
297
303
  const onPageRevert = (index: number) => {
@@ -300,7 +306,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
300
306
  return Promise.resolve(null);
301
307
  }
302
308
  return runRevert(buildPageDiffPayload(index), { compareFormState: getPropsPanelFormState?.() }).then((result) =>
303
- result ? editorService.revertPageStep(index) : null,
309
+ result ? services?.editorService.revertPageStep(index) : null,
304
310
  );
305
311
  };
306
312
 
@@ -310,7 +316,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
310
316
  return Promise.resolve(null);
311
317
  }
312
318
  return runRevert(buildDataSourceDiffPayload(id, index)).then((result) =>
313
- result ? dataSourceService.revert(id, index) : null,
319
+ result ? services?.dataSourceService.revert(id, index) : null,
314
320
  );
315
321
  };
316
322
 
@@ -320,7 +326,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
320
326
  return Promise.resolve(null);
321
327
  }
322
328
  return runRevert(buildCodeBlockDiffPayload(id, index)).then((result) =>
323
- result ? codeBlockService.revert(id, index) : null,
329
+ result ? services?.codeBlockService.revert(id, index) : null,
324
330
  );
325
331
  };
326
332
 
@@ -335,6 +341,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
335
341
  appContext,
336
342
  extendState,
337
343
  services,
344
+ width: dialogWidth,
338
345
  compareFormState: getPropsPanelFormState?.(),
339
346
  });
340
347
  }
@@ -342,12 +349,12 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
342
349
 
343
350
  const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
344
351
  const payload = buildDataSourceDiffPayload(id, index);
345
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
352
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
346
353
  };
347
354
 
348
355
  const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
349
356
  const payload = buildCodeBlockDiffPayload(id, index);
350
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
357
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
351
358
  };
352
359
 
353
360
  /**
@@ -374,6 +381,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
374
381
  const confirmed = await runRevert(revertOptions.diffPayload ?? null, {
375
382
  loadConfig: revertOptions.loadConfig,
376
383
  selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
384
+ width: revertOptions.width,
377
385
  });
378
386
  if (!confirmed) return null;
379
387
  return await revertOptions.revert();
@@ -384,7 +392,14 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
384
392
  * 可透传 `loadConfig` / `selfDiffFieldTypes`。payload 为 null(不可对比)时静默返回。
385
393
  */
386
394
  const viewDiff = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<void> | void => {
387
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, ...extra });
395
+ if (payload)
396
+ return viewHistoryDiffDialog(payload, {
397
+ appContext,
398
+ extendState,
399
+ services,
400
+ ...extra,
401
+ width: extra?.width ?? dialogWidth,
402
+ });
388
403
  };
389
404
 
390
405
  return {
@@ -85,6 +85,7 @@ class CodeBlock extends BaseService {
85
85
  public async setCodeDsl(codeDsl: CodeBlockDSL): Promise<void> {
86
86
  this.state.codeDsl = codeDsl;
87
87
  this.emit('code-dsl-change', this.state.codeDsl);
88
+ this.emit('change', this.state.codeDsl);
88
89
  }
89
90
 
90
91
  /**
@@ -199,6 +200,7 @@ class CodeBlock extends BaseService {
199
200
  }
200
201
 
201
202
  this.emit('addOrUpdate', id, codeDsl[id]);
203
+ this.emit('change', this.getCodeDsl());
202
204
  }
203
205
 
204
206
  /**
@@ -318,6 +320,8 @@ class CodeBlock extends BaseService {
318
320
 
319
321
  this.emit('remove', id);
320
322
  });
323
+
324
+ this.emit('change', this.getCodeDsl());
321
325
  }
322
326
 
323
327
  // #region AndGetHistoryId
@@ -147,6 +147,7 @@ class DataSource extends BaseService {
147
147
  }
148
148
 
149
149
  this.emit('add', newConfig);
150
+ this.emit('change', this.get('dataSources'));
150
151
 
151
152
  return newConfig;
152
153
  }
@@ -192,6 +193,7 @@ class DataSource extends BaseService {
192
193
  oldConfig,
193
194
  changeRecords,
194
195
  });
196
+ this.emit('change', this.get('dataSources'));
195
197
 
196
198
  return newConfig;
197
199
  }
@@ -220,6 +222,7 @@ class DataSource extends BaseService {
220
222
  }
221
223
 
222
224
  this.emit('remove', id);
225
+ this.emit('change', this.get('dataSources'));
223
226
  }
224
227
 
225
228
  // #region AndGetHistoryId
@@ -535,6 +535,10 @@ class Editor extends BaseService {
535
535
  }
536
536
 
537
537
  this.emit('add', newNodes);
538
+ this.emit('change', {
539
+ type: 'add',
540
+ data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
541
+ });
538
542
 
539
543
  return Array.isArray(addNode) ? newNodes : newNodes[0];
540
544
  }
@@ -635,6 +639,9 @@ class Editor extends BaseService {
635
639
 
636
640
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
637
641
 
642
+ // 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
643
+ const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
644
+
638
645
  const removedItems: StepDiffItem<MNode>[] = [];
639
646
  let pageForOp: { name: string; id: Id } | null = null;
640
647
  if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
@@ -670,6 +677,7 @@ class Editor extends BaseService {
670
677
  }
671
678
 
672
679
  this.emit('remove', nodes);
680
+ this.emit('change', { type: 'remove', data: changeItems });
673
681
  }
674
682
 
675
683
  public async doUpdate(
@@ -805,6 +813,10 @@ class Editor extends BaseService {
805
813
  }
806
814
 
807
815
  this.emit('update', updateData);
816
+ this.emit('change', {
817
+ type: 'update',
818
+ data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
819
+ });
808
820
  return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
809
821
  }
810
822
 
@@ -1046,6 +1058,11 @@ class Editor extends BaseService {
1046
1058
  }
1047
1059
 
1048
1060
  this.emit('move-layer', offset);
1061
+ this.emit('change', {
1062
+ type: 'move-layer',
1063
+ data: [{ node, page: this.getPageOfNode(node.id) }],
1064
+ offset,
1065
+ });
1049
1066
  }
1050
1067
 
1051
1068
  /**
@@ -1225,6 +1242,12 @@ class Editor extends BaseService {
1225
1242
  }
1226
1243
 
1227
1244
  this.emit('drag-to', { targetIndex, configs, targetParent });
1245
+ this.emit('change', {
1246
+ type: 'drag-to',
1247
+ data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
1248
+ targetIndex,
1249
+ targetParent,
1250
+ });
1228
1251
  }
1229
1252
 
1230
1253
  // #region AndGetHistoryId
@@ -1558,6 +1581,20 @@ class Editor extends BaseService {
1558
1581
  this.get('modifiedNodeIds').set(id, id);
1559
1582
  }
1560
1583
 
1584
+ /**
1585
+ * 获取指定节点所属的页面 / 页面片:
1586
+ * - 普通节点返回其所在的 page;
1587
+ * - 节点本身就是 page / pageFragment 时返回它自己;
1588
+ * - 找不到时返回 null。
1589
+ * 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
1590
+ */
1591
+ private getPageOfNode(id: Id): MPage | MPageFragment | null {
1592
+ const { node, page } = this.getNodeInfo(id, false);
1593
+ if (page) return page;
1594
+ if (node && (isPage(node) || isPageFragment(node))) return node as MPage | MPageFragment;
1595
+ return null;
1596
+ }
1597
+
1561
1598
  private captureSelectionBeforeOp() {
1562
1599
  if (this.selectionBeforeOp) return;
1563
1600
  this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
package/src/type.ts CHANGED
@@ -1218,8 +1218,41 @@ export interface EditorEvents {
1218
1218
  'move-layer': [offset: number | LayerOffset];
1219
1219
  'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
1220
1220
  'history-change': [data: MPage | MPageFragment];
1221
+ /**
1222
+ * DSL 发生变更后统一触发,免去分别监听 add / remove / update / move-layer / drag-to。
1223
+ * 回调参数为 {@link EditorChangeEvent},按 `type` 区分操作类型并携带各自的操作内容(payload)
1224
+ * 以及变更所在的当前 page(可能为 null)。撤销 / 重做内部同样会经由
1225
+ * add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 `history-change`。
1226
+ */
1227
+ change: [event: EditorChangeEvent];
1228
+ }
1229
+
1230
+ // #region EditorChangeEvent
1231
+ /** `change` 事件中单个变更项:变更的 node 及其所属的 page(可能为 null)。 */
1232
+ export interface EditorChangeItem {
1233
+ node: MNode;
1234
+ page: StoreState['page'];
1221
1235
  }
1222
1236
 
1237
+ /** `update` 类型变更项:node 为前后快照及 form 端变更记录,page 为其所属页面。 */
1238
+ export interface EditorUpdateChangeItem {
1239
+ node: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] };
1240
+ page: StoreState['page'];
1241
+ }
1242
+
1243
+ /**
1244
+ * {@link EditorEvents.change} 的回调参数:以 `type` 区分操作类型,并携带对应的操作内容。
1245
+ * `data` 为本次变更的节点列表,每项包含 node 及其所属的 page(可能为 null);
1246
+ * `move-layer` 额外带层级偏移 `offset`,`drag-to` 额外带目标位置 `targetIndex` / `targetParent`。
1247
+ */
1248
+ export type EditorChangeEvent =
1249
+ | { type: 'add'; data: EditorChangeItem[] }
1250
+ | { type: 'remove'; data: EditorChangeItem[] }
1251
+ | { type: 'update'; data: EditorUpdateChangeItem[] }
1252
+ | { type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset }
1253
+ | { type: 'drag-to'; data: EditorChangeItem[]; targetIndex: number; targetParent: MContainer };
1254
+ // #endregion EditorChangeEvent
1255
+
1223
1256
  export interface HistoryEvents {
1224
1257
  change: [
1225
1258
  state: BaseStepValue | StepValue | CodeBlockStepValue | DataSourceStepValue,
@@ -1319,6 +1352,14 @@ export interface DiffDialogPayload {
1319
1352
  targetLabel?: string;
1320
1353
  /** 用于标题展示的目标 id */
1321
1354
  id?: string | number;
1355
+ /**
1356
+ * 指定打开时的初始对比模式:
1357
+ * - before:该步骤修改前 vs 修改后
1358
+ * - current:该步骤修改后 vs 当前最新值
1359
+ * 不传时按是否存在「修改后的值」/「当前值」自动推断。
1360
+ * 若指定的模式当前不可用(对应数据缺失),将回退到自动推断结果。
1361
+ */
1362
+ mode?: 'before' | 'current';
1322
1363
  }
1323
1364
 
1324
1365
  /**
@@ -1353,6 +1394,8 @@ export interface HistoryBucketConfig<T extends BaseStepValue = BaseStepValue> ex
1353
1394
  showInitial?: boolean;
1354
1395
  /** 是否支持「跳转到该记录」(goto),默认 true。 */
1355
1396
  gotoEnabled?: boolean;
1397
+ /** 是否展示顶部「清空」按钮,默认 true。无需提供清空能力的自定义历史可传 false。 */
1398
+ showClear?: boolean;
1356
1399
  }
1357
1400
 
1358
1401
  export interface UseHistoryRevertOptions {
@@ -1373,6 +1416,12 @@ export interface UseHistoryRevertOptions {
1373
1416
  * 以保证两处 filterFunction 读取到一致的运行态上下文。
1374
1417
  */
1375
1418
  getPropsPanelFormState?: () => FormState | undefined;
1419
+ /**
1420
+ * 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialog 的 `width`),
1421
+ * 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
1422
+ * 业务自有历史(`viewDiff` / `confirmAndRevert`)可在调用时通过各自入参的 `width` 单独覆盖。
1423
+ */
1424
+ dialogWidth?: string;
1376
1425
  }
1377
1426
 
1378
1427
  /**
@@ -1393,6 +1442,11 @@ export interface CustomDiffFormOptions {
1393
1442
  * 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
1394
1443
  */
1395
1444
  compareFormState?: FormState;
1445
+ /**
1446
+ * 差异 / 确认回滚弹窗宽度(透传给 HistoryDiffDialog 的 TMagicDialog `width`),
1447
+ * 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
1448
+ */
1449
+ width?: string;
1396
1450
  }
1397
1451
 
1398
1452
  /**