diff --git a/packages/editor/src/layouts/history-list/GroupRow.vue b/packages/editor/src/layouts/history-list/GroupRow.vue
index cf661e8d..94ab7b27 100644
--- a/packages/editor/src/layouts/history-list/GroupRow.vue
+++ b/packages/editor/src/layouts/history-list/GroupRow.vue
@@ -49,6 +49,13 @@
>{{ sourceLabel(group.source) }}
+ {{ group.operator }}
+
{{ sourceLabel(s.source) }}
+ {{
+ s.operator
+ }}
{{ s.time }}
diff --git a/packages/editor/src/layouts/history-list/composables.ts b/packages/editor/src/layouts/history-list/composables.ts
index 7cc5c7a7..b3fdd6ef 100644
--- a/packages/editor/src/layouts/history-list/composables.ts
+++ b/packages/editor/src/layouts/history-list/composables.ts
@@ -25,51 +25,49 @@ export interface HistoryBucketGroup {
steps: { index: number; applied: boolean; isCurrent?: boolean; step: T }[];
}
-/** GroupRow 渲染所需的单个子步视图模型(已由 {@link toRowGroup} 预先派生,组件内部不再触碰原始 step)。 */
-export interface HistoryRowStep {
- /** 该子步在所属栈中的稳定索引。 */
- index: number;
+/**
+ * GroupRow 的组头部与子步共用的展示字段(均由 {@link toRowGroup} 预先派生)。
+ * 抽出公共部分避免 {@link HistoryRowStep} / {@link HistoryRowGroup} 重复声明,
+ * 也便于消费方用本类型收窄「组头 / 子步」通用渲染逻辑的入参。
+ */
+export interface HistoryRowDisplay {
/** 是否已应用(false 表示已被 undo,UI 灰态)。 */
applied: boolean;
- /** 是否为当前所在步骤。 */
- isCurrent?: boolean;
- /** 是否为最近一次保存的记录。 */
- saved?: boolean;
- /** 子步描述文案。 */
+ /** 是否为当前所在步骤 / 分组。 */
+ isCurrent: boolean;
+ /** 描述文案。 */
desc: string;
- /** 是否可查看差异。 */
- diffable?: boolean;
- /** 是否可回滚。 */
- revertable?: boolean;
/** 操作途径。 */
source?: HistoryOpSource;
+ /** 操作人。 */
+ operator?: string;
/** 时间文案。 */
time?: string;
/** 时间的完整 title 提示。 */
timeTitle?: string;
}
+/** GroupRow 渲染所需的单个子步视图模型(已由 {@link toRowGroup} 预先派生,组件内部不再触碰原始 step)。 */
+export interface HistoryRowStep extends HistoryRowDisplay {
+ /** 该子步在所属栈中的稳定索引。 */
+ index: number;
+ /** 是否为最近一次保存的记录。 */
+ saved?: boolean;
+ /** 是否可查看差异。 */
+ diffable?: boolean;
+ /** 是否可回滚。 */
+ revertable?: boolean;
+}
+
/**
* GroupRow 渲染所需的整组视图模型(由 {@link toRowGroup} 统一派生)。
* 把原先 GroupRow 上十多个扁平 props 收敛为单一对象,header 信息与子步列表一并携带。
*/
-export interface HistoryRowGroup {
+export interface HistoryRowGroup extends HistoryRowDisplay {
/** 分组的稳定 key,作为 toggle 事件 payload 与折叠状态的索引。 */
key: string;
- /** 组内最后一步是否已应用。 */
- applied: boolean;
- /** 是否为当前所在分组。 */
- isCurrent: boolean;
/** 操作类型,用于徽标颜色与文案。 */
opType: HistoryOpType;
- /** 组整体描述文案。 */
- desc: string;
- /** 组的操作途径(取组内最近一步)。 */
- source?: HistoryOpSource;
- /** 组头部时间文案(取组内最近一步)。 */
- time?: string;
- /** 组头部时间的完整 title 提示。 */
- timeTitle?: string;
/** 子步列表(时间正序);其长度即合并步数,length > 1 即为合并组。 */
subSteps: HistoryRowStep[];
}
@@ -144,6 +142,10 @@ export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
group.steps[group.steps.length - 1]?.step.source;
+/** 取一组历史步骤里最后一步(最近一次)的操作人,用于组头部展示。 */
+export const groupOperator = (group: { steps: { step: { operator?: string } }[] }): string | undefined =>
+ group.steps[group.steps.length - 1]?.step.operator;
+
/** {@link toRowGroup} 接受的最小分组结构,PageHistoryGroup 与 HistoryBucketGroup 均满足。 */
interface RowGroupInput {
applied: boolean;
@@ -173,17 +175,19 @@ export const toRowGroup = (
opType: group.opType,
desc: describeGroup ? describeGroup(group) : describeStep(lastStep),
source: groupSource(group),
+ operator: groupOperator(group),
time: formatHistoryTime(timestamp),
timeTitle: formatHistoryFullTime(timestamp),
subSteps: group.steps.map((s) => ({
index: s.index,
applied: s.applied,
- isCurrent: s.isCurrent,
+ isCurrent: Boolean(s.isCurrent),
saved: s.step.saved,
desc: describeStep(s.step),
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
source: s.step.source,
+ operator: s.step.operator,
time: formatHistoryTime(s.step.timestamp),
timeTitle: formatHistoryFullTime(s.step.timestamp),
})),
diff --git a/packages/editor/src/services/history.ts b/packages/editor/src/services/history.ts
index 1d2f3e25..b424bea5 100644
--- a/packages/editor/src/services/history.ts
+++ b/packages/editor/src/services/history.ts
@@ -17,6 +17,7 @@
*/
import { reactive } from 'vue';
+import type { Writable } from 'type-fest';
import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core';
import type { ChangeRecord } from '@tmagic/form';
@@ -33,6 +34,7 @@ import type {
PersistedHistoryState,
StackHistoryGroup,
StepValue,
+ SyncHookPlugin,
} from '@editor/type';
import { getEditorConfig } from '@editor/utils/config';
import {
@@ -51,6 +53,22 @@ import { UndoRedo } from '@editor/utils/undo-redo';
import BaseService from './BaseService';
import editorService from './editor';
+const canUsePluginMethods = {
+ sync: [
+ 'push',
+ 'pushCodeBlock',
+ 'pushDataSource',
+ 'undoCodeBlock',
+ 'redoCodeBlock',
+ 'undoDataSource',
+ 'redoDataSource',
+ 'undo',
+ 'redo',
+ ] as const,
+};
+
+type SyncMethodName = Writable<(typeof canUsePluginMethods)['sync']>;
+
/** 历史记录持久化快照的默认存储位置与结构版本。 */
const DEFAULT_DB_NAME = 'tmagic-editor';
const DEFAULT_STORE_NAME = 'history';
@@ -69,7 +87,7 @@ class History extends BaseService {
});
constructor() {
- super([]);
+ super([...canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false }))]);
this.on('change', this.setCanUndoRedo);
}
@@ -630,6 +648,10 @@ class History extends BaseService {
return groups;
}
+ public usePlugin(options: SyncHookPlugin): void {
+ super.usePlugin(options);
+ }
+
/**
* 取出指定页面的栈;不传 pageId 时按当前活动页取。
*
diff --git a/packages/editor/src/theme/history-list-panel.scss b/packages/editor/src/theme/history-list-panel.scss
index fad8dc50..7601bcbb 100644
--- a/packages/editor/src/theme/history-list-panel.scss
+++ b/packages/editor/src/theme/history-list-panel.scss
@@ -330,6 +330,20 @@
font-weight: 400; // 防止被合并组头部的粗体继承
}
+ // 「操作人」徽标:浅蓝描边胶囊,弱化展示操作人,仅在 step.operator 有值时渲染。
+ .m-editor-history-list-item-operator {
+ flex: 0 0 auto;
+ padding: 0 6px;
+ border: 1px solid #c6e2ff;
+ border-radius: 8px;
+ font-size: 10px;
+ line-height: 14px;
+ color: #409eff;
+ background-color: #ecf5ff;
+ white-space: nowrap;
+ font-weight: 400; // 防止被合并组头部的粗体继承
+ }
+
// 「已保存」徽标:绿色实心胶囊,标记最近一次保存对应的历史记录(与 historyService.markSaved 对应)。
.m-editor-history-list-item-saved {
flex: 0 0 auto;
diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts
index 3677cd97..0b94e441 100644
--- a/packages/editor/src/type.ts
+++ b/packages/editor/src/type.ts
@@ -820,6 +820,10 @@ export interface BaseStepValue {
* 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
*/
rootStep?: boolean;
+ /** 操作人 */
+ operator?: string;
+ /** 扩展信息 */
+ extra?: Record;
}
// #endregion BaseStepValue
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 3c342ceb..069cb000 100644
--- a/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts
+++ b/packages/editor/tests/unit/layouts/history-list/GroupRow.spec.ts
@@ -23,6 +23,7 @@ const makeGroup = (overrides: Partial = {}): HistoryRowGroup =>
/** 构造单个子步,缺省值贴近真实派生结果。 */
const makeStep = (overrides: Partial & Pick): HistoryRowStep => ({
applied: true,
+ isCurrent: false,
desc: '',
...overrides,
});