fix(来信提示): 未填孕产期不显示「宝宝来信」提示 —— 卡与红点同一次判定一起收 (v26092211)

产品口径(2026-09-22):**没填孕产期(pregEmpty)就不该出现宝宝来信提示**,
显示了不合理。这条**覆盖**了代码里原先那条决定(「来信与建档是两件事,故意不看
pregEmpty」)—— 那条当时是为了修「红点有、卡没有」,方向反了,见下。

做法:把首页卡与底 tab 红点收进**唯一渲染入口** renderLetterReminder(),用同一个
判据同时决定两处(pregEmpty 是硬门槛 → 建档后看 unread > 0 → 上游降级保持现状),
并把最近一次未读结果存进 _laixinUnread,供建档状态变化时重算:
  · syncLaixinReminder():拿到结果后交给 render(不再自己 toggle 红点);
  · syncPregnancy():未建档分支与建档分支各调一次 render。
于是无论 laixin 与 /profile 谁先回来,都收敛到同一结果(时序无关)。

顺带消掉上一版残留的竞态:此前红点与卡各写一份判据 —— laixin 先回来(亮红点 + 显示卡)、
/profile 后回来(只藏卡),就得到「红点有、卡没有」。现在同源同判。
另外把已不存在的 applyLaixinReminder() 的注释引用一并改掉(js + index.html 共 3 处)。

无头浏览器 + 桩后端实测(profile 延迟 0 / 1.5 / 3s,覆盖两种返回顺序):
  未建档 unread=1 → 卡与红点**都无**(三档一致)
  已建档 unread=1 → **都有**(确认没把建档用户一起关掉)
  已建档 unread=0 → 都无
  访客(无 openid)→ 都无
This commit is contained in:
2026-09-22 21:20:46 +08:00
parent ab8a3a5ea0
commit 3ab8af2bf5
2 changed files with 57 additions and 41 deletions

