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 @@
$emit('toggle', key)"
@goto="(index: number) => $emit('goto', bucketId, index)"
@diff-step="(index: number) => $emit('diff-step', bucketId, index)"
@@ -41,6 +42,7 @@
@@ -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 @@
+
+ 暂无操作记录
+
+ $emit('toggle', key)"
+ @goto="(id: string | number, index: number) => $emit('goto', id, index)"
+ @goto-initial="(id: string | number) => $emit('goto-initial', id)"
+ @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
+ @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
+ />
+
+
+
+
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 @@
-
- 暂无操作记录
-
- $emit('toggle', key)"
- @goto="(id: string | number, index: number) => $emit('goto', id, index)"
- @goto-initial="(id: string | number) => $emit('goto-initial', id)"
- @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
- @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
- />
-
-
-
-
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 @@
-
- 暂无操作记录
-
- $emit('toggle', key)"
- @goto="(id: string | number, index: number) => $emit('goto', id, index)"
- @goto-initial="(id: string | number) => $emit('goto-initial', id)"
- @diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
- @revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
- />
-
-
-
-
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 @@
$emit('toggle', key)"
@goto="(index: number) => $emit('goto', index)"
@diff-step="(index: number) => $emit('diff-step', index)"
@@ -55,7 +55,11 @@ defineOptions({
const props = defineProps<{
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
list: PageHistoryGroup[];
- /** 共享的折叠状态表(key -> 是否展开),由顶层 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);
});