refactor(editor): 历史对比 services 改为显式传入并可选

CompareForm / HistoryDiffDialog / useHistoryRevert 不再通过 useServices()
默认 inject 获取 services,改为由调用方显式传入并设为可选,避免在
createApp 动态挂载游离弹窗场景下 inject 链不可靠。同步更新调用点、
测试与文档示例签名。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
roymondchen 2026-06-26 15:00:08 +08:00
parent 5ea992f5f7
commit 5e661d0958
7 changed files with 39 additions and 44 deletions

View File

@ -82,7 +82,7 @@ const {
onPageDiff,
onDataSourceDiff,
onCodeBlockDiff,
} = useHistoryRevert(editorRef.value);
} = useHistoryRevert({}, editorRef.value);
// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
// 命中前置校验或用户取消时不执行,返回 null

View File

@ -24,7 +24,6 @@ import { isEqual } from 'lodash-es';
import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
import { useServices } from '@editor/hooks/use-services';
import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
@ -76,18 +75,15 @@ const props = withDefaults(
*/
loadConfig?: CompareFormLoadConfig;
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
services: Services;
services?: Services;
}>(),
{
category: 'node',
labelWidth: '120px',
services: () => useServices(),
extendState: (state: FormState) => state,
},
);
const { propsService, dataSourceService, codeBlockService, editorService } = props.services;
provide('services', props.services);
const config = ref<FormConfig>([]);
@ -192,17 +188,21 @@ const mergedExtendState = (state: FormState) => {
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`方便复用与二次加工
*/
const defaultLoadConfig = async (): Promise<FormConfig> => {
if (!props.services) {
return [];
}
switch (props.category) {
case 'node': {
if (!props.type) {
return [];
}
return removeStyleDisplayConfig(
await propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
);
}
case 'data-source': {
const config = dataSourceService.getFormConfig(props.type || 'base');
const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
// tab status 'fields'tab-pane name 'fields'
// active Tabs '0' 'fields'
// DataSourceConfigPanel tab
@ -210,7 +210,7 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
}
case 'code-block': {
return getCodeBlockFormConfig({
paramColConfig: codeBlockService.getParamsColConfig(),
paramColConfig: props.services.codeBlockService.getParamsColConfig(),
// dataSourceType "" props.dataSourceType
// step
isDataSource: () => Boolean(props.dataSourceType),
@ -258,11 +258,9 @@ const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
* - services整个 useServices() 返回的服务集合
* - stage当前 editorService.get('stage') 的最新值
*/
const stage = computed(() => editorService.get('stage'));
watchEffect(() => {
if (formRef.value) {
formRef.value.formState.stage = stage.value;
if (formRef.value && props.services) {
formRef.value.formState.stage = props.services.editorService.get('stage');
formRef.value.formState.services = props.services;
}
});

View File

@ -81,7 +81,6 @@ import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagic
import type { FormState } from '@tmagic/form';
import CompareForm from '@editor/components/CompareForm.vue';
import { useServices } from '@editor/hooks/use-services';
import CodeEditor from '@editor/layouts/CodeEditor.vue';
import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload, Services } from '@editor/type';
@ -92,7 +91,7 @@ defineOptions({
const props = withDefaults(
defineProps<{
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
services: Services;
services?: Services;
/**
* 来自 Editor 顶层的 `extendFormState`用于扩展 MForm.formState
* 透传给 CompareForm从而让差异对比时表单 item 中依赖业务上下文的
@ -111,7 +110,6 @@ const props = withDefaults(
compareFormState?: FormState;
}>(),
{
services: () => useServices(),
width: '900px',
},
);

View File

@ -313,10 +313,10 @@ const onCodeBlockGotoInitial = (id: string | number) => {
/**
* 单步回滚查看差异的完整逻辑收敛到 useHistoryRevert面板与业务方共用
* 二者均由 useHistoryRevert 内部按需动态挂载 HistoryDiffDialog
* 业务方亦可直接 import useHistoryRevert(services) 调用无需自行挂载任何弹窗
* 业务方亦可直接 import useHistoryRevert(options, services) 调用无需自行挂载任何弹窗
*/
const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
useHistoryRevert(services, { extendState: extendFormState, getPropsPanelFormState });
useHistoryRevert({ extendState: extendFormState, getPropsPanelFormState }, services);
/**
* 把内存中已清空对应类别后的历史状态重新写回 IndexedDB

View File

@ -76,7 +76,7 @@ interface MountedDiffDialog {
const mountHistoryDiffDialog = async (
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Services;
services?: Services;
isConfirm?: boolean;
onClose?: () => void;
},
@ -125,7 +125,7 @@ const confirmRevertWithDiffDialog = async (
payload: DiffDialogPayload,
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Services;
services?: Services;
},
): Promise<boolean> => {
const { instance, destroy } = await mountHistoryDiffDialog({
@ -147,7 +147,7 @@ const viewHistoryDiffDialog = async (
payload: DiffDialogPayload,
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Services;
services?: Services;
},
): Promise<void> => {
// onClose 在用户关闭弹窗时才触发,此时 handle.destroy 早已赋值。
@ -184,13 +184,12 @@ const viewHistoryDiffDialog = async (
* ```ts
* import { useHistoryRevert } from '@tmagic/editor';
*
* const { onPageRevert, onPageDiff } = useHistoryRevert(editorRef.value); // editorRef.value 即 Editor 暴露的 services
* const { onPageRevert, onPageDiff } = useHistoryRevert({}, editorRef.value); // editorRef.value 即 Editor 暴露的 services
* await onPageRevert(index); // 弹出差异 / 二次确认弹窗后回滚
* onPageDiff(index); // 弹出只读差异弹窗查看前后值差异
* ```
*/
export const useHistoryRevert = (services: Services, options: UseHistoryRevertOptions = {}) => {
const { editorService, dataSourceService, codeBlockService, historyService } = services;
export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
// 自动捕获调用方所在组件的 appContext在 setup 中调用时),业务方亦可显式覆盖。
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
const { extendState, getPropsPanelFormState } = options;
@ -226,8 +225,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
buildDiffPayload(
{
category: 'node',
groups: () => historyService.getHistoryGroups('page', editorService.get('page')?.id),
getCurrent: (id) => editorService.getNodeById(id) as Record<string, any> | null,
groups: () => services?.historyService.getHistoryGroups('page', services?.editorService.get('page')?.id) ?? [],
getCurrent: (id) => services?.editorService.getNodeById(id) as Record<string, any> | null,
resolveType: (n, o) => n.type || o.type || '',
resolveLabel: (n, o) => n.name || o.name || n.type || o.type || '',
},
@ -238,8 +237,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
buildDiffPayload(
{
category: 'data-source',
groups: () => historyService.getHistoryGroups('dataSource'),
getCurrent: (id) => dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
groups: () => services?.historyService.getHistoryGroups('dataSource') ?? [],
getCurrent: (id) => services?.dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
resolveType: (n, o) => n.type || o.type || 'base',
resolveLabel: (n, o, id) => n.title || o.title || `${id}`,
},
@ -251,8 +250,8 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
buildDiffPayload(
{
category: 'code-block',
groups: () => historyService.getHistoryGroups('codeBlock'),
getCurrent: (id) => codeBlockService.getCodeContentById(id) as Record<string, any> | null,
groups: () => services?.historyService.getHistoryGroups('codeBlock') ?? [],
getCurrent: (id) => services?.codeBlockService.getCodeContentById(id) as Record<string, any> | null,
resolveLabel: (n, o, id) => n.name || o.name || `${id}`,
},
index,
@ -266,17 +265,17 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
* add使
*/
const isPageRevertTargetMissing = (index: number): boolean => {
const step = historyService.getStepList('page', editorService.get('page')?.id)[index]?.step;
const step = services?.historyService.getStepList('page', services?.editorService.get('page')?.id)[index]?.step;
if (!step) return false;
if (step.opType === 'update') {
return (step.diff ?? []).some((item) => {
const id = item.newSchema?.id ?? item.oldSchema?.id;
return id !== undefined && !editorService.getNodeById(id, false);
return id !== undefined && !services?.editorService.getNodeById(id, false);
});
}
if (step.opType === 'remove') {
return (step.diff ?? []).some(
(item) => item.parentId !== undefined && !editorService.getNodeById(item.parentId, false),
(item) => item.parentId !== undefined && !services?.editorService.getNodeById(item.parentId, false),
);
}
return false;
@ -284,14 +283,14 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
/** 数据源 update 步骤回滚时,对应数据源必须仍存在(已删除则无处写回旧值)。 */
const isDataSourceRevertTargetMissing = (id: string | number, index: number): boolean => {
const step = historyService.getStepList('dataSource', id)[index]?.step;
return Boolean(step?.opType === 'update' && !dataSourceService.getDataSourceById(`${id}`));
const step = services?.historyService.getStepList('dataSource', id)[index]?.step;
return Boolean(step?.opType === 'update' && !services?.dataSourceService.getDataSourceById(`${id}`));
};
/** 代码块 update 步骤回滚时,对应代码块必须仍存在(已删除则无处写回旧值)。 */
const isCodeBlockRevertTargetMissing = (id: string | number, index: number): boolean => {
const step = historyService.getStepList('codeBlock', id)[index]?.step;
return Boolean(step?.opType === 'update' && !codeBlockService.getCodeContentById(id));
const step = services?.historyService.getStepList('codeBlock', id)[index]?.step;
return Boolean(step?.opType === 'update' && !services?.codeBlockService.getCodeContentById(id));
};
const onPageRevert = (index: number) => {
@ -300,7 +299,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
return Promise.resolve(null);
}
return runRevert(buildPageDiffPayload(index), { compareFormState: getPropsPanelFormState?.() }).then((result) =>
result ? editorService.revertPageStep(index) : null,
result ? services?.editorService.revertPageStep(index) : null,
);
};
@ -310,7 +309,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
return Promise.resolve(null);
}
return runRevert(buildDataSourceDiffPayload(id, index)).then((result) =>
result ? dataSourceService.revert(id, index) : null,
result ? services?.dataSourceService.revert(id, index) : null,
);
};
@ -320,7 +319,7 @@ export const useHistoryRevert = (services: Services, options: UseHistoryRevertOp
return Promise.resolve(null);
}
return runRevert(buildCodeBlockDiffPayload(id, index)).then((result) =>
result ? codeBlockService.revert(id, index) : null,
result ? services?.codeBlockService.revert(id, index) : null,
);
};

View File

@ -87,7 +87,7 @@ const services = {} as any;
const factory = () =>
mount(HistoryDiffDialog, {
// 让 Teleport 内容内联渲染,便于通过 wrapper 查询
global: { stubs: { teleport: true }, provide: { services } },
global: { stubs: { teleport: true } },
});
const basePayload = (extra: any = {}) => ({

View File

@ -62,7 +62,7 @@ describe('useHistoryRevert', () => {
]);
services.editorService.getNodeById.mockReturnValue(null);
const { onPageRevert } = useHistoryRevert(services);
const { onPageRevert } = useHistoryRevert({}, services);
await onPageRevert(0);
expect(tMagicMessage.error).toHaveBeenCalledWith('回滚失败:该记录对应的数据已被删除');
@ -80,7 +80,7 @@ describe('useHistoryRevert', () => {
},
]);
const { onPageRevert } = useHistoryRevert(services);
const { onPageRevert } = useHistoryRevert({}, services);
await onPageRevert(0);
expect(confirmHistoryAction).toHaveBeenCalled();
@ -99,7 +99,7 @@ describe('useHistoryRevert', () => {
]);
services.dataSourceService.getDataSourceById.mockReturnValue(null);
const { onDataSourceRevert } = useHistoryRevert(services);
const { onDataSourceRevert } = useHistoryRevert({}, services);
await onDataSourceRevert('ds_1', 0);
expect(tMagicMessage.error).toHaveBeenCalledWith('回滚失败:该记录对应的数据已被删除');