饮食页此前是个占位壳:一段说明文 + 当前日期 + 一句"页面状态会按账号保留"。需求要把它变成真页面:按日期回看、按餐次分组、能量区间与缺口一目了然。
本轮最值得记的不是页面本身,而是两个判断:这个需求其实不需要动云端,以及"能回看"不等于"能补录"。
判断一:先看云端能不能用
按惯例,"做新页面 → 加一个云函数 action",本轮会平白多出一个 diet 云函数和一堆重复查询逻辑。实际检查后发现:
- 云端记录函数的日摘要接口本来就接受任意
recordDate,返回按组分类的记录 + 缺口信息; - 仓储层已能按日期翻页取全(回归里跑过 205 条)。
接口早就支持按日期查,只是页面没用。于是本轮零云端变更:不新增云函数、集合与字段,只把既有接口按 recordDate 用起来。代价是需要理解既有返回结构,收益是省掉一整套需要部署与联调的重复链路。
顺手把这个判断写成自检断言:饮食页确实只调日摘要接口,且部署清单里没有新增 diet 函数——防止后人"顺手加一个"。
判断二:"能回看"不等于"能补录"
"回看历史不代表任意日期可新增"很容易被实现成"既然能翻到 8 月 1 日,那就在 8 月 1 日也能加一条"。但补录窗口(当天与前一天)是已确认口径,回看是只读能力,两者不该互相放大。
实现上把窗口判定做成纯域函数,守住两处:
isWithinBackfillWindow(selectedDate, todayDate) // 只允许当天与前一天
- 窗口外:不渲染"新增记录"按钮,改显示"只读回看";事件处理里再留一道早退守卫(双保险,防 UI 漏判);
- 向未来翻页直接拦截。
自检特意覆盖跨月场景(8 月 31 日相对 9 月 1 日是前一天,仍可写)。
判断三:未知不等于零
把未知条目按 0 计入,日汇总会显得又小又精确——正好是最误导人的形态。所以能量汇总的语义是一张状态表:
| 情况 | min/max | 标记 |
|---|---|---|
| 全部已知 | 已知区间合计 | 完整 |
| 部分未知 | 只累加已知 | "汇总不完整" + 未知条数 |
| 全部未知 | null | "汇总不完整",文案留空(不是 0 kcal) |
| 无条目 | null | 不标记完整也不显示能量 |
页面上未知条目自己的能量格也是空的——0 kcal 和"没估算"在视觉上必须能区分。
两个工程细节
快速翻页要丢弃过期响应。 日期翻页是并发请求,连点三次「前一天」,返回顺序可能乱。用请求 token 比对后再落库:
const token = recordDate;
const result = await fetchDaySummary({ recordDate });
if (this.data.selectedDate !== token) return; // 过期响应直接丢弃
空组比隐藏更能表达缺口。 第一版只渲染有记录的分组,页面更"干净",但用户看不出"今天少记了一餐,还是压根没吃"。四个餐次始终成组呈现(空组留白),再配"缺早餐"这类短标签,信息量才完整。
一个踩坑:相对路径多一层,越出 miniprogramRoot
新写的 pages/diet/view-model.ts 按"训练复盘页"的写法抄了 ../../../shared/...。但复盘页在 pages/training/review/(深一层),饮食页在 pages/diet/——三个 .. 指到了仓库根目录。
后果不是编译报错,而是 shared-runtime 检查直接抛断言:import 越出 miniprogramRoot。跨页复制 import 时,先数层级。
刻意不做的事
规则要求"未确认草稿独立呈现",但草稿是今日页的本地会话状态,没有跨页只读接口。在饮食页自己再存一份草稿,会立刻制造两处不同步的草稿——用户在哪一页看到"待确认"都不可信。所以本轮不复制:饮食页只呈现已确认记录,草稿区写进 spec 的延期项与解锁条件。
"显式延期"和"默默不做"的区别,在于延期项有名字、有条件、有解锁路径。
小结
- 做新页面前先读一遍云端接口,"加一个 action"不是默认选项;
- 只读能力和写入权限是两件事,各用各的守卫;
- 汇总必须能表达"不完整",未知按 0 算是最误导人的形态;
- 并发请求用 token 丢弃过期响应;
- 空态要设计:空组留白 + 缺口短标签,比隐藏信息量大。