Files
Kanban/docs/adr.md
T
chenshoulin 4e064e82ea feat: 个人轻量看板工具 MVP
从零实现一个 local-first 的个人任务看板,数据只存在浏览器本地,不依赖后端。

功能
- 固定三列看板(待办 / 进行中 / 已完成),支持拖拽改状态与列内排序
- 快速新增、详情编辑、删除撤销、已完成归档
- 搜索与筛选(关键词 / 优先级 / 标签 / 今日到期 / 逾期)
- 今日视图:按紧急度分组(逾期 > 今天到期 > 进行中)+ 统计卡片
- 导出导入 JSON;启动时自动留快照,保留最近 3 份
- 键盘快捷键(N / 「/」/ ? / Esc)
- PWA:可安装、离线可用、更新提示

技术要点
- 持久化采用 TaskRepository 端口 + 整份快照读写,schemaVersion 迁移表
  显式登记升级规则(当前 v2,v1 -> v2 为归档功能补 archivedAt)
- 同列排序用相邻中点插入,间距不足时按列 normalize,避免浮点精度耗尽
- 拖拽只消费 Sortable 的事件,store 保持唯一数据源,不接管数据
- 数据损坏时先备份到 *.corrupt.<时间戳> 再报错,不覆盖用户数据
- 引入 vue-router 拆分看板与今日两个视图(见 docs/adr.md ADR-005)

验证
- 单元测试 437 个(33 个文件),行覆盖率 96.8%
- 端到端测试 23 个(Playwright),含真实拖拽与断网离线场景
- npm run build 通过,产物可静态托管

注:部署时需配置 SPA 回落,否则直接访问 /today 会 404(详见 README)。
2026-10-18 11:59:49 +08:00

5.8 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 —— 目前没有分享带筛选链接的需求, 迁移只会增加复杂度。