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 架构总览
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 目录结构(本次新增/修改)
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 路由与容器
date 状态提升到 CheckinPage:日历里点某天 → 切回打卡视图时即为该日,实现「日历与打卡联动」。
3.2 组件树与关键状态
统一约定:
- 加载中 →
DotLoading;错误 → error-banner + 重试;空 → antd-mobile Empty + 引导按钮(三态齐全,U-06)。
- 页面重新可见时静默刷新(
useRefreshOnVisible),与既有模块一致。
3.3 环形进度组件 RingProgress
| 项 |
设计 |
| 实现 |
双 <circle>:灰底环 + 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` |
| 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 关键流程:打卡
4.4 关键流程:月历聚合
- 取全部 Habit(含归档)→ 过滤
archived=false 得 active。
- 一次查询当月
[01, 月末] 全部 CheckIn,按 habitId → date → count 分组。
- 逐日遍历:仅统计
Habit.createdAt <= 该日 的项;累加 expected += expectedPerDay(...)、actual += min(count, expected);任一 count > expected 记 over。
status:全完成且无超额 → full;全完成且有超额 → over;有完成未全完成 → partial;否则 empty。
- 完成率
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+)
- 提醒与通知:
Habit.remindAt + Tauri 本地通知,解决「忘打卡」。
- 补打放宽 / 历史编辑:当前 7 天限制可配置化。
- 统计视图:连续天数榜单、月/年热力图(届时引入
HabitDailyStat 汇总表)。
- 打卡项分组与排序拖拽:现有
sortOrder 已预留,仅需交互。
- 清单协作/分享:引入
CheckListShare。
- 数据导出:清单完成后导出为图片或文本(便于发群里确认)。