# iBoard V0.2 设计文档 — 打卡 / 日历 / 清单
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
| 文档状态 | 已实现(与代码一致;含 2026-10-22 设计变更,见 §10) |
| 上游文档 | `需求文档.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 之上**新增「打卡」模块并升为默认首页**(底部 Tab 由 5 个精简为 4 个:打卡 / 宏观 / 监控 / 我),页面内以三个子视图承载三类需求:
| 子视图 | 解决问题 | 核心隐喻 |
| --- | --- | --- |
| **打卡** | 周期性习惯的量化坚持 | 环形进度,点环即打卡 |
| **日历** | 回看昨天、执行今天、预知明天 | 月历网格 + 日状态点 |
| **清单** | 出差/回老家等一次性场景的漏项检查 | 勾选清单 + 进度条 |
> **Tab 结构(V0.2 终态)**
>
> | 顺序 | Tab | 路由 | 说明 |
> | --- | --- | --- | --- |
> | 1 | **打卡(默认)** | `/checkin` | 根路径 `/` 重定向到此;`/checkin` 内含 打卡/日历/清单 三视图 |
> | 2 | 宏观 | `/macro/*` | 油价 / 经济数据 / 策略研究 / 经济文章 / **报警** / **新闻**(后两者由原「首页」并入) |
> | 3 | 监控 | `/monitor` | 服务器 + 米家环境 |
> | 4 | 我 | `/me` | 用户信息 + 前端 OTA 升级 |
>
> 原「首页」Tab 已移除:其中的**报警**、**新闻** 两个模块并入宏观,作为其第 5、6 个子 Tab(`/macro/alerts`、`/macro/news`),新闻详情改为 `/macro/news/:id` 以保持宏观 Tab 高亮。
**不纳入 V0.2**:多用户共享清单、提醒推送、数据导出、打卡项分组、年视图热力图(见 §9 演进)。
### 1.2 架构总览
```
┌──────────────────────── 前端 (React + antd-mobile, 打包进 Tauri / OTA) ────────────────────────┐
│ App.jsx 路由 (默认 / → /checkin) │
│ └ /checkin ──► CheckinPage ── SwipeTabs ──┬── HabitView (打卡) │
│ ├── CalendarView (日历) │
│ └── ListView (清单) │
│ └ /macro/* ──► MacroPage ── 油价/经济数据/策略研究/经济文章/报警/新闻 │
│ 组件: 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
api/ota.js # OTA 封装 + 判新模式(version/sequence)
utils/version.js # 版本号字段比较(纯函数, 可单测)
utils/format.js # 日期格式化
components/RingProgress.jsx # 环形进度 + Celebrate 特效
components/BottomNav.jsx # Tab: 打卡(默认)/宏观/监控/我
components/TabIcon.jsx # 图标集(移除 home)
pages/checkin/CheckinPage.jsx # 容器:三视图切换
pages/checkin/HabitView.jsx # 打卡视图
pages/checkin/CalendarView.jsx # 日历视图
pages/checkin/ListView.jsx # 清单视图
pages/checkin/date.js # 前端日期工具
pages/MacroPage.jsx # 宏观: 6 个子 Tab(含报警/新闻)
pages/macro/AlertsView.jsx # 报警(原首页)
pages/macro/NewsView.jsx # 新闻(原首页)
pages/MePage.jsx # 我: OTA 组内「开发版本」开关
App.jsx # / → /checkin; /macro/*; 移除 /news/:id
styles/globals.css # 打卡相关样式段
scripts/ci/check-version.mjs # CI: 校验分支名 ↔ package.json 版本
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 路由与容器
```
底部 Tab (BottomNav) 打卡(默认) ─ 宏观 ─ 监控 ─ 我
/ →
/checkin → CheckinPage
├─ NavBar「打卡」
└─ SwipeTabs(左右滑动切换,与既有模块交互一致)
├─ habit → HabitView (受 date 影响)
├─ calendar → CalendarView (持有选中 date,可回写容器)
└─ list → ListView (独立)
/macro/* → MacroPage (油价 / 经济数据 / 策略研究 / 经济文章 / 报警 / 新闻)
```
`activeKeyFromPath()` 对未匹配路径兜底返回**第一个 Tab(`/checkin`)**,与默认路由保持一致。
`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 // 新建/编辑弹窗
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` 态半透明;**完成特效恒开**(V0.2 设计变更:取消页面内特效开关,无任何关闭入口) |
**完成特效 `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` 关闭(用户明确要求「特效永远打开」)。
### 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 跳过未结束周期) | 全通过 |
| 单元 | 版本比较 10 条断言(`-ota` 后缀、`v` 前缀、缺位补 0、`0.10 > 0.9`、`beta.3 < beta.10`、空串) | 全通过 |
| 接口冒烟 | 自签 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 可加汇总表 |
| 未来日期禁止打卡 | 防止「提前打完一周」刷进度,符合「日历看明天=计划」语义 | 明天视图只读展示「计划」,明确文案 |
| 特效恒开可能打扰 | 用户明确要求「特效永远打开」,不再提供开关 | 单次特效 ≤900ms 且 `pointer-events:none`,不影响连续打卡;若后续需要再评估恢复开关 |
| Tab 调整为 4 个(打卡置左并默认) | 首页被取消,报警/新闻并入宏观后宏观有 6 个子 Tab | 子 Tab 可横向滚动;报警角标保留在子 Tab 上;底部 4 项在 375px 下热区充足(≥44px) |
---
## 9. 后续演进(V0.3+)
1. **提醒与通知**:`Habit.remindAt` + Tauri 本地通知,解决「忘打卡」。
2. **补打放宽 / 历史编辑**:当前 7 天限制可配置化。
3. **统计视图**:连续天数榜单、月/年热力图(届时引入 `HabitDailyStat` 汇总表)。
4. **打卡项分组与排序拖拽**:现有 `sortOrder` 已预留,仅需交互。
5. **清单协作/分享**:引入 `CheckListShare`。
6. **数据导出**:清单完成后导出为图片或文本(便于发群里确认)。
---
## 10. 设计变更记录(2026-10-22)
### 10.1 变更清单
| # | 类型 | 变更 | 影响文件 |
| --- | --- | --- | --- |
| 1 | Bug | OTA 包版本与分支不对应:`dev-0.2` 上仍是 `0.1.0` → OTA 报成 `0.1.0-ota`。统一升到 `0.2.0`,OTA 包变为 **`0.2.0-ota`**,并加 CI 强校验 | `package.json`、`src-tauri/tauri.conf.json`、`src-tauri/Cargo.toml`、`Cargo.lock`、`scripts/ci/check-version.mjs`、`docker-build.yml` |
| 2 | 功能 | 我的页 OTA 组新增**「开发版本」Switch**(默认关),切换判新口径:开=`build sequence`,关=版本号字段比较;开关值持久化在设备端 | `src/api/ota.js`、`src/utils/version.js`、`src/pages/MePage.jsx` |
| 3 | 设计 | 打卡 Tab 移到**最左边并默认选中**;打卡页移除**特效开关**,特效永远打开 | `BottomNav.jsx`、`App.jsx`、`TabIcon.jsx`、`HabitView.jsx`、`RingProgress.jsx`、`globals.css` |
| 4 | 设计 | 取消「首页」Tab,其**报警 / 新闻**并入宏观,成为宏观的第 5、6 个子 Tab | `MacroPage.jsx`、新增 `macro/AlertsView.jsx`、`macro/NewsView.jsx`、删除 `HomePage.jsx` |
### 10.2 版本号为什么必须跟分支走
`scripts/build-ota.mjs` 里 `manifest.version = package.json.version + '-ota'`,也就是**版本号的唯一来源仍是 `package.json`**。
分支按版本命名(`dev-0.2`),但版本号是手工改的,一旦忘改就会出现「分支 0.2、包版本 0.1」的错位,客户端判新逻辑全部失准。
**同一版本号的两种产物写法**(2026-10-22 补充确认):
```
分支 dev-0.2 → 0.2.0
├─ APK / 桌面安装包 0.2.0 裸版本号,无后缀
├─ 前端运行时 0.2.0 裸版本号,无后缀
└─ OTA 热更包 0.2.0-ota 多一个 -ota 标记
```
即 APK 与 OTA 的版本主体完全一致,唯一差别是 OTA 多 `-ota` 后缀;`package.json` 只写裸版本号,后缀由打包脚本追加。
| 环节 | 取值 | 现状 |
| --- | --- | --- |
| `package.json` `version` | `0.2.0` | 唯一手动管理点,**不得带 `-ota`** |
| `src-tauri/tauri.conf.json` `version` | `0.2.0` | 决定 **APK `versionName`**(经 `tauri android init` 写入 `tauri.properties`)与桌面包版本 |
| `src-tauri/Cargo.toml` / `Cargo.lock` | `0.2.0` | Rust 包版本,与上两者保持一致 |
| OTA `latest.json.version` | `0.2.0-ota` | 打包脚本自动拼 `-ota` 后缀 |
| `src/version.js` `APP_VERSION` | `0.2.0` | 前端展示与版本比较的基准 |
| APK `versionCode` | `2000` | `major*1000000 + minor*1000 + patch`,必须单调递增才能覆盖安装(`0.1.0` = 1000) |
| OTA `latest.json.min_binary_version` | `0.1.0` | **与前端版本解耦**:前端 bundle 要求的最低原生二进制版本。现有 APK 原生层仍是 0.1.0,跟着抬会导致旧 APK 收不到更新,CI 固定传 `--min-binary 0.1.0` |
| CI 守卫 | `scripts/ci/check-version.mjs` | ① 四处版本必须完全相等且不含 `-ota`(`main`/`master` 也查);② 从 `GITHUB_REF_NAME` 解析 `dev-.`,要求 `version` 以该前缀开头;任一不满足**构建直接失败**。`docker-build.yml` 与 `android-build.yml` 都在构建前执行 |
### 10.3 判新口径:版本号 vs build sequence
「我的 → 前端升级 (OTA)」组内新增 `开发版本` 开关(antd-mobile `Switch`),默认**关闭**:
| 开关 | 判新依据 | 实现路径 | 适用场景 |
| --- | --- | --- | --- |
| **关(默认)** | **版本号字段大小** | `GET /api/ota/check/0` 取 `manifest.version` → `compareVersion(manifest.version, 当前运行版本) > 0` 才算新 | 生产口径。旧分支被重新构建(sequence 变大但版本更小)时不会把设备拉回旧版 |
| **开** | **build sequence** | `tauri-plugin-hotswap` 的 `checkUpdate()`(内部拿本地 sequence 请求 `/api/ota/check/:seq`,服务端只回比它大的) | 开发联调。每次构建 sequence 都变,保证任何一次提交都能推到设备上 |
补充约定:
- **持久化**:`localStorage['iboard_ota_dev_mode'] = '1' | '0'`(默认 `'0'`)。值只存在设备端 WebView 存储里,不进数据库、不上报,换设备/清数据即回默认。
- **当前运行版本**:已应用过 OTA 覆盖包时取覆盖包 `version`(插件 `getVersionInfo()`),否则取内置包 `APP_VERSION`;避免「内置包 0.1.0 + 覆盖包 0.2.0」时永远判定有新版本。
- **切换开关会重置检查结果**(`otaState='idle'`、清空 `otaInfo`),避免上一种口径的结论误导用户。
- **Web 端**:浏览器里拿不到本地 sequence。默认口径按版本号比较;开发口径退化为「服务端版本 ≠ 本地版本即算新」(近似 sequence 语义)。Web 端只提示刷新,不下载不替换,与原设计一致。
**版本比较算法**(`src/utils/version.js`,纯函数可单测):
```js
parseVersionFields('0.2.0-ota') // → [0, 2, 0]
compareVersion('0.2.0-ota','0.1.0') // → 1 (0.2.0 更新)
compareVersion('0.2','0.2.0') // → 0 (缺位补 0)
```
规则:按非数字字符切段 → 过滤空段 → 转数字 → 逐段比较,位数不足补 0。单测 10 条覆盖 `-ota` 后缀、`v` 前缀、多位段(`0.10 > 0.9`)、空串、`beta.3 < beta.10`,全部通过。
### 10.4 交互与信息架构变更
| 项 | 变更前 | 变更后 |
| --- | --- | --- |
| 底部 Tab | 首页 / 宏观 / 监控 / 打卡 / 我(5 个) | **打卡 / 宏观 / 监控 / 我(4 个)**,打卡在最左 |
| 默认落地 | `/` → 首页(默认选中新闻) | `/` → **`/checkin`**;`RedirectIfAuthed` 与 404 兜底同样指向 `/checkin` |
| 报警 / 新闻 | 首页内的 2 个 Tab | 宏观的第 5、6 个子 Tab(`/macro/alerts`、`/macro/news`),报警角标保留 |
| 新闻详情 | `/news/:id`(底部无 Tab 高亮) | **`/macro/news/:id`**(宏观 Tab 保持高亮) |
| 打卡页头部 | `[特效开/关]` + `[+ 新建]` | 只保留 `[+ 新建]`;特效恒开 |
| 特效实现 | `effectsEnabled()` 读 `localStorage.iboard_celebrate` + `prefers-reduced-motion` 降级 | 删除开关与降级判断,`Celebrate` 只要 `fire` 变化就播放 |
### 10.5 界面设计同步
`界面设计.html` 已同步:底部 Tab 改为 4 项且「打卡」高亮在首位、打卡页去掉特效开关按钮、新增「我的 · OTA 开发版本开关」屏、宏观新增「报警 / 新闻」子 Tab 与合并后的 6 Tab 导航。
---
## 11. 界面调整(2026-10-23)
### 11.1 变更清单
| # | 范围 | 变更 | 状态 |
| --- | --- | --- | --- |
| 1 | 页面头部 | 打卡 NavBar 高度减半(45 → 23px)、标题改主色 12px;宏观 / 监控补标题;「我」页整条去掉 | **已实现** |
| 2 | 打卡页 | 日历与打卡合并为一个视图(上日历、下打卡,按天/周/月分组、去掉年分组、卡片紧凑化) | **待确认**(设计稿已出,见《界面设计.html》2.12 ②节) |
| 3 | 清单页 | 新增「模板」分类(与进行中 / 已归档并列),独立 CRUD + 字母序;新建清单去掉场景、模板改右侧滑出选择 | **已实现** |
| 4 | 宏观页 | 报警 / 新闻移到最左并默认选中新闻;tab 条增加右侧溢出提示 | **已实现** |
### 11.2 页面头部统一
新增组件 `src/components/PageHeader.jsx`(高度 23px、标题 12px/600 主色、字距 1px、背景取页面底色 `#f5f5f5`),只被 **打卡 / 宏观 / 监控** 三页引用:
| 页 | 变更前 | 变更后 |
| --- | --- | --- |
| 打卡 | `NavBar` 45px,标题 17px 深色 | `PageHeader` 23px,标题主色 |
| 宏观 | `NavBar` 45px,空白 | `PageHeader` + 标题「宏观」 |
| 监控 | `NavBar` 45px,空白 | `PageHeader` + 标题「监控」 |
| 我 | `NavBar` 45px 空白条 | **移除**,`me-profile` 直接贴顶 |
> 「我」页不引用该组件,因此调整打卡/宏观/监控的头部不会波及「我」;反之亦然。三个页面之间也各自持有独立实例,互不影响。
### 11.3 清单模板分类
**接口**(`server/routes/checklists.js`):
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/checklists/templates` | 列表,**服务端按名称字母顺序排序**(原为 `builtin desc, id asc`) |
| GET | `/api/checklists/templates/:id` | 详情(新增) |
| POST | `/api/checklists/templates` | 新建(名称 ≤20 字,≥1 条检查项) |
| PUT | `/api/checklists/templates/:id` | 改名称 / 检查项(新增;`builtin=true` 拒绝,404) |
| DELETE | `/api/checklists/templates/:id` | 删自建模板(`builtin=true` 拒绝) |
排序口径:`Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })` —— 中文按拼音、数字按数值。前端 `ListView` 拿到列表后再排一次,保证与后端一致(网络返回顺序不可控时也不乱)。
**前端**(`src/pages/checkin/ListView.jsx`):
- 分段由 2 项扩为 3 项:`active`(进行中)/ `archived`(已归档)/ `template`(模板);右上角按钮随分段切换为「+ 新建」/「+ 新建模板」。
- 模板卡片:名称 + 「预置」标记 + 项数;最多展示 4 条检查项摘要,超出折叠为 `+n`;操作「由此新建 / 编辑 / 删除」,预置模板只保留「由此新建」。
- 新建 / 编辑模板共用底部弹层:名称 + 检查项逐条编辑(≤40 字,可删可加)。
- **新建清单表单**:仅「名称」+「模板」两项 —— **场景选择器已移除**(`scene` 改为沿用所选模板自带的场景,无模板则为空;数据库字段保留,历史数据照常展示)。
- **模板选择**:点击模板行从**右侧滑出** `78vw` 面板(antd-mobile `Popup position="right"`),首项固定「不使用模板」,其后为字母序模板列表(预置带标记 + 项数),选中项主色高亮。
### 11.4 宏观 tab 顺序与溢出提示
- Tab 顺序改为 **报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章**,默认选中 **新闻**;`/macro` 由路由直接 `Navigate` 到 `/macro/news`(`App.jsx`),`activeFromPath` 兜底同样返回 `news`。
- `SwipeTabs` 增加溢出提示层:测量 `.adm-tabs-tab-list` 的 `scrollLeft + clientWidth < scrollWidth`,为真时在顶部右侧渲染 `.tab-more-hint`(白色渐变 + 主色圆底箭头「›」,1.5s 轻微摆动动画,点击向右翻页 60% 视宽),滚到末尾自动消失。
- 切换 tab 时把激活项平滑滚到可视区中央,避免选中项藏在屏幕外。
- 为什么不用框架自带的:antd-mobile Tabs 自带 `.adm-tabs-header-mask-right` 只有 30px 纯渐变、无箭头、无交互,用户难以判断"右侧还有内容",故叠加此提示层。
### 11.5 待确认项:日历与打卡合并
方案已写入《界面设计.html》2.12 ②节(含主视图、未来日期计划态、卡片「⋯」操作面板三屏)。要点:
| 项 | 方案 |
| --- | --- |
| 结构 | SwipeTabs 由 3 个减为 2 个:`日历(含打卡)` / `清单` |
| 日历 | 月历 + 月份切换 + 回到今天;**去掉「昨天/今天/明天」分段**;日期格 38 → 31px |
| 打卡 | 沿用现有卡片样式,紧凑化:内边距 12→8px、环形 58→42px(环宽 6→5)、间距 10→7px |
| 操作 | 撤销 / 编辑 / 归档收进「⋯」底部面板(原为卡片底部三个文字链接) |
| 分组 | **每天 → 每周 → 每月**,组标题左侧主色竖条;**去掉「每年」分组** |
| 联动 | 点日期即切换当日数据;未来日期只读,按钮灰显「计划」 |
确认后再落地:`CheckinPage` 收敛为 2 个 tab、`CalendarView` 与 `HabitView` 合成新视图组件、`HabitView` 的循环/编辑逻辑抽出复用、样式新增 `.mv-*` 段。