饮食页此前是个占位壳:一段说明文 + 当前日期 + 一句"页面状态会按账号保留"。需求要把它变成真页面:按日期回看、按餐次分组、能量区间与缺口一目了然。

本轮最值得记的不是页面本身,而是两个判断:这个需求其实不需要动云端,以及"能回看"不等于"能补录"。

判断一:先看云端能不能用

按惯例,"做新页面 → 加一个云函数 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 丢弃过期响应;
  • 空态要设计:空组留白 + 缺口短标签,比隐藏信息量大。