Files
Kanban/docs/adr.md
T
chenshoulin 43e813b93a docs: 更新批量操作与备份恢复说明,新增 ADR-006
- README 补充批量添加/删除、导入前自动备份、归档恢复与 CI 说明
- ADR-006 记录覆盖式操作前快照、id 查重与未保存确认等决策
2026-10-19 17:21:53 +08:00

8.4 KiB
Raw Blame History

架构决策记录(ADR)

本文件记录个人看板工具 MVP 的关键技术决策。每条包含:背景、结论、被否决的方案及原因。


ADR-001 · 看板列固定为三列,不支持配置

背景 初版设计在设置页提供「列配置」,但数据模型中 status 为固定枚举 todo | doing | done,两者互相矛盾。

结论 MVP 固定三列:todo(待办)/ doing(进行中)/ done(已完成)。status 使用 TypeScript 字面量联合类型,不使用 enum(同时满足 erasableSyntaxOnly 编译约束)。

被否决的方案

  • 引入 columns 表 + task.columnId:需要额外的列 CRUD、列删除时的任务迁移、列顺序持久化。对单人任务管理而言,收益为零,成本约为原方案的两倍。
  • 用 enum 定义列:与 erasableSyntaxOnly: true 冲突,且 enum 在纯类型场景下没有优势。

影响 若未来确需自定义列,需一次性迁移 status → columnId,并在 schemaVersion 中登记。


ADR-002 · 存储采用 TaskRepository 接口 + localStorage 实现

背景 初版方案直接选用 Dexie(IndexedDB)。但个人看板在 200 个任务规模下,数据量约 60KB,远低于 localStorage 5MB 上限。

结论 定义 TaskRepository 接口(list / get / create / update / remove / replaceAll),MVP 只提供 localStorage 实现。UI 与 Store 仅依赖接口,不感知具体实现。

被否决的方案

  • 直接用 Dexie:收益是索引查询与大容量,在该规模下用不上;代价是 Pinia 与 DB 双份状态、异步竞态、多标签页同步、测试需引入 fake-indexeddb,约多花 1.5 人天。
  • 直接在组件里读写 localStorage:数据访问散落各处,无法测试,也无法替换实现。

影响 数据量增长到万级或需要复杂查询时,新增一个 Dexie 实现并切换注入即可,接口不变。


ADR-003 · dueDate 只表示日期,不表示时刻

背景 dueDate 若同时承载日期与时刻,需要处理时区转换(本机为 Asia/Hong_Kong),并会在「今天到期」的判定上产生歧义。

结论 dueDate 类型为 string | null,格式固定 YYYY-MM-DD,不含时刻与时区信息。「是否逾期」以本地日历日比较得出。

被否决的方案

  • 存储 ISO 8601 完整时间戳:需要引入时区处理,且个人看板不需要精确到分钟的截止时间。
  • 存储 Date 对象:JSON 序列化后仍会退化为字符串,且容易在反序列化时引入时区偏移。

影响 若未来需要提醒功能,应新增独立字段(如 remindAt),而不是改变 dueDate 的语义。


ADR-004 · 不引入 vue-router

状态:已被 ADR-005 取代。 本决策在 MVP 阶段成立;当「今日视图」真的落地后, 它预判的触发条件("若未来增加今日视图、统计等真正的独立页面")已经满足。

背景 初版信息架构包含「主看板页」「任务详情」「设置页」三个页面。

结论 MVP 为单视图应用:主看板常驻,任务详情使用 Drawer,导入导出与快捷键说明使用 Modal。不引入 vue-router。

被否决的方案

  • 引入 vue-router:三个「页面」中有两个本质是覆盖层(overlay),用路由表达会让 URL 与状态同步复杂化,且多一个运行时依赖。
  • 用 URL query 承载筛选状态:MVP 不做分享与深链需求,暂不需要。

影响 若未来增加「今日视图」「统计」等真正的独立页面,再引入路由;届时筛选状态可一并迁移到 query。


ADR-005 · 引入 vue-router,拆分看板与今日两个视图

背景 ADR-004 决定 MVP 不引入路由,并注明「若未来增加今日视图、统计等真正的独立页面,再引入路由」。 现在补做了「今日视图 + 简单统计」,触发条件成立。

