feat: 全局撤销/重做(ADR-010)
This commit is contained in:
+56
@@ -191,6 +191,8 @@ MVP 有「启动快照 + 手动导出」两道保护,但两个整份覆盖的
|
||||
- _给删除加二次确认_:删除是高频操作,弹窗会拖慢节奏;软删除 + 回收站已经提供了比确认框更好的退路。
|
||||
- _批量改属性做 `[{id, before}]` 快照式撤销_:需要额外的内存态与 UI 语义,收益低于成本;
|
||||
标签是增删语义,改错了再改回来即可。批量删除 / 归档仍有 Toast 撤销。
|
||||
(**已被 [ADR-010](#adr-010--引入全局撤销栈取代逐个操作做撤销) 推翻**:批量编辑器扩张到
|
||||
四类属性之后,这个取舍不再成立;但实现方式改为整份快照,而非字段级 diff。)
|
||||
- _合并时对整列 `sortOrder` 做 normalize_:会悄悄重排用户已有数据。新增项追加列尾已经足够,
|
||||
既有冲突留给后续显式整理。
|
||||
- _给回收站做搜索 / 筛选_:列表按删除时间倒序已经能覆盖「刚删错」这个主场景,先不加。
|
||||
@@ -282,3 +284,57 @@ ADR-008 否掉了「完成列按时间排序」,理由是维持单一顺序来
|
||||
`BoardView.visibleColumns`,测试覆盖了「顺序与 `sortOrder` 无关」这一条。
|
||||
- 已完成列成为只读顺序:拖拽、`↑` `↓`、批量移动(只能移出该列)都不会改变其内部顺序。
|
||||
|
||||
---
|
||||
|
||||
## ADR-010 · 引入全局撤销栈,取代逐个操作做撤销
|
||||
|
||||
**背景**
|
||||
|
||||
ADR-007 明确否决了「批量改属性做 `[{id, before}]` 快照式撤销」,理由是「需要额外的内存态与 UI 语义,
|
||||
收益低于成本」。当时这个判断成立:批量编辑器只能改标签,改错了再改回来即可。
|
||||
|
||||
但现在批量编辑器已经能一次改动目标列、优先级、截止日期、归档状态四类属性 —— 一次误操作波及的是
|
||||
几十条任务的多个字段,逐条手工回滚已不现实。同时 `Ctrl+Z` 是所有人对「撤销」的肌肉记忆,
|
||||
按下去没反应本身就是一个缺陷。重新评估之后,结论反过来了。
|
||||
|
||||
**结论**
|
||||
|
||||
1. 撤销 / 重做基于**整份 `tasks` 数组快照**,而不是命令模式。`stores/tasks.ts` 的所有变更都是
|
||||
不可变替换(`tasks.value = [...]`),未被改动的 Task 对象在相邻快照之间共享引用 ——
|
||||
50 步历史的实际内存增量只是「被改过的那几个对象」,与任务总数无关。
|
||||
2. 快照的采集点是 `commit()` 这唯一的写入出口:维护一个 `lastCommitted` 指针指向
|
||||
「上一次提交后的状态」,`commit()` 时把它压栈并更新指针。**三十多个写方法一行都没改**,
|
||||
因此不存在「新增操作忘了写 undo」这类漏点。
|
||||
3. `undo()` / `redo()` 刻意**不走 `commit()`**:撤销本身不是一次新的业务变更。若也压栈,
|
||||
用户按 `Ctrl+Z` 就会在两步之间来回跳。
|
||||
4. `commit()` 同时清空 redo 栈 —— 撤销之后又做了新操作,原来那条重做分支已经与当前状态分叉。
|
||||
5. 上限 50 步,超出丢弃最旧的;历史不落盘,刷新即清空。
|
||||
6. 快捷键单开一条通道(`matchUndoShortcut`),不复用 `matchShortcut`:后者的语义是「裸键」,
|
||||
带修饰键的组合一律交还浏览器(`Ctrl+F` 查找、`Cmd+R` 刷新)。把撤销混进去,这条放行规则就失效了。
|
||||
7. 在输入框内不拦截 `Ctrl+Z`:编辑任务详情时应当撤销文字输入,而不是把整个任务的状态回滚。
|
||||
8. 历史为空时弹一句短提示(2 秒);成功时不提示 —— 列表已经变了,本身就是反馈。
|
||||
|
||||
**被否决的方案**
|
||||
|
||||
- _命令模式(每个操作记录逆操作)_:需要为 `createTasks` / `updateTasks` / `moveTask` /
|
||||
`archiveTasks` / `renameTag` 等三十多个方法各写一份逆操作,且之后每加一个写方法都要记得补。
|
||||
快照方案把「必须做对」的点从 N 处收敛到 `commit()` 一处。
|
||||
- _字段级 diff 栈_:字段之间有关联(`status` 与 `completedAt` 必须同步、换列要重算 `sortOrder`),
|
||||
字段级还原极易留下不一致的中间态。整份快照天然自洽。
|
||||
- _持久化撤销历史到 localStorage_:刷新后仍能撤销「上一次会话」的操作,会让历史与当前数据的
|
||||
边界变模糊,也显著放大配额占用。编辑器的撤销本来就是会话级的。
|
||||
- _复用 `matchShortcut` 处理 `Ctrl+Z`_:会迫使该函数区分「允许哪些修饰键」,`Ctrl+F` 之类的
|
||||
放行规则随之含糊。
|
||||
- _撤销时弹 Toast 说明撤销了什么_:需要给 `commit()` 加操作名参数,进而改动所有调用点;
|
||||
收益只是「多说一句话」,而视觉变化已经足够。
|
||||
|
||||
**影响**
|
||||
|
||||
- `stores/tasks.ts` 新增 `undo` / `redo` / `canUndo` / `canRedo` / `resetHistory`;
|
||||
`undoStack` / `redoStack` 只存在于内存。
|
||||
- 跨标签页收到同步消息后(`reloadFromRepository`)调用 `resetHistory`:本页的历史已与最新数据分叉,
|
||||
保留下来会在下次撤销时抹掉对方的改动。
|
||||
- 启动时的过期回收站清理(`purgeExpiredTrash`)也走 `commit()`,因此在 `init()` 末尾显式
|
||||
`resetHistory()`,避免「打开页面就能撤销一次数据清理」。
|
||||
- ADR-007 中「_批量改属性不做撤销_」一条作废;其余结论(软删除、`activeTasks` 单一剔除点、
|
||||
标签增删语义)不变。
|
||||
|
||||
Reference in New Issue
Block a user