Skip to content

历史记录面板

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

开启面板

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

html
<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 拿到了该条记录的 uuid(见 uuid 与 *AndGetHistoryId 返回的 historyIds),也可以直接按 uuid 回滚(无需再关心 index / id,且 uuid 不会随栈内步骤增删而变化):

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

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

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

ts
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 正常工作。

若只需要无确认、无校验的静默回滚,直接用上面的 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,追加在「页面 / 数据源 / 代码块」之后。适用于某个自定义模块维护自己的操作历史,需要在历史记录面板中独立展示与回滚的场景。

html
<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 的字段说明:

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

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

自定义对比判断

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

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

相关 API

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

Powered by 腾讯视频会员平台技术中心