单条烹饪会话的详情视图:顶部统计、温度曲线、关键事件时间线、可编辑笔记、删除入口。代码也处理 CookSessionStatus.active 且 endTime == null 的会话。
共享图表轴/图例组件:lib/features/thermometer/presentation/widgets/cook_graph_axis.dart(提供 drawCookGraphYNodes、drawCookGraphTimeMarker、LegendChip,供本页与 cooking 页共用,保证 Y 轴节点、阈值虚线、竖向时间 marker 和图例样式一致)
主文件:lib/features/thermometer/presentation/pages/cook_log_detail_page.dart(791 行)
Provider:cookLogProvider(写入笔记 + 删除)、temperatureUnitProvider
入参:CookingSession(构造函数)
app.dart 的 routes map);通过 navigateInstant(context, CookLogDetailPage(session: session)) 直接 push补充:温度曲线图不是只画一条主色折线。_CookLogGraphPainter 会先按折线路径生成一个封闭区域,再在曲线下方绘制从 CulinaColors.primary 30% 透明度到底部 0% 透明度的纵向渐变填充,因此图表视觉上是“折线 + 半透明面积图”的组合。
顶部状态 banner 的图标和颜色会按 session.status 切换:completed 为绿色 Icons.check_circle_rounded,cancelled 为红色 Icons.cancel_rounded,disconnected 为黄色 Icons.warning_amber_rounded,active 为主色 Icons.local_fire_department_rounded。
补充:温度图的 Y 轴不是按这条会话的最高/最低温自动缩放,而是与 cooking 页共用固定量程 0–120 °C(内部仍以 °F 存储并换算显示)。轴上固定画出 0 °C / 120 °C 边界线,另叠加 target 绿虚线,以及 early-warning 开启时的 amber 虚线。
补充:温度图区域只有在 session.history.isNotEmpty 时才会渲染;如果这条会话还没有任何历史点,详情页不会显示温度图标题、图例或图表画布。另外,图例实际只渲染 Sensor、Target,以及 early-warning 开启时的 Early Warning 三项;即使图表已绘制蓝色 session-complete 竖向 marker,也没有对应的蓝色 legend 项。
补充:central recorder 在首个真实样本到达且其时间晚于 session.startTime 超过 1 秒时,会先补一条 timestamp == session.startTime 的 seed 点(温度值沿用首个真实样本),避免图表因为 X 轴从 cook start 开始而在左侧留出一段空白。
补充:顶部信息区实际是 4 组卡片,不仅有目标/峰值/时长,还显示 Meat、Device、Probe、Started、Ended。Cook Time 卡片的值来自 _formatDuration(session.elapsed),只显示小时和分钟:有小时则格式为 Xh Ym,不足 1 小时则为 Xm;因此少于 1 分钟的会话会显示 0m,不会显示秒。其中 Meat 行里的熟度文案不是直接读取会话里已有值,而是根据 session.displayMeatName、session.targetTemp 和 session.deviceId.boosterFamily 重新计算后再本地化显示。此外,Device 卡片在 session.deviceId.isEmpty 时显示 l.cookLogDetailDeviceUnknown;Peak 在 peakTempF == null 时显示 —;Ended 在 endTime == null 时也显示 —。
顶部返回箭头 + 居中标题 l.cookLogDetailTitle 对应的本地化标题。
补充:事件时间线本身也有固定的视觉编码:Started 使用绿色 Icons.play_arrow_rounded,Early Warning 使用橙色 Icons.notifications_active_rounded,Target Reached 使用主色 Icons.flag_rounded,结束节点使用灰色 Icons.stop_circle_rounded。每个节点左侧都是 28×28 的圆形淡色底图标,右侧时间文本颜色固定为 CulinaColors.textSecondary。另外,Early Warning 这条事件文案里插入的不是绝对阈值温度,而是当前 level 对应的提前量:摄氏显示 5°C / 10°C,华氏显示 9°F / 18°F。
补充:本页日期/时间显示没有走本地化日期格式。顶部状态 banner 的日期由 _formatDate 用硬编码英文月份缩写输出,格式固定为 Jan 2, 2026 这一类;Started / Ended 卡片以及事件时间线时刻由 _formatTime 输出为 12 小时制 h:mm AM/PM。下方滚动:开始日期 + 状态卡片(具体时刻在下方 Started 卡片)、4 行 2 列信息卡片(Meat/Device、Probe/Cook Time、Target/Peak、Started/Ended)、温度曲线图(自绘 _CookLogGraphPainter,渲染 session.history 内 TemperatureReading 列表)、事件时间线(开始、early-warning 触达、目标到达,以及 endTime != null 时的结束状态);它不是通用报警历史列表。、可编辑 3 行笔记输入框(带 hint),底部一个红色 删除按钮使用 l.cookLogDetailDeleteEntry 对应的本地化文案 按钮。
session.history 录入路径(b58946 / TAPD #1003099):由独立 cookSessionRecorderProvider(device_providers.dart 内,app.dart watch 保活)从 connectedBoostersProvider 监听遥测、对每个 active session 调 CookingSessionsNotifier.recordSample(...) 落盘,page-independent——用户没打开过的探针(CM2 White 等)也会被录到完整曲线,闭合「CM2 Black/CM1 只录到部分」、「White 全空」三类回归。Hybrid throttle:≤1 sample / 5 s、≥0.5 °F 或 30 s heartbeat、cap 500 点、超出按 2× decimation 保全 span(不掐头);ambient temp 按当前探针值写入;但当 p.ambientTemp == null 时会回退写入 0.0,并非无条件都是真实环境温度(之前 cooking-page-coupled addReading hardcoded 0)。详见 烹饪 §状态变化时如何更新。
X 轴 / 时间标签(b58946 / 问题 2):X 轴端点钉到 (session.startTime, session.endTime ?? history.last.timestamp)(active session fallback last sample),自适应 2–5 等距 tick(f332af7 / TAPD #1003179——固定 5 tick 在长 cook 下末两个 label 会撞在一起,如 298m27s 比 0s 宽很多;现按 TextPainter.layout 量宽、从 (durSec.floor()+1).clamp(2,5) 起递减挑能保证相邻 label 之间 ≥10 px 间距的最大 n,过不去就回退到端点-only 2 tick;首尾「0s」与累计耗时 label 始终保留,只丢中间 tick)、最后一个 tick 在 endTime != null 时对齐 cook-end 时间;若会话仍为 active 且 endTime == null,则对齐 history.last.timestamp。label 为累计耗时:不足 1 分钟时显示 Ss,1 分钟及以上时显示 NmSSs(中间无空格)。;之前用 intervals 数组 + 间距挑选算法的实现已撤回。同算法已镜像到 cooking 页实时 _TempGraphPainter(详见 §烹饪 Action #4 温度图)。
早期警告 marker(b58946 / 问题 3):遵循 session.earlyWarnLevel——0 不渲染 marker,1 走 AlarmThresholds.earlyWarn2DeltaF / earlyWarn2DeltaC(=5 °C / 9 °F),2 走 earlyWarn1DeltaF / earlyWarn1DeltaC(=10 °C / 18 °F);命名 inversely(level 1 → earlyWarn2…)来自 alarm_service 的历史延续,详见 告警与通知 §报警类型与优先级。之前 hardcoded 9 °F、与 alarm evaluator 实际门槛脱钩,且部分机型曲线不全时根本不画 marker——现 history 完整且按 level 渲染,所有机型一致。⚠️ 15e7e6d / TAPD #1003117 加竖向时间节点 marker:除了原有的横向 Y 轴参考线(28c930e 起由共享 drawCookGraphYNodes 渲染——amber 早期警告 + green 目标 dashed),now 在曲线首次穿越每个阈值的位置再多丢一条竖向虚线 + 实心点,由共享 helper drawCookGraphTimeMarker(cook_graph_axis.dart)绘制:early-warn 节点 amber(earlyWarnLevel == 0 时整条不画)、target 节点 green(无 earlyWarnLevel gate,但同样仅在曲线首次穿越 target 时画——从未达到目标则不画)。竖线与横线在曲线穿越点 正好 相交,时间轴 + 温度轴双向读数对齐。同 helper 同色方案也用于 cooking 页实时 _TempGraphPainter(详见 §烹饪 Action #4 温度图)。⚠️ 62621923 / TAPD #1003117 Layer B 起接进第 3 个 session-complete marker(蓝):钉在 endTime 处(曲线末端),由同一 drawCookGraphTimeMarker helper 绘制;Per Liang 2026-06-02 cook 达到目标后继续录制到 4-条件 session-complete 状态机收尾(详见 状态模型 §烹饪会话),所以 session-complete 节点与 green target 节点不再重合,三色节点(amber 早警 / green target / blue complete)一一独立。
补充:事件时间线里的“目标到达”节点不是简单遍历 session.history 查首个 >= target 的样本,而是调用 session.resolveTargetReached()。该 helper 会先扫描 history;只有当 status == CookSessionStatus.completed && endTime != null 时,才回退用 endTime 与 peakTempF/targetTemp 推出节点。它这里并不会读取 targetReachedTime,因此 cancelled / disconnected 且历史里没有跨线样本的会话,仍不会补出“目标到达”节点。
补充:与 target 节点不同,early-warning 节点仍然是顺序遍历 session.history,找到首个 internalTemp >= session.targetTemp - earlyWarnDeltaF 的样本后直接取其 timestamp 渲染;代码没有为 early-warning 走类似 resolveTargetReached() 的模型辅助。
IconButton.onPressed → Navigator.pop(context) → 回到烹饪日志列表补充:在非 mock 且启用真实账号同步时,笔记编辑不只是本地 SharedPreferences 持久化。cookLogProvider.updateNotes(...) 会把该条日志的 updatedAt 更新为当前时间、加入 dirty 集合,并通过 _scheduleSync() 以 2 秒 debounce 触发 cook log 云端同步。
补充:保存前会对输入执行 trim(),因此首尾空格会被去掉;如果输入只包含空白字符,最终持久化为空字符串。
TextField.onChanged: (_) => _saveNotes() —— 每次按键都触发持久化TextField.onChanged
→ _saveNotes()
→ cookLogProvider.notifier.updateNotes(session.id, _notesController.text.trim())
→ SharedPreferences 持久化
补充:在非 mock 且启用真实账号同步时,删除不只是本地移除。cookLogProvider.deleteSession(...) 会清掉该条记录的 dirty 标记;如果该日志 id 是 UUID,还会写入 tombstone,并通过 _scheduleSync() 以 2 秒 debounce 安排云端同步,避免已删记录被其他设备回拉复活。
OutlinedButton.onPressed: _confirmAndDeleteOutlinedButton.onPressed → _confirmAndDelete
→ showDialog<bool>(...)
→ 用户确认 → cookLogProvider.notifier.deleteSession(session.id) → SharedPreferences 持久化
→ Navigator.pop(context)
Navigator.pop(ctx, bool)watch:temperatureUnitProvider(°C/°F 切换重建以更新数字)。read:cookLogProvider.notifier(写笔记 + 删除)。
会话本身(widget.session)是构造函数注入的快照 —— 不订阅 cookLogProvider,因此其他地方修改这条会话不会自动反映在本页(笔记修改通过本地 _notesController + _saveNotes 闭环)。删除后立即 pop,不存在「页面看到自己被删除」的歧义。
无显著差异。
onChanged: (_) => _saveNotes() 每次输入都触发 cookLogProvider.notifier.updateNotes → 持久化。Debounce 到失焦或 500ms 后写更合理。widget.session 是 stale snapshot:构造函数注入后不订阅 Provider,若同一会话在另一处被改了笔记或状态,本页不会同步。当前没有这种竞争场景,但未来若有 cloud sync 会有歧义。