界面设计.html - 新增 2.12「V0.2.1 界面调整」章节: 头部统一 / 日历·打卡合并 / 模板分类 / 宏观 tab 溢出 - 已实现部分 (①③④) 与待确认草案 (②) 分别标注 - 2.9 宏观截图同步新 tab 顺序与右侧透支提示; 2.3 / 2.7 清单补「模板」分段 - 全部原型头部改用 23px 紧凑头部, 「我」页去掉头部条 设计文档.md - 新增 §11 界面调整: 变更清单、头部统一对照表、模板接口与排序口径、宏观 tab 顺序与溢出实现、 待确认的日历+打卡合并方案 (含确认后需改动的文件与样式段)
33 KiB
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-mobileEmpty+ 引导按钮(三态齐全,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` |
| 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 关键流程:月历聚合
- 取全部 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 跳过未结束周期) | 全通过 |
| 单元 | 版本比较 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+)
- 提醒与通知:
Habit.remindAt+ Tauri 本地通知,解决「忘打卡」。 - 补打放宽 / 历史编辑:当前 7 天限制可配置化。
- 统计视图:连续天数榜单、月/年热力图(届时引入
HabitDailyStat汇总表)。 - 打卡项分组与排序拖拽:现有
sortOrder已预留,仅需交互。 - 清单协作/分享:引入
CheckListShare。 - 数据导出:清单完成后导出为图片或文本(便于发群里确认)。
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,纯函数可单测):
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-mobilePopup 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-* 段。