fix(今日脚印): 读不到不编数据 —— 四种显隐模式收口到 fpRender(唯一 owner)(v26092214)

产品口径(2026-09-22):
  · **无档案**(上游查无此人 / 本地未关联)→ 显示引导占位(#tlGuide:「还没有你的脚印 /
    去「数胎动」记一笔,这里就会长出你今天的脚印」)
  · **有档案** → 必须从接口读;**读不到就不显示**(整块 .today-line 收起),不再回退演示行

改动:
- index.html:删掉 4 条**写死的演示行**(胎动 12 次 / 体重 58.2kg / 产检提醒 / 睡前散步 20 分钟)——
  它们只在「接口读不到」时露出来,等于上游一抖页面就开始编数据。换成一行中性「正在获取今天的脚印…」
  +新增 #tlGuide 引导块,并在注释里写明四种模式与判定。
- 新增 fpApply()/fpRender():模块显隐**只有一个 owner**(pending / rows / empty / guide / hide,
  外加「未建档 / 产后」硬门槛优先于接口结论)。
  ⚠️ 之前 syncPregnancy 也直接改 .today-line 的 display,两个 owner 撞出竞态 —— 实测「有档案 +
  上游故障(degraded)」会被 profile 回来后重新显示出来(该收起的没收起)。现在 syncPregnancy 两处
  都只调 fpRender()。
- 没有真实事件时给 .today-line 挂 .no-ai,AI 气泡一并收起:那句「今天你数到我动了 3 次」是写死的
  演示文案,今天没记 / 未关联 / 读不到时显示它就是在说假话。拿到真实事件时才显示气泡
  (把那个数字接到真实次数属另一件事,等产品定文案)。

本地验证(桩后端,五种模式各跑一遍,并把 profile 延迟 1.5s 以复现前面那个竞态):
  rows → 模块显示 + 真实事件 + 气泡显示;empty → 「还没有足迹」+ 无气泡;
  guide → 引导块显示 + 时间轴收起 + 无气泡;degraded → **整块不显示**;
  fail(接口 500) → **整块不显示**;未建档 → 整块不显示。全程无 JS 异常。
This commit is contained in:
2026-09-22 23:48:44 +08:00
parent 67b10aac00
commit dedce247e9
3 changed files with 88 additions and 24 deletions

