从零实现一个 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)。
5.8 KiB
架构决策记录(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 —— 目前没有分享带筛选链接的需求, 迁移只会增加复杂度。