@@ -3,7 +3,7 @@
( function ( ) {
'use strict' ;
/* 页面版本号( YYMMDD+序号) : 显示在「关于兜爸心选」页底部; bump 时同步 head 注释与 ?v= */
var VER = '26110460 ' ;
var VER = '26110461 ' ;
const $ = ( s , c ) => ( c || document ) . querySelector ( s ) ;
const $$ = ( s , c ) => Array . from ( ( c || document ) . querySelectorAll ( s ) ) ;
@@ -2729,6 +2729,41 @@
}
} ;
const RES _ORDER = [ 'book' , 'food' , 'music' , 'report' , 'sport' , 'diet' , 'sleep' , 'med' ] ;
/* 2026-11-05 用户: 福利资料改「3 兜豆兑换 → 立刻弹网盘地址」,详情页要说清「这份资料里有什么」。
PACK = 文件形式 + 内容清单( AI 助手起草,用户待核对;网盘里实际文件变了要同步改这里)。
价格以接口 claim-status.price 为准, RES_PRICE 只是没拉到时的默认值。 */
let RES _PRICE = 3 ;
const PACK = {
book : { fmt : [ 'PDF · 18 页' , '可打印' ] , items : [ '糖耐检查流程图: 3 次抽血的时间点一张图看懂' , '检查前 3 天饮食清单(每天碳水够 150g 的吃法)' , '检查当天携带物品清单' , '结果对照表:空腹 / 1 小时 / 2 小时三个数怎么看' , '万一超标:一周控糖食谱示例' , '妈妈们最常问的 12 个问题' ] } ,
food : { fmt : [ 'PDF · 86 页' , '按孕周分章' ] , items : [ '孕早 / 中 / 晚期分章节,每章先讲这阶段缺什么' , '40 周每周一日三餐参考' , '钙、铁、DHA、叶酸常见食物含量表' , '孕期要少碰的食物清单' , '加餐搭配 30 例' ] } ,
music : { fmt : [ 'MP3 · 30 首' , '共 96 分钟' , '附听法说明' ] , items : [ '古典小品 10 首' , '轻钢琴 10 首' , '自然音 10 首(雨声、溪流、鸟鸣)' , '每日播放安排表:什么时候听、听多久' , '音量和距离的注意事项' ] } ,
report : { fmt : [ 'PDF · 6 页' , '附可打印表格' ] , items : [ '全孕期 13 次产检:时间 + 检查项目一览' , '每次产检要带的东西' , '「问医生」问题清单模板' , '产检报告存放登记表' ] } ,
sport : { fmt : [ 'PDF · 22 页' , '动作图解' ] , items : [ '快走、游泳、孕妇瑜伽、固定单车的要点' , '12 个孕期瑜伽动作分步图解' , '运动强度怎么把握' , '必须避开的动作清单' , '出现哪些情况要立刻停下' ] } ,
diet : { fmt : [ 'PDF · 16 页' , '附体重记录表' ] , items : [ '孕前 BMI 对应的增重范围对照表' , '每周体重记录表(可打印)' , '正确称重的方法' , '长得太快 / 太慢怎么调整' ] } ,
sleep : { fmt : [ 'PDF · 14 页' , '附助眠音频 5 段' ] , items : [ '孕期失眠的 7 个改善方法' , '左侧卧 + 垫枕头的睡姿图解' , '睡前放松呼吸练习(音频)' , '一周睡眠记录表' ] } ,
med : { fmt : [ 'PDF · 12 页' , '附自评量表' ] , items : [ '情绪自评量表 + 计分说明' , '需要警惕的信号清单' , '情绪日记模板' , '可以求助的渠道' ] }
} ;
/* 已兑换的 key( 来自 claim-status.resGot) ; 兑换成功后本地追加 */
const resGot = [ ] ;
function loadResGot ( ) {
const oid = hashParam ( 'openid' ) ;
if ( ! oid ) return Promise . resolve ( ) ;
return fetch ( '/api/mp/public/mama-box/resources/claim-status?openid=' + encodeURIComponent ( oid ) )
. then ( r => r . ok ? r . json ( ) : null ) . then ( j => {
const x = j && j . result ;
if ( ! x ) return ;
if ( typeof x . price === 'number' && x . price > 0 ) RES _PRICE = x . price ;
resGot . length = 0 ;
( x . resGot || [ ] ) . forEach ( k => resGot . push ( k ) ) ;
syncResPrice ( ) ;
} ) . catch ( ( ) => { } ) ;
}
/* 首页书架 / 列表 / 详情页上所有「几兜豆」文案 */
function syncResPrice ( ) {
$$ ( '.shelf.gift2 .sb-row .pill-free' ) . forEach ( el => { el . textContent = RES _PRICE + ' 豆兑' ; } ) ;
$$ ( '.res-price-n' ) . forEach ( el => { el . textContent = RES _PRICE ; } ) ;
setRdGet ( ) ;
}
/* ⚠️ 2026-09-19( 9.19 需求 1.2):这里原来有 PAN_DEFAULT / PAN 两张**百度网盘
链接表**、panOf()、以及企业微信兑换码 WX_CODE / wxCodeOK()。
@@ -2752,7 +2787,7 @@
'<span class="ri-row">' +
'<em class="ri-cat">' + r . cat + '</em>' +
'<em class="ri-ga">' + r . meta [ 2 ] + '</em>' +
'<em class="pill-free">免费 </em>' +
'<em class="pill-free">' + ( resGot . indexOf ( k ) >= 0 ? '已兑换' : RES _PRICE + ' 兜豆' ) + ' </em>' +
'</span></span></button>' ;
}
function bindResCards ( root ) {
@@ -2800,6 +2835,12 @@
$ ( '#rdTitle' ) . textContent = r . title ;
$ ( '#rdDesc' ) . textContent = r . desc ;
$ ( '#rdMeta' ) . innerHTML = r . meta . map ( m => '<span>' + m + '</span>' ) . join ( '' ) ;
const pk = PACK [ state . resKey ] ;
$ ( '#rdPack' ) . innerHTML = pk ? '<h3 class="d-sec">这份资料里有什么</h3>' +
'<div class="rp-box"><div class="rp-fmt">' + pk . fmt . map ( f => '<span>' + f + '</span>' ) . join ( '' ) + '</div>' +
'<ul class="rp-list">' + pk . items . map ( t => '<li>' + t + '</li>' ) . join ( '' ) + '</ul>' +
'<p class="rp-how"><b class="res-price-n">' + RES _PRICE + '</b> 兜豆兑换,兑换后马上给你网盘地址和提取码,复制去下载;兑换过的再点也能看,不重复扣豆。</p></div>' +
'<h3 class="d-sec">内容抢先看</h3>' : '' ;
$ ( '#rdBody' ) . innerHTML = r . body
. map ( p => '<h4>' + p [ 0 ] + '</h4><p>' + p [ 1 ] + '</p>' ) . join ( '' ) ;
$ ( '#rdMore' ) . innerHTML = RES _ORDER
@@ -2814,7 +2855,7 @@
/* 底部左键已从「收藏」改成「资源库」,是个固定文案的导航按钮,不再需要回显。
⚠️ 右键「立即领取」原来会按 state.resGot 回显成「再拿一次链接」,
9.19 需求取消「已领」概念后改回**固定文案**(点它只是弹顾问二维码)。 */
$ ( '#r dGet' ) . textContent = '立即领取' ;
setR dGet( ) ;
$ ( '#detailRes .detail-body' ) . scrollTop = 0 ;
}
$ ( '#resStar' ) . addEventListener ( 'click' , function ( ) {
@@ -2857,7 +2898,85 @@
顾问直接发资料」之后,闸门、兑换码、网盘弹窗整条链路都删了(连同 givePan()、
state.wxUnlocked、wxPending) , 这颗按钮现在只做一件事: 弹二维码。
不存在「已解锁 / 未解锁」两种起点,所以这里也没有任何状态判断 —— 别再加回来。 */
$ ( '#rdGet' ) . addEventListener ( 'click' , ( ) => { openWx ( ) ; } ) ;
/* 2026-11-05 用户改:「立即领取」→ 用兜豆兑换,兑换完立刻弹网盘地址(顾问二维码不要了)。
后端 POST /resources/{key}/claim: 同份同人只扣一次, 兑换过的再点直接回链接( spent=0) 。 */
function setRdGet ( ) {
const b = $ ( '#rdGet' ) ;
if ( ! b ) return ;
const got = resGot . indexOf ( state . resKey ) >= 0 ;
b . textContent = got ? '查看网盘地址' : RES _PRICE + ' 兜豆兑换' ;
b . classList . toggle ( 'is-got' , got ) ;
}
let resBusy = false ;
function redeemRes ( key ) {
const oid = hashParam ( 'openid' ) , uid = hashParam ( 'unionid' ) ;
if ( ! oid ) { toast ( '请从小程序里打开后再兑换' ) ; return ; }
if ( resBusy ) return ;
resBusy = true ;
const btn = $ ( '#rbOk' ) ; if ( btn ) btn . textContent = '兑换中…' ;
fetch ( '/api/mp/public/mama-box/resources/' + key + '/claim?openid=' + encodeURIComponent ( oid ) +
( uid ? '&unionid=' + encodeURIComponent ( uid ) : '' ) , { method : 'POST' } )
. then ( r => r . json ( ) . catch ( ( ) => null ) )
. then ( j => {
const x = ( j && j . result ) || { } ;
if ( j && j . code === 200 && x . pan && x . pan . url ) {
if ( resGot . indexOf ( key ) < 0 ) resGot . push ( key ) ;
setRdGet ( ) ;
renderResList ( state . resCat ) ;
showPan ( key , x . pan , x . spent > 0 ? x . spent : 0 ) ;
if ( x . spent > 0 ) loadDou ( ) ; // 刷新「我的」页兜豆余额
} else if ( j && j . code === 409 && x . insufficient ) {
closeModal ( ) ;
$ ( '#rsShort' ) . textContent = x . short || RES _PRICE ;
$ ( '#rsBal' ) . textContent = typeof x . balance === 'number' ? x . balance : 0 ;
openModal ( 'modalResShort' ) ;
} else {
closeModal ( ) ;
toast ( ( j && j . message ) || '网络不太好,稍后再试' ) ;
}
} )
. catch ( ( ) => { closeModal ( ) ; toast ( '网络不太好,稍后再试' ) ; } )
. then ( ( ) => { resBusy = false ; if ( btn ) btn . textContent = '确认兑换' ; } ) ;
}
function showPan ( key , pan , spent ) {
const r = RES [ key ] || { } ;
$ ( '#rpTitle' ) . textContent = spent ? '兑换成功' : '你的网盘地址' ;
$ ( '#rpLead' ) . textContent = ( spent ? '已扣 ' + spent + ' 兜豆,' : '' ) + '复制下面的地址,打开浏览器或百度网盘 App 就能下载《' + String ( r . title || '' ) . replace ( /[《》]/g , '' ) + '》' ;
$ ( '#rpUrl' ) . textContent = pan . url ;
$ ( '#rpCode' ) . textContent = pan . code || '无' ;
$ ( '#rpCodeRow' ) . classList . toggle ( 'hide' , ! pan . code ) ;
openModal ( 'modalResPan' ) ;
}
function copyText ( t ) {
if ( navigator . clipboard && window . isSecureContext ) return navigator . clipboard . writeText ( t ) . catch ( ( ) => legacyCopy ( t ) ) ;
return Promise . resolve ( legacyCopy ( t ) ) ;
}
function legacyCopy ( t ) {
const ta = document . createElement ( 'textarea' ) ;
ta . value = t ; ta . setAttribute ( 'readonly' , '' ) ; ta . style . cssText = 'position:fixed;top:-99px;opacity:0' ;
document . body . appendChild ( ta ) ; ta . select ( ) ; ta . setSelectionRange ( 0 , t . length ) ;
try { document . execCommand ( 'copy' ) ; } catch ( e ) { /* 复制不了也给提示,用户可长按地址手动复制 */ }
ta . remove ( ) ;
}
$ ( '#rpCopy' ) . addEventListener ( 'click' , ( ) => {
const code = $ ( '#rpCodeRow' ) . classList . contains ( 'hide' ) ? '' : $ ( '#rpCode' ) . textContent ;
copyText ( $ ( '#rpUrl' ) . textContent + ( code ? ' 提取码: ' + code : '' ) ) . then ( ( ) => toast ( '已复制,去浏览器或网盘 App 粘贴打开' ) ) ;
haptic ( ) ;
} ) ;
$ ( '#rdGet' ) . addEventListener ( 'click' , ( ) => {
const key = state . resKey ;
if ( ! hashParam ( 'openid' ) ) { toast ( '请从小程序里打开后再兑换' ) ; return ; }
if ( resGot . indexOf ( key ) >= 0 ) { redeemRes ( key ) ; return ; } // 兑换过:直接拿链接,不扣豆
const r = RES [ key ] || { } ;
$ ( '#rbName' ) . textContent = '《' + String ( r . title || '' ) . replace ( /[《》]/g , '' ) + '》' ;
$ ( '#rbPrice' ) . textContent = RES _PRICE ;
const n = $ ( '#meDouN' ) && ! $ ( '#meDouBal' ) . classList . contains ( 'hide' ) ? $ ( '#meDouN' ) . textContent : '' ;
$ ( '#rbBal' ) . textContent = n ? '你现在有 ' + n + ' 兜豆' : '' ;
openModal ( 'modalResBuy' ) ;
} ) ;
$ ( '#rbOk' ) . addEventListener ( 'click' , ( ) => redeemRes ( state . resKey ) ) ;
$ ( '#rsGo' ) . addEventListener ( 'click' , ( ) => { closeModal ( ) ; openExchange ( ) ; } ) ;
loadResGot ( ) ;
/* ================= 企业微信顾问弹窗(单态) =================
三个入口共用这一个壳:「我的」页服务卡 #meWxCard、资源库顶部试用装 banner #rlGift、
@@ -2877,6 +2996,7 @@
⚠️ 走 openWx() 而不是挂 data-open="modalWx":那条通用通道直接调 openModal,
不经过这里 —— 今天两者等价,但留一个入口,以后要统一加埋点/清理只改一处。 */
$ ( '#rlGift' ) . addEventListener ( 'click' , ( ) => openWx ( ) ) ;
$ ( '#rlGift' ) . classList . add ( 'hide' ) ; // 2026-11-05 用户:不保留顾问二维码,资源库顶部这条 banner 一起藏( DOM 留着)
/* ⚠️ 2026-09-19( 9.19 需求):这里原来有一个 syncUnlockCopy(),按「已解锁 / 未解锁」
把首页书架与资源库的两句引导语改成两个版本,并给资源库顶部试用装 banner 挂 .hide。
@@ -3961,10 +4081,12 @@
渲染(用 r.img) 无需改动; 正文(body)仍用本地(接口未下发,保证详情不空)。 */
function _ _applyResources ( items ) {
if ( ! items || ! items . length ) return ;
/* 2026-11-05 修:原来先清空 RES 再取 old, 内置正文 body 全丢了,详情页「内容」一直是空的 —— 先留一份旧表 */
var prev = Object . assign ( { } , RES ) ;
Object . keys ( RES ) . forEach ( function ( k ) { delete RES [ k ] ; } ) ;
var order = [ ] ;
items . forEach ( function ( it ) {
var old = RES [ it . key ] || { body : [ ] } ;
var old = prev [ it . key ] || { body : [ ] } ;
RES [ it . key ] = {
img : it . imgUrl || old . img || '' , cat : it . cat , tint : it . tint ,
title : it . title , desc : it . desc , meta : it . meta || old . meta || [ ] ,