View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26092213';
var VER = '26092214';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -1115,9 +1115,9 @@
孕周数字换成引导文案,避免新用户看到"孕 27 周"这类假数据。 */
if (state.pregEmpty) {
const nm = state.babyName || '宝宝';
/* 脚印时间轴是纯演示内容(无真实数据源),访客/未完善态整块隐藏 */
const _tl = document.querySelector('.today-line');
if (_tl) _tl.style.display = 'none';
/* 脚印时间轴:未建档整块隐藏 —— 交给 fpRender() 统一判(它同时看 footprint 的结论),
别再直接改 display:与本模块的另一个 owner 撞竞态(2026-09-22 实测漏过 degraded 场景)。 */
fpRender();
/* ① 我的 · 孕期卡 → 引导 */
$('#pcTitle').textContent = '待完善孕期信息';
$('#pcSub').textContent = '填好预产期,开启孕期陪伴';
@@ -1173,8 +1173,8 @@
setGrowthDataEmpty(false); /* 有真实孕周了:恢复面板(数据源接入前仍是演示值,见函数注释) */
/* 刚建档/填了孕产期 → 按新状态重算来信提示:此前因未建档被压住的未读,这时该出现 */
renderLetterReminder();
const _tl2 = document.querySelector('.today-line');
if (_tl2) _tl2.style.display = '';
/* 脚印时间轴:建档状态变了要重算 —— 同样只经 fpRender(footprint 还没回来时它保持「加载中」) */
fpRender();
const days = week * 7 + day; // 一起生活了多少天
const weekTxt = '孕 ' + week + ' 周' + (day ? ' +' + day + ' 天' : '');
/* 剩余天数按 40 周足月倒推,比直接用 date 差值更贴合「孕周」这个语境 */
@@ -3092,9 +3092,53 @@
⚠️ 竞态:本函数与 applyRemoteProfile 并行。profile 回来若判 pregEmpty 会
隐藏时间轴,所以这里落笔前再看一眼 state.pregEmpty,别把刚藏好的放出来。 */
var _footprintApplied = false;
/* 今日脚印的显示规则(2026-09-22 产品口径)—— **四种模式,全部由 fpApply 一处决定**:
rows 有档案 + 拿到事件 → 渲染真实事件
empty 有档案 + 今天没记 → 「还没有足迹」
guide **无档案**(上游查无此人 / 本地未关联)→ 引导占位 #tlGuide,收起时间轴
hide 有档案但**接口读不到**(网络 / 非 200 / 上游故障且无旧值)→ **整块不显示**
⚠️ 原来「读不到」是回退到 4 条**写死的演示行**(胎动 12 次 / 体重 58.2kg…)——
上游一抖页面就开始编数据。那 4 行已从 index.html 删掉,换成一行为中性「正在获取…」。
⚠️ 除 rows 外一律给 .today-line 挂 .no-ai:AI 气泡里的次数是写死的演示文案,
没有真实事件时显示它等于说假话(见 index.html 里那段注释与待办)。 */
/* 当前模式 + 事件缓存在这里 —— fpRender() 是**模块显隐的唯一 owner**。
⚠️ 之前 syncPregnancy 也直接改 .today-line 的 display,两个 owner 会撞竞态:
接口先判「不显示」(hide),profile 后回来又把模块显示回来 → degraded 场景就是这么漏出来的。 */
let _fpMode = 'pending'; // pending(加载中) | rows | empty | guide | hide
let _fpEvents = [];
function fpApply(mode, events) { _fpMode = mode; _fpEvents = events || []; fpRender(); }
function fpRender() {
const tl = document.querySelector('.today-line'), steps = $('#tlSteps'), guide = $('#tlGuide');
if (!tl || !steps) return;
/* 未建档 / 产后:整块不给(不看接口怎么回)——这是硬门槛,优先于接口结论 */
const mode = (state.pregEmpty || state.postpartum) ? 'hide' : _fpMode;
if (mode === 'hide') { tl.style.display = 'none'; return; }
tl.style.display = '';
/* 只有拿到真实事件才显示 AI 气泡(见 index.html 那段注释与待办) */
tl.classList.toggle('no-ai', mode !== 'rows');
if (guide) guide.style.display = (mode === 'guide') ? '' : 'none';
if (mode === 'guide') { steps.style.display = 'none'; steps.innerHTML = ''; return; }
steps.style.display = '';
steps.innerHTML = '';
if (mode === 'empty') { steps.append(emptyFootprintStep()); return; }
if (mode === 'pending') { steps.append(pendingFootprintStep()); return; }
_fpEvents.forEach(e => steps.append(buildFootprintStep(e)));
}
/* 加载中的中性占位(不是假数据):与 index.html 里那条首屏占位同构 */
function pendingFootprintStep() {
const d = document.createElement('div'); d.className = 'step';
const time = document.createElement('span'); time.className = 'st-time'; time.textContent = '今天';
const dot = document.createElement('span'); dot.className = 'st-dot';
const txt = document.createElement('span'); txt.className = 'st-txt'; txt.textContent = '正在获取今天的脚印…';
const from = document.createElement('span'); from.className = 'st-from';
d.append(time, dot, txt, from);
return d;
}
function applyRemoteFootprint() {
if (_footprintApplied) return; // init 只跑一次,防重复 fetch
_footprintApplied = true;
fpRender(); // 首屏先出「加载中」中性占位(不是假数据)
const openid = hashParam('openid');
if (!openid) return; // 访客态: pregEmpty 分支已隐藏,保持
const qs = ['openid=' + encodeURIComponent(openid)];
@@ -3103,19 +3147,16 @@
fetch('/api/mp/public/mama-box/footprint/today?' + qs.join('&'))
.then(r => (r.ok ? r.json() : null))
.then(j => {
if (!j || j.code !== 200 || !j.result) return; // 拉不到 → 回退演示行
const r = j.result;
const tl = document.querySelector('.today-line');
const steps = $('#tlSteps');
if (!tl || !steps) return;
if (!r.linked) { tl.style.display = 'none'; return; }
if (state.pregEmpty) return; // 别把 profile 刚藏好的放出来
const events = (r.events || []).slice(-8); // 升序取最近 8 条
steps.innerHTML = '';
if (!events.length) { steps.append(emptyFootprintStep()); return; }
events.forEach(e => steps.append(buildFootprintStep(e)));
const r = (j && j.code === 200 && j.result) ? j.result : null;
if (!r) { fpApply('hide'); return; } // 读不到 → 不显示(不再编数据)
if (state.pregEmpty) { fpApply('hide'); return; } // 未建档:本模块不给(原有口径)
if (!r.linked) { fpApply('guide'); return; } // 无档案 → 引导占位
if (r.degraded) { fpApply('hide'); return; } // 有档案、上游故障且无旧值 → 不显示
const events = (r.events || []).slice(-8); // 升序取最近 8 条
if (!events.length) { fpApply('empty'); return; } // 有档案、今天没记 → 「还没有足迹」
fpApply('rows', events); // 有档案 + 有事件(含 stale 旧值)→ 真数据
})
.catch(function () { /* 静默回退演示行 */ });
.catch(function () { fpApply('hide'); }); // 读不到 → 不显示
}
/* 一行脚印:与 index.html 演示行同构(时间/圆点/文案/来源)。