tmagic-editor/docs/guide/advanced/history-list.md
roymondchen 83f4e52845 feat(editor): support custom width and initial diff mode for history diff dialog
为历史差异/回滚确认弹窗新增可配置宽度(dialogWidth / width 透传至 TMagicDialog),
并支持通过 payload.mode 指定打开时的初始对比模式,不可用时回退自动推断。
2026-06-29 20:23:19 +08:00

9.4 KiB
Raw Permalink Blame History

历史记录面板

编辑器内置了一个可视化的「历史记录面板」,用于查看与回溯编辑过程中产生的所有操作。相比顶部菜单栏只能「撤销 / 重做」相邻一步,历史记录面板提供了对整条历史栈的全局视角:可以按页面、数据源、代码块分类浏览,点击任意一步直接跳转,查看每一步的前后差异,甚至像 git revert 一样单独回滚某一步而不破坏后续操作。

开启面板

历史记录面板以一个内置菜单项 'history-list' 的形式提供,将它加入 menu 配置即可在顶部工具栏出现一个时钟图标,点击展开面板:

<template>
  <m-editor :menu="menu"></m-editor>
</template>

<script setup>
import { ref } from 'vue';

const menu = ref({
  left: [],
  center: ['delete', 'undo', 'redo', '/', 'history-list'],
  right: [],
});
</script>

面板结构

面板分为三个 tab分别对应三类可被历史记录追踪的对象tab 标题后的数字为各自的分组数量:

Tab 内容 跳转 API
页面 当前活动页面的节点操作历史 editorService.gotoPageStep(cursor)
数据源 dataSource.id 分组的数据源变更历史 dataSourceService.goto(id, cursor)
代码块 codeBlock.id 分组的代码块变更历史 codeBlockService.goto(id, cursor)

相邻同目标自动合并

为了避免「连续微调同一个节点 / 数据源 / 代码块」时产生大量碎片化记录,面板会把相邻的、针对同一目标的连续 update 自动合并成一个分组:

  • 页面 tab连续修改同一节点按节点 id 判定)的多步合并为一组,点击组头部可展开查看每一子步;
  • 数据源 / 代码块 tab相邻的连续 update 按目标 id 合并;add / remove 始终独立成组(语义上是一次性事件)。

合并仅作用于展示与交互,不改变底层 undo/redo 栈的真实结构。

交互能力

每个分组 / 步骤支持以下操作:

1. 点击跳转

点击任意一条记录编辑器会跳转到「应用至该步完成」的状态。其本质是把对应栈的游标cursor移动到 step.index + 1,由 service 层的 undo/redo 链路完成中间步骤的批量正向 / 反向应用。

2. 回到初始状态

每个 tab 列表底部提供「回到初始状态」入口,等价于把对应栈游标移到 0(所有真实步骤全部撤销)。

3. 单步回滚(类 git revert

对于历史中间的某一步,可以单独「回滚」它,而保留它之后的所有操作。该行为不会倒带游标,而是把目标步骤的修改反向应用为一次全新的操作并压入栈顶,因此不会破坏既有历史结构:

  • 页面:editorService.revertPageStep(index)
  • 数据源:dataSourceService.revert(id, index)
  • 代码块:codeBlockService.revert(id, index)

如果业务侧在执行操作时已通过 *AndGetHistoryId 拿到了该条记录的 uuiduuid 与 *AndGetHistoryId 返回的 historyIds),也可以直接按 uuid 回滚(无需再关心 index / id且 uuid 不会随栈内步骤增删而变化):

  • 页面:editorService.revertPageStepById(uuids)
  • 数据源:dataSourceService.revertById(uuids)
  • 代码块:codeBlockService.revertById(uuids)

复用面板的「交互式回滚 / 查看差异」流程

上面的 revert* 方法是静默的:它们直接执行反向应用,不做任何校验与二次确认。如果业务方想在自己的入口(自定义按钮、右键菜单等)里复用历史面板那一套完整交互流程——即「目标数据已删除的前置校验 + 失败提示」「可差异步骤弹差异确认弹窗 / 其余步骤弹普通二次确认框」「用户确认后才回滚」,以及「查看差异」弹窗——可以直接 import useHistoryRevert

import { useHistoryRevert } from '@tmagic/editor';

