Files
iboard/docs/V0.2/设计文档.md
T
cheney 072f304cc2
Docker Build and Push / build-image (push) Successful in 4m14s
docs(V0.2): 界面设计同步 2026-10-23 调整 (含日历+打卡合并草案) + 设计文档 §11
界面设计.html
- 新增 2.12「V0.2.1 界面调整」章节: 头部统一 / 日历·打卡合并 / 模板分类 / 宏观 tab 溢出
- 已实现部分 (①③④) 与待确认草案 (②) 分别标注
- 2.9 宏观截图同步新 tab 顺序与右侧透支提示; 2.3 / 2.7 清单补「模板」分段
- 全部原型头部改用 23px 紧凑头部, 「我」页去掉头部条

设计文档.md
- 新增 §11 界面调整: 变更清单、头部统一对照表、模板接口与排序口径、宏观 tab 顺序与溢出实现、
  待确认的日历+打卡合并方案 (含确认后需改动的文件与样式段)
2026-10-23 16:41:04 +08:00

517 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) 打卡(默认) ─ 宏观 ─ 监控 ─ 我
/ → <Navigate to="/checkin" replace />
/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`
| 项 | 设计 |
| --- | --- |
| 实现 | 双 `<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` 态半透明;**完成特效恒开**(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-<major>.<minor>`,要求 `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-*` 段。