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:
158
index.html
158
index.html
@@ -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
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
|
||||
|
||||
111
mama-box.css
111
mama-box.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user