feat(editor): 优化历史记录对比与回滚流程

This commit is contained in:
roymondchen 2026-06-25 16:30:01 +08:00
parent 1c936ff439
commit ce65b18dbb
8 changed files with 79 additions and 22 deletions

View File

@ -135,7 +135,7 @@
<script lang="ts" setup>
import { EventEmitter } from 'events';
import { provide } from 'vue';
import { provide, ref } from 'vue';
import type { MApp } from '@tmagic/core';
@ -227,6 +227,8 @@ const stageOptions: StageOptions = {
stageOverlayService.set('stageOptions', stageOptions);
const propsPanelRef = ref<InstanceType<typeof FormPanel> | null>(null);
provide('services', services);
provide('codeOptions', props.codeOptions);
@ -237,6 +239,11 @@ provide('stageOptions', stageOptions);
* PropsPanel 通过 `:extend-state` 显式传入的方式保持等价
*/
provide('extendFormState', props.extendFormState);
/**
* 提供 PropsPanel 主属性表单的 formState getter供历史差异弹窗复用
* CompareForm PropsPanel filterFunction 上下文保持一致
*/
provide('getPropsPanelFormState', () => propsPanelRef.value?.configForm?.formState);
/**
* 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel它挂在 NavMenu
@ -248,9 +255,11 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
provide<EventBus>('eventBus', new EventEmitter());
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
propsPanelRef.value = e;
emit('props-panel-mounted', e);
};
const propsPanelUnmountedHandler = () => {
propsPanelRef.value = null;
emit('props-panel-unmounted');
};

View File

@ -10,8 +10,9 @@
:is-compare="true"
:disabled="true"
:label-width="labelWidth"
:extend-state="extendState"
:extend-state="mergedExtendState"
:show-diff="showDiff"
:self-diff-field-types="selfDiffFieldTypes"
></MForm>
</div>
</template>
@ -61,6 +62,13 @@ const props = withDefaults(
* 因此在差异对比场景下也需要透传避免出现 `formState.xxx is undefined` 的运行时错误
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 外部透传的基础 formState通常来自 PropsPanel 主属性表单
* CompareForm 会提取其中的扩展字段覆盖到自己的 formState保证 filterFunction 上下文一致
*/
baseFormState?: FormState;
/** 需要走 self diff 的字段类型(例如 mod-cond。 */
selfDiffFieldTypes?: string[];
/**
* 自定义 FormConfig 加载逻辑传入后将接管内置的按 `category`(node/data-source/code-block)
* 取配置逻辑调用方可根据业务自行返回或异步返回表单配置可通过
@ -74,6 +82,7 @@ const props = withDefaults(
category: 'node',
labelWidth: '120px',
services: () => useServices(),
extendState: (state: FormState) => state,
},
);
@ -174,6 +183,10 @@ const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
};
});
const mergedExtendState = (state: FormState) => {
return props.extendState(props.baseFormState || state);
};
/**
* 内置的默认 FormConfig 加载逻辑 `category` 从对应 service / 工具取配置
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`方便复用与二次加工

View File

@ -30,10 +30,10 @@
<template #suffix>
<Icon :icon="Coin" />
</template>
<template #default="{ item }">
<template #default="slotProps">
<div style="display: flex; flex-direction: column; line-height: 1.2em">
<div>{{ item.text }}</div>
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ item.value }}</span>
<div>{{ slotProps?.item?.text || '' }}</div>
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ slotProps?.item?.value || '' }}</span>
</div>
</template>
</component>

View File

@ -43,6 +43,7 @@
:data-source-type="payload.dataSourceType"
:value="rightValue"
:last-value="leftValue"
:base-form-state="compareFormState"
:extend-state="extendState"
:load-config="loadConfig"
:self-diff-field-types="selfDiffFieldTypes"
@ -107,6 +108,7 @@ const props = withDefaults(
isConfirm?: boolean;
onConfirm?: () => void;
selfDiffFieldTypes?: string[];
compareFormState?: FormState;
}>(),
{
services: () => useServices(),

View File

@ -18,7 +18,9 @@
<TMagicTabs v-model="activeTab" class="m-editor-history-list-tabs">
<component
:is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="tabPaneComponent?.props({ name: 'page', label: `${pageName} (${pageGroups.length})` }) || {}"
v-bind="
tabPaneComponent?.props({ name: 'page', label: `${pageName} (${pageGroups.length})`, lazy: true }) || {}
"
>
<PageTab
:list="pageGroupsDisplay"
@ -38,8 +40,11 @@
v-if="!disabledDataSource"
:is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="
tabPaneComponent?.props({ name: 'data-source', label: `${dataSourceName} (${dataSourceGroups.length})` }) ||
{}
tabPaneComponent?.props({
name: 'data-source',
label: `${dataSourceName} (${dataSourceGroups.length})`,
lazy: true,
}) || {}
"
>
<BucketTab
@ -59,7 +64,11 @@
v-if="!disabledCodeBlock"
:is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="
tabPaneComponent?.props({ name: 'code-block', label: `${codeBlockName} (${codeBlockGroups.length})` }) || {}
tabPaneComponent?.props({
name: 'code-block',
label: `${codeBlockName} (${codeBlockGroups.length})`,
lazy: true,
}) || {}
"
>
<BucketTab
@ -79,7 +88,7 @@
v-for="tab in extraTabs"
:key="tab.name"
:is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab), lazy: true }) || {}"
>
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
</component>
@ -193,6 +202,7 @@ const extendFormState = inject<((_state: FormState) => Record<string, any> | Pro
'extendFormState',
undefined,
);
const getPropsPanelFormState = inject<(() => FormState | undefined) | undefined>('getPropsPanelFormState', undefined);
const {
expanded,
@ -306,7 +316,7 @@ const onCodeBlockGotoInitial = (id: string | number) => {
* 业务方亦可直接 import useHistoryRevert(services) 调用无需自行挂载任何弹窗
*/
const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
useHistoryRevert(services, { extendState: extendFormState });
useHistoryRevert(services, { extendState: extendFormState, getPropsPanelFormState });
/**
* 把内存中已清空对应类别后的历史状态重新写回 IndexedDB

View File

@ -59,6 +59,7 @@ const buildDiffPayload = (source: DiffPayloadSource, index: number, id?: string
};
interface MountedDiffDialog {
app: ReturnType<typeof createApp>;
instance: {
open: (_payload: DiffDialogPayload) => void;
confirm: (_payload: DiffDialogPayload) => Promise<boolean>;
@ -75,7 +76,7 @@ interface MountedDiffDialog {
const mountHistoryDiffDialog = async (
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
services: Services;
isConfirm?: boolean;
onClose?: () => void;
},
@ -92,6 +93,7 @@ const mountHistoryDiffDialog = async (
extendState: options.extendState,
loadConfig: options.loadConfig,
selfDiffFieldTypes: options.selfDiffFieldTypes,
compareFormState: options.compareFormState,
onClose: options.onClose,
});
if (options.appContext) {
@ -111,7 +113,7 @@ const mountHistoryDiffDialog = async (
}, 300);
};
return { instance, destroy };
return { app, instance, destroy };
};
/**
@ -123,7 +125,7 @@ const confirmRevertWithDiffDialog = async (
payload: DiffDialogPayload,
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
services: Services;
},
): Promise<boolean> => {
const { instance, destroy } = await mountHistoryDiffDialog({
@ -145,7 +147,7 @@ const viewHistoryDiffDialog = async (
payload: DiffDialogPayload,
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
CustomDiffFormOptions & {
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
services: Services;
},
): Promise<void> => {
// onClose 在用户关闭弹窗时才触发,此时 handle.destroy 早已赋值。
@ -187,14 +189,11 @@ const viewHistoryDiffDialog = async (
* onPageDiff(index); // 弹出只读差异弹窗查看前后值差异
* ```
*/
export const useHistoryRevert = (
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>,
options: UseHistoryRevertOptions = {},
) => {
export const useHistoryRevert = (services: Services, options: UseHistoryRevertOptions = {}) => {
const { editorService, dataSourceService, codeBlockService, historyService } = services;
// 自动捕获调用方所在组件的 appContext在 setup 中调用时),业务方亦可显式覆盖。
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
const { extendState } = options;
const { extendState, getPropsPanelFormState } = options;
/** 目标数据已被删除、无法回滚时的统一提示。 */
const showRevertTargetMissing = () => {
@ -300,7 +299,7 @@ export const useHistoryRevert = (
showRevertTargetMissing();
return Promise.resolve(null);
}
return runRevert(buildPageDiffPayload(index)).then((result) =>
return runRevert(buildPageDiffPayload(index), { compareFormState: getPropsPanelFormState?.() }).then((result) =>
result ? editorService.revertPageStep(index) : null,
);
};
@ -331,7 +330,14 @@ export const useHistoryRevert = (
*/
const onPageDiff = (index: number): Promise<void> | void => {
const payload = buildPageDiffPayload(index);
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
if (payload) {
return viewHistoryDiffDialog(payload, {
appContext,
extendState,
services,
compareFormState: getPropsPanelFormState?.(),
});
}
};
const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {

View File

@ -1367,6 +1367,12 @@ export interface UseHistoryRevertOptions {
* 使 `display` / `disabled` filterFunction
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* PropsPanel FormPanel -> MForm formState
* / 使 formState CompareForm
* filterFunction
*/
getPropsPanelFormState?: () => FormState | undefined;
}
/**
@ -1382,6 +1388,11 @@ export interface CustomDiffFormOptions {
loadConfig?: CompareFormLoadConfig;
/** 需要走 self diff 的字段类型(如模块的 mod-cond。 */
selfDiffFieldTypes?: string[];
/**
* formState PropsPanel
* CompareForm
*/
compareFormState?: FormState;
}
/**

View File

@ -69,6 +69,8 @@ vi.mock('@tmagic/design', async () => {
class: 'select-field-btn',
onClick: () => emit('select', { value: 'a', type: 'field' }),
}),
// 模拟部分实现会以 undefined 触发默认插槽参数
slots.default?.(),
slots.suffix?.(),
]);
};
@ -164,6 +166,10 @@ describe('DataSourceInput', () => {
expect(wrapper.find('.fake-autocomplete').exists()).toBe(true);
});
test('autocomplete 默认插槽参数为 undefined 时不应报错', () => {
expect(() => mountIt('text-value', true)).not.toThrow();
});
test('未 focus 时显示文本视图', () => {
const wrapper = mountIt('hello');
expect(wrapper.find('.tmagic-data-source-input-text').exists()).toBe(true);