# 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-*` 段。