View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26092210';
var VER = '26092211';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -727,10 +727,15 @@
底 tab 早已改跳 laixin、角标读 /api/mp/public/laixin/unread,
但这张卡原来由 pushLatestToHome() 按本地 LETTERS(box_app 的 /mama-box/letters)驱动,
而那条线对绝大多数用户是 0 封 —— 于是同一屏上「红点说有新信、卡里什么都没有」。
现在两者**共用同一个 laixin 未读结果**:
· 唯一 owner:syncLaixinReminder()(拉数)→ applyLaixinReminder()(落卡 + 落红点)
· 判据:unread > 0 才显示;归零则收起;上游降级(ok:false)保持现状
· 点击:跳 laixin(与底 tab 同落点)
现在两者**共用同一个 laixin 未读结果**,而且**只有一个渲染入口**:
· 拉数:syncLaixinReminder()(首屏 + 每次 visibilitychange)
· 渲染:renderLetterReminder() —— 卡与底 tab 红点由它**一次判定**同时落,
不会再出现「一边有一边没有」的屏
· 判据(2026-09-22 产品口径):**未建档(pregEmpty)= 一律不提示**;
建档后看 unread > 0;归零则一起收;上游降级(ok:false)保持现状
· 点击:跳 laixin(与底 tab 同落点)
⚠️ 判据翻转过一次(旧版「故意不看 pregEmpty」已被产品否掉),细节与理由写在
renderLetterReminder() 上方,改之前先读那段。
⚠️ 卡里只有数字与时间(接口就给这些),所以文案是**通用提醒**,不做单封预览。
将来第三方上 SSE 的 event: letter 之后,才有标题/摘要可显示
(见 docs/laixin-sse-requirement.md)。
@@ -742,18 +747,31 @@
if (body) body.classList.add('no-letter');
}
/* laixin 未读结果 → 首页卡。badge 由 syncLaixinReminder() 一并处理。 */
function applyLaixinReminder(r) {
/* 来信提示(首页卡 + 底 tab 红点)的**唯一渲染入口**。
两个触发方:syncLaixinReminder()(拿到新未读结果)与 syncPregnancy()(建档状态变了)。
卡与红点永远由这**一次判定**同时决定 —— 之前它们各写一份判据,结果就是
「红点有、卡没有」那种自相矛盾的屏(2026-09-22 定时复现过)。
判定(2026-09-22 产品口径):
① **未建档(pregEmpty,即没填孕产期)→ 一律不提示**:卡与红点一起收。
口径翻转过一次,说明白免得被改回去:
· 旧注释写的是「来信与建档是两件事,故意不看 pregEmpty」;
· 产品实测后确认**不合理** —— 没填孕产期的用户本来就不该被「宝宝来信」打扰
(那时连宝宝档案都没有),所以现在 pregEmpty 是**硬门槛**;
· 别再按「unread>0 就该提示」把它改回去。
② 建档了 → 只看未读:unread>0 才出(卡 + 红点),归零则一起收。
③ 上游降级(ok:false)→ 保持现状不动(不误导)。 */
let _laixinUnread = null; // 最近一次未读结果;建档状态变化时靠它重算
function renderLetterReminder() {
const badge = $('#tabBadge');
const r = _laixinUnread;
if (state.pregEmpty || !r || r.ok === false || !(Number(r.unread) > 0)) {
if (badge) badge.classList.remove('show');
hideLetterCard();
return;
}
const card = $('#homeLetter'), body = $('#homeBody');
if (!card) return;
/* 上游降级(ok:false)或未读为 0 —— 收起。
⚠️ 与底 tab 红点**同一套判据**,两处别各写一份(写歪一处就会出现
「红点亮了、卡没出来」这种自相矛盾的屏)。
⚠️⚠️ 这里**故意不看 state.pregEmpty**:那是「兜爸心选档案未建档」,
与「宝宝来信里有没有信」是两件事。加过一版,结果生产上实测出
「红点有 1 封未读、卡不显示」—— 用户能从 laixin 收到信,
却未必在 mama-box 填过预产期。别再加回来。 */
if (!r || r.ok === false || !(Number(r.unread) > 0)) { hideLetterCard(); return; }
const letters = Number(r.unreadLetters) || 0;
const replies = Number(r.unreadReplies) || 0;
/* 两种都有时分开讲,别合成一个含糊的总数(用户不知道「3」是什么东西) */
@@ -768,6 +786,7 @@
card.classList.add('has-new');
card.classList.remove('hide');
if (body) body.classList.remove('no-letter');
if (badge) badge.classList.add('show');
}
$$('[data-detail="close"]').forEach(b => b.addEventListener('click', closePanel));
@@ -1137,21 +1156,18 @@
const _t = new Date();
$('#noteWhen').textContent = mdTxt(_t);
syncBabyWords();
/* ⚠️ 这里**不能** hideLetterCard()(2026-09-22 修「红点有、首页卡没有」):
本分支(未建档)原来顺手把来信卡收起了,理由是「applyLaixinReminder 里也判 pregEmpty」
—— 那个理由**是错的**:applyLaixinReminder 明确故意不看 pregEmpty(见它的注释),
来信卡与红点的唯一 owner 是 syncLaixinReminder → applyLaixinReminder。
两边规则打架的后果是一个**竞态**:
· laixin 先回来(快)→ 红点亮 + 卡显示;
· /profile 后回来(它可能要先打上游用户中心,常见更慢)→ 走到这里 → 只把卡藏了。
而 hideLetterCard() **不动 #tabBadge** → 最终「红点在、卡没有」,与「红点说有新信、
卡里什么都没有」那类自相矛盾的屏完全一致(定时复现:profile 延迟 1.5s 必现;
同时返回则正常。卡里的文案已经写好、只是被 .hide 盖住,正是「先显示后藏掉」的痕迹)。
卡在 HTML 里**默认就是 .hide + .no-letter**,不需要谁来收;让它只由 laixin 结果决定。 */
/* 未建档 → 不给「宝宝来信」提示(卡 + 红点一起收)。
⚠️ 这里**必须**重算一次,不能只写 hideLetterCard():来信提示的唯一渲染入口是
renderLetterReminder(),它同时管卡和底 tab 红点。只藏卡会留下「红点有、卡没有」
(2026-09-22 定时复现:laixin 先回来亮红点、/profile 后回来藏卡,就是那个不一致)。
现在两处判据合成一条(pregEmpty 是硬门槛),谁先回来都收敛到同一结果。 */
renderLetterReminder();
return;
}
const week = state.week, day = state.day, name = state.babyName;
setGrowthDataEmpty(false); /* 有真实孕周了:恢复面板(数据源接入前仍是演示值,见函数注释) */
/* 刚建档/填了孕产期 → 按新状态重算来信提示:此前因未建档被压住的未读,这时该出现 */
renderLetterReminder();
const _tl2 = document.querySelector('.today-line');
if (_tl2) _tl2.style.display = '';
const days = week * 7 + day; // 一起生活了多少天
@@ -2795,10 +2811,10 @@
state.pregEmpty = true;
state.due = null; state.week = null; state.day = null;
state.babyName = ''; state.userName = '';
syncPregnancy(); /* 占位 + 隐藏脚印时间轴 */
/* 首页来信卡归 laixin(红点与卡都由 syncLaixinReminder 一处落,见它的无 openid 早退分支),
这里直接收起是**兜底**:访客态不该出现任何「你有新信」的暗示。 */
hideLetterCard();
syncPregnancy(); /* 占位 + 隐藏脚印时间轴(内含 renderLetterReminder(),会收起来信提示) */
/* 兜底再算一次:访客态不该出现任何「你有新信」的暗示(卡 + 红点一起收)。
唯一渲染入口是 renderLetterReminder(),别再这里单独 hideLetterCard()。 */
renderLetterReminder();
return;
}
const base = '/api/mp/public/mama-box/';
@@ -3104,13 +3120,12 @@
⚠️ 容错:接口返回 ok=false(上游抖动)时**保持现状** —— 既不把已经点亮的红点抹掉,
也不因为一次失败就冒出一个红点。角标不是关键路径,失败一律静默。 */
function syncLaixinReminder() {
const badge = $('#tabBadge');
const openid = hashParam('openid');
/* 接口按 openid 查,没有身份就别问(浏览器直开调试时常见)
—— 此时红点与首页卡都收起(访客态不该出现任何「你有新信」的暗示)。 */
—— 清掉上次结果后交给 renderLetterReminder() 统一收起(它也会因 pregEmpty 收)。 */
if (!openid) {
if (badge) badge.classList.remove('show');
hideLetterCard();
_laixinUnread = null;
renderLetterReminder();
return;
}
const unionid = hashParam('unionid');
@@ -3121,8 +3136,8 @@
.then(j => {
const r = j && j.result;
if (!r || r.ok === false) return; // 降级:保持现状(红点与卡都不动)
if (badge) badge.classList.toggle('show', Number(r.unread) > 0);
applyLaixinReminder(r);
_laixinUnread = r; // 存下来:建档状态变化时要靠它重算
renderLetterReminder();
})
.catch(() => { /* 静默 */ });
}