From 7a161cab007ced3b569d6b71eb4395a0d725f118 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 2 Jun 2026 19:07:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=95=B0=E6=8D=AE=E6=BA=90/=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9D=97=20tab=20=E5=A4=8D=E7=94=A8=E9=80=9A=E7=94=A8=20Bucket?= =?UTF-8?q?Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/layouts/history-list/Bucket.vue | 13 +- .../src/layouts/history-list/BucketTab.vue | 77 ++++++++ .../src/layouts/history-list/CodeBlockTab.vue | 61 ------- .../layouts/history-list/DataSourceTab.vue | 61 ------- .../src/layouts/history-list/GroupRow.vue | 80 ++++----- .../history-list/HistoryDiffDialog.vue | 37 ++-- .../layouts/history-list/HistoryListPanel.vue | 168 ++++++++++++------ .../src/layouts/history-list/InitialRow.vue | 23 ++- .../src/layouts/history-list/PageTab.vue | 14 +- .../src/layouts/history-list/composables.ts | 5 +- .../editor/src/theme/history-list-panel.scss | 44 +++-- packages/editor/src/type.ts | 20 +++ .../unit/layouts/history-list/Bucket.spec.ts | 10 +- .../layouts/history-list/CodeBlockTab.spec.ts | 71 +++++--- .../history-list/DataSourceTab.spec.ts | 71 +++++--- .../layouts/history-list/GroupRow.spec.ts | 17 +- .../history-list/HistoryListPanel.spec.ts | 14 +- .../layouts/history-list/InitialRow.spec.ts | 8 +- .../unit/layouts/history-list/PageTab.spec.ts | 14 +- 19 files changed, 464 insertions(+), 344 deletions(-) create mode 100644 packages/editor/src/layouts/history-list/BucketTab.vue delete mode 100644 packages/editor/src/layouts/history-list/CodeBlockTab.vue delete mode 100644 packages/editor/src/layouts/history-list/DataSourceTab.vue diff --git a/packages/editor/src/layouts/history-list/Bucket.vue b/packages/editor/src/layouts/history-list/Bucket.vue index 6faa5ea8..82de021f 100644 --- a/packages/editor/src/layouts/history-list/Bucket.vue +++ b/packages/editor/src/layouts/history-list/Bucket.vue @@ -8,9 +8,9 @@ @@ -87,9 +89,12 @@ const props = withDefaults( isStepDiffable?: (_step: any) => boolean; /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */ expanded: Record; + /** 是否支持「跳转到该记录」(goto)。默认 true。 */ + gotoEnabled?: boolean; }>(), { showInitial: true, + gotoEnabled: true, }, ); diff --git a/packages/editor/src/layouts/history-list/BucketTab.vue b/packages/editor/src/layouts/history-list/BucketTab.vue new file mode 100644 index 00000000..5b7932a9 --- /dev/null +++ b/packages/editor/src/layouts/history-list/BucketTab.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/editor/src/layouts/history-list/CodeBlockTab.vue b/packages/editor/src/layouts/history-list/CodeBlockTab.vue deleted file mode 100644 index f6b6a2fe..00000000 --- a/packages/editor/src/layouts/history-list/CodeBlockTab.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/packages/editor/src/layouts/history-list/DataSourceTab.vue b/packages/editor/src/layouts/history-list/DataSourceTab.vue deleted file mode 100644 index d1b6166e..00000000 --- a/packages/editor/src/layouts/history-list/DataSourceTab.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/packages/editor/src/layouts/history-list/GroupRow.vue b/packages/editor/src/layouts/history-list/GroupRow.vue index e6e0e135..a021f2fa 100644 --- a/packages/editor/src/layouts/history-list/GroupRow.vue +++ b/packages/editor/src/layouts/history-list/GroupRow.vue @@ -12,15 +12,9 @@ {{ headIndexLabel }} {{ opLabel(opType) }} {{ desc }} - 当前 - 查看差异 + 合并 {{ stepCount }} 步 + 回滚 + 回到 + 查看差异 @@ -35,20 +43,11 @@
  • #{{ s.index + 1 }} {{ s.desc }} - 当前 - 查看差异 回滚 + 回到 + 查看差异
  • @@ -74,7 +87,7 @@ defineOptions({ const props = withDefaults( defineProps<{ - /** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */ + /** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${首步 index}` / `ds-${id}-${首步 index}` / `cb-${id}-${首步 index}`,以稳定的 step 索引标识分组。 */ groupKey: string; /** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */ applied: boolean; @@ -142,47 +155,34 @@ const emit = defineEmits<{ }>(); /** - * 单步组:头部可点击 goto(需 gotoEnabled);合并组:头部可点击切换展开。 - * 当前组(isCurrent)或禁用 goto 时,单步组头部不可点击。 + * 仅合并组头部可点击(切换展开 / 收起); + * 单步组的跳转改由头部的「回退」按钮触发,整行不再可点击。 */ -const isHeadClickable = computed(() => { - if (props.merged) return true; - return props.gotoEnabled && !props.isCurrent; -}); +const isHeadClickable = computed(() => props.merged); const headTitle = computed(() => { if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步'; if (props.isCurrent) return '当前所在记录'; - if (!props.gotoEnabled) return ''; - return '点击跳转到该记录'; + return ''; }); /** - * 头部点击行为分流: - * - 合并组:仅切换展开 / 收起,不触发 goto; - * - 单步组:跳转到该唯一步骤;当前组忽略点击。 + * 头部点击行为:仅合并组切换展开 / 收起;单步组不再响应整行点击。 */ const onHeadClick = () => { if (props.merged) { emit('toggle', props.groupKey); - return; } - if (props.isCurrent) return; - if (!props.gotoEnabled) return; - if (!props.subSteps.length) return; - emit('goto', props.subSteps[0].index); }; -const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => { - if (s.isCurrent) return; +const onGotoClick = (index: number) => { if (!props.gotoEnabled) return; - emit('goto', s.index); + emit('goto', index); }; const subStepTitle = (s: { isCurrent?: boolean }) => { if (s.isCurrent) return '当前所在记录'; - if (!props.gotoEnabled) return ''; - return '点击跳转到该记录'; + return ''; }; /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */ diff --git a/packages/editor/src/layouts/history-list/HistoryDiffDialog.vue b/packages/editor/src/layouts/history-list/HistoryDiffDialog.vue index 4a8b3793..678853c5 100644 --- a/packages/editor/src/layouts/history-list/HistoryDiffDialog.vue +++ b/packages/editor/src/layouts/history-list/HistoryDiffDialog.vue @@ -59,7 +59,11 @@ @@ -74,13 +78,13 @@ import type { FormState } from '@tmagic/form'; import CompareForm from '@editor/components/CompareForm.vue'; import CodeEditor from '@editor/layouts/CodeEditor.vue'; -import type { CompareCategory, CompareFormLoadConfig } from '@editor/type'; +import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload } from '@editor/type'; defineOptions({ name: 'MEditorHistoryDiffDialog', }); -withDefaults( +const props = withDefaults( defineProps<{ /** * 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。 @@ -94,32 +98,13 @@ withDefaults( */ loadConfig?: CompareFormLoadConfig; width?: string; + onConfirm?: () => void; }>(), { width: '900px', }, ); -/** 差异对话框的入参 */ -export interface DiffDialogPayload { - /** 表单类别 */ - category: CompareCategory; - /** 节点类型 / 数据源类型 */ - type?: string; - /** 代码块场景下的数据源类型 */ - dataSourceType?: string; - /** 该 step 修改前的值(oldNode / oldSchema / oldContent) */ - lastValue: Record; - /** 该 step 修改后的值(newNode / newSchema / newContent) */ - value: Record; - /** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */ - currentValue?: Record | null; - /** 用于标题展示的目标名称 */ - targetLabel?: string; - /** 用于标题展示的目标 id */ - id?: string | number; -} - /** * 差异对比模式: * - before:该步骤修改前 vs 该步骤修改后(默认行为,体现这一步带来的变化) @@ -184,6 +169,12 @@ const isSameAsCurrent = computed(() => { return isEqual(payload.value.value, payload.value.currentValue); }); +const onConfirmClick = () => { + const cb = props.onConfirm; + visible.value = false; + cb?.(); +}; + const targetText = computed(() => { if (!payload.value) return ''; const categoryText: Record = { diff --git a/packages/editor/src/layouts/history-list/HistoryListPanel.vue b/packages/editor/src/layouts/history-list/HistoryListPanel.vue index c8791de7..1ad6e8f6 100644 --- a/packages/editor/src/layouts/history-list/HistoryListPanel.vue +++ b/packages/editor/src/layouts/history-list/HistoryListPanel.vue @@ -36,9 +36,14 @@ :is="tabPaneComponent?.component || 'el-tab-pane'" v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}" > - - - + diff --git a/packages/editor/src/layouts/history-list/InitialRow.vue b/packages/editor/src/layouts/history-list/InitialRow.vue index 7782f9a7..998ff30b 100644 --- a/packages/editor/src/layouts/history-list/InitialRow.vue +++ b/packages/editor/src/layouts/history-list/InitialRow.vue @@ -3,12 +3,17 @@ class="m-editor-history-list-item m-editor-history-list-initial" :class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }" :title="isCurrent ? '当前已回到未修改的初始状态' : '点击回到未修改的初始状态'" - @click="onClick" > #0 初始 未修改的初始状态 - 当前 + 回到 @@ -24,10 +29,16 @@ defineOptions({ name: 'MEditorHistoryListInitialRow', }); -const props = defineProps<{ - /** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */ - isCurrent: boolean; -}>(); +const props = withDefaults( + defineProps<{ + /** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */ + isCurrent: boolean; + gotoEnabled?: boolean; + }>(), + { + gotoEnabled: true, + }, +); const emit = defineEmits<{ /** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */ diff --git a/packages/editor/src/layouts/history-list/PageTab.vue b/packages/editor/src/layouts/history-list/PageTab.vue index 5cd5876c..c7c316b7 100644 --- a/packages/editor/src/layouts/history-list/PageTab.vue +++ b/packages/editor/src/layouts/history-list/PageTab.vue @@ -3,9 +3,9 @@
      是否展开),由顶层 panel 统一维护。本 tab 使用 `pg-${idx}` 作为 key。 */ + /** + * 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。 + * 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组, + * 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。 + */ expanded: Record; }>(); diff --git a/packages/editor/src/layouts/history-list/composables.ts b/packages/editor/src/layouts/history-list/composables.ts index dcbc98ec..f818e938 100644 --- a/packages/editor/src/layouts/history-list/composables.ts +++ b/packages/editor/src/layouts/history-list/composables.ts @@ -22,7 +22,10 @@ import type { export const useHistoryList = () => { const { historyService } = useServices(); - /** 折叠状态:key 形如 `pg-${groupIdx}` / `ds-${id}-${groupIdx}` / `cb-${id}-${groupIdx}`。 */ + /** + * 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。 + * 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。 + */ const expanded = reactive>({}); const toggleGroup = (key: string) => { expanded[key] = !expanded[key]; diff --git a/packages/editor/src/theme/history-list-panel.scss b/packages/editor/src/theme/history-list-panel.scss index 2fd8ad55..a4d7a3b0 100644 --- a/packages/editor/src/theme/history-list-panel.scss +++ b/packages/editor/src/theme/history-list-panel.scss @@ -125,19 +125,19 @@ &.is-merged { margin: 4px 0; padding: 4px 8px 6px; - background-color: rgba(144, 105, 219, 0.06); - border: 1px solid rgba(144, 105, 219, 0.18); - border-left: 3px solid #9069db; + background-color: rgba(47, 84, 235, 0.06); + border: 1px solid rgba(47, 84, 235, 0.18); + border-left: 3px solid #2f54eb; border-radius: 4px; // 卡片本体已经有背景色,hover 状态以更深的同色提示交互 &:hover { - background-color: rgba(144, 105, 219, 0.1); + background-color: rgba(47, 84, 235, 0.1); } .m-editor-history-list-group-head { font-weight: 600; - color: #5b3fa5; + color: #1d39c4; } // 已撤销态:整张卡片去色 @@ -169,7 +169,7 @@ margin: 6px 0 0 6px; padding: 0; list-style: none; - border-left: 1px dashed rgba(144, 105, 219, 0.45); + border-left: 1px dashed rgba(47, 84, 235, 0.45); li { display: flex; @@ -185,7 +185,7 @@ cursor: pointer; &:hover { - background-color: rgba(144, 105, 219, 0.1); + background-color: rgba(47, 84, 235, 0.1); } } @@ -240,7 +240,7 @@ } &.op-update { - background-color: #409eff; + background-color: #e6a23c; } &.op-initial { @@ -279,7 +279,7 @@ font-size: 10px; line-height: 16px; color: #fff; - background-color: #9069db; + background-color: #2f54eb; font-weight: 500; letter-spacing: 0.2px; } @@ -300,21 +300,39 @@ } } + // 「跳转」按钮:将历史游标移动到该 step,替代原先点击整行跳转的交互。 + // 使用与组卡片一致的紫色色系,与「查看差异」「回滚」区分开。 + .m-editor-history-list-item-goto { + flex: 0 0 auto; + padding: 0 6px; + border-radius: 2px; + font-size: 10px; + line-height: 16px; + color: #606266; + background-color: rgba(96, 98, 102, 0.1); + cursor: pointer; + user-select: none; + + &:hover { + background-color: rgba(96, 98, 102, 0.18); + } + } + // 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。 - // 使用与「查看差异」不同的色调(橙黄),用来区分"可逆操作"与"只读对比"。 + // 使用红色色调,强调其为"破坏性/可逆操作",与「查看差异」「跳转」区分开。 .m-editor-history-list-item-revert { flex: 0 0 auto; padding: 0 6px; border-radius: 2px; font-size: 10px; line-height: 16px; - color: #e6a23c; - background-color: rgba(230, 162, 60, 0.12); + color: #f56c6c; + background-color: rgba(245, 108, 108, 0.12); cursor: pointer; user-select: none; &:hover { - background-color: rgba(230, 162, 60, 0.25); + background-color: rgba(245, 108, 108, 0.25); } } diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index ab419808..8997f1bd 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -1113,3 +1113,23 @@ export interface DslOpOptions extends HistoryOpOptions { doNotSelect?: boolean; doNotSwitchPage?: boolean; } + +/** 差异对话框的入参 */ +export interface DiffDialogPayload { + /** 表单类别 */ + category: CompareCategory; + /** 节点类型 / 数据源类型 */ + type?: string; + /** 代码块场景下的数据源类型 */ + dataSourceType?: string; + /** 该 step 修改前的值(oldNode / oldSchema / oldContent) */ + lastValue: Record; + /** 该 step 修改后的值(newNode / newSchema / newContent) */ + value: Record; + /** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */ + currentValue?: Record | null; + /** 用于标题展示的目标名称 */ + targetLabel?: string; + /** 用于标题展示的目标 id */ + id?: string | number; +} diff --git a/packages/editor/tests/unit/layouts/history-list/Bucket.spec.ts b/packages/editor/tests/unit/layouts/history-list/Bucket.spec.ts index 4eb33f9d..7a480e97 100644 --- a/packages/editor/tests/unit/layouts/history-list/Bucket.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/Bucket.spec.ts @@ -90,7 +90,7 @@ describe('Bucket.vue', () => { expect(wrapper.emitted('goto')).toBeFalsy(); }); - test('单步组头部点击 → goto 事件被透传到 Bucket,并附带 bucketId', async () => { + test('单步组「回到」按钮点击 → goto 事件被透传到 Bucket,并附带 bucketId', async () => { const wrapper = mount(Bucket, { props: { title: '代码块', @@ -102,13 +102,13 @@ describe('Bucket.vue', () => { expanded: {}, }, }); - await wrapper.find('.m-editor-history-list-group-head').trigger('click'); + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); const events = wrapper.emitted('goto'); expect(events).toBeTruthy(); expect(events![0]).toEqual(['code_1', 0]); }); - test('合并组展开后点击子步 → goto 透传,附带子步 index', async () => { + test('合并组展开后点击子步「回到」按钮 → goto 透传,附带子步 index', async () => { const wrapper = mount(Bucket, { props: { title: '代码块', @@ -123,7 +123,7 @@ describe('Bucket.vue', () => { const subItems = wrapper.findAll('.m-editor-history-list-substeps li'); expect(subItems).toHaveLength(2); // 子步倒序渲染:subItems[0] 对应 index=1 - await subItems[0].trigger('click'); + await subItems[0].find('.m-editor-history-list-item-goto').trigger('click'); const events = wrapper.emitted('goto'); expect(events).toBeTruthy(); expect(events![0]).toEqual(['code_1', 1]); @@ -166,7 +166,7 @@ describe('Bucket.vue', () => { // 已有 applied 组,初始项不应为当前 expect(initial.classes()).not.toContain('is-current'); - await initial.trigger('click'); + await initial.find('.m-editor-history-list-item-goto').trigger('click'); const events = wrapper.emitted('goto-initial'); expect(events).toBeTruthy(); expect(events![0]).toEqual(['ds_1']); diff --git a/packages/editor/tests/unit/layouts/history-list/CodeBlockTab.spec.ts b/packages/editor/tests/unit/layouts/history-list/CodeBlockTab.spec.ts index d41594dc..ac754b82 100644 --- a/packages/editor/tests/unit/layouts/history-list/CodeBlockTab.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/CodeBlockTab.spec.ts @@ -7,8 +7,9 @@ import { describe, expect, test, vi } from 'vitest'; import { defineComponent, h } from 'vue'; import { mount } from '@vue/test-utils'; -import CodeBlockTab from '@editor/layouts/history-list/CodeBlockTab.vue'; -import type { CodeBlockHistoryGroup } from '@editor/type'; +import BucketTab from '@editor/layouts/history-list/BucketTab.vue'; +import { describeCodeBlockGroup, describeCodeBlockStep } from '@editor/layouts/history-list/composables'; +import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type'; vi.mock('@tmagic/design', () => ({ TMagicScrollbar: defineComponent({ @@ -25,17 +26,31 @@ const buildGroup = ( opType: 'add' | 'remove' | 'update', steps: any[], applied = true, + startIndex = 0, ): CodeBlockHistoryGroup => ({ kind: 'code-block', id, opType, applied, - steps: steps.map((s, i) => ({ step: s, index: i, applied })), + steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })), }); +/** 代码块 tab 复用通用 BucketTab,固定注入代码块的 title/prefix/describe/isStepDiffable。 */ +const mountCodeBlockTab = (props: { buckets: any[]; expanded: Record }) => + mount(BucketTab, { + props: { + title: '代码块', + prefix: 'cb', + describeGroup: describeCodeBlockGroup, + describeStep: describeCodeBlockStep, + isStepDiffable: (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent), + ...props, + }, + }); + describe('CodeBlockTab.vue', () => { test('buckets 为空时显示空态', () => { - const wrapper = mount(CodeBlockTab, { props: { buckets: [], expanded: {} } }); + const wrapper = mountCodeBlockTab({ buckets: [], expanded: {} }); expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true); }); @@ -48,7 +63,7 @@ describe('CodeBlockTab.vue', () => { ], }, ]; - const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } }); + const wrapper = mountCodeBlockTab({ buckets, expanded: {} }); expect(wrapper.find('.m-editor-history-list-bucket-title').text()).toContain('代码块'); expect(wrapper.find('.m-editor-history-list-bucket-title code').text()).toBe('code_1'); @@ -75,29 +90,35 @@ describe('CodeBlockTab.vue', () => { changeRecords: [{ propPath: 'b' }], }, ]), - buildGroup('code_1', 'update', [ - { - id: 'code_1', - oldContent: { id: 'code_1', name: 'fn' }, - newContent: { id: 'code_1', name: 'fn' }, - changeRecords: [{ propPath: 'c' }], - }, - { - id: 'code_1', - oldContent: { id: 'code_1', name: 'fn' }, - newContent: { id: 'code_1', name: 'fn' }, - changeRecords: [{ propPath: 'd' }], - }, - ]), + buildGroup( + 'code_1', + 'update', + [ + { + id: 'code_1', + oldContent: { id: 'code_1', name: 'fn' }, + newContent: { id: 'code_1', name: 'fn' }, + changeRecords: [{ propPath: 'c' }], + }, + { + id: 'code_1', + oldContent: { id: 'code_1', name: 'fn' }, + newContent: { id: 'code_1', name: 'fn' }, + changeRecords: [{ propPath: 'd' }], + }, + ], + true, + 2, + ), ], }, ]; - const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } }); + const wrapper = mountCodeBlockTab({ buckets, expanded: {} }); const heads = wrapper.findAll('.m-editor-history-list-group-head'); await heads[0].trigger('click'); expect(wrapper.emitted('toggle')![0]).toEqual(['cb-code_1-0']); await heads[1].trigger('click'); - expect(wrapper.emitted('toggle')![1]).toEqual(['cb-code_1-1']); + expect(wrapper.emitted('toggle')![1]).toEqual(['cb-code_1-2']); }); test('goto 透传:携带 codeBlock id 与最后一步 index', async () => { @@ -109,8 +130,8 @@ describe('CodeBlockTab.vue', () => { ], }, ]; - const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } }); - await wrapper.find('.m-editor-history-list-group-head').trigger('click'); + const wrapper = mountCodeBlockTab({ buckets, expanded: {} }); + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); const events = wrapper.emitted('goto'); expect(events).toBeTruthy(); expect(events![0]).toEqual(['code_1', 0]); @@ -138,9 +159,7 @@ describe('CodeBlockTab.vue', () => { ], }, ]; - const wrapper = mount(CodeBlockTab, { - props: { buckets, expanded: { 'cb-code_1-0': true } }, - }); + const wrapper = mountCodeBlockTab({ buckets, expanded: { 'cb-code_1-0': true } }); const items = wrapper.findAll('.m-editor-history-list-substeps li'); expect(items).toHaveLength(2); // 子步倒序渲染(最新在上):params 在前,content 在后 diff --git a/packages/editor/tests/unit/layouts/history-list/DataSourceTab.spec.ts b/packages/editor/tests/unit/layouts/history-list/DataSourceTab.spec.ts index f41ab176..13af9084 100644 --- a/packages/editor/tests/unit/layouts/history-list/DataSourceTab.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/DataSourceTab.spec.ts @@ -7,8 +7,9 @@ import { describe, expect, test, vi } from 'vitest'; import { defineComponent, h } from 'vue'; import { mount } from '@vue/test-utils'; -import DataSourceTab from '@editor/layouts/history-list/DataSourceTab.vue'; -import type { DataSourceHistoryGroup } from '@editor/type'; +import BucketTab from '@editor/layouts/history-list/BucketTab.vue'; +import { describeDataSourceGroup, describeDataSourceStep } from '@editor/layouts/history-list/composables'; +import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type'; vi.mock('@tmagic/design', () => ({ TMagicScrollbar: defineComponent({ @@ -25,17 +26,31 @@ const buildGroup = ( opType: 'add' | 'remove' | 'update', steps: any[], applied = true, + startIndex = 0, ): DataSourceHistoryGroup => ({ kind: 'data-source', id, opType, applied, - steps: steps.map((s, i) => ({ step: s, index: i, applied })), + steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })), }); +/** 数据源 tab 复用通用 BucketTab,固定注入数据源的 title/prefix/describe/isStepDiffable。 */ +const mountDataSourceTab = (props: { buckets: any[]; expanded: Record }) => + mount(BucketTab, { + props: { + title: '数据源', + prefix: 'ds', + describeGroup: describeDataSourceGroup, + describeStep: describeDataSourceStep, + isStepDiffable: (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema), + ...props, + }, + }); + describe('DataSourceTab.vue', () => { test('buckets 为空时显示空态', () => { - const wrapper = mount(DataSourceTab, { props: { buckets: [], expanded: {} } }); + const wrapper = mountDataSourceTab({ buckets: [], expanded: {} }); expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true); }); @@ -52,7 +67,7 @@ describe('DataSourceTab.vue', () => { ], }, ]; - const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } }); + const wrapper = mountDataSourceTab({ buckets, expanded: {} }); const titles = wrapper.findAll('.m-editor-history-list-bucket-title'); expect(titles).toHaveLength(2); expect(titles[0].text()).toContain('数据源'); @@ -86,27 +101,33 @@ describe('DataSourceTab.vue', () => { changeRecords: [{ propPath: 'b' }], }, ]), - buildGroup('ds_1', 'update', [ - { - id: 'ds_1', - oldSchema: { id: 'ds_1', title: 'A' }, - newSchema: { id: 'ds_1', title: 'A2' }, - changeRecords: [{ propPath: 'c' }], - }, - { - id: 'ds_1', - oldSchema: { id: 'ds_1', title: 'A2' }, - newSchema: { id: 'ds_1', title: 'A3' }, - changeRecords: [{ propPath: 'd' }], - }, - ]), + buildGroup( + 'ds_1', + 'update', + [ + { + id: 'ds_1', + oldSchema: { id: 'ds_1', title: 'A' }, + newSchema: { id: 'ds_1', title: 'A2' }, + changeRecords: [{ propPath: 'c' }], + }, + { + id: 'ds_1', + oldSchema: { id: 'ds_1', title: 'A2' }, + newSchema: { id: 'ds_1', title: 'A3' }, + changeRecords: [{ propPath: 'd' }], + }, + ], + true, + 2, + ), ], }, ]; - const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } }); + const wrapper = mountDataSourceTab({ buckets, expanded: {} }); const heads = wrapper.findAll('.m-editor-history-list-group-head'); await heads[1].trigger('click'); - expect(wrapper.emitted('toggle')![0]).toEqual(['ds-ds_1-1']); + expect(wrapper.emitted('toggle')![0]).toEqual(['ds-ds_1-2']); }); test('goto 透传:携带 dataSource id 与最后一步 index', async () => { @@ -116,8 +137,8 @@ describe('DataSourceTab.vue', () => { groups: [buildGroup('ds_1', 'add', [{ id: 'ds_1', oldSchema: null, newSchema: { id: 'ds_1', title: 'A' } }])], }, ]; - const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } }); - await wrapper.find('.m-editor-history-list-group-head').trigger('click'); + const wrapper = mountDataSourceTab({ buckets, expanded: {} }); + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); const events = wrapper.emitted('goto'); expect(events).toBeTruthy(); expect(events![0]).toEqual(['ds_1', 0]); @@ -145,9 +166,7 @@ describe('DataSourceTab.vue', () => { ], }, ]; - const wrapper = mount(DataSourceTab, { - props: { buckets, expanded: { 'ds-ds_1-0': true } }, - }); + const wrapper = mountDataSourceTab({ buckets, expanded: { 'ds-ds_1-0': true } }); expect(wrapper.findAll('.m-editor-history-list-substeps li')).toHaveLength(2); }); }); diff --git a/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts b/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts index c500d81a..af7f612f 100644 --- a/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts @@ -103,7 +103,7 @@ describe('GroupRow.vue', () => { expect(wrapper.emitted('goto')).toBeFalsy(); }); - test('点击单步组(非合并)头部触发 goto,携带该唯一 step 的 index', async () => { + test('点击单步组(非合并)的「回到」按钮触发 goto,携带该唯一 step 的 index', async () => { const wrapper = mount(GroupRow, { props: { ...baseProps, @@ -111,7 +111,11 @@ describe('GroupRow.vue', () => { subSteps: [{ index: 7, applied: true, desc: 'a' }], }, }); + // 点击头部本身不再触发 goto(整行不可点击) await wrapper.find('.m-editor-history-list-group-head').trigger('click'); + expect(wrapper.emitted('goto')).toBeFalsy(); + // 仅点击「回到」按钮才触发 goto + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); expect(wrapper.emitted('goto')).toBeTruthy(); expect(wrapper.emitted('goto')![0]).toEqual([7]); // 单步组没有展开概念,不应触发 toggle @@ -149,7 +153,7 @@ describe('GroupRow.vue', () => { expect(wrapper.emitted('goto')).toBeFalsy(); }); - test('点击子步触发 goto 携带该子步 index;当前子步点击无效', async () => { + test('点击子步「回退」按钮触发 goto 携带该子步 index;当前子步无回退按钮', async () => { const wrapper = mount(GroupRow, { props: { ...baseProps, @@ -162,11 +166,14 @@ describe('GroupRow.vue', () => { ], }, }); - // 子步倒序渲染:subItems[0] 为 index=1(非当前,可点击),subItems[1] 为 index=0(当前) + // 子步倒序渲染:subItems[0] 为 index=1(非当前,含跳转按钮),subItems[1] 为 index=0(当前,无跳转按钮) const subItems = wrapper.findAll('.m-editor-history-list-substeps li'); - await subItems[1].trigger('click'); - expect(wrapper.emitted('goto')).toBeFalsy(); + expect(subItems[1].find('.m-editor-history-list-item-goto').exists()).toBe(false); + // 点击子步行本身不再触发 goto await subItems[0].trigger('click'); + expect(wrapper.emitted('goto')).toBeFalsy(); + // 仅点击「跳转」按钮才触发 goto + await subItems[0].find('.m-editor-history-list-item-goto').trigger('click'); expect(wrapper.emitted('goto')![0]).toEqual([1]); }); }); diff --git a/packages/editor/tests/unit/layouts/history-list/HistoryListPanel.spec.ts b/packages/editor/tests/unit/layouts/history-list/HistoryListPanel.spec.ts index 24e19e39..4349cacd 100644 --- a/packages/editor/tests/unit/layouts/history-list/HistoryListPanel.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/HistoryListPanel.spec.ts @@ -187,11 +187,11 @@ describe('HistoryListPanel.vue', () => { const heads = wrapper.findAll('.m-editor-history-list-group-head'); expect(heads.length).toBeGreaterThanOrEqual(2); // 第二行(pg-1)对应原始 step.index = 0;cursor 应为 0+1 = 1 - await heads[1].trigger('click'); + await heads[1].find('.m-editor-history-list-item-goto').trigger('click'); expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1); expect(editorService.gotoPageStep).toHaveBeenCalledWith(1); - // 当前组点击不触发 goto + // 当前组没有「回到」按钮,点击头部不触发 goto await head.trigger('click'); expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1); }); @@ -213,7 +213,7 @@ describe('HistoryListPanel.vue', () => { // 找到数据源 tab 那一组 const dsHead = heads.find((h) => h.text().includes('创建 DS')); expect(dsHead).toBeTruthy(); - await dsHead!.trigger('click'); + await dsHead!.find('.m-editor-history-list-item-goto').trigger('click'); expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1); }); @@ -232,7 +232,7 @@ describe('HistoryListPanel.vue', () => { const heads = wrapper.findAll('.m-editor-history-list-group-head'); const cbHead = heads.find((h) => h.text().includes('创建 CB')); expect(cbHead).toBeTruthy(); - await cbHead!.trigger('click'); + await cbHead!.find('.m-editor-history-list-item-goto').trigger('click'); expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1); }); @@ -251,7 +251,7 @@ describe('HistoryListPanel.vue', () => { const initials = wrapper.findAll('.m-editor-history-list-initial'); expect(initials.length).toBeGreaterThanOrEqual(1); // 第一项(页面 tab)应为页面 tab 的初始项;page tab 在三个 tab 中最先渲染 - await initials[0].trigger('click'); + await initials[0].find('.m-editor-history-list-item-goto').trigger('click'); expect(editorService.gotoPageStep).toHaveBeenCalledWith(0); }); @@ -307,10 +307,10 @@ describe('HistoryListPanel.vue', () => { // 顺序:tab 渲染顺序是 page → data-source → code-block // 因此 initials[0] 属于 ds_x,initials[1] 属于 code_x - await initials[0].trigger('click'); + await initials[0].find('.m-editor-history-list-item-goto').trigger('click'); expect(dataSourceService.goto).toHaveBeenCalledWith('ds_x', 0); - await initials[1].trigger('click'); + await initials[1].find('.m-editor-history-list-item-goto').trigger('click'); expect(codeBlockService.goto).toHaveBeenCalledWith('code_x', 0); }); }); diff --git a/packages/editor/tests/unit/layouts/history-list/InitialRow.spec.ts b/packages/editor/tests/unit/layouts/history-list/InitialRow.spec.ts index d2700766..4d8d3b50 100644 --- a/packages/editor/tests/unit/layouts/history-list/InitialRow.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/InitialRow.spec.ts @@ -17,15 +17,15 @@ describe('InitialRow.vue', () => { expect(wrapper.find('.m-editor-history-list-item-desc').text()).toBe('未修改的初始状态'); }); - test('isCurrent=true 时附 is-current 类名并显示「当前」徽标', () => { + test('isCurrent=true 时附 is-current 类名且不展示「回到」按钮', () => { const wrapper = mount(InitialRow, { props: { isCurrent: true } }); expect(wrapper.find('.m-editor-history-list-initial').classes()).toContain('is-current'); - expect(wrapper.find('.m-editor-history-list-item-current').exists()).toBe(true); + expect(wrapper.find('.m-editor-history-list-item-goto').exists()).toBe(false); }); - test('非当前时点击触发 goto-initial 事件', async () => { + test('非当前时点击「回到」按钮触发 goto-initial 事件', async () => { const wrapper = mount(InitialRow, { props: { isCurrent: false } }); - await wrapper.find('.m-editor-history-list-initial').trigger('click'); + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); expect(wrapper.emitted('goto-initial')).toBeTruthy(); expect(wrapper.emitted('goto-initial')).toHaveLength(1); }); diff --git a/packages/editor/tests/unit/layouts/history-list/PageTab.spec.ts b/packages/editor/tests/unit/layouts/history-list/PageTab.spec.ts index 182112c6..2aac5712 100644 --- a/packages/editor/tests/unit/layouts/history-list/PageTab.spec.ts +++ b/packages/editor/tests/unit/layouts/history-list/PageTab.spec.ts @@ -26,6 +26,7 @@ const buildPageGroup = ( applied = true, targetName?: string, targetId?: string, + startIndex = 0, ): PageHistoryGroup => ({ kind: 'page', pageId: 'p1', @@ -33,7 +34,7 @@ const buildPageGroup = ( applied, targetId, targetName, - steps: steps.map((s, i) => ({ step: s, index: i, applied })), + steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })), }); describe('PageTab.vue', () => { @@ -148,6 +149,7 @@ describe('PageTab.vue', () => { true, '按钮2', 'btn2', + 2, ), ]; const wrapper = mount(PageTab, { props: { list, expanded: {} } }); @@ -155,15 +157,15 @@ describe('PageTab.vue', () => { await heads[1].trigger('click'); const events = wrapper.emitted('toggle'); expect(events).toBeTruthy(); - expect(events![0]).toEqual(['pg-1']); + expect(events![0]).toEqual(['pg-2']); // 合并组头部不应触发 goto expect(wrapper.emitted('goto')).toBeFalsy(); }); - test('点击单步组头部透传 goto 事件,携带该 step 的 index', async () => { + test('点击单步组「回到」按钮透传 goto 事件,携带该 step 的 index', async () => { const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }])]; const wrapper = mount(PageTab, { props: { list, expanded: {} } }); - await wrapper.find('.m-editor-history-list-group-head').trigger('click'); + await wrapper.find('.m-editor-history-list-item-goto').trigger('click'); expect(wrapper.emitted('goto')).toBeTruthy(); expect(wrapper.emitted('goto')![0]).toEqual([0]); expect(wrapper.emitted('toggle')).toBeFalsy(); @@ -203,10 +205,10 @@ describe('PageTab.vue', () => { expect(initial.classes()).not.toContain('is-current'); }); - test('点击非当前的初始项透传 goto-initial 事件', async () => { + test('点击非当前初始项的「回到」按钮透传 goto-initial 事件', async () => { const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }], true)]; const wrapper = mount(PageTab, { props: { list, expanded: {} } }); - await wrapper.find('.m-editor-history-list-initial').trigger('click'); + await wrapper.find('.m-editor-history-list-initial .m-editor-history-list-item-goto').trigger('click'); expect(wrapper.emitted('goto-initial')).toBeTruthy(); expect(wrapper.emitted('goto-initial')).toHaveLength(1); });