// editorRef 为 <m-editor> 组件实例,其 expose 出的即各 service 集合Services
const {
  onPageRevert,
  onDataSourceRevert,
  onCodeBlockRevert,
  onPageDiff,
  onDataSourceDiff,
  onCodeBlockDiff,
} = useHistoryRevert({}, editorRef.value);

// 回滚:可差异步骤弹出差异确认弹窗、其余步骤弹普通二次确认框;用户点「确定」后回滚第 index 步,
// 命中前置校验或用户取消时不执行,返回 null
await onPageRevert(index);
await onDataSourceRevert(id, index);
await onCodeBlockRevert(id, index);

// 查看差异:可差异步骤弹出只读差异弹窗展示前后值差异,无可对比内容时不弹窗
onPageDiff(index);
onDataSourceDiff(id, index);
onCodeBlockDiff(id, index);

回滚确认弹窗与查看差异弹窗均由 useHistoryRevert 内部按需动态挂载 HistoryDiffDialog 实现,业务方无需自行挂载任何弹窗组件。第二个参数 options 可选:

字段 必填 说明
appContext 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件Element Plus、@tmagic/form 字段组件等)。在组件 setup 中调用时会自动取当前组件的 appContext,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 editorApp._context)。
extendState 透传给差异确认弹窗的 extendState(同 Editor 的 extendFormState),使对比表单中依赖业务上下文的 display / disabledfilterFunction 正常工作。
dialogWidth 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialogwidth),如 '1200px' / '80%'。缺省时使用弹窗内置默认宽度(900px)。业务自有历史可在 viewDiff / confirmAndRevert 调用时通过各自入参的 width 单独覆盖。

若只需要无确认、无校验的静默回滚,直接用上面的 editorService.revertPageStep 等即可,无需 useHistoryRevert

4. 差异对比

在前后值都存在的 update 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换:

  • 对比对象
    • 与修改前对比:该步骤修改前 vs 修改后(默认,体现这一步带来的变化);
    • 与当前对比:该步骤修改后 vs 编辑器中的最新值(用于确认「这一步之后是否又被改动过」,当前值缺失时禁用)。
  • 展示形态
    • 表单对比:以属性表单形式逐字段对比,可读性更好(基于 表单对比 能力);
    • 源码对比:以 JSON 源码做整体 diff基于 monaco diff 编辑器),可以看到表单未覆盖到的字段。

::: tip 表单对比依赖 @tmagic/form 的对比模式(isCompare / lastValues)。对于 event-selectcode-selectcode-select-col 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。 :::

扩展自定义 tab

内置的三个 tab 之外,业务方可以通过 Editor 的 historyListExtraTabs 在面板中追加自定义的历史 tab追加在「页面 / 数据源 / 代码块」之后。适用于某个自定义模块维护自己的操作历史,需要在历史记录面板中独立展示与回滚的场景。

<template>
  <m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
</template>

<script setup>
import { markRaw } from 'vue';

import MyModuleHistoryTab from './MyModuleHistoryTab.vue';

const historyListExtraTabs = [
  {
    name: 'my-module',
    // label 支持字符串或函数,函数形式便于展示动态数量
    label: () => `我的模块 (${getMyModuleHistory().length})`,
    component: markRaw(MyModuleHistoryTab),
    props: { foo: 'bar' },
    listeners: {
      goto: (cursor) => console.log(cursor),
    },
  },
];
</script>

每个扩展 tab 的字段说明:

字段 必填 说明
name tab 唯一标识,作为内部 TMagicTabsname
label tab 显示文案,支持字符串或返回字符串的函数(便于展示动态数量)
component tab 内容区渲染的组件
props 传入内容组件的 props
listeners 内容组件的事件监听

内容组件内部可自行通过 useServices() 拿到 historyService 等服务,读取并回滚自定义模块自己维护的历史。

自定义对比判断

差异对话框中的「表单对比」最终透传到 MForm,你可以通过 Editor 顶层注入的 extendFormState 让对比表单拿到完整业务上下文,从而让依赖上下文的 display / disabledfilterFunction 正常工作。

若某些字段语义上相等但结构不同(例如 code-select 字段中 ''{ hookType: 'code', hookData: [] } 应视为相等),可借助 @tmagic/formshowDiff 自定义判断函数避免被误判为差异。

相关 API

历史面板的数据均来自 historyService 暴露的聚合方法,详见 historyService 方法