「今日视图」与看板的关系值得说清楚:它不是看板的另一种筛选,而是换了一个组织维度 —— 看板按「状态」组织(待办/进行中/已完成),今日视图按「紧急度」组织(逾期 → 今天到期 → 进行中)。 同一批数据,两种视角,用户需要在它们之间来回切换,而不是叠加在同一屏里。

结论 引入 vue-router,两条路由:

  • / — 看板
  • /today — 今日视图(含统计卡片与按紧急度排序的待办列表)

使用 createWebHistory(而非 hash),并导出 createAppRouter(history) 工厂函数而非单例, 以便测试注入 memory history。

被否决的方案

  • 把今日视图做成一个 Drawer / Modal:Drawer 适合「聚焦单条记录」(任务详情、备份面板), 不适合承载一个完整的列表视图;而且它无法被收藏或分享,PWA 的 manifest shortcuts 也无处可指。
  • 在工具栏加一个「今日」筛选按钮:那只是筛选,不是视图。用户仍然面对三列结构, 还得自己在心里把「逾期」和「进行中」拼起来。
  • 用 hash 路由避免服务器配置:URL 会变成 /#/today,不美观,且 PWA 的 start_url 与 shortcuts 指向带 hash 的地址会让某些平台的安装体验变差。代价是部署时必须配置 SPA 回落 —— 这一点已在 README 的部署章节明确写出。

影响

  • 部署要求变化:服务器必须把未知路径回落到 index.html,否则直接访问 /today 会 404。 README 已给出 Nginx 与 Netlify 的配置。
  • 今日视图按路由懒加载,不进入首屏包(独立 chunk 约 4KB)。
  • Service Worker 的 navigateFallback 设为 index.html,离线时深链 /today 同样可用。
  • 看板原有的筛选状态仍留在 Pinia 中,未迁移到 query —— 目前没有分享带筛选链接的需求, 迁移只会增加复杂度。

ADR-006 · 覆盖式操作前自动快照,归档任务提供恢复入口

背景

MVP 有「启动快照 + 手动导出」两道保护,但两个整份覆盖的动作没有退路:导入 JSON 与从快照恢复 都会直接替换当前数据。归档功能的设计说明写的是「可随时恢复」,实际只有归档后 5 秒内的 Toast 撤销能恢复,界面上找不到入口。另外,详情抽屉不能改状态、未保存的修改点一下遮罩就丢, 是日常使用里最高频的两处摩擦。

结论

  1. importJson / restoreSnapshot 在覆盖前调用 repository.snapshot(当前数据); 快照写入失败不阻断主流程。空看板不写,避免产生无意义备份。
  2. 导入校验增加 id 唯一性检查:严格导入路径直接拒绝并指出重复的两条; 宽容读取路径保留先出现的那条,避免重复 id 破坏 v-for 与 findIndex。
  3. 详情抽屉增加「状态」三列切换,并展示归档状态与「恢复」入口。
  4. 详情抽屉维护 dirty 状态:有未保存修改时关闭需要二次确认,不再静默丢弃草稿。
  5. Toast 自动关闭支持悬停/聚焦暂停,移开后按剩余时间继续;撤销不再「刚要点击就消失」。
  6. 存储写失败时把 persistent 置为 false 并把提示提升到 App 级,两个视图都能看到告警。

被否决的方案

  • 给删除加二次确认弹窗:与现有「立即删除 + Toast 撤销」的约定冲突,且删除频率高, 弹窗会拖慢操作;改为延长撤销的可操作性(暂停计时)成本更低。
  • 把归档任务从列内排序序列中剥离,让归档后排序仍可用:需要同时改 sortOrder 计算、 拖拽下标映射与渲染顺序,收益仅是「已完成列可排序」。当前保留禁用策略, 把原因写进工具栏提示(「归档 N 个,列内排序已暂停」),让用户知道为什么拖不动。
  • 导入后自动生成版本快照:没有服务端与版本库,快照只是本地兜底,不应膨胀成版本历史。

影响

  • 快照最多占用 4 份全量数据(主数据 + 3 份快照);任务量接近 localStorage 配额时 需要清理旧快照或按 ADR-002 迁移到 IndexedDB。
  • persistent 现在会随写入成功/失败翻转,UI 可据此判断「改动是否真的落盘」。
  • 关闭详情抽屉的默认行为从「直接关闭」变为「有改动先确认」,相关组件测试已同步调整。
  • 新增 .gitea/workflows/ci.yml,把 lint / typecheck / test / build 纳入推送与合并请求检查。