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 @@