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

@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title>
<!-- V:26092213 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26092214 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
@@ -303,12 +303,23 @@
<!-- ⚠️ aria-label 里也有小名,所以它跟 #tlTitle 一起交给 syncBabyWords()
刷。标题跟着小名走、读屏却一直念旧名字,是最难被发现的那种不同步 ——
眼睛看不见它。 -->
<!-- ⚠️ 这里原来躺着 4 条**写死的演示行**(胎动 12 次 / 体重 58.2kg / 产检提醒 / 睡前散步)。
它们只在「接口读不到」时露出来 —— 也就是说,上游一抖动,页面就开始编数据。
2026-09-22 按产品口径删掉,换成一条中性占位;四种显示模式由下方 JS 的 fpApply() 决定:
· 有档案 + 拿到事件 → 渲染真实事件(覆盖 #tlSteps)
· 有档案 + 今天没记 → 「还没有足迹」
· 有档案 + 接口读不到 → **整块不显示**(不编数据)
· 无档案(上游查无此人)→ 显示 #tlGuide 引导占位
首次渲染时 `#tlSteps` 里是这条「正在获取…」,不是假数据;
JS 的 pendingFootprintStep() 会建一份**同构**的行,两处改文案要一起改。 -->
<div class="steps ripple" id="tlSteps" data-goto="archive" role="button" tabindex="0"
aria-label="宝宝的今日脚印,查看完整档案">
<div class="step"><span class="st-time">09:10</span><span class="st-dot"></span><span class="st-txt">胎动 <b>12</b> 次</span><span class="st-from">来源 · 数胎动</span></div>
<div class="step"><span class="st-time">13:00</span><span class="st-dot"></span><span class="st-txt">体重 <b>58.2kg</b></span><span class="st-from">来源 · 孕期体重</span></div>
<div class="step"><span class="st-time">16:30</span><span class="st-dot"></span><span class="st-txt">产检提醒</span><span class="st-from">来源 · 孕检日历</span></div>
<div class="step"><span class="st-time">20:15</span><span class="st-dot"></span><span class="st-txt">睡前散步 <b>20</b> 分钟</span><span class="st-from">来源 · 运动记录</span></div>
<div class="step"><span class="st-time">今天</span><span class="st-dot"></span><span class="st-txt">正在获取今天的脚印…</span><span class="st-from"></span></div>
</div>
<!-- 无档案时的引导占位(fpApply('guide') 才显示;此时 #tlSteps 收起) -->
<div class="tl-guide" id="tlGuide" style="display:none">
<div class="tl-guide-t">还没有你的脚印</div>
<div class="tl-guide-s">去「数胎动」记一笔,这里就会长出你今天的脚印</div>
</div>
<!-- 这一天的收尾节点。三个直接子元素刻意和 .step 一样排
(42px 的左格 + 圆点 + 剩下的),点才落得进同一条竖线里。
@@ -323,7 +334,11 @@
<b class="ab-tag" id="abTag">宝宝今天想说</b>
<!-- 这句是 app.js 里 AI_LOGS[0].txt 的手写副本(这里要 <b> 和 <br> 排版,
没法直接共用那个字符串)。那边改口吻,这里必须跟着改,
否则首页气泡说「小家伙」、点进去小结说「我」,像两个人在写。 -->
否则首页气泡说「小家伙」、点进去小结说「我」,像两个人在写。
⚠️ 它是**演示文案**(数字写死),所以只有拿到真实事件的当天才显示:
.today-line 挂上 .no-ai 时整颗气泡收起(见 JS 的 fpApply)。
待办:把「3 次」接到真实次数(卡片接口 kick.value / events 里数胎动那条),
并给「今天没记」定一句不说假话的文案 —— 产品确认口径后再改。 -->
<span class="ab-say">妈妈,今天你数到我动了 <b>3</b> 次,<br>我今天精神特别好。</span>
</span>
<!-- 「AI自动汇总你每日记录」按用户要求删了,这行现在只剩右边的入口。

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 演示行同构(时间/圆点/文案/来源)。

View File

@@ -639,6 +639,14 @@ a.skip:focus{left:16px;top:16px}
320px 视口下 .step 一行内容总宽约 311 > 可用 264,flex 就去压唯一压得动的时间列,
把它压到约 36px,圆点跟着左移,看上去就是「时间贴到竖线上」。
现在三处统一 flex:0 0 var(--tl-time),谁都不许缩。 */
/* 今日脚印:「无档案」时的引导占位(替换掉收起的时间轴) */
.tl-guide{background:#fff;border-radius:22px;padding:20px 18px;box-shadow:var(--sh);text-align:center}
.tl-guide-t{font-size:15px;font-weight:800;color:var(--ink)}
.tl-guide-s{font-size:12.5px;color:var(--ink-2);margin-top:6px;line-height:1.6}
/* 没有真实事件的当天不显示 AI 气泡:那句话里的次数是**写死的**(见 index.html 的注释),
今天没记 / 未关联时显示它等于编数据。拿到真实事件(fpApply('rows'))时才摘掉这个类。 */
.today-line.no-ai .ai-bubble{display:none}
.today-line{
position:relative;
--tl-time:44px; /* 时间列宽:够放 "13:00" 还余一点 */