mama-box 9.19 需求: 资料弹窗去兑换码闸门、底tab改回宝宝来信、我的页卡片改兜爸心选商城 (v26092001)
资料详情页(需求 1.1/1.2): - #modalWx 删掉兑换码输入框与「以后再说」「解锁全部资料」两颗按钮,is-gate/is-gift/is-done 三模式合并成单态(唯一出口是右上角 .modal-x);二维码下方文案改需求指定句 - 需求方确认「扫码加顾问 → 回复领资料 → 顾问直接发资料」,故「解锁」概念整体取消 死代码清理(D1 的连带): - 删 PAN/PAN_DEFAULT 网盘链接表、panOf、WX_CODE/wxCodeOK、givePan、#modalPan 弹窗、 state.wxUnlocked/WXOK_KEY、syncUnlockCopy、fetchRemoteConfig - 删「已领」整套:state.resGot/RESGOT_KEY/isGot/列表「已领 ✓」标与筛选 chip/详情页「再拿一次链接」 底 tab(需求 2,D2=C): - 「兜爸心选」→「宝宝来信」,图标换回 ic-letter,点击改走 jumpApp 跳第三方 H5 mp.yizhezhe.com/laixin/(App 表 identifier=laixin,取不到时兜底硬编码) - 红点 #tabBadge 挪回该 tab;角标数据源改为 box_app 封装的 /laixin/unread (syncLaixinBadge,ok=false 时保持现状,不把红点抹掉) 其它: - 我的页卡片「兜爸心选」→「兜爸心选商城」(需求 3) - 内置来信屏 #screen-letter 与 box_app /letters 链路保留不动,但不再有底 tab 入口
This commit is contained in:
528
js/app.js
528
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26091901';
|
||||
var VER = '26092001';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
resCat: 'all',
|
||||
resKey: 'book',
|
||||
resFav: [], // 已收藏的资源 key,数组顺序即收藏顺序
|
||||
resGot: [], // 已领取的资源 key(点过「立即领取」的)
|
||||
babyName: '小汤圆',
|
||||
/* 这三项一进门就是真值(默认预产期 = 今天 +3 个月,见上面 DEFAULT_DUE)。
|
||||
以前它们是写死的占位 24/3/null,等 init() 才补 —— 但信件日期是按孕周
|
||||
@@ -66,7 +65,7 @@
|
||||
/* 来信列表左边那颗宝宝头像。空 = 还没设,显示占位的「宝」字;
|
||||
设了就是一段 data URI(点头像选图后就地读出来的,没有后端)。
|
||||
⚠️ 和 userName / babyName 一样**不写 localStorage** —— 现状只持久化
|
||||
resFav/resGot/wxUnlocked/pending,刷新会回到「宝」字。
|
||||
resFav,刷新会回到「宝」字。
|
||||
这是已知现状、本批没扩范围,不是漏做。
|
||||
⚠️ 更要紧的是:data URI 动辄几百 KB,而 localStorage 通常只有 5MB。
|
||||
哪天真要持久化它,得先压缩(画到 canvas 缩到 ~128px 再 toDataURL),
|
||||
@@ -77,16 +76,19 @@
|
||||
/* ⚠️ 这里原来还有一条 userBio(一句话签名)。整块已按用户要求删除,
|
||||
连同 #pfBio 输入框、#meBio 展示行、syncBio()。别只加回这一条 ——
|
||||
一个没人读的 state 字段是最难发现的死代码。 */
|
||||
wxUnlocked: false // 是否已用兑换码解锁资料领取(见下面 WXOK_KEY)
|
||||
/* ⚠️ 这里原来还有一条 wxUnlocked(兑换码解锁态)。2026-09-19(9.19 需求)
|
||||
「资料改由顾问直接发」之后解锁概念整体取消,它连同
|
||||
WXOK_KEY / saveWxUnlocked / syncUnlockCopy 一起删了,别再加回来。 */
|
||||
};
|
||||
|
||||
/* ---------- 资源的收藏 / 已领(都跨会话保留) ----------
|
||||
/* ---------- 资源的收藏(跨会话保留) ----------
|
||||
只存 key,资源本身仍从 RES 读,改文案不会让旧记录失效。
|
||||
「已领」原来只是详情页那个按钮上的一个 class,退出去再进来就没了 ——
|
||||
用户根本记不住自己领过哪几份,回到列表页只能一个个点进去看。
|
||||
现在和收藏一样落盘,列表页直接打「已领」标。 */
|
||||
⚠️ 原来还有一整套「已领」:state.resGot / RESGOT_KEY / isGot() / 列表页的
|
||||
「已领 ✓」标与筛选 chip / 详情页的「再拿一次链接」。9.19 需求取消了
|
||||
「点立即领取就到手」这个动作(资料由顾问在微信里直接发),H5 侧不再记录
|
||||
「领过哪几份」,整块连同 localStorage 'mmbox.resgot.v1' 一起删除。
|
||||
要恢复得先回问需求方。 */
|
||||
const RESFAV_KEY = 'mmbox.resfav.v1';
|
||||
const RESGOT_KEY = 'mmbox.resgot.v1';
|
||||
const loadKeys = (k, into) => {
|
||||
try {
|
||||
const arr = JSON.parse(localStorage.getItem(k) || 'null');
|
||||
@@ -94,25 +96,9 @@
|
||||
} catch (e) { /* 隐私模式下会抛错,忽略即可,只是不跨会话 */ }
|
||||
};
|
||||
loadKeys(RESFAV_KEY, 'resFav');
|
||||
loadKeys(RESGOT_KEY, 'resGot');
|
||||
function saveResFav() {
|
||||
try { localStorage.setItem(RESFAV_KEY, JSON.stringify(state.resFav)); } catch (e) {}
|
||||
}
|
||||
function saveResGot() {
|
||||
try { localStorage.setItem(RESGOT_KEY, JSON.stringify(state.resGot)); } catch (e) {}
|
||||
}
|
||||
|
||||
/* ---------- 加企业微信换来的「解锁」 ----------
|
||||
和上面两个一样跨会话,但**不复用 loadKeys()**:那是个数组读取器,
|
||||
把一个布尔值塞成 ['1'] 只是为了省几行,代价是数据形状被扭曲,
|
||||
以后读这段的人得先想明白「为什么解锁状态是个数组」。
|
||||
⚠️ 判定写死成 === '1':读不出、读成别的、localStorage 整个抛错,
|
||||
一律往「锁着」倒。解锁是要给东西出去的动作,含糊时不能默认放行。 */
|
||||
const WXOK_KEY = 'mmbox.wxunlock.v1';
|
||||
try { state.wxUnlocked = localStorage.getItem(WXOK_KEY) === '1'; } catch (e) {}
|
||||
function saveWxUnlocked() {
|
||||
try { localStorage.setItem(WXOK_KEY, '1'); } catch (e) {}
|
||||
}
|
||||
|
||||
/* ---------- Toast ---------- */
|
||||
const toastEl = $('#toast');
|
||||
@@ -1310,20 +1296,22 @@
|
||||
const unread = LETTERS.filter(L => L.unread).length;
|
||||
const news = LETTERS.reduce((n, L) => n + L.thread.filter(m => m.isNew).length, 0);
|
||||
state.unread = unread + news;
|
||||
/* 未完善资料:hero 计数给占位符(没有"共 24 封"这种演示数字),无未读角标 */
|
||||
/* 未完善资料:hero 计数给占位符(没有"共 24 封"这种演示数字) */
|
||||
if (state.pregEmpty) {
|
||||
$('#letterTotal').textContent = '--';
|
||||
$('#unreadNum').textContent = '0';
|
||||
$('#unreadNum').classList.add('zero');
|
||||
$('#tabBadge').classList.remove('show');
|
||||
return;
|
||||
}
|
||||
/* 实际数量:LETTERS 即服务端返回的真实信件(LETTER_ARCHIVED 是原型虚构值,别加) */
|
||||
$('#letterTotal').textContent = LETTERS.length;
|
||||
$('#unreadNum').textContent = state.unread;
|
||||
/* 全读完了就把红点徽标收起来,别在标签上挂一个孤零零的「0」 */
|
||||
/* 全读完了就把数字标灰,别挂一个孤零零的「0」 */
|
||||
$('#unreadNum').classList.toggle('zero', state.unread === 0);
|
||||
$('#tabBadge').classList.toggle('show', state.unread > 0);
|
||||
/* ⚠️ 这里原来还会开关底 tab 的 #tabBadge。2026-09-19 起那枚 tab 是「宝宝来信」
|
||||
(跳第三方 H5),角标口径必须跟它一致 —— owner 是 laixin 的 /laixin/unread
|
||||
(见 syncLaixinBadge)。本地内置来信屏的未读数与它不是同一套,别在这里写 #tabBadge,
|
||||
两处都写会互相覆盖。 */
|
||||
}
|
||||
|
||||
/* 宝宝的新回信同时推到首页头部那张「宝宝来信」卡上。
|
||||
@@ -2567,61 +2555,29 @@
|
||||
};
|
||||
const RES_ORDER = ['book', 'food', 'music', 'report', 'sport', 'diet', 'sleep', 'med'];
|
||||
|
||||
/* 资源本体统一放在百度网盘,详情页只负责呈现内容。
|
||||
——「后台配置」就是改下面这张表,页面结构一行都不用动。
|
||||
没单独配置的走 PAN_DEFAULT,保证新资源上架不会点出个空弹窗。 */
|
||||
const PAN_DEFAULT = { url: 'https://pan.baidu.com/s/1mamabox-pregnancy', code: 'mmhz' };
|
||||
const PAN = {
|
||||
book: { url: 'https://pan.baidu.com/s/1mmb-tangnai', code: 'tn28' },
|
||||
food: { url: 'https://pan.baidu.com/s/1mmb-yingyang', code: 'yy66' },
|
||||
music: { url: 'https://pan.baidu.com/s/1mmb-taijiao', code: 'tj30' },
|
||||
report: { url: 'https://pan.baidu.com/s/1mmb-chanjian', code: 'cj12' },
|
||||
sport: { url: 'https://pan.baidu.com/s/1mmb-yundong', code: 'yd08' },
|
||||
diet: { url: 'https://pan.baidu.com/s/1mmb-shipu', code: 'sp21' },
|
||||
sleep: { url: 'https://pan.baidu.com/s/1mmb-shuimian', code: 'sm07' },
|
||||
med: { url: 'https://pan.baidu.com/s/1mmb-qingxu', code: 'qx19' }
|
||||
};
|
||||
const panOf = k => PAN[k] || PAN_DEFAULT;
|
||||
/* ⚠️ 2026-09-19(9.19 需求 1.2):这里原来有 PAN_DEFAULT / PAN 两张**百度网盘
|
||||
链接表**、panOf()、以及企业微信兑换码 WX_CODE / wxCodeOK()。
|
||||
需求方确认「扫码加顾问 → 回复领资料 → 顾问直接发资料」之后,H5 不再发放网盘
|
||||
链接,这套(连同 #modalPan 弹窗、givePan()、后台下发的 pan/panDefault/wxCode)
|
||||
整块删除。要恢复「H5 自己发链接」,等于把「解锁」概念加回来,先回问需求方。 */
|
||||
|
||||
/* 加企业微信之后顾问发的兑换码。和 PAN 表放一起是因为它们是同一类东西 ——
|
||||
「运营在后台配的值」,改的时候一并改,不用满文件找。
|
||||
⚠️ 请不要把它当访问控制看。整个站是一个纯前端单文件,这个码就明写在
|
||||
源码里,翻一下就有;localStorage 改一行也能绕过去。它的作用是给
|
||||
「白拿资料」加一道需要真人接触的摩擦,把人导到顾问那儿去,仅此而已。
|
||||
真要锁住内容,得让网盘链接由服务端在校验后下发。
|
||||
⚠️ trim() + toUpperCase() 两个都不能省:这个码是顾问在微信里发出来、
|
||||
用户长按复制过来的,尾部带个空格、被输入法首字母大写成小写,
|
||||
在真机上几乎必然发生。为一个空格把一个已经加完好友的真实用户挡在
|
||||
门外,是在自己辛苦搭的漏斗最后一格上抹毒药。
|
||||
⚠️ 声明是 let 不是 const:本页托管在 box_app(/mama-box/)时,
|
||||
fetchRemoteConfig() 会用后台下发的值覆盖它(见文件末尾)。 */
|
||||
let WX_CODE = 'MAMA2025';
|
||||
const wxCodeOK = v => String(v || '').trim().toUpperCase() === WX_CODE;
|
||||
|
||||
const isGot = k => state.resGot.indexOf(k) >= 0;
|
||||
const isFav = k => state.resFav.indexOf(k) >= 0;
|
||||
function resCardHTML(k) {
|
||||
const r = RES[k];
|
||||
const got = isGot(k);
|
||||
/* 领过的那一份,「免费」标换成「已领 ✓」并给整张卡加 .is-got:
|
||||
「免费」是招揽的话,对已经领到手的人已经没意义了,占着同一个位置正好。
|
||||
缩略图打勾角标是给快速扫列表用的 —— 一眼扫过去哪几份领过,不用逐行读标签。 */
|
||||
return '<button class="res-item ripple' + (got ? ' is-got' : '') + '"' +
|
||||
/* ⚠️ 这里原来还有「领过的那一份」分支:整张卡加 .is-got、缩略图右上角打 ✓、
|
||||
底行的「免费」标换成「已领 ✓」。9.19 需求取消「已领」概念后一并删除,
|
||||
现在每张卡都是同一个样子(免费)。 */
|
||||
return '<button class="res-item ripple"' +
|
||||
' data-open="detailRes" data-res="' + k + '">' +
|
||||
'<span class="ri-thumb ' + r.tint + '"><img src="' + r.img + '" alt="">' +
|
||||
(got ? '<em class="ri-got-tick" aria-hidden="true">✓</em>' : '') + '</span>' +
|
||||
'<span class="ri-thumb ' + r.tint + '"><img src="' + r.img + '" alt=""></span>' +
|
||||
'<span class="ri-body"><span class="ri-title">' + r.title + '</span>' +
|
||||
'<span class="ri-desc">' + r.desc + '</span>' +
|
||||
/* 底部这行的顺序换过一次:
|
||||
原来是「免费 / 分类 …… 8 分钟读完」。两处毛病 ——
|
||||
「几分钟读完」对着一个网盘资料包说不通(音乐那份写的是「30 首 · 共 96 分钟」,
|
||||
压根不是一回事),换成 meta[2] 的适用孕周:在孕育资源库里,
|
||||
「这份现在适不适合我」比「要读多久」有用得多;
|
||||
状态标(免费 / 已领 ✓)顶到最右,整列对齐,
|
||||
扫列表时哪几份领过一眼就出来,不用逐行读到第一格。 */
|
||||
/* 底部这行:分类 + 适用孕周 + 右侧状态标。
|
||||
⚠️ 状态标现在恒为「免费」(原来领过的会换成「已领 ✓」,已随「已领」概念删除)。 */
|
||||
'<span class="ri-row">' +
|
||||
'<em class="ri-cat">' + r.cat + '</em>' +
|
||||
'<em class="ri-ga">' + r.meta[2] + '</em>' +
|
||||
(got ? '<em class="pill-got">已领 ✓</em>' : '<em class="pill-free">免费</em>') +
|
||||
'<em class="pill-free">免费</em>' +
|
||||
'</span></span></button>';
|
||||
}
|
||||
function bindResCards(root) {
|
||||
@@ -2633,32 +2589,24 @@
|
||||
/* 收藏不是分类而是筛选,默认排在最末。但只要用户真收藏过东西,
|
||||
它就成了最常用的一格 —— 排在末尾要横向滑到头才够得着,所以提到「全部」后面。
|
||||
用 insertBefore/appendChild 挪节点(不是重建 innerHTML),init 时绑的点击事件才不会丢。
|
||||
已经在第二位时 children[1] 就是 favChip 自己,insertBefore 原地不动,无副作用。 */
|
||||
已经在第二位时 children[1] 就是 favChip 自己,insertBefore 原地不动,无副作用。
|
||||
⚠️ 原来还有个 .chip-got(✓ 已领)跟着一起挪,已随「已领」概念删除。 */
|
||||
const chipsRow = $('#resChips'), favChip = $('.chip-fav', chipsRow);
|
||||
const gotChip = $('.chip-got', chipsRow);
|
||||
const favN = state.resFav.length, gotN = state.resGot.length;
|
||||
/* 「已领」排在「收藏」前面:领过的东西比收藏的更常回来找(要密码、要链接),
|
||||
两个都有内容时它更靠手。所以先插收藏、再插已领,已领就压在第二位。 */
|
||||
const favN = state.resFav.length;
|
||||
if (favN) chipsRow.insertBefore(favChip, chipsRow.children[1]);
|
||||
else chipsRow.appendChild(favChip);
|
||||
if (gotN) chipsRow.insertBefore(gotChip, chipsRow.children[1]);
|
||||
else chipsRow.appendChild(gotChip);
|
||||
favChip.textContent = favN ? '★ 收藏 ' + favN : '★ 收藏';
|
||||
gotChip.textContent = gotN ? '✓ 已领 ' + gotN : '✓ 已领';
|
||||
|
||||
/* fav / got 都不是分类,是「只看收藏过的 / 只看领过的」,所以各走一支 */
|
||||
const fav = state.resCat === 'fav', got = state.resCat === 'got';
|
||||
const keys = fav ? RES_ORDER.filter(k => state.resFav.indexOf(k) >= 0)
|
||||
: got ? RES_ORDER.filter(isGot)
|
||||
/* fav 不是分类,是「只看收藏过的」,所以单走一支 */
|
||||
const fav = state.resCat === 'fav';
|
||||
const keys = fav ? RES_ORDER.filter(isFav)
|
||||
: RES_ORDER.filter(k => state.resCat === 'all' || RES[k].cat === state.resCat);
|
||||
$('#resList').innerHTML = keys.map(resCardHTML).join('');
|
||||
bindResCards($('#resList'));
|
||||
$('#resEmptyHint').textContent = keys.length
|
||||
? (fav ? '已收藏 ' + keys.length + ' 份资源'
|
||||
: got ? '已领 ' + keys.length + ' 份 · 领过的资源随时回来这里取链接和提取码'
|
||||
: '共 ' + keys.length + ' 份资源 · 更多内容按孕周陆续解锁')
|
||||
: '共 ' + keys.length + ' 份资源 · 更多内容按孕周陆续更新')
|
||||
: (fav ? '还没有收藏 · 打开任意资源,点右上角 ☆ 就能收进来'
|
||||
: got ? '还没领过 · 打开任意资源,点底下「立即领取」就会记在这里'
|
||||
: '该分类下暂无资源,换一个试试~');
|
||||
$$('#resChips .chip').forEach(c =>
|
||||
c.classList.toggle('is-active', c.dataset.rescat === state.resCat));
|
||||
@@ -2684,18 +2632,14 @@
|
||||
bindResCards($('#rdMore'));
|
||||
/* 原来这里无条件重置成 ☆,收藏过的资源再打开会显示成没收藏。
|
||||
改成按 state.resFav 回显 */
|
||||
const isFav = state.resFav.indexOf(state.resKey) >= 0;
|
||||
const fav = state.resFav.indexOf(state.resKey) >= 0;
|
||||
const star = $('#resStar');
|
||||
star.classList.toggle('on', isFav);
|
||||
star.textContent = isFav ? '★' : '☆';
|
||||
/* 底部左键已从「收藏」改成「资源库」,是个固定文案的导航按钮,不再需要回显。 */
|
||||
/* 和上面收藏同一个毛病:原来无条件重置成「立即领取」,
|
||||
领过的资源再打开又变回没领过的样子。现在按 state.resGot 回显。
|
||||
领过之后按钮写「再拿一次链接」而不是「已领取 ✓」—— 它还是能点的,
|
||||
用户回来多半就是为了再看一眼提取码,写成完成态反而像被禁用了。 */
|
||||
const got = isGot(state.resKey);
|
||||
$('#rdGet').textContent = got ? '再拿一次链接' : '立即领取';
|
||||
$('#rdGet').classList.toggle('got', got);
|
||||
star.classList.toggle('on', fav);
|
||||
star.textContent = fav ? '★' : '☆';
|
||||
/* 底部左键已从「收藏」改成「资源库」,是个固定文案的导航按钮,不再需要回显。
|
||||
⚠️ 右键「立即领取」原来会按 state.resGot 回显成「再拿一次链接」,
|
||||
9.19 需求取消「已领」概念后改回**固定文案**(点它只是弹顾问二维码)。 */
|
||||
$('#rdGet').textContent = '立即领取';
|
||||
$('#detailRes .detail-body').scrollTop = 0;
|
||||
}
|
||||
$('#resStar').addEventListener('click', function () {
|
||||
@@ -2732,236 +2676,40 @@
|
||||
}
|
||||
});
|
||||
|
||||
/* 真正把一份资源交出去:填网盘弹窗 → 记进已领 → 刷列表和按钮 → 弹出来。
|
||||
从事件处理器里整个抽出来,是因为「用兑换码解锁成功」之后要原地做完全
|
||||
一样的事。抄一份过去当然更快,但那两份迟早会分叉:哪天改了领取行为
|
||||
(比如加个埋点、换个 toast),只改得到眼前这条路,走闸门解锁的那批用户
|
||||
拿到的还是旧行为,而且不报错、没人会发现。一个函数两处调,就没这回事。
|
||||
参数收的是 key 而不是读 state.resKey:解锁那条路要给的是**当初被拦下的
|
||||
那一份**,不是「详情页此刻停在哪一份」。 */
|
||||
function givePan(key) {
|
||||
const r = RES[key] || RES.book;
|
||||
const p = panOf(key);
|
||||
$('#panTitle').textContent = r.title.replace(/[《》]/g, '');
|
||||
$('#panUrl').value = p.url;
|
||||
$('#panCode').value = p.code;
|
||||
/* 记进 state.resGot 并落盘,列表页那张卡就打上「已领 ✓」。
|
||||
原来只给按钮加个 class,退出详情页就没了 —— 用户回到列表根本
|
||||
看不出自己领过哪几份,只能一个个点进去确认。
|
||||
重复点不再入库(已经领过就是再拿一次链接),renderResList 也就不用重画。 */
|
||||
if (!isGot(key)) {
|
||||
state.resGot.push(key);
|
||||
saveResGot();
|
||||
renderResList(state.resCat);
|
||||
}
|
||||
const btn = $('#rdGet');
|
||||
btn.classList.add('got');
|
||||
btn.textContent = '再拿一次链接';
|
||||
openModal('modalPan');
|
||||
haptic();
|
||||
}
|
||||
/* 点「立即领取」= 打开企业微信顾问二维码。
|
||||
⚠️ 2026-09-19(9.19 需求 1.2):这里原来是「兑换码闸门」—— 未解锁的用户先被拦下,
|
||||
填码解锁之后 H5 才把网盘链接交给他。需求方确认「扫码加顾问 → 回复「领资料」 →
|
||||
顾问直接发资料」之后,闸门、兑换码、网盘弹窗整条链路都删了(连同 givePan()、
|
||||
state.wxUnlocked、wxPending),这颗按钮现在只做一件事:弹二维码。
|
||||
不存在「已解锁 / 未解锁」两种起点,所以这里也没有任何状态判断 —— 别再加回来。 */
|
||||
$('#rdGet').addEventListener('click', () => { openWx(); });
|
||||
|
||||
/* 点「立即领取」不再假装存进书架,而是把网盘链接和提取码给出来。
|
||||
未解锁的用户先撞上企业微信二维码 —— 这是这一版加的那道闸门。 */
|
||||
$('#rdGet').addEventListener('click', () => {
|
||||
/* ⚠️⚠️ 这个 return 必须发生在 givePan 之前,也就是在 resGot.push 之前。
|
||||
写反了(先记一笔再拦)会一次坏两处:
|
||||
1. 这一份当场变成「已领」,用户关掉二维码再点一次,就从下面
|
||||
`!isGot(key)` 那个免验证通道大摇大摆走过去 —— 闸门两下失效;
|
||||
2. 列表页会给一份用户根本没拿到手的资料打上「已领 ✓」,
|
||||
他去「已领」筛选里找,找到的是一份自己从没见过链接的东西。
|
||||
所以:拦下的时候,一个字段都不许落盘。
|
||||
`!isGot(key)` 是有意的 —— 闸门上线前就领过的老用户继续免验证,
|
||||
已经给出去的东西不追回来(代价:那批人从此看不见这道闸门)。
|
||||
⚠️ 判定用 isGot(key) 而不是 state.resGot.length:那样一来,领过任意
|
||||
一份就等于全站解锁,闸门只在最开头拦一次。 */
|
||||
const key = state.resKey;
|
||||
if (!state.wxUnlocked && !isGot(key)) { openWx('gate', key); return; }
|
||||
givePan(key);
|
||||
});
|
||||
|
||||
/* ================= 企业微信顾问弹窗 =================
|
||||
两个入口共用这一个弹窗:
|
||||
gate —「我的资料被拦下了」,解锁成功后要接着把那份给他;
|
||||
card —「我的」页那张服务卡主动点开的,没有待领的东西。
|
||||
两条路的差别只有「解锁完之后做什么」,所以只用一个 wxPending 记着。 */
|
||||
const wxCodeEl = $('#wxCode'), wxErrEl = $('#wxErr'), wxModal = $('#modalWx');
|
||||
/* 被闸门拦下的那份资源 key。解锁成功后要用它把资料补给用户 ——
|
||||
用户当时点的是「立即领取」,不是「解锁」;让他解锁完再回去点一遍,
|
||||
等于把同一道摩擦向他收两遍。 */
|
||||
let wxPending = null;
|
||||
|
||||
function wxErr(msg) {
|
||||
wxErrEl.textContent = msg || '';
|
||||
wxCodeEl.classList.toggle('is-err', !!msg);
|
||||
}
|
||||
|
||||
/* 弹窗那句引导语里,把「全部解锁」四个字挑出来加重(用户点名要突出)。
|
||||
⚠️ 不用 innerHTML。这几句将来极可能要拼小名(babyWord 到处在拼),
|
||||
小名是用户自己敲进输入框的 —— 一旦它经 innerHTML 落地,那就是一条
|
||||
现成的注入路径,而且是在「这句话看起来只是文案」的伪装下。
|
||||
这里按纯文本切三段、中间那段包一个 <em>,拼进来什么都只会是字。
|
||||
⚠️ 找不到那四个字时**不能**当异常处理:done 分支根本没有这句话,
|
||||
直接整句塞回去就行,不要为了「一定要有高亮」去改文案。 */
|
||||
const LEAD_HI = '全部解锁';
|
||||
function setLead(el, text) {
|
||||
el.textContent = '';
|
||||
const i = text.indexOf(LEAD_HI);
|
||||
if (i < 0) { el.textContent = text; return; }
|
||||
el.appendChild(document.createTextNode(text.slice(0, i)));
|
||||
const em = document.createElement('em');
|
||||
em.className = 'lead-hi';
|
||||
em.textContent = LEAD_HI;
|
||||
el.appendChild(em);
|
||||
el.appendChild(document.createTextNode(text.slice(i + LEAD_HI.length)));
|
||||
}
|
||||
|
||||
/* 打开弹窗的**唯一**入口。所有和上一次打开有关的痕迹都在这里无条件清干净。
|
||||
⚠️ 五样东西每次都要重写,尤其是 wxPending —— 只在 gate 分支给它赋值、
|
||||
card 分支不管,会出这么一档事:闸门拦下《糖耐攻略》→ 用户按 Esc 关掉 →
|
||||
过一会儿从「我的」页服务卡进来输码 → 弹出的是《糖耐攻略》的网盘链接,
|
||||
他压根没要那份,而那份还被记成了「已领」。
|
||||
⚠️ 清理只能放在**打开**这一侧:closeModal() 是通用的(遮罩、Esc、右上角叉、
|
||||
openModal 开头那句都会走到它),它不认识 wxPending 也不该认识。 */
|
||||
function openWx(mode, key) {
|
||||
/* 默认清空,只有闸门才写。别改回 `mode === 'gate' ? (key||null) : null`
|
||||
那种三目 —— 写成默认行为,是让「非闸门入口不可能带着待领资源」这条
|
||||
不变量由结构保证,而不是靠某个分支记得写 null。
|
||||
(诚实交待:gift 入口现在根本解不了锁,所以「串台」这个场景已经无法
|
||||
复现,也就没有断言能量到这一行。它是结构性防线,不是被测行为。) */
|
||||
wxPending = null;
|
||||
if (mode === 'gate') wxPending = key || null;
|
||||
/* 已经解锁过的人再点福利入口,不该再被要一次兑换码 —— 那格输入框对他
|
||||
已经没有意义了(is-done 在 CSS 里把整个表单收起来)。
|
||||
⚠️ 这句只可能把 gift 抬成 done:gate 只在未解锁时才被调用。
|
||||
所以 done 的受众是 100%「已解锁的人来点试用装福利」,文案得照这个写。 */
|
||||
const m = state.wxUnlocked ? 'done' : mode;
|
||||
wxModal.classList.toggle('is-gate', m === 'gate');
|
||||
wxModal.classList.toggle('is-gift', m === 'gift');
|
||||
wxModal.classList.toggle('is-done', m === 'done');
|
||||
setLead($('#wxLead'),
|
||||
m === 'done' ? '资料已经解锁了。试用装每人限领一份,扫码找顾问登记,她会帮你安排。'
|
||||
: m === 'gate' ? '扫码添加顾问,说一句「领资料」就会把兑换码发给你。填进下面这格,资源库里的资料就全部解锁。'
|
||||
/* ⚠️ gift 里兑换码那一格是收起来的,所以这句**必须**把「码去哪儿填」讲明白。
|
||||
去掉输入框省下的那道摩擦,代价就是用户拿到码之后不知道往哪儿去 ——
|
||||
这句话是唯一补上这个断点的地方,别精简。
|
||||
也别写成「填进下面这格」:这儿根本没有那一格,而且 smoke 里有条
|
||||
断言正钉着这一点。 */
|
||||
: '扫码添加顾问,说一句「领试用装」就帮你登记,每人限领一份。她也会把资源库的兑换码一并发给你——到资源库点「立即领取」时填进去。');
|
||||
/* 右边那颗「解锁全部资料」只有闸门才有(is-done / is-gift 都被 CSS 收起来了)。
|
||||
done 模式下左键是唯一出口,「以后再说」不合适 —— 事情已经办完,没有什么「以后」。
|
||||
⚠️ gift 模式整条 .modal-foot 都被 `.is-gift .modal-foot{display:none}` 收起来了
|
||||
(原来那颗「先不领」按用户要求删了:对着一句「免费领」说「先不领」,
|
||||
像在替用户拒绝)。所以 gift 下这里赋什么值都不显示,留 '' 只是别留脏字。
|
||||
gift 的退路是右上角那颗 .modal-x —— 删按钮之前专门确认过它在,
|
||||
不然就把人关在弹窗里了。 */
|
||||
$('#wxLater').textContent = m === 'done' ? '知道了' : m === 'gift' ? '' : '以后再说';
|
||||
wxCodeEl.value = '';
|
||||
wxErr('');
|
||||
/* ================= 企业微信顾问弹窗(单态) =================
|
||||
三个入口共用这一个壳:「我的」页服务卡 #meWxCard、资源库顶部试用装 banner #rlGift、
|
||||
资料详情页「立即领取」#rdGet。三者说同一句话,openWx() 只有一个参数都没有。
|
||||
⚠️ 弹窗里**没有任何按钮**,唯一出口是右上角 .modal-x(外加遮罩 / Esc)——
|
||||
删按钮之前确认过它在;别做「头部无按钮时隐藏」这种优化。
|
||||
⚠️ 这里原来有 openWx(mode, key) + is-gate/is-gift/is-done 三模式类名 + wxPending +
|
||||
setLead() 高亮 + 兑换码校验 + #wxOk / #wxCode 监听,全部随闸门删除。
|
||||
别再引入「按入口改写文案」的分支:三个入口同一件事,分叉只会让以后改文案的人漏一条。 */
|
||||
const wxModal = $('#modalWx');
|
||||
function openWx() {
|
||||
openModal('modalWx');
|
||||
}
|
||||
|
||||
$('#meWxCard').addEventListener('click', () => openWx('gift'));
|
||||
/* 资源库顶部那条 banner 和「我的」页服务卡是同一件事、同一个入口模式。
|
||||
⚠️ 走 openWx('gift') 而不是挂 data-open="modalWx":那条通用通道直接调
|
||||
openModal,会绕过上面那一整套重置。 */
|
||||
$('#rlGift').addEventListener('click', () => openWx('gift'));
|
||||
/* 一改就把红字收掉。让上一次的报错挂在那儿,用户会以为自己刚敲的这个也错了 */
|
||||
wxCodeEl.addEventListener('input', () => wxErr(''));
|
||||
wxCodeEl.addEventListener('keydown', e => { if (e.key === 'Enter') $('#wxOk').click(); });
|
||||
$('#meWxCard').addEventListener('click', () => openWx());
|
||||
/* 资源库顶部那条 banner 和「我的」页服务卡是同一件事、同一个入口。
|
||||
⚠️ 走 openWx() 而不是挂 data-open="modalWx":那条通用通道直接调 openModal,
|
||||
不经过这里 —— 今天两者等价,但留一个入口,以后要统一加埋点/清理只改一处。 */
|
||||
$('#rlGift').addEventListener('click', () => openWx());
|
||||
|
||||
$('#wxOk').addEventListener('click', () => {
|
||||
/* ⚠️ 只有闸门模式才受理。别删,也别当成「CSS 都 display:none 了,多余」:
|
||||
- jsdom 实测:display:none 的按钮 dispatchEvent 照样触发监听;
|
||||
- 真机上今天确实点不到,但**被触发的后果比意外解锁更难看** ——
|
||||
「兑换码不对」那行红字会冒在一个根本没有输入框的弹窗上,
|
||||
用户看着一个无处可填的界面被指责填错了;
|
||||
- 两条看起来无害的改动会立刻把它变成活 bug:把上面那条 display:none
|
||||
换成 visibility:hidden(视觉一模一样,但仍可聚焦、仍能回车),
|
||||
或者给弹窗套一层 <form>(回车默认提交)。
|
||||
读 classList 而不是另立一个 wxMode 变量:模式本来就存在那儿,
|
||||
openWx() 是唯一写入方,多一个变量就多一处会走神的真相。 */
|
||||
if (!wxModal.classList.contains('is-gate')) return;
|
||||
if (!wxCodeOK(wxCodeEl.value)) {
|
||||
/* 码不对:给出红字、标红输入框、把光标送回去。
|
||||
⚠️ 一个字段都不落盘 —— 不写 wxUnlocked、不动 resGot。 */
|
||||
wxErr('兑换码不对,跟顾问再要一次?');
|
||||
wxCodeEl.focus();
|
||||
haptic();
|
||||
return;
|
||||
}
|
||||
state.wxUnlocked = true;
|
||||
saveWxUnlocked();
|
||||
syncUnlockCopy();
|
||||
/* 取出来就清空:这一份已经交待完了,不能留到下一次打开还生效 */
|
||||
const key = wxPending;
|
||||
wxPending = null;
|
||||
if (key) givePan(key); // givePan 里的 openModal 会顺手把这个弹窗换掉
|
||||
else closeModal();
|
||||
/* 等弹窗切换的那段动画走完再冒 toast,否则它和正在上移的网盘弹窗叠在一起 */
|
||||
setTimeout(() => toast('已解锁 · 资源库里的资料随时来取'), 260);
|
||||
});
|
||||
|
||||
/* 未解锁时,两处「已为你解锁 N 份资源」是句谎话 —— 用户读完这句去点领取,
|
||||
撞上的是一张二维码。所以锁着的时候换个说法,并且把「首次领取要加顾问」
|
||||
提前说清楚,别等人点下去才发现。
|
||||
⚠️ 首页那句只改 <span id="shelfLeadTxt"> 这一层,绝不能对整个 <p> 写
|
||||
innerHTML:里面还嵌着「查看全部 ›」那个 <a data-open>,它的点击监听是
|
||||
init 时 $$('[data-open]') 一次性绑上去的。整段重写会把节点连同监听一起
|
||||
换掉,而且是**静默**的 —— 链接看着还是品牌色,就是点不动,控制台也不报错。 */
|
||||
function syncUnlockCopy() {
|
||||
$('#shelfLeadTxt').innerHTML = state.wxUnlocked
|
||||
? '已为你解锁 <b>128</b> 份资源'
|
||||
: '<b>128</b> 份孕育资料等你来领';
|
||||
$('#rlLead').innerHTML = state.wxUnlocked
|
||||
? '已为你解锁 <b>128</b> 份资源 · 按孕周持续更新'
|
||||
: '<b>128</b> 份资源按孕周持续更新 · 首次领取需添加顾问微信';
|
||||
/* 解锁过的人不再看到试用装 banner —— 他早就加过顾问了,这条对他是噪声。
|
||||
⚠️ banner 的显隐**只有这一个 owner**。别在 renderResList 里再 toggle
|
||||
一遍:两个地方管同一件事,迟早跑偏成「一边显一边隐」。
|
||||
时机是够的:#rlGift 是静态标记,renderResList 只重写 #resList.innerHTML
|
||||
和上面那排 chips,碰不到它;而 init() 在 load 时**同步**调了这个函数,
|
||||
资源库面板必然在那之后才可能被打开 —— 没有「闪一下」的窗口。
|
||||
⚠️ 刻意的不一致:banner 藏了,「我的」页那张服务卡**不藏**。
|
||||
那是「我的」页的常驻入口,藏掉会缺一块。所以已解锁的人在「我的」页
|
||||
仍会看到福利文案 —— 这可接受,前提是弹窗 done 那段文案把
|
||||
「试用装限领一份」讲清楚(已经写了),动线才连得上。不是 bug。 */
|
||||
$('#rlGift').classList.toggle('hide', state.wxUnlocked);
|
||||
}
|
||||
|
||||
const panText = () => '链接:' + $('#panUrl').value + ' 提取码:' + $('#panCode').value;
|
||||
$('#panCopy').addEventListener('click', () => {
|
||||
const txt = panText();
|
||||
// navigator.clipboard 需要安全上下文,微信 / 老安卓里常常没有,
|
||||
// 兜底用 input.select() + execCommand,两条路都断了才提示手动长按复制
|
||||
const fallback = () => {
|
||||
const el = $('#panUrl');
|
||||
try {
|
||||
el.value = txt; el.removeAttribute('readonly');
|
||||
el.select(); el.setSelectionRange(0, txt.length);
|
||||
const done = document.execCommand && document.execCommand('copy');
|
||||
el.setAttribute('readonly', 'readonly'); el.value = panOf(state.resKey).url;
|
||||
el.blur();
|
||||
toast(done ? '链接和提取码已复制' : '请长按上方链接手动复制');
|
||||
} catch (e) {
|
||||
el.setAttribute('readonly', 'readonly');
|
||||
toast('请长按上方链接手动复制');
|
||||
}
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(txt).then(() => toast('链接和提取码已复制'), fallback);
|
||||
} else fallback();
|
||||
haptic();
|
||||
});
|
||||
$('#panOpen').addEventListener('click', () => {
|
||||
const url = $('#panUrl').value;
|
||||
// 先复制提取码再跳,省得用户到了网盘还要回来翻
|
||||
if (navigator.clipboard && navigator.clipboard.writeText)
|
||||
navigator.clipboard.writeText($('#panCode').value).catch(() => {});
|
||||
toast('提取码 ' + $('#panCode').value + ' 已复制,正在打开网盘');
|
||||
window.open(url, '_blank', 'noopener');
|
||||
haptic();
|
||||
});
|
||||
/* ⚠️ 2026-09-19(9.19 需求):这里原来有一个 syncUnlockCopy(),按「已解锁 / 未解锁」
|
||||
把首页书架与资源库的两句引导语改成两个版本,并给资源库顶部试用装 banner 挂 .hide。
|
||||
兑换码闸门取消后不存在解锁态:
|
||||
· 两句文案改为 index.html 里的**静态文案**(#shelfLeadTxt / #rlLead 的 id 也删了,
|
||||
顺带解掉「给 <p> 写 innerHTML 会静默换掉「查看全部 ›」的监听」那个坑);
|
||||
· banner 恒显示,`.hide` 那套不再需要。
|
||||
别把这个函数加回来 —— 它唯一的存在理由就是「两种解锁状态」。 */
|
||||
|
||||
/* ================= 档案生成:周档案 / 完整档案 =================
|
||||
产物必须是一张图(要能存相册、发朋友圈、拿去打印),所以直接用 canvas
|
||||
@@ -4159,12 +3907,12 @@
|
||||
进门先按真实 state 刷一遍,免得首屏闪一下旧数字 */
|
||||
syncPregnancy();
|
||||
/* ⚠️ 这里原来还有一次 syncBio()。一句话签名整块已删(见 state 里那段说明)。 */
|
||||
/* 解锁状态是从 localStorage 读回来的,标记里写的是「没解锁」那一版。
|
||||
老用户(上次已经输过码)不刷这一下,会在首页看到「还没领」的说法,
|
||||
点进去却畅通无阻 —— 文案和实际能力对不上。 */
|
||||
syncUnlockCopy();
|
||||
/* ⚠️ 这里原来还有一次 syncUnlockCopy()(按解锁态改写首页/资源库两句引导语)。
|
||||
闸门取消后文案是静态的,函数已删。 */
|
||||
animateRings();
|
||||
$('#tabBadge').classList.toggle('show', state.unread > 0);
|
||||
/* ⚠️ 角标 #tabBadge 的 owner 是 laixin 未读数(syncLaixinBadge),不是本地信件数。
|
||||
这里曾经用 state.unread 初始它,现已删除 —— 两处都写会互相覆盖。
|
||||
角标在 syncLaixinBadge() 拉到数之前保持隐藏。 */
|
||||
/* 来信页的默认标签:永远落在「全部」。
|
||||
曾经改成过「有未读就落在未读」,但那样一进页面就看不到全部信件,
|
||||
未读的红点本来就在提示了,不需要再替人把列表筛一遍。
|
||||
@@ -4190,39 +3938,17 @@
|
||||
// 第四批远程数据(今日脚印,来自数胎动)—— 渲染首页 .today-line 时间轴,
|
||||
// 拉不到/未关联都各有回退,见 applyRemoteFootprint 内的分支说明
|
||||
applyRemoteFootprint();
|
||||
// 底 tab「宝宝来信」未读角标(box_app 代理第三方 laixin 未读数)
|
||||
syncLaixinBadge();
|
||||
}
|
||||
/* ---------- 远程配置(box_app 后台下发) ----------
|
||||
本页托管在 box_app(/mama-box/)时,启动后拉一次运营配置:
|
||||
PAN / PAN_DEFAULT 网盘链接表和 WX_CODE 兑换码都由后台统一配。
|
||||
拉不到 —— 静态托管在别处 / 断网 / 接口挂了 —— 就用上面写死的内置值,
|
||||
本页「单文件离线可用」的性质不受影响。
|
||||
⚠️ PAN 的覆盖是「整表替换」不是逐项合并:后台删掉某条链接的语义就是
|
||||
「这条不配了,走 PAN_DEFAULT」,逐项合并会让删除永远删不掉。
|
||||
⚠️ 竞态说明:fetch 是异步的,回来之前用户可能已经点开过详情页——
|
||||
那一次看到的是内置值,回来之后的都走新值。panOf()/wxCodeOK()
|
||||
是实时读表的,不需要重新渲染。 */
|
||||
(function fetchRemoteConfig() {
|
||||
fetch('/api/mp/public/mama-box/config')
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(j => {
|
||||
if (!j || j.code !== 200 || !j.result) return;
|
||||
const c = j.result;
|
||||
if (c.isEnabled === false) return;
|
||||
Object.keys(PAN).forEach(k => { delete PAN[k]; });
|
||||
if (c.pan && typeof c.pan === 'object') {
|
||||
Object.keys(c.pan).forEach(k => {
|
||||
const p = c.pan[k];
|
||||
if (p && p.url) PAN[k] = { url: String(p.url), code: String(p.code || '') };
|
||||
});
|
||||
}
|
||||
if (c.panDefault && c.panDefault.url) {
|
||||
PAN_DEFAULT.url = String(c.panDefault.url);
|
||||
PAN_DEFAULT.code = String(c.panDefault.code || '');
|
||||
}
|
||||
if (c.wxCode) WX_CODE = String(c.wxCode);
|
||||
})
|
||||
.catch(() => { /* 静默回退内置值 */ });
|
||||
})();
|
||||
/* ---------- 远程配置(box_app 后台下发):已下线 ----------
|
||||
⚠️ 2026-09-19(9.19 需求 1.2):这里原来拉 /api/mp/public/mama-box/config,
|
||||
用后台下发的 pan / panDefault / wxCode 覆盖页面里的网盘链接表与兑换码。
|
||||
H5 不再发放网盘链接(资料由顾问在微信里直接发)之后,配置里已经没有一项是
|
||||
H5 还读的,整个 fetchRemoteConfig() 删除。
|
||||
⚠️ 后端那个接口与 MamaBoxConfig 模型(pan / panDefault / wxCode)暂时**保留未删** ——
|
||||
它们还有管理端读写、以及 /resources/{key}/claim 那条老接口在用;
|
||||
等确认没有任何消费者之后再一起清理(见 docs/mama-box-change-list-20260919.md 的 T1b)。 */
|
||||
|
||||
/* ---------- 远程个人档案(box_app 下发,openid 单一身份键) ----------
|
||||
小程序首页把 openid 经 #hash 传给本页(微信 web-view 丢 query,hash 不丢),
|
||||
@@ -4261,7 +3987,7 @@
|
||||
LETTERS.splice(0, LETTERS.length);
|
||||
state.curLetter = ''; state.homeLetter = '';
|
||||
syncPregnancy(); /* 占位 + 隐藏脚印时间轴 + 信件引导态 */
|
||||
syncLetterCounts(); /* hero 计数 '--'、未读角标隐藏 */
|
||||
syncLetterCounts(); /* hero 计数切 '--'(角标 #tabBadge 已不由本地信件数驱动) */
|
||||
return;
|
||||
}
|
||||
const base = '/api/mp/public/mama-box/';
|
||||
@@ -4318,13 +4044,11 @@
|
||||
⚠️ 下面 stampLetters/filterLetters/syncStaticDates 全按 state.week 重算,
|
||||
而 pregEmpty 时 state.week 还是内置默认 27 —— 不拦住的话,
|
||||
刚写好的占位会被改回「孕 27 周 / 9月15日」演示值。 */
|
||||
if (state.pregEmpty) { $('#tabBadge').classList.remove('show'); return; }
|
||||
if (state.pregEmpty) { return; }
|
||||
syncBabyWords();
|
||||
stampLetters();
|
||||
filterLetters(state.letterFilter);
|
||||
syncUnlockCopy();
|
||||
syncStaticDates();
|
||||
if (state.unread > 0) $('#tabBadge').classList.toggle('show', true);
|
||||
}).catch(function () { /* 静默回退内置值 */ });
|
||||
}
|
||||
|
||||
@@ -4345,11 +4069,12 @@
|
||||
body: old.body || [],
|
||||
};
|
||||
order.push(it.key);
|
||||
RES[it.key].fav = !!it.fav; RES[it.key].got = !!it.got;
|
||||
RES[it.key].fav = !!it.fav;
|
||||
/* ⚠️ 接口还返回 got(已领),但 H5 侧已无「已领」概念(9.19 需求),
|
||||
这里**有意不读** —— 别顺手把它加回 RES/state。 */
|
||||
});
|
||||
// 同步收藏/已领状态(对齐后端)
|
||||
// 同步收藏状态(对齐后端)。已领(resGot)已删除,见文件顶部 state 那段说明。
|
||||
state.resFav = items.filter(function (i) { return i.fav; }).map(function (i) { return i.key; });
|
||||
state.resGot = items.filter(function (i) { return i.got; }).map(function (i) { return i.key; });
|
||||
renderResList(state.resCat);
|
||||
}
|
||||
function __applyRecords(rec) {
|
||||
@@ -4557,13 +4282,68 @@
|
||||
toast('请在微信小程序中打开商城');
|
||||
}
|
||||
}
|
||||
/* 第九批:底部 tab「兜爸心选」(#tabMall)也走同一个 goMall()。
|
||||
它**不挂 data-tab**,因此不在上面的 tabs 集合里、不参与 go() 高亮切换;
|
||||
点击只做「跳商城」这一件事。落点与「我的」页「进入商城」完全一致。 */
|
||||
$$('#btnMall, #meMallCard, #tabMall').forEach(function (b) {
|
||||
/* 商城入口:「我的」页服务卡与首页商城主卡。
|
||||
⚠️ 2026-09-19:底 tab #tabMall 已经改回「宝宝来信」(见下面的 #tabLetter 绑定),
|
||||
不再走 goMall —— 所以这里只留两颗真正的商城入口。 */
|
||||
$$('#btnMall, #meMallCard').forEach(function (b) {
|
||||
b.addEventListener('click', goMall);
|
||||
});
|
||||
|
||||
/* ---------- 宝宝来信(第三方 H5)入口 ----------
|
||||
底 tab「宝宝来信」(#tabLetter) → 跳 mp.yizhezhe.com/laixin/。
|
||||
⚠️ 2026-09-19(9.19 需求第 2 条,D2=C):这枚 tab 原来跳的是小程序原生商城页
|
||||
(goMall),现在改跳**第三方宝宝来信 H5**。落点不是硬编码,而是走 App 表:
|
||||
用户在应用后台新增了「宝宝来信」应用(identifier=laixin,URL 由
|
||||
/api/mp/apps 下发),所以这里复用 jumpApp() —— 与宫格 / 工具卡跳其它 H5 应用
|
||||
完全一致(location.href 同页跳转 + query 带 openid/unionid + 上报分享地址)。
|
||||
⚠️ 拿不到 App 表记录时的兜底写死业务域名入口:mp.yizhezhe.com/laixin/(nginx 反代
|
||||
到 taidong.douba.top/laixin/)。两边都没有就只能提示,别静默什么都不做。
|
||||
⚠️ 这枚 tab **不挂 data-tab**,所以它不在 tabs 集合里、不参与 go() 高亮切换 ——
|
||||
跳过去就离开本页了,没有高亮态可言。 */
|
||||
const LAIXIN_FALLBACK_URL = 'https://mp.yizhezhe.com/laixin/';
|
||||
function openLetterApp() {
|
||||
const app = APP_BY_ID['laixin'];
|
||||
const url = (app && app.url) ? app.url : LAIXIN_FALLBACK_URL;
|
||||
if (!url) { toast('「宝宝来信」即将上线'); return; }
|
||||
jumpApp(url);
|
||||
}
|
||||
$('#tabLetter').addEventListener('click', openLetterApp);
|
||||
|
||||
/* ---------- 底 tab 角标:宝宝来信未读数 ----------
|
||||
数据源:box_app 封装的联动接口 GET /api/mp/public/mama-box/laixin/unread
|
||||
(box_app 后端再去调第三方 https://taidong.douba.top/laixin/api/laixin/unread)。
|
||||
⚠️ 为什么不直连 taidong:那台的 CORS 白名单只放了自己的域名,页面直连会被浏览器拦;
|
||||
而且「mama-box ↔ laixin 的联动接口由 box_app 封装」是本项目的既定边界
|
||||
(见 docs/mama-box-change-list-20260919.md §1.5)。
|
||||
⚠️ 口径:#tabBadge 的 owner **只有这一个函数**。别在 syncLetterCounts() 或
|
||||
applyRemoteProfile() 里再写它 —— 内置来信屏的未读数(box_app /letters)
|
||||
与 laixin 的未读数是两套数,两处都写会互相覆盖(谁后跑谁说了算)。
|
||||
⚠️ 容错:接口返回 ok=false(上游抖动)时**保持现状** —— 既不把已经点亮的红点抹掉,
|
||||
也不因为一次失败就冒出一个红点。角标不是关键路径,失败一律静默。 */
|
||||
function syncLaixinBadge() {
|
||||
const badge = $('#tabBadge');
|
||||
if (!badge) return;
|
||||
const openid = hashParam('openid');
|
||||
/* 接口按 openid 查,没有身份就别问(浏览器直开调试时常见)——此时不显示角标 */
|
||||
if (!openid) { badge.classList.remove('show'); return; }
|
||||
const unionid = hashParam('unionid');
|
||||
const qs = 'openid=' + encodeURIComponent(openid) +
|
||||
(unionid ? '&unionid=' + encodeURIComponent(unionid) : '');
|
||||
fetch('/api/mp/public/mama-box/laixin/unread?' + qs)
|
||||
.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);
|
||||
})
|
||||
.catch(() => { /* 静默 */ });
|
||||
}
|
||||
/* 从「宝宝来信」H5 返回、或小程序 onShow 把 web-view 唤回前台时刷一次
|
||||
(上游未读数是「在来信页读完就归零」,所以回来这一下必须重新拉)。 */
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) syncLaixinBadge();
|
||||
});
|
||||
|
||||
/* 「授权完善资料」:H5 → navigateTo → 小程序端独立授权页(pages/annex/auth/index)。
|
||||
说明:微信 web-view 的 postMessage 非实时送达(只在分享/后退/onHide 等时机
|
||||
才派发 bindmessage),点按钮时端上收不到 → 改用 wx.miniProgram.navigateTo
|
||||
|
||||
Reference in New Issue
Block a user