diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 43fa118f..d35d5a13 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -102,6 +102,10 @@ export default defineConfig({
text: '数据源',
link: '/guide/advanced/data-source.md'
},
+ {
+ text: '历史记录面板',
+ link: '/guide/advanced/history-list.md',
+ },
{
text: '@tmagic/form',
diff --git a/docs/api/editor/props.md b/docs/api/editor/props.md
index f6e56d59..0f97a2a5 100644
--- a/docs/api/editor/props.md
+++ b/docs/api/editor/props.md
@@ -260,7 +260,7 @@ icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/ico
顶部工具栏
- 系统提供了几个常用功能: `'/' | 'delete' | 'undo' | 'redo' | 'zoom-in' | 'zoom-out' | 'zoom' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit'`
+ 系统提供了几个常用功能: `'/' | 'delete' | 'undo' | 'redo' | 'zoom-in' | 'zoom-out' | 'zoom' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit' | 'history-list'`
'/': 分隔符
@@ -284,6 +284,8 @@ icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/ico
'scale-to-fit': 缩放以适应
+ 'history-list': 历史记录面板(按 页面 / 数据源 / 代码块 三个 tab 展示操作历史,相邻同目标修改自动合并,支持点击跳转、回到初始状态、单步回滚及差异对比,详见[历史记录面板](/guide/advanced/history-list.md))
+
- **默认值:**
```js
diff --git a/docs/form-config/compare.md b/docs/form-config/compare.md
index 12fad615..e31b1c73 100644
--- a/docs/form-config/compare.md
+++ b/docs/form-config/compare.md
@@ -1,7 +1,30 @@
# 表单对比
tmagic-form可以支持两个版本的表单值对比,如果有容器嵌套,将在tab标签页展示对应tab下存在的差异数,便于在复杂嵌套表单场景下直观的看到差异情况
+
## 使用方法
-在初始化表单时,需要传入当前版本的表单值,上一版本的表单值,以及表单配置,具体可参见[Form Playground](https://tencent.github.io/tmagic-editor/playground/index.html#/form)的demo演示
+在初始化表单时,开启对比模式 `is-compare`,并传入当前版本的表单值(`init-values`)、上一版本的表单值(`last-values`)以及表单配置,具体可参见[Form Playground](https://tencent.github.io/tmagic-editor/playground/index.html#/form)的demo演示。
+
+```html
+
+```
+
+相关属性详见 Form 组件 props:
+
+- [`isCompare`](/api/form/form-props.html#iscompare):是否开启对比模式;
+- [`lastValues`](/api/form/form-props.html#lastvalues):需对比的上一版本表单值;
+- [`showDiff`](/api/form/form-props.html#showdiff):自定义「是否展示对比内容」的判断函数,用于规避语义相等但结构不同导致的误判。
+
+## 对比模式下的字段行为
+对比模式下,表单仅做只读展示:增删、复制、排序、导入、编辑等写操作按钮会被隐藏。对于由列表或嵌套子表单组成的复合字段(如 `event-select`、`code-select`、`code-select-col`),表单会按索引对齐前后值,逐项展示新增 / 删除 / 修改的高亮差异,而不会渲染出两套独立组件。
+
+## 应用场景
+编辑器的[历史记录面板](/guide/advanced/history-list.md)即基于该能力,对历史步骤的前后值做表单形式的差异对比。
+
## 效果展示
diff --git a/docs/guide/advanced/history-list.md b/docs/guide/advanced/history-list.md
new file mode 100644
index 00000000..6c2d9058
--- /dev/null
+++ b/docs/guide/advanced/history-list.md
@@ -0,0 +1,87 @@
+# 历史记录面板
+
+编辑器内置了一个可视化的「历史记录面板」,用于查看与回溯编辑过程中产生的所有操作。相比顶部菜单栏只能「撤销 / 重做」相邻一步,历史记录面板提供了对整条历史栈的全局视角:可以按页面、数据源、代码块分类浏览,点击任意一步直接跳转,查看每一步的前后差异,甚至像 `git revert` 一样单独回滚某一步而不破坏后续操作。
+
+## 开启面板
+
+历史记录面板以一个内置菜单项 `'history-list'` 的形式提供,将它加入 [`menu`](/api/editor/props.html#menu) 配置即可在顶部工具栏出现一个时钟图标,点击展开面板:
+
+```html
+
+
+
+
+
+```
+
+## 面板结构
+
+面板分为三个 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)`
+
+### 4. 差异对比
+
+在前后值都存在的 `update` 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换:
+
+- **对比对象**
+ - `与修改前对比`:该步骤修改前 vs 修改后(默认,体现这一步带来的变化);
+ - `与当前对比`:该步骤修改后 vs 编辑器中的最新值(用于确认「这一步之后是否又被改动过」,当前值缺失时禁用)。
+- **展示形态**
+ - `表单对比`:以属性表单形式逐字段对比,可读性更好(基于 [表单对比](/form-config/compare.md) 能力);
+ - `源码对比`:以 JSON 源码做整体 diff(基于 monaco diff 编辑器),可以看到表单未覆盖到的字段。
+
+::: tip
+表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select`、`code-select`、`code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。
+:::
+
+## 自定义对比判断
+
+差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。
+
+若某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''` 与 `{ hookType: 'code', hookData: [] }` 应视为相等),可借助 `@tmagic/form` 的 [`showDiff`](/api/form/form-props.html#showdiff) 自定义判断函数避免被误判为差异。
+
+## 相关 API
+
+历史面板的数据均来自 `historyService` 暴露的聚合方法,详见 [historyService 方法](/api/editor/historyServiceMethods.md)。
diff --git a/packages/editor/src/components/CodeParams.vue b/packages/editor/src/components/CodeParams.vue
index d098697e..caa73631 100644
--- a/packages/editor/src/components/CodeParams.vue
+++ b/packages/editor/src/components/CodeParams.vue
@@ -3,6 +3,8 @@
ref="form"
:config="codeParamsConfig"
:init-values="model"
+ :last-values="lastValues"
+ :is-compare="isCompare"
:disabled="disabled"
:size="size"
:watch-props="false"
@@ -24,6 +26,10 @@ defineOptions({
const props = defineProps<{
model: any;
+ /** 对比模式下的历史值,透传给内部 MForm 用于逐项展示参数差异 */
+ lastValues?: any;
+ /** 是否开启对比模式 */
+ isCompare?: boolean;
size?: 'small' | 'default' | 'large';
disabled?: boolean;
name: string;
diff --git a/packages/editor/src/fields/CodeSelect.vue b/packages/editor/src/fields/CodeSelect.vue
index 7c2fcd27..ee038e73 100644
--- a/packages/editor/src/fields/CodeSelect.vue
+++ b/packages/editor/src/fields/CodeSelect.vue
@@ -6,7 +6,8 @@
:size="size"
:prop="prop"
:disabled="disabled"
- :lastValues="lastValues"
+ :is-compare="isCompareMode"
+ :last-values="lastValues?.[name]"
:model="model[name]"
@change="changeHandler"
>
@@ -38,6 +39,21 @@ const { dataSourceService, codeBlockService } = useServices();
const props = withDefaults(defineProps>(), {});
+/**
+ * 对比模式判定:
+ *
+ * code-select 仅是对内部「钩子列表」group-list 的包裹,本身不渲染叶子字段。父级 `MFormContainer`
+ * 已将其归入「自接管对比字段」(见 Container.vue 的 `SELF_DIFF_FIELD_TYPES`),即对比时只渲染一次
+ * 本组件,并把当前值 `model` 与历史值 `lastValues` 一并传入,由本组件把 `is-compare`/`lastValues`
+ * 透传给内部 MContainer,再由 group-list / code-select-col 等子级逐项展示前后差异。
+ *
+ * 注意:`model` 传入的是 `model[name]`(钩子值本身),因此 `lastValues` 也必须同层取 `lastValues[name]`,
+ * 否则前后值的嵌套层级不一致会导致对比错位。
+ *
+ * 仅当存在历史值时才启用对比,避免 lastValues 缺失时退化为「全部新增」的空对比。
+ */
+const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
+
const codeConfig = computed(() => ({
type: 'group-list',
name: 'hookData',
diff --git a/packages/editor/src/fields/CodeSelectCol.vue b/packages/editor/src/fields/CodeSelectCol.vue
index 0f44901e..ab8ed253 100644
--- a/packages/editor/src/fields/CodeSelectCol.vue
+++ b/packages/editor/src/fields/CodeSelectCol.vue
@@ -2,7 +2,20 @@
+
+
-
+
>(), {
disabled: false,
});
+/**
+ * 对比模式判定:
+ *
+ * code-select-col 由「代码块下拉框 + 参数子表单」组成,属于复合字段。父级 `MFormContainer` 已将其
+ * 归入「自接管对比字段」(见 Container.vue 的 `SELF_DIFF_FIELD_TYPES`),即对比时只渲染一次本组件,
+ * 并把当前值 `model` 与历史值 `lastValues` 一并传入,由本组件把 `is-compare`/`lastValues` 透传给
+ * 内部的下拉框(MFormContainer)与参数表单(CodeParams),逐项展示前后差异。
+ *
+ * 仅当存在历史值时才启用对比,避免 lastValues 缺失时退化为「全部新增」的空对比。
+ */
+const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
+
const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
const hasCodeBlockSidePanel = computed(() =>
diff --git a/packages/editor/src/fields/DataSourceFieldSelect/FieldSelect.vue b/packages/editor/src/fields/DataSourceFieldSelect/FieldSelect.vue
index f290ff01..08b4fdbf 100644
--- a/packages/editor/src/fields/DataSourceFieldSelect/FieldSelect.vue
+++ b/packages/editor/src/fields/DataSourceFieldSelect/FieldSelect.vue
@@ -72,7 +72,10 @@
@change="onChangeHandler"
>
-
+
@@ -133,6 +136,9 @@ const dataSources = computed(() => {
const valueIsKey = computed(() => props.value === 'key');
const notEditable = computed(() => filterFunction(mForm, props.notEditable, props));
+/** 对比模式下隐藏查看/编辑操作按钮,仅保留只读展示。 */
+const isCompare = computed(() => Boolean(mForm?.isCompare));
+
const dataSourcesOptions = computed(() =>
dataSources.value.map((ds) => ({
text: ds.title || ds.id,
diff --git a/packages/editor/src/fields/DataSourceFieldSelect/Index.vue b/packages/editor/src/fields/DataSourceFieldSelect/Index.vue
index 3b9812e5..6d89e00b 100644
--- a/packages/editor/src/fields/DataSourceFieldSelect/Index.vue
+++ b/packages/editor/src/fields/DataSourceFieldSelect/Index.vue
@@ -28,14 +28,14 @@
>
diff --git a/packages/editor/src/fields/DataSourceFields.vue b/packages/editor/src/fields/DataSourceFields.vue
index eb18e3a1..4a93fb39 100644
--- a/packages/editor/src/fields/DataSourceFields.vue
+++ b/packages/editor/src/fields/DataSourceFields.vue
@@ -1,8 +1,8 @@
-
+
-