不会。
@@ -774,7 +774,6 @@
stampLetters();
state.curLetter = LETTERS[0].id;
- state.homeLetter = LETTERS[0].id;
/* ---------- 延迟投递队列(3~5 小时) ---------- */
/* v1 只存了排队记录,没存产生排队的那封回信 —— 一半存一半不存。
@@ -877,7 +876,6 @@
saveStore();
renderLetters();
syncLetterCounts();
- pushLatestToHome();
toast('宝宝给你回信啦 💌');
}
function deliverBabyReply(p) {
@@ -1310,87 +1308,52 @@
$('#unreadNum').classList.toggle('zero', state.unread === 0);
/* ⚠️ 这里原来还会开关底 tab 的 #tabBadge。2026-09-19 起那枚 tab 是「宝宝来信」
(跳第三方 H5),角标口径必须跟它一致 —— owner 是 laixin 的 /laixin/unread
- (见 syncLaixinBadge)。本地内置来信屏的未读数与它不是同一套,别在这里写 #tabBadge,
+ (见 syncLaixinReminder)。本地内置来信屏的未读数与它不是同一套,别在这里写 #tabBadge,
两处都写会互相覆盖。 */
}
- /* 宝宝的新回信同时推到首页头部那张「宝宝来信」卡上。
- 卡是「有信才在」:既没有未读的信、也没有没看过的新回信时,整张收起来 ——
- 全读完了还硬顶一封旧信在首页正中间,是在假装有新消息。
- 收起来会空出 64px 的流高(卡是 margin-top:-16 + height:80),
- 所以同时给 .home-body 挂 .no-letter 把下面的标题往下推一点,
- 免得「孕期必用工具」直接贴到橙色头图底边上。
-
- ⚠️⚠️ 2026-09-20:卡的显隐**要等 /letters 回来才算数**(lettersResolved)。
- 之前的写法是「先把内置样例/本地队列拼出来的卡显示出来,数据回来再决定」——
- 结果商城↔首页来回切时会闪一下:先冒出一张(甚至写着「宝宝回信啦」)的卡,
- 几百毫秒后数据说没有未读,又缩回去。现在反过来:
- · index.html 里 #homeLetter 默认带 .hide、#homeBody 默认带 .no-letter;
- · 没确定(!lettersResolved)→ 一律保持收起;
- · 确定之后才可能显示,且只有在真有未读/新回信时才显示。
- 代价是「有信」的用户看到卡出现在数据回来那一刻(晚一点点,但不闪)。 */
- let lettersResolved = false; // /letters 已经问过结果(含失败/访客态)—— 见 applyRemoteLetters
-
+ /* 首页那张「宝宝来信」卡:**数据源是 laixin(第三方宝宝来信应用)**,不是本页的 LETTERS。
+ ⚠️⚠️ 2026-09-20 —— 理顺一处数据源不一致:
+ 底 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 同落点),**不再**打开内置来信详情
+ ⚠️ 卡里只有数字与时间(接口就给这些),所以文案是**通用提醒**,不做单封预览。
+ 将来第三方上 SSE 的 event: letter 之后,才有标题/摘要可显示
+ (见 docs/laixin-sse-requirement.md)。
+ ⚠️ 内置来信屏 #screen-letter 与 LETTERS 仍在(左右滑动还能进),
+ 只是不再往首页推卡 —— 别在这里把 LETTERS 那套再接回来。 */
function hideLetterCard() {
const card = $('#homeLetter'), body = $('#homeBody');
if (card) { card.classList.add('hide'); card.classList.remove('has-new'); }
if (body) body.classList.add('no-letter');
}
- function pushLatestToHome() {
- const card = $('#homeLetter');
- const body = $('#homeBody');
- /* 还没问出结果、访客态(没身份)、或后端明确说该用户没建档 —— 都别显示。
- ⚠️ hashParam 是函数声明(在文件靠后定义),这里调用靠提升,没问题。 */
- if (!lettersResolved || !hashParam('openid') || state.pregEmpty) { hideLetterCard(); return; }
- const show = () => { card.classList.remove('hide'); body.classList.remove('no-letter'); };
- let msg = null, owner = null;
- LETTERS.forEach(L => L.thread.forEach(m => {
- if (m.who === 'baby' && m.isNew && (!msg || m.at > msg.at)) { msg = m; owner = L; }
- }));
- if (msg) {
- $('#lcTitle').textContent = babyWord('回信啦');
- $('#lcDesc').textContent = msg.text;
- /* 这行连着尖括号只够一行,别把信的标题拼进来 ——
- 原来的「看看我的小手会握…的回复」正好撑破,尖括号会被挤到第四行 */
- $('#lcLink').textContent = '看看' + babyWord('的回复');
- state.homeLetter = owner.id;
- card.classList.add('has-new');
- show();
- return;
- }
- /* 这里以前是 LETTERS.find(...) || LETTERS[0],那个 || 就是「哪怕全读完了
- 也硬挑第一封顶上」的来源。去掉之后 top 可能是 undefined —— 那正是要藏的情况。 */
- const top = LETTERS.find(L => L.unread);
- if (!top) {
- card.classList.add('hide');
- card.classList.remove('has-new');
- body.classList.add('no-letter');
- /* 卡都不显示了,这个 id 只是兜底:openDetail() 是 openLetter(state.homeLetter),
- 留 null 的话别处万一调到就炸。 */
- state.homeLetter = LETTERS[0] ? LETTERS[0].id : null;
- return;
- }
- /* 「小汤圆来信」。上一批这句加长成过「小汤圆的新消息」(配合底 tab 改口叫
- 「宝宝消息」),本批用户要求整条线退回「来信」,这里跟着退。
- ⚠️ 退的是「来信」不是「的来信」—— 加长的唯一理由是当时要改口,
- 理由没了就退回原样,别顺手留个中间态。
- ⚠️ 上面那条分支是 babyWord('回信啦'),两次改口都**没跟着动**,这是有意的:
- 那条说的是「宝宝回了你的回信」,是个更具体的事件;这条说的是
- 「有一封你还没看过的信」。两句本来就不是一回事,抹平反而丢信息。
- ⚠️ 宽度:.lc-body = 卡 249 − 内边 25.5 − 信封 64 − gap 9 = 150.5px,
- .lc-title 是 16px。「小汤圆来信」5 × 16 = 80 ✓。
- 后缀从「的新消息」4 字缩回「来信」2 字,
- **不被省略号啃掉的小名上限也从 5 字回到了 7 字**。
- 但小名输入框 maxlength=12,7 字仍然不够 —— 所以 .lc-title 那条
- text-overflow:ellipsis **继续留着**,不能因为这次退回就顺手删掉。
- 再想加长这个后缀之前,先回来看这笔账。 */
+ /* laixin 未读结果 → 首页卡。badge 由 syncLaixinReminder() 一并处理。 */
+ function applyLaixinReminder(r) {
+ const card = $('#homeLetter'), body = $('#homeBody');
+ if (!card) return;
+ /* 访客态(没身份)、后端说他没建档、上游降级、未读为 0 —— 一律收起。
+ ⚠️ 与底 tab 角标同一套判据,两处别各写一份。 */
+ const has = r && r.ok !== false && Number(r.unread) > 0;
+ if (!has || !hashParam('openid') || state.pregEmpty) { hideLetterCard(); return; }
+ const letters = Number(r.unreadLetters) || 0;
+ const replies = Number(r.unreadReplies) || 0;
+ /* 两种都有时分开讲,别合成一个含糊的总数(用户不知道「3」是什么东西) */
+ let desc = (letters && replies) ? '有 ' + letters + ' 封新信、' + replies + ' 条新回信'
+ : replies ? '宝宝回了你 ' + replies + ' 条话'
+ : '宝宝给你写了 ' + letters + ' 封信';
+ const at = r.latestLetterAt ? Date.parse(r.latestLetterAt) : NaN;
+ if (!isNaN(at)) desc += ' · 最近一封 ' + fmtWhen(at);
$('#lcTitle').textContent = babyWord('来信');
- $('#lcDesc').textContent = ellip(top.preview); /* 首页这张卡也是摘要,同样收省略号 */
+ $('#lcDesc').textContent = desc;
$('#lcLink').textContent = '读读' + babyWord('的话');
- state.homeLetter = top.id;
card.classList.add('has-new');
- show();
+ card.classList.remove('hide');
+ if (body) body.classList.remove('no-letter');
}
/* ---------- 详情页 ---------- */
@@ -1402,10 +1365,8 @@
renderLetterDetail(L);
renderLetters();
syncLetterCounts();
- pushLatestToHome();
openPanel('detailLetter');
}
- function openDetail() { openLetter(state.homeLetter); }
/* ---------- 把一句话拆成一个个歪歪扭扭的小方块 ----------
光靠 font-family 只在装了翩翩体的 iOS/macOS 上才歪;
@@ -1612,7 +1573,8 @@
deliverDue();
renderLetters();
syncLetterCounts();
- pushLatestToHome();
+ /* ⚠️ 这里原来有一次 pushLatestToHome():把内置样例信推上首页卡。
+ 卡现在归 laixin(默认收起、有未读才显示),这一推会先冒出一张假卡再被收起 —— 删掉。 */
renderLetterDetail(LETTERS[0]); // 详情页先垫上最新那封,免得首次打开有一帧空白
/* 30 秒一跳。间隔取得宽松,是因为这里的等待本来就以小时计 */
setInterval(deliverDue, 30000);
@@ -1967,7 +1929,7 @@
const _t = new Date();
$('#noteWhen').textContent = mdTxt(_t);
syncBabyWords();
- pushLatestToHome();
+ hideLetterCard();
renderLetters();
renderLetterDetail(curLetter());
return;
@@ -2055,7 +2017,8 @@
$('#replyTo').textContent = name;
syncBabyWords();
stampLetters();
- pushLatestToHome();
+ /* ⚠️ 原来这里有一次 pushLatestToHome():小名/孕周改动要跟着刷新首页那张卡。
+ 卡现在归 laixin(内容与 LETTERS 无关),这一推已无意义,删掉。 */
renderLetters();
renderLetterDetail(curLetter());
@@ -3930,9 +3893,9 @@
/* ⚠️ 这里原来还有一次 syncUnlockCopy()(按解锁态改写首页/资源库两句引导语)。
闸门取消后文案是静态的,函数已删。 */
animateRings();
- /* ⚠️ 角标 #tabBadge 的 owner 是 laixin 未读数(syncLaixinBadge),不是本地信件数。
- 这里曾经用 state.unread 初始它,现已删除 —— 两处都写会互相覆盖。
- 角标在 syncLaixinBadge() 拉到数之前保持隐藏。 */
+ /* ⚠️ 底 tab 红点与首页「宝宝来信」卡的 owner 都是 laixin 未读数(syncLaixinReminder),
+ 不是本地信件数。这里曾经用 state.unread 初始角标,现已删除 —— 两处都写会互相覆盖。
+ 两者在 syncLaixinReminder() 拉到数之前保持隐藏/收起(index.html 里就是默认态)。 */
/* 来信页的默认标签:永远落在「全部」。
曾经改成过「有未读就落在未读」,但那样一进页面就看不到全部信件,
未读的红点本来就在提示了,不需要再替人把列表筛一遍。
@@ -3959,7 +3922,7 @@
// 拉不到/未关联都各有回退,见 applyRemoteFootprint 内的分支说明
applyRemoteFootprint();
// 底 tab「宝宝来信」未读角标(box_app 代理第三方 laixin 未读数)
- syncLaixinBadge();
+ syncLaixinReminder();
}
/* ---------- 远程配置(box_app 后台下发):已下线 ----------
⚠️ 2026-09-19(9.19 需求 1.2):这里原来拉 /api/mp/public/mama-box/config,
@@ -4005,13 +3968,11 @@
state.due = null; state.week = null; state.day = null;
state.babyName = ''; state.userName = '';
LETTERS.splice(0, LETTERS.length);
- state.curLetter = ''; state.homeLetter = '';
+ state.curLetter = '';
syncPregnancy(); /* 占位 + 隐藏脚印时间轴 + 信件引导态 */
syncLetterCounts(); /* hero 计数切 '--'(角标 #tabBadge 已不由本地信件数驱动) */
- /* ⚠️ 必须重算首页来信卡:上面刚把 LETTERS 清空、pregEmpty 置真,
- 不重算的话那张卡会停在「显示」状态,顶着一封已经不存在的信。 */
- lettersResolved = true;
- pushLatestToHome();
+ /* 首页来信卡归 laixin(applyLaixinReminder 里也判 pregEmpty/无身份),这里直接收起 */
+ hideLetterCard();
return;
}
const base = '/api/mp/public/mama-box/';
@@ -4143,28 +4104,22 @@
拉不到 —— 无 openid / 接口挂 / 返回码不对 —— 就静默回落内置样例,页面照常可用。 */
function applyRemoteLetters() {
var openid = hashParam('openid');
- /* ⚠️ 下面每条**终态**都必须把 lettersResolved 置 true(含失败/无身份),
- 否则首页那张来信卡会永远停在收起态(宁可收起也不能闪,见 pushLatestToHome)。
- 无身份这条:卡本来就不该出现,置 true 只是让状态机收敛。 */
- if (!openid) { lettersResolved = true; pushLatestToHome(); return; }
+ /* ⚠️ 2026-09-20:本函数只管**内置来信屏**(#screen-letter)的数据。
+ 它原来还负责驱动首页那张「宝宝来信」卡(lettersResolved + pushLatestToHome),
+ 那张卡现在完全归 laixin(syncLaixinReminder → applyLaixinReminder),
+ 两套数据别再互相写同一个 DOM。 */
+ if (!openid) return;
var base = '/api/mp/public/mama-box/';
var j = function (u) { return fetch(u).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); };
j(base + 'letters?openid=' + encodeURIComponent(openid)).then(function (lj) {
- /* 拉不到(接口挂/断网/返回码不对)→ 静默回落内置样例。
- 这**也是**一个终态:样板信照样要能上首页卡(离线可用),所以这里要置位并重算。 */
- if (!lj || lj.code !== 200 || !lj.result || !lj.result.items) {
- lettersResolved = true; pushLatestToHome(); return;
- }
+ if (!lj || lj.code !== 200 || !lj.result || !lj.result.items) return; /* 静默回落内置样例 */
/* 新用户空态:后端成功返回空列表 → 清掉内置样例信,展示「这里还没有内容」。
⚠️ 不能直接 return —— 那会把 LETTERS 留在内置样例上,新用户看到演示信。 */
if (!lj.result.items.length) {
- lettersResolved = true;
LETTERS.splice(0, LETTERS.length);
state.curLetter = '';
- state.homeLetter = '';
renderLetters();
syncLetterCounts();
- pushLatestToHome();
return;
}
Promise.all(lj.result.items.map(function (it) {
@@ -4186,14 +4141,11 @@
try {
LETTERS.splice(0, LETTERS.length, ...apiList);
state.curLetter = LETTERS[0].id;
- state.homeLetter = LETTERS[0].id;
- lettersResolved = true;
renderLetters();
syncLetterCounts();
- pushLatestToHome();
} catch (e) { /* 映射 / 渲染异常就回退内置样例 */ }
- }).catch(function () { lettersResolved = true; pushLatestToHome(); /* 静默回退内置样例 */ });
- }).catch(function () { lettersResolved = true; pushLatestToHome(); /* 静默回退内置样例 */ });
+ }).catch(function () { /* 静默回退内置样例 */ });
+ }).catch(function () { /* 静默回退内置样例 */ });
}
/* ---------- 今日脚印(box_app → 数胎动对外 API,动态获取) ----------
@@ -4341,8 +4293,12 @@
jumpApp(url);
}
$('#tabLetter').addEventListener('click', openLetterApp);
+ /* 首页那张「宝宝来信」卡与底 tab 是**同一个入口**(2026-09-20):
+ 卡的数据源已经是 laixin,点它当然也该进 laixin —— 以前它打开的是内置来信详情,
+ 那会「卡上写着有 2 封新信、点进去是空的」(两套数据)。 */
+ $('#homeLetter').addEventListener('click', openLetterApp);
- /* ---------- 底 tab 角标:宝宝来信未读数 ----------
+ /* ---------- 底 tab 红点 + 首页来信卡:宝宝来信未读数(唯一 owner) ----------
数据源:box_app 封装的联动接口 GET /api/mp/public/laixin/unread
(box_app 后端再去调第三方 https://taidong.douba.top/laixin/api/laixin/unread)。
⚠️ 路径按应用命名空间走 /api/mp/public/
/<资源>(app 段 = App 表 identifier),
@@ -4351,17 +4307,21 @@
⚠️ 为什么不直连 taidong:那台的 CORS 白名单只放了自己的域名,页面直连会被浏览器拦;
而且「mama-box ↔ laixin 的联动接口由 box_app 封装」是本项目的既定边界
(见 docs/mama-box-change-list-20260919.md §1.5)。
- ⚠️ 口径:#tabBadge 的 owner **只有这一个函数**。别在 syncLetterCounts() 或
+ ⚠️ 口径:#tabBadge 与首页「宝宝来信」卡的 owner **只有这一个函数**(改名自 syncLaixinBadge)。别在 syncLetterCounts() 或
applyRemoteProfile() 里再写它 —— 内置来信屏的未读数(box_app /letters)
与 laixin 的未读数是两套数,两处都写会互相覆盖(谁后跑谁说了算)。
⚠️ 容错:接口返回 ok=false(上游抖动)时**保持现状** —— 既不把已经点亮的红点抹掉,
也不因为一次失败就冒出一个红点。角标不是关键路径,失败一律静默。 */
- function syncLaixinBadge() {
+ function syncLaixinReminder() {
const badge = $('#tabBadge');
- if (!badge) return;
const openid = hashParam('openid');
- /* 接口按 openid 查,没有身份就别问(浏览器直开调试时常见)——此时不显示角标 */
- if (!openid) { badge.classList.remove('show'); return; }
+ /* 接口按 openid 查,没有身份就别问(浏览器直开调试时常见)
+ —— 此时红点与首页卡都收起(访客态不该出现任何「你有新信」的暗示)。 */
+ if (!openid) {
+ if (badge) badge.classList.remove('show');
+ hideLetterCard();
+ return;
+ }
const unionid = hashParam('unionid');
const qs = 'openid=' + encodeURIComponent(openid) +
(unionid ? '&unionid=' + encodeURIComponent(unionid) : '');
@@ -4369,15 +4329,17 @@
.then(r => (r.ok ? r.json() : null))
.then(j => {
const r = j && j.result;
- if (!r || r.ok === false) return; // 降级:保持现状
- badge.classList.toggle('show', Number(r.unread) > 0);
+ if (!r || r.ok === false) return; // 降级:保持现状(红点与卡都不动)
+ if (badge) badge.classList.toggle('show', Number(r.unread) > 0);
+ applyLaixinReminder(r);
})
.catch(() => { /* 静默 */ });
}
/* 从「宝宝来信」H5 返回、或小程序 onShow 把 web-view 唤回前台时刷一次
- (上游未读数是「在来信页读完就归零」,所以回来这一下必须重新拉)。 */
+ (上游未读数是「在来信页读完就归零」,所以回来这一下必须重新拉)。
+ 将来第三方上 SSE 后,这里就是它的降级路径(见 docs/laixin-sse-requirement.md)。 */
document.addEventListener('visibilitychange', function () {
- if (!document.hidden) syncLaixinBadge();
+ if (!document.hidden) syncLaixinReminder();
});
/* 「授权完善资料」:H5 → navigateTo → 小程序端独立授权页(pages/annex/auth/index)。