feat(editor): support custom width and initial diff mode for history diff dialog

为历史差异/回滚确认弹窗新增可配置宽度(dialogWidth / width 透传至 TMagicDialog),
并支持通过 payload.mode 指定打开时的初始对比模式,不可用时回退自动推断。
This commit is contained in:
roymondchen 2026-06-29 20:23:19 +08:00
parent 4330738b9f
commit 83f4e52845
5 changed files with 174 additions and 9 deletions

View File

@ -102,6 +102,7 @@ onCodeBlockDiff(id, index);
| --- | --- | --- |
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled``filterFunction` 正常工作。 |
| `dialogWidth` | 否 | 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 `TMagicDialog``width`),如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(`900px`)。业务自有历史可在 `viewDiff` / `confirmAndRevert` 调用时通过各自入参的 `width` 单独覆盖。 |
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`

View File

@ -162,11 +162,19 @@ const hasCurrent = computed(() => payload.value?.currentValue !== undefined && p
/** 是否存在该步「修改后的值」:不存在(如仅删除)时「与修改前对比」无意义,置灰禁用。 */
const hasValue = computed(() => payload.value?.value !== undefined && payload.value?.value !== null);
/** 指定模式当前是否可用before 依赖「修改后的值」current 依赖「当前值」。 */
const isModeAvailable = (m: DiffMode): boolean => (m === 'current' ? hasCurrent.value : hasValue.value);
/**
* 计算 open 时的初始对比模式
* 没有修改后的值但有当前值时与修改前对比不可用默认进入与当前对比否则默认与修改前对比
* - 调用方通过 payload.mode 指定且该模式可用时优先使用指定模式
* - 否则没有修改后的值但有当前值时与修改前对比不可用默认进入与当前对比
* - 其余情况默认与修改前对比
*/
const resolveInitialMode = (): DiffMode => (!hasValue.value && hasCurrent.value ? 'current' : 'before');
const resolveInitialMode = (specified?: DiffMode): DiffMode => {
if (specified && isModeAvailable(specified)) return specified;
return !hasValue.value && hasCurrent.value ? 'current' : 'before';
};
/** 左侧(旧/参照)值 */
const leftValue = computed<Record<string, any>>(() => {
@ -222,8 +230,8 @@ const targetText = computed(() => {
const open = (p: DiffDialogPayload) => {
payload.value = p;
// payload hasValue / hasCurrent payload
//
mode.value = resolveInitialMode();
// 使 payload.mode
mode.value = resolveInitialMode(p.mode);
//
viewMode.value = 'form';
visible.value = true;

View File

@ -94,6 +94,7 @@ const mountHistoryDiffDialog = async (
loadConfig: options.loadConfig,
selfDiffFieldTypes: options.selfDiffFieldTypes,
compareFormState: options.compareFormState,
width: options.width,
onClose: options.onClose,
});
if (options.appContext) {
@ -192,7 +193,7 @@ const viewHistoryDiffDialog = async (
export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
// 自动捕获调用方所在组件的 appContext在 setup 中调用时),业务方亦可显式覆盖。
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
const { extendState, getPropsPanelFormState } = options;
const { extendState, getPropsPanelFormState, dialogWidth } = options;
/** 目标数据已被删除、无法回滚时的统一提示。 */
const showRevertTargetMissing = () => {
@ -216,7 +217,13 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
*/
const runRevert = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<boolean> => {
if (payload) {
return confirmRevertWithDiffDialog(payload, { appContext, extendState, services, ...extra });
return confirmRevertWithDiffDialog(payload, {
appContext,
extendState,
services,
...extra,
width: extra?.width ?? dialogWidth,
});
}
return confirmRevert();
};
@ -334,6 +341,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
appContext,
extendState,
services,
width: dialogWidth,
compareFormState: getPropsPanelFormState?.(),
});
}
@ -341,12 +349,12 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
const payload = buildDataSourceDiffPayload(id, index);
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
};
const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
const payload = buildCodeBlockDiffPayload(id, index);
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
};
/**
@ -373,6 +381,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
const confirmed = await runRevert(revertOptions.diffPayload ?? null, {
loadConfig: revertOptions.loadConfig,
selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
width: revertOptions.width,
});
if (!confirmed) return null;
return await revertOptions.revert();
@ -383,7 +392,14 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
* `loadConfig` / `selfDiffFieldTypes`payload null
*/
const viewDiff = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<void> | void => {
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, ...extra });
if (payload)
return viewHistoryDiffDialog(payload, {
appContext,
extendState,
services,
...extra,
width: extra?.width ?? dialogWidth,
});
};
return {

View File

@ -1352,6 +1352,14 @@ export interface DiffDialogPayload {
targetLabel?: string;
/** 用于标题展示的目标 id */
id?: string | number;
/**
*
* - before vs
* - current vs
* /
* 退
*/
mode?: 'before' | 'current';
}
/**
@ -1408,6 +1416,12 @@ export interface UseHistoryRevertOptions {
* filterFunction
*/
getPropsPanelFormState?: () => FormState | undefined;
/**
* / / / TMagicDialog `width`
* `'1200px'` / `'80%'`使900px
* `viewDiff` / `confirmAndRevert` `width`
*/
dialogWidth?: string;
}
/**
@ -1428,6 +1442,11 @@ export interface CustomDiffFormOptions {
* CompareForm
*/
compareFormState?: FormState;
/**
* / HistoryDiffDialog TMagicDialog `width`
* `'1200px'` / `'80%'`使900px
*/
width?: string;
}
/**

View File

@ -24,6 +24,36 @@ vi.mock('@editor/layouts/history-list/composables', async () => {
};
});
// 捕获动态挂载 HistoryDiffDialog 时传入 createApp 的 props用于断言 width 透传),
// 并桩掉弹窗实例的 open / confirm避免真正渲染组件。
const dialogInstance = {
open: vi.fn(),
confirm: vi.fn(async () => true),
};
const appMock = {
_context: {},
mount: vi.fn(() => dialogInstance),
unmount: vi.fn(),
};
const createAppMock = vi.fn((..._args: any[]) => appMock);
vi.mock('vue', async () => {
const actual = await vi.importActual<typeof import('vue')>('vue');
return {
...actual,
createApp: (...args: any[]) => createAppMock(...args),
// setup 之外调用,强制返回 null避免依赖宿主组件上下文
getCurrentInstance: () => null,
};
});
vi.mock('@editor/layouts/history-list/HistoryDiffDialog.vue', () => ({
default: { name: 'MEditorHistoryDiffDialog' },
}));
/** 读取最近一次挂载弹窗时传入的 propscreateApp 第二个参数)。 */
const lastDialogProps = () => createAppMock.mock.calls.at(-1)?.[1] as unknown as Record<string, any>;
const createServices = () =>
({
editorService: {
@ -45,6 +75,19 @@ const createServices = () =>
},
}) as any;
/** 构造一个可差异对比(单实体 update前后值都在的历史分组用于触发差异弹窗。 */
const diffableGroups = (id: string | number = 'p1') => [
{
id,
steps: [
{
index: 0,
step: { diff: [{ oldSchema: { id: 'n1', name: 'A' }, newSchema: { id: 'n1', name: 'B' } }] },
},
],
},
];
afterEach(() => {
vi.clearAllMocks();
});
@ -105,4 +148,82 @@ describe('useHistoryRevert', () => {
expect(tMagicMessage.error).toHaveBeenCalledWith('回滚失败:该记录对应的数据已被删除');
expect(services.dataSourceService.revert).not.toHaveBeenCalled();
});
describe('弹窗宽度透传', () => {
test('onPageDiff 使用 options.dialogWidth 作为弹窗宽度并打开弹窗', async () => {
const services = createServices();
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
const { onPageDiff } = useHistoryRevert({ dialogWidth: '1200px' }, services);
await onPageDiff(0);
expect(createAppMock).toHaveBeenCalledTimes(1);
expect(lastDialogProps().width).toBe('1200px');
expect(lastDialogProps().isConfirm).toBe(false);
expect(dialogInstance.open).toHaveBeenCalled();
});
test('未配置 dialogWidth 时不强制 width由弹窗内置默认值兜底', async () => {
const services = createServices();
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
const { onPageDiff } = useHistoryRevert({}, services);
await onPageDiff(0);
expect(createAppMock).toHaveBeenCalledTimes(1);
expect(lastDialogProps().width).toBeUndefined();
});
test('onPageRevert 在可差异步骤上走差异确认弹窗并透传 dialogWidth', async () => {
const services = createServices();
services.historyService.getStepList.mockReturnValue([
{ step: { opType: 'update', diff: [{ newSchema: { id: 'n1' }, oldSchema: { id: 'n1' } }] } },
]);
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
const { onPageRevert } = useHistoryRevert({ dialogWidth: '80%' }, services);
await onPageRevert(0);
expect(lastDialogProps().width).toBe('80%');
expect(lastDialogProps().isConfirm).toBe(true);
expect(dialogInstance.confirm).toHaveBeenCalled();
expect(services.editorService.revertPageStep).toHaveBeenCalledWith(0);
});
test('viewDiff 的逐次 width 入参可覆盖 dialogWidth 默认值', async () => {
const services = createServices();
const { viewDiff } = useHistoryRevert({ dialogWidth: '1000px' }, services);
await viewDiff({ category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any, { width: '600px' });
expect(lastDialogProps().width).toBe('600px');
});
test('viewDiff 未传 width 时回退到 dialogWidth 默认值', async () => {
const services = createServices();
const { viewDiff } = useHistoryRevert({ dialogWidth: '1000px' }, services);
await viewDiff({ category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any);
expect(lastDialogProps().width).toBe('1000px');
});
test('confirmAndRevert 透传 width 至确认弹窗并在确认后执行 revert', async () => {
const services = createServices();
const revert = vi.fn(async () => 'done');
const { confirmAndRevert } = useHistoryRevert({ dialogWidth: '1000px' }, services);
const result = await confirmAndRevert({
diffPayload: { category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any,
width: '720px',
revert,
});
expect(lastDialogProps().width).toBe('720px');
expect(lastDialogProps().isConfirm).toBe(true);
expect(revert).toHaveBeenCalled();
expect(result).toBe('done');
});
});
});