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:
2026-09-20 01:36:06 +08:00
parent d39e52cf4f
commit aa53d5c6e6
3 changed files with 238 additions and 559 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:26091802 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26092001 缓存版本标记:改 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 第四节) -->
@@ -18,10 +18,7 @@
<div class="phone" id="phone">
<nav class="tabbar" id="tabbar">
<button class="tab is-active" data-tab="home">
<!-- ⚠️ 未读红点 #tabBadge 本批(第九批)从原「宝宝来信」tab 挪到这里:
那枚 tab 改成了商城入口(见下方 #tabMall),把信件未读红点挂在商城里会误导。
挪动是纯 DOM 位置调整,syncLetterCounts/applyRemote* 仍只按 id 取,JS 一行没改。 -->
<span class="t-ico ic-home"></span><span class="t-badge" id="tabBadge"></span><span class="t-txt">首页</span>
<span class="t-ico ic-home"></span><span class="t-txt">首页</span>
</button>
<button class="tab" data-tab="archive">
<span class="t-ico ic-archive"></span><span class="t-txt">档案</span>
@@ -29,16 +26,18 @@
<button class="tab tab-fab" data-sheet="open">
<span class="fab"><img src="assets/img/69818ea975da.png" alt="" aria-hidden="true"></span><span class="t-txt">记一下</span>
</button>
<button class="tab" id="tabMall">
<!-- ⚠️ 本批(第九批)按用户要求,把原「宝宝来信」这枚底 tab 改成「兜爸心选」商城入口。
⚠️ 与「档案」tab 同款:**不挂 data-tab**,点了直接走 goMall() 跳小程序原生商城页
(switchTab /pages/goods_cate/goods_cate),所以它**没有** is-active 高亮态,
也不参与 go() 的 tab 高亮切换。
⚠️ 来信屏 #screen-letter **没有被删**:它仍在 swipe() 的 order 里,左右滑动还能进;
只是底 tab 不再提供入口(用户本批明确选择「保留屏幕、滑动仍可进入」)。
想再把这枚 tab 挂回来信之前先去问,别自己动。
⚠️ 未读红点已挪到「首页」tab(原 #tabBadge 的位置),别再挂回这枚商城 tab。 -->
<span class="t-ico ic-shop"></span><span class="t-txt">兜爸心选</span>
<button class="tab" id="tabLetter">
<!-- ⚠️ 2026-09-19(9.19 需求第 2 条):这枚 tab 从「兜爸心选」商城入口改回「宝宝来信」。
与第九批那次改动**方向相反**,但落点不同:这次不是切回内置来信屏,
而是跳**第三方「宝宝来信」H5**(mp.yizhezhe.com/laixin/)。
⚠️ 因此依旧**不挂 data-tab**:它不参与 go() 的 tab 高亮切换,也不切 #screen-letter。
点了走 jumpApp()(同页 location.href + query 带 openid/unionid),
与其它 H5 应用(宫格、工具卡)完全一致 —— 见 app.js 的 #tabLetter 绑定。
⚠️ 内置来信屏 #screen-letter **仍在**(swipe 的 order 里,左右滑动还能进),
它的未读数(box_app /letters)与这枚 tab 的角标**不是同一套数**:
角标 owner 是 laixin 的 /laixin/unread,见 syncLaixinBadge()。
⚠️ 红点 #tabBadge 挂在这枚 tab 上(第九批曾挪到「首页」,现已挪回)。 -->
<span class="t-ico ic-letter"></span><span class="t-badge" id="tabBadge"></span><span class="t-txt">宝宝来信</span>
</button>
<button class="tab" data-tab="me">
<span class="t-ico ic-me"></span><span class="t-txt">我的</span>
@@ -357,12 +356,13 @@
<span class="sb-row"><span class="sb-faces" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></span><i class="sb-got">9.6 万妈妈已领取</i><span class="pill-free">免费</span></span>
</button>
</div>
<!-- ⚠️ 那句话包在 <span id="shelfLeadTxt"> 里,是为了给 syncUnlockCopy()
一个**只含文字**的靶子。旁边「查看全部 ›」是个 <a data-open>,
它的点击监听是 init 时一次性绑上去的;谁要是图省事对整个 <p>
写 innerHTML,链接会连同监听被静默换掉 —— 看着一模一样,就是点不动。
这里写的是「还没解锁」那一版,解锁过的用户由 init() 刷成另一句。 -->
<p class="unlock"><span id="shelfLeadTxt"><b>128</b> 份孕育资料等你来领</span> · <a data-open="detailResList">查看全部 ›</a></p>
<!-- ⚠️ 这句话现在是**静态文案**(2026-09-19 9.19 需求:兑换码闸门取消,
不再有「已解锁/未解锁」两版口径,原来那个给 syncUnlockCopy() 用的
#shelfLeadTxt 靶子和该函数一起删了)。
旁边「查看全部 ›」是个 <a data-open>,它的点击监听是 init 时一次性
绑上去的 —— 谁要是图省事对整个 <p> 写 innerHTML,链接会连同监听被静默
换掉(看着一模一样,就是点不动),别再引入这种写法。 -->
<p class="unlock"><b>128</b> 份孕育资料等你来领 · <a data-open="detailResList">查看全部 ›</a></p>
</section>
</div>
</section>
@@ -1022,7 +1022,7 @@
</span>
<span class="wx-main">
<span class="wx-head">
<span class="wx-title">兜爸心选</span>
<span class="wx-title">兜爸心选商城</span>
<span class="wx-lim">母婴 · 好物</span>
</span>
<span class="wx-sub">为宝宝甄选的母婴好物,值得认真挑一挑</span>
@@ -1190,8 +1190,8 @@
⚠️ 里面这张示意图**绝不能带 .wx-qr 类**。冒烟测试有一条钉着
「全页只有一处二维码」,蹭了那个类名会当场变红 —— 而且它也确实
不是二维码。用独立的 .rlg-fig。
⚠️ 已解锁的用户看不到这条 banner,由 syncUnlockCopy() 挂 .hide 完成。
服务卡则**不**跟着藏,理由写在服务卡那段注释里。
⚠️ 2026-09-19:兑换码闸门取消后不再有「已解锁」状态,所以这条 banner
**恒显示**(原来 syncUnlockCopy() 会按解锁态给它挂 .hide,已删除)。
换真图:把 .rlg-fig 整个 <svg> 换成一个 img,类名照旧写 rlg-fig,
CSS 一行不用动(.rlg-fig 同时管 svg 和 img)。
⚠️ 这里**不要**写出图片路径的字面量 —— build.js 的 ASSET_RE
@@ -1273,10 +1273,10 @@
⚠️ 必须是 <span>:外层 .rl-gift 本身是 <button>,按钮套按钮非法。 -->
<span class="rlg-cta">立即免费领取</span>
</button>
<!-- 静态值写「还没解锁」那一版,解锁过的用户由 syncUnlockCopy() 改口。
<!-- 静态文案(原「还没解锁 / 已解锁」两版已随闸门一起取消)。
「首次领取需添加顾问微信」这句必须提前说:让人一路读到底、点了
「立即领取」才撞上二维码,是把条件藏到最后一步才亮出来。 -->
<p class="rl-lead" id="rlLead"><b>128</b> 份资源按孕周持续更新 · 首次领取需添加顾问微信</p>
<p class="rl-lead"><b>128</b> 份资源按孕周持续更新 · 首次领取需添加顾问微信</p>
<div class="chips-row" id="resChips">
<button class="chip is-active" data-rescat="all">全部</button>
<button class="chip" data-rescat="产检">产检</button>
@@ -1286,11 +1286,10 @@
<button class="chip" data-rescat="心理">心理</button>
<!-- 收藏不是分类而是筛选,所以默认排在最后。但一旦用户有了收藏,
renderResList() 会把它挪到「全部」后面并带上数量 —— 常用的东西
不该让人横向滑到头才找得着。这里写的是「还没收藏过」时的样子 -->
不该让人横向滑到头才找得着。这里写的是「还没收藏过」时的样子。
⚠️ 原来的「✓ 已领」chip 已删除:9.19 需求取消了「立即领取即到手」
这个动作(资料由顾问在微信里发),H5 侧没有「已领」这个概念了。 -->
<button class="chip chip-fav" data-rescat="fav">★ 收藏</button>
<!-- 「已领」同理,也是筛选不是分类。领过东西之后它会被挪到「全部」后面
(比收藏还靠前 —— 回来找提取码比回来翻收藏更常发生) -->
<button class="chip chip-got" data-rescat="got">✓ 已领</button>
</div>
<div class="res-list" id="resList"></div>
<p class="unlock" id="resEmptyHint"></p>
@@ -1563,52 +1562,29 @@
</div>
</section>
<!-- ================= 百度网盘领取 =================
资源本体放在网盘上,详情页只负责呈现内容;点「立即领取」才把
链接和提取码给出来。链接与提取码在 js 顶部的 PAN 表里配置
(后台改那一处就行,页面不用动)。 -->
<section class="modal" id="modalPan" role="dialog" aria-modal="true" aria-labelledby="modalPanTitle">
<div class="modal-head">
<h3 id="modalPanTitle">资源已为你准备好</h3>
<button class="modal-x" data-modal="close" aria-label="关闭">
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
</svg>
</button>
</div>
<p class="modal-lead">《<b id="panTitle">糖耐检查全攻略</b>》已上传到百度网盘,复制下面的链接到浏览器或网盘 App 打开即可。</p>
<div class="pan-box">
<div class="pan-row">
<span class="pan-k">网盘链接</span>
<!-- readonly 而不是纯文本:iOS 上长按可直接「全选 / 拷贝」 -->
<input class="pan-v" id="panUrl" type="text" readonly value="">
</div>
<div class="pan-row">
<span class="pan-k">提取码</span>
<input class="pan-v code" id="panCode" type="text" readonly value="">
</div>
</div>
<p class="pan-note">链接长期有效。若提示「链接已失效」,可在「帮助与反馈」里联系我们补发。</p>
<div class="modal-foot">
<button class="dl-btn ghost" id="panCopy">复制链接和提取码</button>
<button class="dl-btn main" id="panOpen">打开网盘</button>
</div>
</section>
<!-- ================= 百度网盘领取(已下线) =================
⚠️ 2026-09-19(9.19 需求 1.2 + 需求方确认):资料改由企业微信顾问**直接发放**,
H5 不再发网盘链接和提取码 —— 原来这个 #modalPan 弹窗、js 里的 PAN/PAN_DEFAULT
链接表、givePan()、「已领」标记已整块删除。要恢复「H5 自己发链接」,
得先回问需求方(那等于把「解锁」概念加回来)。
⚠️ 后端 /api/mp/public/mama-box/config 里的 pan / panDefault / wxCode 三个字段
也随之失效(box_app 侧后续清理)。 -->
<!-- ================= 加企业微信顾问 =================
三个入口共用这一个壳,模式由 openWx() 挂类名决定:
gift —— 「我的」页服务卡 + 资源库顶部那条试用装 banner。
**没有兑换码输入框**:这两处主打的是免费领试用装,
顾问会把资料兑换码连同试用装一起发给用户,码在闸门那儿填。
gate —— 点「立即领取」被拦下时。**只有这里有输入框**。
done —— 已解锁的人再从 gift 那两个入口进来。
⚠️ 静态类写 is-gift(原来是 is-card)。默认态因此从「显示输入框」
变成「不显示」—— 万一有人绕过 openWx() 直接 openModal('modalWx'),
他看到的是一张二维码,而不是一个怎么填都不对的码框。
⚠️ 2026-09-19(9.19 需求 1.1 / 1.2):这个弹窗从「兑换码闸门」简化成**纯提示**。
改动前后:
· 删掉兑换码输入框 + 「以后再说」「解锁全部资料」两颗按钮(需求 1.2);
· 原来的 is-gate / is-gift / is-done 三模式**合并成单态** —— 不再有「解锁」
这个状态,弹窗只负责把人引导到顾问那儿,资料由顾问在微信里直接发(需求方确认);
· 二维码下方文案改成需求指定的「请添加资料小助手,回复『领资料』」(需求 1.1)。
三个入口(资料详情「立即领取」、「我的」页服务卡、资源库试用装 banner)
共用这一个壳、同一句话,openWx() 不再按入口改写文案。
⚠️ data-autofocus="off":这个弹窗的主角是中间那张二维码,用户第一件事
是举起另一台手机去扫它。默认的自动聚焦会把输入法键盘顶起来,
正好盖住二维码下半截 —— 人得先把键盘收掉才能扫。 -->
<section class="modal wx-modal is-gift" id="modalWx" data-autofocus="off"
正好盖住二维码下半截 —— 人得先把键盘收掉才能扫。
⚠️ 弹窗**没有底部按钮**:唯一出口是右上角 .modal-x(外加遮罩 / Esc)。
别把「优化掉没有按钮的头部」这种想法落到这颗叉上 —— 那会把人关在弹窗里。 -->
<section class="modal wx-modal" id="modalWx" data-autofocus="off"
role="dialog" aria-modal="true" aria-labelledby="wxTitle">
<div class="modal-head">
<!-- 兜底写「宝宝的」;填过小名的由 syncBabyWords() 刷成小名。
@@ -1621,10 +1597,10 @@
</svg>
</button>
</div>
<!-- 静态兜底写 gift 那一版(和上面的静态类 is-gift 配套):
运行时三种模式的文案都由 openWx() 改写,这里只保证「万一没走 openWx」
时读到的是一句说得通的话,而不是一句要人填码、却没有码框的话。 -->
<p class="modal-lead" id="wxLead">扫码添加顾问,说一句「领试用装」就帮你登记,每人限领一份。她也会把资源库的兑换码一并发给你 —— 到资源库点「立即领取」时填进去。</p>
<!-- 单态文案:说清「加顾问 → 说一句领资料 → 她直接发」,不要求用户填任何东西。
⚠️ 称呼是「顾问」(与标题一致);二维码下方那句按需求原文写「资料小助手」。
这一处不一致是需求方明确保留的(见改动清单 D3),别顺手「统一」。 -->
<p class="modal-lead" id="wxLead">扫码添加顾问,说一句「领资料」,她会把资料直接发给你。</p>
<div class="wx-qr-box">
<!-- ⚠️ 这是**占位**二维码,扫不出任何东西。
@@ -1659,34 +1635,8 @@
<rect x="14" y="45" width="72" height="14" rx="4" fill="rgba(243,236,230,.94)"/>
<text x="50" y="54.6" text-anchor="middle" font-size="8.4" font-weight="700" fill="#9A8B80">示例二维码 · 待替换</text>
</svg>
<p class="wx-qr-cap">长按识别,或截图后用微信扫一扫</p>
</div>
<div class="wx-form">
<!-- ⚠️ placeholder 里绝不能写出真码("例如 MAMA2025" 这种)。
这道闸门换的就是「用户去加一次顾问」,把答案印在输入框里,
等于门开着、旁边立个牌子写「请敲门」。 -->
<label class="fld"><span>顾问发给你的兑换码</span>
<input class="wx-code" id="wxCode" type="text" maxlength="16"
autocomplete="off" autocapitalize="characters" spellcheck="false"
placeholder="添加顾问后向她索取"></label>
<!-- 空着也占一行高(CSS 里的 min-height):红字冒出来时弹窗不会往下跳一格,
跳一下的话,用户正要按的「解锁」按钮会在他手指落下前挪走位置 -->
<p class="wx-err" id="wxErr"></p>
</div>
<!-- is-done 和 is-gift **两种**模式下,上面整个 .wx-form 和右边这颗
「解锁全部资料」都会被 CSS 收起来:
done —— 对已经解锁的人再要一次兑换码,是在问一个他已经回答过的问题;
gift —— 那两个入口卖的是免费试用装,摆一个码框等于在福利前面收门票。
剩下的左键文案由 openWx() 按模式改写(done「知道了」/ gate「以后再说」)。
⚠️ gift 模式没有左键:整条 .modal-foot 被 `.is-gift .modal-foot{display:none}`
收起来了(原来那颗「先不领」按用户要求删了)。gift 的退路是右上角那颗
.modal-x —— 删按钮前专门确认过它在,测试里也钉了一条,别把它「优化」掉。
⚠️ 收起来的只是**样式**,#wxOk 的监听还在。所以 app.js 里那个处理器
开头有一句「不是 gate 模式就 return」的 guard —— 别以为 display:none
就等于点不到,jsdom 里 dispatchEvent 照样叫得醒它。 -->
<div class="modal-foot">
<button class="dl-btn ghost" id="wxLater" data-modal="close">以后再说</button>
<button class="dl-btn main" id="wxOk">解锁全部资料</button>
<!-- 需求 1.1 指定的文案,逐字照抄(直角引号沿用站内习惯) -->
<p class="wx-qr-cap">请添加资料小助手,回复「领资料」</p>
</div>
</section>

528
js/app.js
View File

@@ -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

View File

@@ -2279,9 +2279,9 @@ a.skip:focus{left:16px;top:16px}
.ri-row{display:flex;align-items:center;gap:6px}
.ri-row em{font-style:normal;font-size:11px;line-height:1}
/* 状态标顶到最右(原来 margin-left:auto 挂在「8 分钟读完」上)。
「免费」和「已领 ✓」是同一格的两种状态,必须共用这条,
否则领完之后标签会从行首跳到行尾。 */
.pill-free,.pill-got{margin-left:auto}
⚠️ 2026-09-19:这一格原来还有「已领 ✓」这个状态(.pill-got),
9.19 需求取消「已领」概念后只剩「免费」,相关样式一并删除。 */
.pill-free{margin-left:auto}
.pill-free{background:rgba(110,154,86,.14);color:var(--green);font-weight:700;padding:3.5px 7px;border-radius:6px}
.ri-cat{background:rgba(244,89,60,.1);color:var(--brand);font-weight:700;padding:3.5px 7px;border-radius:6px}
/* 适用孕周,顶掉了原来的「几分钟读完」。不给底色 —— 左边已经有一个实心分类标,
@@ -2289,23 +2289,11 @@ a.skip:focus{left:16px;top:16px}
nowrap:「孕24-28周」这种断在中间比溢出还难看 */
.ri-ga{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ---- 已领的资源 ---- */
/* 顶掉同一位置上的「免费」。绿底是这套里表示「成了」的颜色(和领取按钮的 .got 一致),
但比 .pill-free 实一档 —— 「免费」是背景信息,「已领」是状态,该更跳一点 */
.pill-got{background:var(--green);color:#fff;font-weight:700;padding:3.5px 7px;border-radius:6px;white-space:nowrap}
/* 缩略图右下角的小勾。压在图片上,所以 .ri-thumb 要 relative;
它本来是 overflow:hidden 的方形圆角块,角标贴着内边缘不会被裁掉。
加一圈白描边,免得撞上深色缩略图看不清 */
.ri-thumb{position:relative}
.ri-got-tick{position:absolute;right:3px;bottom:3px;width:17px;height:17px;border-radius:50%;
background:var(--green);color:#fff;border:1.5px solid #fff;
font-style:normal;font-size:10px;font-weight:800;line-height:1;display:grid;place-items:center}
/* 整张卡不做灰化处理:领过不等于失效,用户还会回来拿链接。
只把标题的字重降半档,让没领过的在列表里更抢眼一点 */
.res-item.is-got .ri-title{font-weight:700}
/* chip 用绿色,和上面两个标记对齐;选中态复用 .chip-fav 那套翻转(白字 + 实底) */
.chip-got{color:var(--green)}
.chip-got.is-active{color:#fff;background:var(--green);border-color:var(--green)}
/* ---- 已领的资源:整块已删除(2026-09-19) ----
.pill-got / .ri-got-tick / .res-item.is-got / .chip-got 都随「已领」概念一起删了
(9.19 需求:资料由顾问在微信里直接发,H5 不再有「立即领取即到手」这个动作)。
⚠️ .ri-thumb 的 position:relative 曾经挂在这一块里给角标定位用 —— 现在没有角标了,
也就不需要;若以后要往里放绝对定位元素,记得自己加回来。 */
.res-list.slim .ri-thumb{width:50px;height:50px;border-radius:12px}
.res-list.slim .ri-title{font-size:14px}
@@ -2337,25 +2325,17 @@ a.skip:focus{left:16px;top:16px}
content:"";position:absolute;left:0;right:0;bottom:100%;height:22px;pointer-events:none;
background:linear-gradient(rgba(248,243,239,0),rgba(248,243,239,.94));
}
.rd-cta .dl-btn.main.got{background:var(--green);box-shadow:0 10px 22px rgba(110,154,86,.32)}
/* ⚠️ 这里原来有一条 .rd-cta .dl-btn.main.got(领过之后按钮变绿)——
9.19 需求取消「已领」概念后按钮只有一种状态,该条删除。 */
/* 吸底条已占掉底部空间,滚动区不用再留 40px */
#detailRes .detail-body,#detailPoster .detail-body{padding-bottom:18px}
/* 档案预览有三个按钮,挤一点、字小一号 */
.pst-cta .dl-btn{padding-left:0;padding-right:0;font-size:14px}
/* ---- 百度网盘领取弹窗 ---- */
.pan-box{background:#F7F0EA;border-radius:16px;padding:2px 14px;margin-top:4px}
.pan-row{display:flex;align-items:center;gap:12px;padding:11px 0}
.pan-row + .pan-row{border-top:1px solid var(--line)}
.pan-k{flex:0 0 auto;font-size:12.5px;font-weight:700;color:var(--muted)}
/* 用 readonly input 而不是 <span>:长按能直接唤起「全选 / 拷贝」 */
.pan-v{
flex:1;min-width:0;background:none;border:0;padding:0;
font-size:13.5px;color:var(--ink);text-overflow:ellipsis;
font-family:ui-monospace,Menlo,Consolas,monospace;
}
.pan-v.code{font-size:16px;font-weight:800;letter-spacing:3px;color:var(--brand)}
.pan-note{font-size:11.5px;color:var(--muted);line-height:1.7;margin:11px 0 0}
/* ---- 百度网盘领取弹窗:整块已删除(2026-09-19) ----
.pan-box / .pan-row / .pan-k / .pan-v / .pan-note 随 #modalPan 一起删
(9.19 需求:资料由顾问在微信里直接发,H5 不再发网盘链接与提取码)。
要恢复「H5 自己发链接」,等于把「解锁」概念加回来 —— 先回问需求方。 */
/* ---- 加企业微信顾问弹窗 ----
紧挨着上面的网盘弹窗放:这两个弹窗是同一条领取动线的前后两步
@@ -2365,33 +2345,13 @@ a.skip:focus{left:16px;top:16px}
⚠️ 别改成 .wx-qr img 或 svg.wx-qr —— 那样一换成 <img> 尺寸就全丢。 */
.wx-qr{display:block;width:168px;height:168px;margin:0 auto;border-radius:14px;background:#fff}
.wx-qr-cap{font-size:12px;color:var(--muted);margin-top:10px}
.wx-code{letter-spacing:2px;font-weight:700;text-transform:uppercase}
/* 红边只在这一格上加,不动全局 .fld input:focus 那套 */
.wx-code.is-err{border-color:#D9603F;box-shadow:0 0 0 3px rgba(217,96,63,.13)}
/* ⚠️ min-height 是留给一行红字的位置,空着也占着。
不留的话,红字冒出来的一瞬间整个 modal-foot 会往下跳一格 ——
用户手指正落向「解锁全部资料」,按钮却在他按下去之前挪走了。 */
.wx-err{min-height:18px;font-size:12.5px;color:#D9603F;font-weight:600;margin:8px 0 0;line-height:1.5}
/* 三种模式里有两种要收起兑换码那一格和「解锁全部资料」,只剩二维码:
is-done —— 已经解锁过的人再点福利入口,他来找顾问,不是再解锁一遍;
is-gift —— 福利入口本身。这儿只谈领试用装,码由顾问连同试用装一起发,
让人当场填码是给了他一个手上根本没有的东西。
(闸门 is-gate 里那一格照旧留着 —— 那才是填码的地方。)
⚠️ 这条只管「看不见」。JS 那边 #wxOk 开头还有一句
if (!wxModal.classList.contains('is-gate')) return;
—— display:none 的按钮 dispatchEvent 照样会触发监听,
少了那句,「兑换码不对」的红字会冒在一个根本没有输入框的弹窗上。
别看这儿收起来了就把那句 guard 当冗余删掉。 */
.wx-modal.is-done .wx-form,.wx-modal.is-done #wxOk,
.wx-modal.is-gift .wx-form,.wx-modal.is-gift #wxOk{display:none}
/* gift 模式:「先不领」按用户要求删了,而 #wxOk 上面刚被收起来 ——
.modal-foot 里就一个人都不剩了。不收起整条的话,弹窗底下会多出
一截带 padding 的空白,看着像没加载完。
⚠️ 只写 .is-gift,**不能带上 .is-done**:done 模式下「知道了」是那一屏
唯一的按钮,连坐隐藏就没有出口了。
⚠️ gift 的退路是右上角 .modal-x(modalWx 头部有这颗,删按钮前确认过)。
哪天有人想给 .modal-head 做「无按钮时隐藏」的优化,先回来读这一句。 */
.wx-modal.is-gift .modal-foot{display:none}
/* ⚠️ 这里原来有 .wx-code(兑换码输入框)与 .wx-code.is-err(码错标红)——
9.19 需求删掉输入框后一并删除。 */
/* ⚠️ 2026-09-19(9.19 需求 1.2):这里原来是 .wx-err(兑换码红字)以及
is-done / is-gift / is-gate 三模式的收起规则(把 .wx-form 与 #wxOk 藏起来)。
兑换码输入框和两颗按钮整个删掉之后,这些规则没有对应元素了,一并删除。
弹窗现在是单态(只剩标题 + 引导语 + 二维码),唯一出口是右上角 .modal-x。 */
/* ============ 记一下 弹层 ============ */
.scrim{position:absolute;inset:0;background:rgba(50,30,22,.42);backdrop-filter:blur(2px);opacity:0;visibility:hidden;transition:opacity .32s,visibility .32s;z-index:90}
@@ -2469,12 +2429,11 @@ a.skip:focus{left:16px;top:16px}
.t-ico.ic-home{-webkit-mask-image:url(assets/img/1afd85e578dd.png);mask-image:url(assets/img/1afd85e578dd.png)}
.t-ico.ic-archive{-webkit-mask-image:url(assets/img/fa7b793b47e8.png);mask-image:url(assets/img/fa7b793b47e8.png)}
.t-ico.ic-letter{-webkit-mask-image:url(assets/img/949338312a1f.png);mask-image:url(assets/img/949338312a1f.png)}
/* 「兜爸心选」商城 tab 的购物袋图标(第九批新增,占原「宝宝来信」那枚 tab 的坑位)。
实心剪影风格:袋身圆角矩形 + 提手粗环,与另外四枚 mask 图标同规格(30×30 RGBA)。
源图 assets/img/b3253e3c7a83.png。
⚠️ 上面 .ic-letter 的规则与 949338312a1f.png **保留不删**:来信屏还在(swipe 可进),
哪天要把 tab 挂回「宝宝来信」,把 index.html 那枚 span 的 class 换回去即可。 */
.t-ico.ic-shop{-webkit-mask-image:url(assets/img/b3253e3c7a83.png);mask-image:url(assets/img/b3253e3c7a83.png)}
/* ⚠️ 2026-09-19(9.19 需求第 2 条):底 tab 从「兜爸心选」改回「宝宝来信」,
用的是上面 .ic-letter(图标资源一直在,没删过)。原来这条 .ic-shop(购物袋图标)
已随改名删除 —— 商城入口仍在「我的」页服务卡与首页商城主卡上。
图标源文件 assets/img/b3253e3c7a83.png 保留在仓库里(不引用的二进制无害),
哪天要再挂一枚商城 tab,把这条规则抄回来即可。 */
.t-ico.ic-me{-webkit-mask-image:url(assets/img/0ebc7631fea4.png);mask-image:url(assets/img/0ebc7631fea4.png)}
.tab.is-active .t-ico{transform:translateY(-2px) scale(1.06)}
/* nowrap:最长的那条是「兜爸心选」(原「宝宝来信」,同为四个汉字),12.5px ≈ 50px,
@@ -2550,20 +2509,10 @@ a.skip:focus{left:16px;top:16px}
.alog-stats .ai-st em.eq{color:var(--muted)}
.modal-lead{font-size:13px;color:var(--ink-2);margin-top:8px;line-height:1.6}
.modal-lead b{color:var(--brand)}
/* 引导语里被挑出来的那四个字「全部解锁」(setLead 包的 <em class="lead-hi">)。
⚠️ 必须写 font-style:normal —— <em> 默认是斜体,而手写体几乎都没有斜体字重,
浏览器会拿正体去**算法伪斜**(整体切变),中文伪斜出来是歪的、发虚,
比不强调还难看。这一行不是可省的重置。
⚠️ 下划线用 text-decoration 而不是 border-bottom:border 会把行盒撑高,
这句话在 gate 弹窗里正好排到第三行,撑高会顶到下面那格输入框。
underline-offset 给 3px 是让线离汉字底再远一点,不然会贴着「锁」的竖钩。 */
.modal-lead .lead-hi{
font-family:var(--font-kid);font-style:normal;font-weight:800;
font-size:15px;color:var(--brand);
text-decoration:underline;text-decoration-thickness:1.5px;text-underline-offset:3px;
/* 线比字淡一档:--brand #F4593C 兑 .5 alpha。手写体本来笔画就细,
配一条同样浓的实线会显得字被压在线上面。改这里记得跟着 --brand 走。 */
text-decoration-color:rgba(244,89,60,.5)}
/* ⚠️ 这里原来是 .modal-lead .lead-hi(给「全部解锁」四个字加重的 <em>)——
setLead() 已随三模式删除,该样式一并删除。 */
/* 输入框:必须显式给 font-family,否则 textarea/input 会退回成系统等宽字体 */
.modal-ta,.fld input{
font-family:inherit;font-size:15px;color:var(--ink);background:#fff;