cloud-drive-renamer/docs/plans/2026-03-18-last-batch-undo-design.md
Zain bf75cdcb10 feat: 为批量重命名面板增加撤销最近一次操作功能
- 在平台适配器中统一暴露 getCurrentDirectoryKey() 方法,用于确定当前目录范围
- 新增 LastRenameOperation 数据模型和核心操作函数,支持单条撤销记录的创建、合并和范围校验
- 在 file-selector-panel 中集成撤销逻辑,支持执行后保存记录、retry 成功后合并记录以及撤销操作
- 在 config-panel 底部操作区新增撤销按钮,根据当前目录范围动态显示/隐藏
- 添加多语言支持,包含撤销相关文案
- 添加单元测试和集成测试,确保撤销功能的正确性和稳定性
2026-03-18 23:14:53 +08:00

5.8 KiB

Last Batch Undo Design(仅撤销最近一次批量重命名)

Date: 2026-03-18

Goal

在现有批量重命名面板中增加“撤销上一次批量重命名”能力:只回退当前平台、当前目录下最近一次点击“批量重命名”按钮触发的那次批量操作,并允许用户关闭面板查看真实结果后再返回撤销。

Scope

  • 在 file-selector-panel 中保存 1 条可撤销记录,而不是历史队列。
  • 该记录绑定当前平台 + 当前目录。
  • 关闭面板后保留记录;只要用户仍在原目录即可执行撤销。
  • retry 不创建新历史,但 retry 成功项要并入这唯一的一条记录。
  • 在 config-panel 中新增单一撤销按钮,不做 split-button / 下拉历史。
  • 撤销部分失败时,弹窗列出失败文件并提示用户手动处理。

Non-goals

  • 不保留最近 10 次历史。
  • 不支持选择任意历史回退。
  • 不做跨刷新、跨页面、跨平台持久化撤销。
  • 不新增独立弹窗组件;第一版沿用现有 alert/confirm 风格提示。

Decisions

  • 可撤销记录只有 1 条,新的批量重命名会覆盖旧记录。
  • 生命周期采用“当前平台 + 当前目录 + 当前页面会话”的交集:
    • 关闭面板不清空;
    • 离开当前目录、刷新页面、页面卸载、跨平台访问时失效。
  • “离开目录即销毁”第一版采用懒校验:在面板打开、点击撤销、点击批量重命名、点击 retry 时检查当前目录 scope;若 scope 不匹配,则先清空记录。
  • retry 成功项并入当前记录,保证 undo 对应“这次批量操作最终成功改名的全部文件”。
  • 部分撤销失败后,保留失败项为新的可撤销记录,方便用户在未离开目录前再次点击 undo;同时弹窗提示手动处理建议。

Architecture

1) 目录 scope 抽象

为 PlatformAdapter 增加统一能力:

getCurrentDirectoryKey(): string;

实现方式:

  • Quark:复用现有 getCurrentFolderId()。
  • Aliyun:复用现有 getParentIdFromUrl()。
  • Baidu:复用现有 getCurrentPath()。

file-selector-panel 只依赖 adapter.platform + adapter.getCurrentDirectoryKey() 判断当前撤销记录是否仍在有效 scope 内。

2) 单条撤销记录模型

新增一个轻量数据模型,仅保存 undo 必需信息:

interface UndoRenameItem {
  fileId: string;
  original: string;
  renamed: string;
  index: number;
}

interface LastRenameOperation {
  platform: PlatformName;
  directoryKey: string;
  createdAt: number;
  updatedAt: number;
  items: UndoRenameItem[];
}

只存成功改名项;不存完整 FileItem,避免状态冗余。

3) 记录写入与合并

主执行(点击“批量重命名”)

  • handleExecute() 执行完成后,如果存在成功项:
    • 用当前 platform + directoryKey + results.success 创建一条新的 LastRenameOperation;
    • 覆盖旧记录。

Retry

  • handleRetryFailed() 执行完成后,如果存在成功项:
    • 先校验当前 scope 是否仍与现有记录一致;
    • 一致则把 retry 成功项 merge 进现有记录;
    • 若记录已失效,则不再创建新记录(因为 retry 只是补全原操作,不应在丢失上下文后凭空产生新 undo 历史)。

4) 撤销执行

新增 handleUndoLastRename():

  1. 校验当前 scope;不匹配则清空记录并退出。
  2. 从当前记录生成反向任务:fileId 不变,newName = original。
  3. 复用 BatchExecutor 执行这些 tasks。
  4. 成功项通过现有 applyExecutionResults() + syncAfterRename() 回写页面与面板状态。
  5. 若全部成功:清空记录。
  6. 若部分失败:将记录收窄为失败项,并弹窗提示用户手动处理。

5) UI 变更

config-panel 新增:

  • canUndo: boolean
  • undoBusy: boolean
  • @undo 事件

页脚按钮区新增:

  • 撤销上一次批量重命名

按钮行为:

  • 仅当当前 scope 有有效记录时可点击;
  • 正在执行批量重命名或撤销时禁用;
  • 文案建议加辅助说明:仅针对当前目录最近一次批量重命名。

6) 错误处理

  • scope 不匹配:静默清空记录,并隐藏/禁用撤销按钮。
  • 撤销零任务:若记录为空或已无可回退项,直接清空记录并返回。
  • 部分失败:使用 i18n 头部文案 + 文件列表 + 手动处理提示拼接成 alert() 文本。
  • 全部失败:记录保留,便于用户在确认真实页面状态后再次尝试。

Data Flow

A. 主执行

点击 Execute
  -> handleExecute()
  -> BatchExecutor.execute()
  -> applyExecutionResults()
  -> create LastRenameOperation from results.success

B. Retry

点击 Retry
  -> handleRetryFailed()
  -> BatchExecutor.execute(tasks=failed only)
  -> applyExecutionResults()
  -> merge retry success into LastRenameOperation

C. Undo

点击 Undo
  -> validate current scope
  -> build reverse tasks from LastRenameOperation.items
  -> BatchExecutor.execute(tasks=undo)
  -> applyExecutionResults()
  -> clear record OR keep only failed items

Testing Strategy (TDD)

  • 单元测试新建纯函数/帮助模块:
    • 从 BatchResults.success 创建记录;
    • retry 成功项 merge;
    • scope 校验;
    • 部分 undo 失败后保留失败项。
  • 适配器测试覆盖 getCurrentDirectoryKey()。
  • 现有依赖 PlatformAdapter 的 mock 全部补上 getCurrentDirectoryKey() 以通过 typecheck。
  • 回归验证:
    • 普通批量重命名流程不受影响;
    • retry 后 undo 能覆盖首次成功项 + retry 成功项;
    • 离开目录后 undo 按钮消失/失效。

Rollout

  • 第一版优先采用懒校验而不是复杂的实时路由监听。
  • 若后续观察到“切目录后按钮残留到下一次交互前”影响体验,再补目录变化监听器。