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' },
}));
/** 读取最近一次挂载弹窗时传入的 props(createApp 第二个参数)。 */
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');
});
});
});