# iBoard V0.2 设计文档 — 打卡 / 日历 / 清单 | 项 | 内容 | | --- | --- | | 版本 | V0.2(对应分支 `dev-0.2`) | | 文档状态 | 已实现(与代码一致) | | 上游文档 | `需求文档.md`(需求编号 F-CK / F-CL / F-LS / F-SY / U / D / N / E 沿用本文) | | 配套文档 | `数据库设计文档.md`、`界面设计.html` | | 技术栈 | Tauri 2 + React 18 + antd-mobile + React Router v6 + Express 4 + Prisma/MariaDB | | 设计基线 | 移动端优先,视口 420×820,375~430px 自适应;时区统一 `Asia/Shanghai` | --- ## 1. 总体设计 ### 1.1 目标与范围 在既有 iBoard(首页 / 宏观 / 监控 / 我)之上新增**第 5 个底部 Tab「打卡」**,页面内以三个子视图承载三类需求: | 子视图 | 解决问题 | 核心隐喻 | | --- | --- | --- | | **打卡** | 周期性习惯的量化坚持 | 环形进度,点环即打卡 | | **日历** | 回看昨天、执行今天、预知明天 | 月历网格 + 日状态点 | | **清单** | 出差/回老家等一次性场景的漏项检查 | 勾选清单 + 进度条 | **不纳入 V0.2**:多用户共享清单、提醒推送、数据导出、打卡项分组、年视图热力图(见 §9 演进)。 ### 1.2 架构总览 ``` ┌──────────────────────── 前端 (React + antd-mobile, 打包进 Tauri / OTA) ────────────────────────┐ │ App.jsx 路由 │ │ └ /checkin ──► CheckinPage ── SwipeTabs ──┬── HabitView (打卡) │ │ ├── CalendarView (日历) │ │ └── ListView (清单) │ │ 组件: RingProgress(环形) / Celebrate(特效) / BottomNav / TabIcon │ │ 工具: src/pages/checkin/date.js (本地日期, 与后端同口径) apiClient: src/api/checkin.js │ └──────────────────────────────────┬─────────────────────────────────────────────────────────────┘ │ HTTPS + JWT (Authorization: Bearer) ┌──────────────────────────────────▼─────────────────────────────────────────────────────────────┐ │ Express (server/index.js) │ │ /api/checkin ─► routes/checkin.js ─┐ │ │ /api/checklists─► routes/checklists.js ─┤─► middleware.authRequired ─► Prisma Client │ │ └─► checkin/period.js (周期口径唯一来源) │ └──────────────────────────────────┬─────────────────────────────────────────────────────────────┘ ▼ MariaDB: Habit / CheckIn / CheckList / CheckListItem / CheckListTemplate ``` ### 1.3 分层与职责 | 层 | 位置 | 职责 | | --- | --- | --- | | 页面 | `src/pages/checkin/*.jsx` | 视图编排、本地状态、乐观更新与回滚 | | 组件 | `src/components/RingProgress.jsx` | 纯展示:SVG 环形 + 完成特效 + 特效开关 | | 日期工具 | `src/pages/checkin/date.js` | 与后端同口径的本地日期运算(**改一处需同步另一处**) | | API 封装 | `src/api/checkin.js` | 统一走 `api()`(自带 token / 错误解析) | | 路由 | `server/routes/checkin.js`、`checklists.js` | 参数校验、鉴权、组装展示态 | | 领域逻辑 | `server/checkin/period.js` | 周期窗口、日均期望、streak 判定 | | 持久化 | Prisma Client(`generated/prisma`) | 表操作 | ### 1.4 目录结构(本次新增/修改) ``` prisma/ schema.prisma # +5 个 model,User 加反向关系 migrations/20261022120000_add_checkin_checklist/ # 新建表 + 外键 server/ checkin/period.js # 周期计算(唯一口径) routes/checkin.js # /api/checkin/* routes/checklists.js # /api/checklists/* index.js # 挂载两个路由 src/ api/checkin.js # checkinApi / checklistApi components/RingProgress.jsx # 环形进度 + Celebrate 特效 components/BottomNav.jsx # 新增「打卡」Tab components/TabIcon.jsx # 新增 checkin 图标 pages/checkin/CheckinPage.jsx # 容器:三视图切换 pages/checkin/HabitView.jsx # 打卡视图 pages/checkin/CalendarView.jsx # 日历视图 pages/checkin/ListView.jsx # 清单视图 pages/checkin/date.js # 前端日期工具 App.jsx # 新增 /checkin 路由 styles/globals.css # 打卡相关样式段 docs/V0.2/ # 需求 / 设计 / 数据库设计 / 界面设计 ``` --- ## 2. 领域模型与核心口径 ### 2.1 实体 `Habit`(打卡项定义)· `CheckIn`(一次打卡)· `CheckList`(场景清单)· `CheckListItem`(检查项)· `CheckListTemplate`(模板)。字段与索引见《数据库设计文档》。 ### 2.2 周期口径(唯一来源:`server/checkin/period.js`) | 概念 | 定义 | | --- | --- | | 本地日历日 | `Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai' })` → `"YYYY-MM-DD"` | | 周起始 | **周一**(`startOfWeek` 把周日映射到第 7 天) | | 周期窗口 | day = [d, d];week = [周一, 周日];month = [1 号, 月末];year = [1/1, 12/31] | | **环形进度** | `percent = 周期内累计次数 / n`,上限 1 —— 回答「这周期做了多少」 | | **日均期望次数** | day→n;week→`ceil(n/7)`;month→`ceil(n/当月天数)`;year→`ceil(n/365)` —— 回答「今天该做多少」 | | 完成 | `done >= target`(超额允许,`over = done - target` 继续累加) | | streak | 从当前周期向前回溯:达标 +1;**当前周期未结束且未达标 → 跳过而非中断**;已结束周期未达标 → 终止 | >两种口径并存是有意为之:环形看「周期整体」,日历日标记看「当天份额」。例如「每周 3 次」,周三打 1 次 → 环形 33.3%,当天已达标(1 ≥ ceil(3/7)=1)。 ### 2.3 边界规则 | 场景 | 规则 | 编号 | | --- | --- | --- | | 提前打卡(未来日期) | 拒绝,返回 400「不能为未来日期打卡」 | F-CK-11 | | 补打 | 仅限最近 **7 天**,记录 `source='backfill'` | F-CK-10 | | 跨零点 | 以服务端 Asia/Shanghai 日历日为准,前端不自行判断 | E-02 | | 周期中途新建 | 按完整 n 计算,不按剩余天数折算 | E-01 | | 编辑 `target` | 立即生效,历史记录不改动 | F-CK-13 | | 删除打卡项 | 默认归档(`archived=true`),`?hard=1` 物理删并清记录 | E-05 | | 清单删除 | 物理删(一次性场景不留历史) | F-LS-10 | --- ## 3. 前端设计 ### 3.1 路由与容器 ``` /checkin → CheckinPage ├─ NavBar「打卡」 └─ SwipeTabs(左右滑动切换,与既有模块交互一致) ├─ habit → HabitView (受 date 影响) ├─ calendar → CalendarView (持有选中 date,可回写容器) └─ list → ListView (独立) ``` `date` 状态提升到 `CheckinPage`:日历里点某天 → 切回打卡视图时即为该日,实现「日历与打卡联动」。 ### 3.2 组件树与关键状态 ``` CheckinPage ├── useState: active('habit'|'calendar'|'list'), date('YYYY-MM-DD') HabitView(date) ├── data: { today, date, items[], summary:{total,done} } ├── bursts: { habitId: n } // 特效触发计数(+1 即播放一次) ├── form / showForm // 新建/编辑弹窗 └── effects: boolean // 特效开关(持久化 localStorage) CalendarView(date, onDateChange) ├── month, cal(月历), detail(当日), bursts └── quickValue: -1|0|1|null // 昨天/今天/明天 分段控件 ListView() ├── archived, lists, openId(展开的清单), newItem, templates, form/showForm └── bursts ``` 统一约定: - 加载中 → `DotLoading`;错误 → `error-banner` + 重试;空 → antd-mobile `Empty` + 引导按钮(三态齐全,U-06)。 - 页面重新可见时静默刷新(`useRefreshOnVisible`),与既有模块一致。 ### 3.3 环形进度组件 `RingProgress` | 项 | 设计 | | --- | --- | | 实现 | 双 ``:灰底环 + `stroke-dasharray/dashoffset` 进度环,`rotate(-90)` 从 12 点顺时针 | | 尺寸 | `size=58`,`stroke=6`,`r=26`,周长 `2πr≈163.4` | | 动画 | `transition: stroke-dashoffset .45s cubic-bezier(.4,0,.2,1)`(400ms 级,U-01) | | 中心内容 | 未达标 → `done / target` 数字;**达标 → 对勾 SVG**(`ring-check-in` 缩放入场);超额 → 右上角 `+n` 徽标 | | 交互 | 整个环可点 = 打卡(`clickable`),同时卡片右侧保留显式「打卡」按钮,降低发现成本 | | 无障碍/降级 | `disabled` 态半透明;系统「减弱动效」时特效自动关闭 | **完成特效 `Celebrate`**(F-CK-07 / U-02):`fire` 计数器 +1 即触发一次 → 14 个粒子沿圆周角度扩散(`--a/--d` CSS 变量)+ 主色光环脉冲 0.7s,总时长 ≤900ms,`pointer-events:none` 不阻塞后续操作;同时 `navigator.vibrate(18)` 轻震动(U-03)。开关持久化在 `localStorage.iboard_celebrate`,`prefers-reduced-motion` 自动降级(U-07)。 ### 3.4 交互清单 | 视图 | 主要交互 | | --- | --- | | 打卡 | 点环/点按钮打卡(乐观更新,失败回滚 + Toast)、撤销、编辑、归档、新建弹窗(名称/周期/目标次数/颜色/备注)、特效开关 | | 日历 | 月份左右切换、回到今天、「昨天/今天/明天」分段切换、点日期选中、当日详情内打卡(未来日期只读显示「计划」)、本月完成率 | | 清单 | 进行中/已归档切换、展开/收起、逐条勾选、添加检查项、全选/全不选、复制、存为模板、归档/恢复、删除、从模板新建 | 排序:打卡项列表**未完成在前**(F-CK-16);清单按 `updatedAt` 倒序。 ### 3.5 视觉规范 | 类别 | 取值 | | --- | --- | | 主色 | `#1677ff`;成功 `#00b578`;警告/部分 `#ffc53d`;超额 `#a855f7`;危险 `#ff3141` | | 背景 / 卡片 | `#f5f5f5` / `#ffffff`,卡片 `radius 10px` + `0 1px 2px rgba(0,0,0,.04)` | | 文字 | 主 `#333`、次 `#666`、弱 `#999` | | 日历日状态点 | full 绿 / partial 黄 / over 紫 / empty 无 | | 打卡项预设色 | `#1677ff #00b578 #ff8f1f #ff3141 #a855f7 #14b8a6 #f43f9e` | 完整高保真稿见 `界面设计.html`。 --- ## 4. 后端设计 ### 4.1 通用约定 - 两个路由文件首行 `router.use(authRequired)`,**全部接口需登录**;用户维度一律取 `req.user.sub`,不信任 body 中的 `userId`。 - 响应统一 `{ item }` / `{ items }` / `{ ok }`;错误统一 `{ error: '中文提示' }` + 语义化状态码。 - 展示态(percent / completed / streak / dayDone …)由服务端计算后下发,前端不做二次口径推导,避免两端不一致。 ### 4.2 接口清单 #### 打卡 `/api/checkin` | 方法 | 路径 | 说明 | 关键响应字段 | | --- | --- | --- | --- | | GET | `/habits?date=` | 指定日待办 + 周期进度 | `items[]`(含 `done/target/percent/completed/over/streak/expectedPerDay/periodStart,End`)、`summary` | | POST | `/habits` | 新建打卡项 | `item` | | PUT | `/habits/:id` | 编辑(名称/周期/目标/颜色/备注/归档/排序) | `item` | | DELETE | `/habits/:id?hard=1` | 默认归档;`hard=1` 物理删 | `{ ok, removed }` | | POST | `/habits/:id/check` | 打一次卡(`{ date?, note? }`) | `item`(最新展示态) | | DELETE | `/habits/:id/check?date=` | 撤销本周期最近一次 | `item` | | GET | `/day?date=` | 某日每个打卡项明细 | `items[]`(含 `canCheck`)、`isToday`、`isFuture` | | GET | `/calendar?month=YYYY-MM` | 月历 + 完成率 | `days[]`(`status/expected/actual/isToday/isFuture/habits[]`)、`rate`、`habits[]` | #### 清单 `/api/checklists` | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/?archived=0|1` | 清单列表(带 `done/total/percent/completed`) | | POST | `/` | 新建(`{ title, scene, note?, items?, templateId? }`) | | GET | `/:id` | 单条详情 | | PUT | `/:id` | 改名称/场景/备注/归档 | | DELETE | `/:id` | 物理删除(连带检查项) | | POST | `/:id/duplicate` | 复制为新清单(勾选状态重置) | | POST | `/:id/items` | 新增检查项 | | PUT | `/items/:id` | 改标题/备注/勾选(`done` 变更时写 `doneAt`) | | DELETE | `/items/:id` | 删除检查项 | | POST | `/:id/check-all` | 一键全选/全不选(`{ done }`) | | GET | `/templates` | 模板列表(含系统预置,首次访问幂等写入) | | POST | `/templates` | 另存为模板 | | DELETE | `/templates/:id` | 删除**自建**模板(预置模板拒绝) | ### 4.3 关键流程:打卡 ``` 用户点环 │ ├─ 前端: 本地 +1,重算 percent/completed/over(乐观更新) │ └─ POST /api/checkin/habits/:id/check ├─ 校验 habit 归属 → 404 ├─ date > today → 400 不能为未来日期打卡 ├─ date < today-7 → 400 最多只能补打最近 7 天 ├─ seq = max(seq)+1 → insert │ └─ P2002 → 409 该次打卡已记录(幂等) └─ 返回最新 item │ ├─ 成功: 用服务端 item 覆盖本地;若 completed → fire 特效 └─ 失败: setData(before) 回滚 + Toast ``` ### 4.4 关键流程:月历聚合 1. 取全部 Habit(含归档)→ 过滤 `archived=false` 得 `active`。 2. 一次查询当月 `[01, 月末]` 全部 CheckIn,按 `habitId → date → count` 分组。 3. 逐日遍历:仅统计 `Habit.createdAt <= 该日` 的项;累加 `expected += expectedPerDay(...)`、`actual += min(count, expected)`;任一 `count > expected` 记 `over`。 4. `status`:全完成且无超额 → `full`;全完成且有超额 → `over`;有完成未全完成 → `partial`;否则 `empty`。 5. 完成率 `rate = Σactual / Σexpected`(**只统计非未来日期**)。 复杂度:`O(天数 × 打卡项数)`,30×20=600 次内存运算,无 N+1 查询。 ### 4.5 校验与错误码 | 状态码 | 场景 | | --- | --- | | 400 | 名称为空/超长、周期非法、target < 1、未来日期打卡、超出补打窗口、本周期无记录可撤销、模板缺条目 | | 404 | 资源不存在或不属于当前用户 | | 409 | 并发重复打卡(P2002) | | 401 | 未登录 / token 失效(`authRequired`) | 所有写操作均用 `prisma.xxx.findFirst({ where: { id, userId } })` 或 `where: { id, list: { userId } }` 做归属校验,杜绝越权改他人数据。 --- ## 5. 非功能设计 | 维度 | 设计 | | --- | --- | | 首屏 | 三视图懒切换,只有激活视图发请求;单视图首屏 ≤ 1 次列表请求(N-01) | | 弱网/失败 | 乐观更新 + 失败回滚 + Toast;错误横幅带重试;`useRefreshOnVisible` 静默刷新 | | 幂等 | 唯一约束 `(habitId, date, seq)` + 前端按钮 loading,防连点重复计次 | | 时区 | 全链路 Asia/Shanghai;库内 `DATETIME` 存 UTC,业务日期用 `VARCHAR(10)` 本地日历日 | | 兼容 | 375~430px 自适应;Tauri Android WebView 支持 `navigator.vibrate`,不支持时静默跳过 | | 性能 | 月历/周期统计均为「一次范围查询 + 内存聚合」,避免逐项查询 | | 安全 | 全接口 `authRequired`;输入长度截断(名称 20、检查项 40、备注 200);无 SQL 拼接(Prisma 参数化) | | 可维护 | 周期口径单点(`server/checkin/period.js`),前端 `date.js` 与之对齐,改动需同步两处(已在文档与注释中标注) | --- ## 6. 测试与验证 | 层级 | 内容 | 结果 | | --- | --- | --- | | 单元 | 周期工具 23 条断言(窗口边界、周一起始、月末、闰年、日均期望、streak 跳过未结束周期) | 全通过 | | 接口冒烟 | 自签 JWT 端到端:每天 1 次闭合、每周 3 次 33.3%、超额 +1、撤销归零、未来日期 400、月历 31 天、模板生成 11 项、全选完成、复制重置 | 全通过,测试数据已清理 | | 构建 | `npm run build` | 通过 | | 迁移 | `prisma migrate deploy` 到目标库 | 成功 | --- ## 7. 里程碑(对应需求文档 M1~M5) | 里程碑 | 内容 | 状态 | | --- | --- | --- | | M1 | 数据模型 + 迁移 + 周期模块 + 全部后端接口 | 完成 | | M2 | 打卡视图:环形进度、特效、撤销/补打、新建编辑 | 完成 | | M3 | 日历视图:月历 + 昨天/今天/明天 + 完成率 | 完成 | | M4 | 清单视图:CRUD、勾选、进度、模板复用 | 完成 | | M5 | 文档(需求/设计/数据库/界面)+ 构建校验 + 提交 | 完成 | --- ## 8. 风险与权衡 | 项 | 权衡 | 缓解 | | --- | --- | --- | | 周期口径前后端各实现一份 | 前端需离线即时反馈,不能等接口 | 两端算法同源同注释;口径变更必须同步 `period.js` 与 `date.js`(已写入项目记忆) | | 进度实时聚合而非冗余计数 | 换取一致性与配置即时生效,牺牲少量 CPU | 单用户数据量极小(月历 30 天 × 20 项),实测无感;V0.3 可加汇总表 | | 未来日期禁止打卡 | 防止「提前打完一周」刷进度,符合「日历看明天=计划」语义 | 明天视图只读展示「计划」,明确文案 | | 特效可能打扰 | 部分用户不喜动效 | 一键开关 + `prefers-reduced-motion` 自动降级 | | 新增第 5 个 Tab | 底部空间变拥挤 | 图标线性简洁;`TabBar` 原生 safe-area,5 项在 375px 下仍在可点区(≥44px) | --- ## 9. 后续演进(V0.3+) 1. **提醒与通知**:`Habit.remindAt` + Tauri 本地通知,解决「忘打卡」。 2. **补打放宽 / 历史编辑**:当前 7 天限制可配置化。 3. **统计视图**:连续天数榜单、月/年热力图(届时引入 `HabitDailyStat` 汇总表)。 4. **打卡项分组与排序拖拽**:现有 `sortOrder` 已预留,仅需交互。 5. **清单协作/分享**:引入 `CheckListShare`。 6. **数据导出**:清单完成后导出为图片或文本(便于发群里确认)。