Files
iboard/docs/V0.2/设计文档.md
T
cheney 9b4705870d docs(V0.2): 新增设计文档 / 数据库设计文档 / 界面设计静态原型
- 设计文档.md: 架构与分层、目录结构、周期口径与边界规则、前端组件与交互、
  后端接口清单、关键流程时序、非功能设计、测试验证、风险与演进
- 数据库设计文档.md: ER、5 张表逐字段、索引与外键级联、关键设计决策
  (date 用 VARCHAR(10) 的原因 / 不冗余计数 / seq 幂等 / 归档语义 / 模板 JSON)、
  容量估算、典型 SQL、迁移与回滚、V0.3 预留
- 界面设计.html: 420x820 静态高保真原型 8 屏 (打卡/日历/清单/新建弹窗/
  完成特效/明天计划视图/清单完成/空态) + 设计规范、组件状态表、动效与适配约定
2026-10-22 14:12:24 +08:00

19 KiB
Raw Blame History

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

项 设计
实现 双 <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 关键流程:打卡

用户点环
  │
  ├─ 前端: 本地 +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. 数据导出:清单完成后导出为图片或文本(便于发群里确认)。