From 1ade61d62e61c86fcff2033a3dd57c533ce4241c Mon Sep 17 00:00:00 2001 From: roymondchen Date: Thu, 18 Jun 2026 17:24:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=94=AF=E6=8C=81=E5=B1=95=E7=A4=BA=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在历史列表分组头部和子步骤中展示 operator 信息,并补充步骤类型字段以承载操作人与扩展数据,同时收敛相关类型定义与插件方法声明,提升历史记录渲染与扩展能力。 --- .../src/layouts/history-list/GroupRow.vue | 10 ++++ .../src/layouts/history-list/composables.ts | 58 ++++++++++--------- packages/editor/src/services/history.ts | 24 +++++++- .../editor/src/theme/history-list-panel.scss | 14 +++++ packages/editor/src/type.ts | 4 ++ .../layouts/history-list/GroupRow.spec.ts | 1 + 6 files changed, 83 insertions(+), 28 deletions(-) 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, });