diff --git a/README.md b/README.md index f640655..bc8383a 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,15 @@ 一个轻量的个人看板工具:打开即用、数据存在本地、不依赖后端。 - 固定三列:**待办 / 进行中 / 已完成** -- 快速新增、拖拽排序、详情编辑、删除撤销 +- 快速新增、拖拽排序、详情编辑、删除撤销;详情里可直接切换状态 +- **批量添加**:一次录入多行清单,整批共用目标列 / 优先级 / 标签 / 截止日期 +- **批量删除**:多选后整批删除,可一键撤销 - 搜索与筛选(关键词 / 优先级 / 标签 / 今日到期 / 逾期) - **今日视图**:按紧急度分组汇总(逾期 / 今天到期 / 进行中 / 待办),含本周完成等统计 - 键盘快捷键;任务卡片可用方向键换列与调序 - **深色模式**:跟随系统 / 浅色 / 深色三态 -- 导出导入 JSON + 启动自动备份 +- 导出导入 JSON + 启动自动备份;**覆盖式导入或恢复快照前会自动留档** +- 已完成任务可批量归档,归档任务可在详情里随时恢复 - **可安装为 PWA,支持离线使用** --- @@ -22,36 +25,48 @@ npm run dev 打开终端提示的地址即可。首次进入是空看板,在任意列顶部的输入框里输入标题、回车即可创建任务。 +## 批量添加与批量删除 + +工具栏的「批量添加」可以一次录入多条任务:清单里**一行一条**,整段粘贴即可;行首的 `-`、`*`、`1.` 这类列表符号会自动去掉,空行会被忽略。 + +这一批共用同一个目标列、优先级、标签与截止日期 —— 先统一建出来,再单独调整个别任务。 + +不打开面板也行:在任意列的快速新增输入框里**粘贴多行文本**,会按行直接拆成多条任务(插入该列列首);单行粘贴仍是常规输入,不会被拦截。 + +要清理一批任务,点工具栏的「多选」:卡片变成可勾选的方框,点哪张选哪张,操作条上可以「全选 N 个」(只覆盖**当前筛选后可见**的任务)或「清空选择」,然后「删除 N 个」整批删掉。 + +批量添加与批量删除都会弹出带「撤销」的提示,误操作可以整批撤回(删除撤销会连原顺序一起还原)。多选期间拖拽会停用,避免顺手拖乱了顺序;筛选条件一变也会清空选择,保证「删掉的都是眼前看得见的」。 + ## 脚本 -| 命令 | 说明 | -|---|---| -| `npm run dev` | 启动开发服务器 | -| `npm run build` | 类型检查 + 生产构建,产物在 `dist/` | -| `npm run preview` | 本地预览生产构建 | -| `npm run typecheck` | 只做类型检查 | -| `npm run test` | 单元测试(Vitest) | -| `npm run test:coverage` | 单元测试 + 覆盖率报告 | -| `npm run test:e2e` | 端到端测试(Playwright,默认驱动系统已安装的 Chrome) | -| `node scripts/screenshot.mjs` | 生成界面截图(需先起 dev server),产物在 `.screenshots/` | -| `npm run lint` / `npm run format` | 代码检查与格式化 | +| 命令 | 说明 | +| --------------------------------- | --------------------------------------------------------- | +| `npm run dev` | 启动开发服务器 | +| `npm run build` | 类型检查 + 生产构建,产物在 `dist/` | +| `npm run preview` | 本地预览生产构建 | +| `npm run typecheck` | 只做类型检查 | +| `npm run test` | 单元测试(Vitest) | +| `npm run test:coverage` | 单元测试 + 覆盖率报告 | +| `npm run test:e2e` | 端到端测试(Playwright,默认驱动系统已安装的 Chrome) | +| `node scripts/screenshot.mjs` | 生成界面截图(需先起 dev server),产物在 `.screenshots/` | +| `npm run lint` / `npm run format` | 代码检查与格式化 | ## 快捷键 -| 按键 | 作用 | -|---|---| -| `N` | 聚焦「待办」列的快速新增输入框 | -| `/` | 聚焦搜索框 | -| `?` | 打开快捷键帮助 | -| `Esc` | 关闭当前面板 | +| 按键 | 作用 | +| ----- | ------------------------------ | +| `N` | 聚焦「待办」列的快速新增输入框 | +| `/` | 聚焦搜索框 | +| `?` | 打开快捷键帮助 | +| `Esc` | 关闭当前面板 | 卡片获得焦点(`Tab` 或点击)后: -| 按键 | 作用 | -|---|---| +| 按键 | 作用 | +| ------- | ------------------------- | | `←` `→` | 把卡片移到上一列 / 下一列 | -| `↑` `↓` | 在列内上移 / 下移 | -| `Enter` | 打开任务详情 | +| `↑` `↓` | 在列内上移 / 下移 | +| `Enter` | 打开任务详情 | 在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义)。 @@ -61,9 +76,9 @@ npm run dev 顶部导航可在两个视图之间切换: -| 视图 | 路径 | 组织方式 | -|---|---|---| -| 看板 | `/` | 按**状态**:待办 / 进行中 / 已完成 | +| 视图 | 路径 | 组织方式 | +| ---- | -------- | ----------------------------------------------- | +| 看板 | `/` | 按**状态**:待办 / 进行中 / 已完成 | | 今日 | `/today` | 按**紧急度**:已逾期 → 今天到期 → 进行中 → 待办 | 今日视图顶部有四张统计卡片(逾期 / 今天到期 / 进行中 / 本周完成),下方是可直接勾选完成的待办列表。 @@ -103,12 +118,15 @@ python scripts/generate-icons.py - 换浏览器或换设备 - Safari 的 7 天未访问存储清理策略 -因此工具内置了两道保护: +因此工具内置了三道保护: 1. **启动自动备份**:每次打开看板时留一份快照,最多保留最近 3 份,可在「备份」面板中一键恢复。 -2. **手动导出**:「备份」面板 → 导出为 JSON,文件名形如 `kanban-2026-10-18.json`。 +2. **覆盖前自动留档**:导入 JSON 或从快照恢复会整份替换数据,执行前会先把当前数据写一份快照,选错文件或选错备份也能退回去。 +3. **手动导出**:「备份」面板 → 导出为 JSON,文件名形如 `kanban-2026-10-18.json`。 -导入是**覆盖式**的:选中文件后会先校验,再显示将被覆盖的任务数量,需要二次确认才会执行。文件不合法时不会改动任何现有数据。 +导入是**覆盖式**的:选中文件后会先校验,再显示将被覆盖的任务数量,需要二次确认才会执行。确认覆盖前会自动备份当前数据;文件不合法时不会改动任何现有数据。 + +任务详情里的「状态」可以直接切换待办 / 进行中 / 已完成,不必回到看板拖拽。编辑后如果没保存就关闭,抽屉会先提醒,不会静默丢弃草稿。 > 如果需要在多设备间同步,需要自行接入后端;`TaskRepository` 接口就是为此预留的扩展点(见下)。 @@ -174,8 +192,15 @@ docs/ └── tasks.json # 可导入看板的种子数据 e2e/ # Playwright 端到端测试 scripts/ # 图标生成等辅助脚本 +.gitea/workflows/ # CI(lint / typecheck / test / build) ``` +## 持续集成 + +仓库内置 `.gitea/workflows/ci.yml`:推送与合并请求会依次执行 `npm run lint`、`npm run typecheck`、`npm run test`、`npm run build`。 + +E2E 需要真实浏览器,默认不放进 CI(要求 runner 预装 Chrome);请在本地或用带浏览器的 runner 执行 `npm run test:e2e`。 + ## 架构要点 - **单一数据源**:Pinia store 是内存态的唯一来源,所有变更经由 `TaskRepository` 落盘;拖拽也只消费事件,不接管数据。看板与今日视图共享同一份数据。 @@ -188,8 +213,8 @@ scripts/ # 图标生成等辅助脚本 ## 测试 -- 单元测试:480 个用例,覆盖 `src/` 下的纯逻辑与组件行为,行覆盖率约 96%。 -- 端到端测试:25 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 +- 单元测试:605 个用例,覆盖 `src/` 下的纯逻辑与组件行为,行覆盖率约 96%。 +- 端到端测试:28 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「批量添加」「多选批量删除 + 撤销」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 ```bash npm run test:coverage @@ -216,3 +241,4 @@ PLAYWRIGHT_CHANNEL=chromium npm run test:e2e # 用自带 Chromium(需先 npx - 今日视图的分组规则固定(逾期 > 今天到期 > 进行中 > 待办),不可自定义。 - 统计只覆盖本周完成数等基础指标,没有趋势图或历史留存。 - 深色模式只跟随系统或手动三态切换,没有按时间段自动切换。 +- 删除只提供 5 秒内的 Toast 撤销,还没有回收站;误删超过窗口后只能从自动备份里找。 diff --git a/docs/adr.md b/docs/adr.md index 9f60f89..2520eff 100644 --- a/docs/adr.md +++ b/docs/adr.md @@ -13,8 +13,9 @@ MVP 固定三列:`todo`(待办)/ `doing`(进行中)/ `done`(已完成)。`status` 使用 TypeScript 字面量联合类型,**不使用 `enum`**(同时满足 `erasableSyntaxOnly` 编译约束)。 **被否决的方案** -- *引入 `columns` 表 + `task.columnId`*:需要额外的列 CRUD、列删除时的任务迁移、列顺序持久化。对单人任务管理而言,收益为零,成本约为原方案的两倍。 -- *用 `enum` 定义列*:与 `erasableSyntaxOnly: true` 冲突,且 `enum` 在纯类型场景下没有优势。 + +- _引入 `columns` 表 + `task.columnId`_:需要额外的列 CRUD、列删除时的任务迁移、列顺序持久化。对单人任务管理而言,收益为零,成本约为原方案的两倍。 +- _用 `enum` 定义列_:与 `erasableSyntaxOnly: true` 冲突,且 `enum` 在纯类型场景下没有优势。 **影响** 若未来确需自定义列,需一次性迁移 `status` → `columnId`,并在 `schemaVersion` 中登记。 @@ -30,8 +31,9 @@ MVP 固定三列:`todo`(待办)/ `doing`(进行中)/ `done`(已完 定义 `TaskRepository` 接口(`list / get / create / update / remove / replaceAll`),MVP 只提供 localStorage 实现。UI 与 Store 仅依赖接口,不感知具体实现。 **被否决的方案** -- *直接用 Dexie*:收益是索引查询与大容量,在该规模下用不上;代价是 Pinia 与 DB 双份状态、异步竞态、多标签页同步、测试需引入 `fake-indexeddb`,约多花 1.5 人天。 -- *直接在组件里读写 localStorage*:数据访问散落各处,无法测试,也无法替换实现。 + +- _直接用 Dexie_:收益是索引查询与大容量,在该规模下用不上;代价是 Pinia 与 DB 双份状态、异步竞态、多标签页同步、测试需引入 `fake-indexeddb`,约多花 1.5 人天。 +- _直接在组件里读写 localStorage_:数据访问散落各处,无法测试,也无法替换实现。 **影响** 数据量增长到万级或需要复杂查询时,新增一个 Dexie 实现并切换注入即可,接口不变。 @@ -47,8 +49,9 @@ MVP 固定三列:`todo`(待办)/ `doing`(进行中)/ `done`(已完 `dueDate` 类型为 `string | null`,格式固定 `YYYY-MM-DD`,不含时刻与时区信息。「是否逾期」以本地日历日比较得出。 **被否决的方案** -- *存储 ISO 8601 完整时间戳*:需要引入时区处理,且个人看板不需要精确到分钟的截止时间。 -- *存储 `Date` 对象*:JSON 序列化后仍会退化为字符串,且容易在反序列化时引入时区偏移。 + +- _存储 ISO 8601 完整时间戳_:需要引入时区处理,且个人看板不需要精确到分钟的截止时间。 +- _存储 `Date` 对象_:JSON 序列化后仍会退化为字符串,且容易在反序列化时引入时区偏移。 **影响** 若未来需要提醒功能,应新增独立字段(如 `remindAt`),而不是改变 `dueDate` 的语义。 @@ -67,8 +70,9 @@ MVP 固定三列:`todo`(待办)/ `doing`(进行中)/ `done`(已完 MVP 为单视图应用:主看板常驻,任务详情使用 Drawer,导入导出与快捷键说明使用 Modal。不引入 `vue-router`。 **被否决的方案** -- *引入 vue-router*:三个「页面」中有两个本质是覆盖层(overlay),用路由表达会让 URL 与状态同步复杂化,且多一个运行时依赖。 -- *用 URL query 承载筛选状态*:MVP 不做分享与深链需求,暂不需要。 + +- _引入 vue-router_:三个「页面」中有两个本质是覆盖层(overlay),用路由表达会让 URL 与状态同步复杂化,且多一个运行时依赖。 +- _用 URL query 承载筛选状态_:MVP 不做分享与深链需求,暂不需要。 **影响** 若未来增加「今日视图」「统计」等真正的独立页面,再引入路由;届时筛选状态可一并迁移到 query。 @@ -95,18 +99,59 @@ ADR-004 决定 MVP 不引入路由,并注明「若未来增加今日视图、 以便测试注入 memory history。 **被否决的方案** -- *把今日视图做成一个 Drawer / Modal*:Drawer 适合「聚焦单条记录」(任务详情、备份面板), + +- _把今日视图做成一个 Drawer / Modal_:Drawer 适合「聚焦单条记录」(任务详情、备份面板), 不适合承载一个完整的列表视图;而且它无法被收藏或分享,PWA 的 manifest shortcuts 也无处可指。 -- *在工具栏加一个「今日」筛选按钮*:那只是筛选,不是视图。用户仍然面对三列结构, +- _在工具栏加一个「今日」筛选按钮_:那只是筛选,不是视图。用户仍然面对三列结构, 还得自己在心里把「逾期」和「进行中」拼起来。 -- *用 hash 路由避免服务器配置*:URL 会变成 `/#/today`,不美观,且 PWA 的 `start_url` +- _用 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 纳入推送与合并请求检查。