@@ -0,0 +1,193 @@
/* 底部胶囊 Tab( 11-05 用户:「孕期百问、孕期计算器加上跟趣味小测试一样的底部 Tab」)
样式 / 行为照搬 guangchang/index.html 那条(左:本页 · 中:「记一下」橙色圆钮 · 右:返回首页 + 用户头像渐变圈),
类名统一加 mbt- 前缀,避免和首页自己的 .tabbar / .tab 撞车。
用法: MBTab.init({
root: '../../', // 到 mama-box 根目录的相对路径(取头像 / 回首页用)
first: { t: '孕期计算器', ic: 'calc' }, // 左边那格:本页名字 + 图标( calc / qa)
onFirst: fn, // 点左边(默认:滚回顶部)
onHome: fn, // 点「返回首页」(默认:跳 root 带身份)
onRec: fn, // 点「记一下」(默认:自带弹层,列后台已上架的孕期工具,同广场)
mount: el, scroller: el // 挂在哪(默认 body, position:fixed) ; 滚回顶部用的容器
}) */
( function ( g ) {
'use strict' ;
var CSS = '' +
'.mbt-fade{position:fixed;left:0;right:0;bottom:0;z-index:29;pointer-events:none;height:calc(96px + env(safe-area-inset-bottom));background:linear-gradient(180deg,rgba(255,246,241,0),rgba(255,246,241,.75))}' +
'.mbt{position:fixed;left:16px;right:16px;bottom:calc(22px + env(safe-area-inset-bottom));height:62px;z-index:30;border-radius:31px;padding:0 6px;display:grid;grid-template-columns:repeat(3,1fr);font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif}' +
'.mbt::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;pointer-events:none;background:#FFF9F5;box-shadow:inset 0 0 0 1px rgba(255,255,255,.85),inset 0 1px 0 #fff;' +
'-webkit-mask:radial-gradient(circle 31px at 50% 6px,transparent 30.5px,#000 31.5px);mask:radial-gradient(circle 31px at 50% 6px,transparent 30.5px,#000 31.5px)}' +
'.mbt::after{content:"";position:absolute;inset:0;z-index:-2;border-radius:inherit;pointer-events:none;box-shadow:0 14px 34px rgba(200,100,70,.2),0 2px 6px rgba(200,100,70,.06)}' +
'.mbt-t{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:0 0 6px;border:0;background:none;font-family:inherit;color:#A3958D;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.mbt-t.on{color:#F4593C}' +
'.mbt-i{width:25px;height:25px;display:block;position:relative}' +
'.mbt-i svg{display:block;width:100%;height:100%;overflow:visible}' +
'.mbt-i .tf,.mbt-i .tl{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}' +
'.mbt-t.on .tf{fill:currentColor}.mbt-t.on .tl{stroke:#fff}' +
'.mbt-x{font-size:12.5px;font-weight:600;line-height:1.1;letter-spacing:.2px;white-space:nowrap}.mbt-t.on .mbt-x{font-weight:800}' +
'.mbt-t::after{content:"";position:absolute;left:50%;bottom:6px;width:4px;height:4px;margin-left:-2px;border-radius:50%;background:#F4593C;opacity:0}.mbt-t.on::after{opacity:1}' +
'.mbt-fab::after{display:none}' +
'.mbt-fab .fab{position:absolute;left:50%;top:-20px;width:52px;height:52px;margin-left:-26px;border-radius:50%;display:grid;place-items:center;overflow:visible;' +
'background:linear-gradient(150deg,#FF8A6E,#F4593C);box-shadow:0 6px 14px rgba(244,89,60,.32),inset 0 1px 0 rgba(255,255,255,.3);transition:transform .3s cubic-bezier(.3,1.6,.5,1)}' +
'.mbt-fab .fab::before{content:"";display:block;width:20px;height:20px;opacity:.28;border-radius:2px;background:linear-gradient(#fff,#fff) center/100% 3px no-repeat,linear-gradient(#fff,#fff) center/3px 100% no-repeat}' +
'.mbt-fab:active .fab{transform:scale(.9)}' +
'.mbt-w{position:absolute;left:50%;top:50%;transform:translate(calc(-50% - 5px),-54%) rotate(-8deg);display:flex;align-items:center;white-space:nowrap;font-weight:900;font-style:normal;color:#4A1E14;line-height:1;pointer-events:none;' +
'text-shadow:1.6px 0 #fff,-1.6px 0 #fff,0 1.6px #fff,0 -1.6px #fff,1.2px 1.2px #fff,-1.2px 1.2px #fff,1.2px -1.2px #fff,-1.2px -1.2px #fff,0 3px 4px rgba(120,30,10,.25)}' +
'.mbt-w i{font-style:normal;font-size:31px}.mbt-w em{font-style:normal;font-size:12.5px;line-height:1;margin:9px 0 0;letter-spacing:-.5px}.mbt-w s{text-decoration:none;font-size:20px;display:inline-block;transform:skewX(-8deg)}' +
'.mbt-fab .mbt-x{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}' +
'.mbt-home .mbt-i{margin-bottom:6px}.mbt-home .mbt-x{font-size:11.5px;font-weight:600}' +
'.mbt-av{position:absolute;inset:-3px;width:calc(100% + 6px);height:calc(100% + 6px);border-radius:50%;object-fit:cover;display:none;background:#fff;box-shadow:0 0 0 1.5px #fff,0 3px 10px rgba(226,105,74,.3)}' +
'.mbt-hm{position:absolute;right:-8px;bottom:-5px;width:15px;height:15px;z-index:1;border-radius:50%;display:none;background:linear-gradient(150deg,#E2694A,#C24E31);box-shadow:0 0 0 1.5px #FFF6F1}.mbt-hm svg{display:block;width:10px;height:10px;margin:2.5px}' +
'.mbt-i.has-av .mbt-av,.mbt-i.has-av .mbt-hm{display:block}.mbt-i.has-av svg.ts{visibility:hidden}' +
'.mbt-i.has-av::before{content:"";position:absolute;inset:-7px;border-radius:50%;z-index:-1;pointer-events:none;background:conic-gradient(from 200deg,#FFB38A,#F4593C,#E2694A,#FF8FA3,#FFB38A);' +
'-webkit-mask:radial-gradient(circle closest-side,transparent 82%,#000 85%);mask:radial-gradient(circle closest-side,transparent 82%,#000 85%)}' +
/* 「记一下」弹层(同广场:标题一行 + 4 列工具宫格) */
'.mbt-mask{position:fixed;inset:0;z-index:60;background:rgba(40,20,14,.38);opacity:0;pointer-events:none;transition:opacity .25s}.mbt-mask.on{opacity:1;pointer-events:auto}' +
'.mbt-sh{position:fixed;left:0;right:0;bottom:0;z-index:61;max-height:80vh;overflow:auto;padding:8px 18px calc(20px + env(safe-area-inset-bottom));border-radius:22px 22px 0 0;background:#FFF9F5;transform:translateY(105%);transition:transform .3s cubic-bezier(.2,.9,.3,1);font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif}' +
'.mbt-sh.on{transform:none}' +
'.mbt-rh{display:flex;align-items:baseline;gap:10px;padding:10px 2px 14px}.mbt-rh b{font-size:19px;font-weight:800;color:#2A1E1E;flex:none}' +
'.mbt-rh span{font-size:12px;color:#9C8F88;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.mbt-rx{flex:none;align-self:center;width:30px;height:30px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(120,70,50,.08);color:#9C8F88;font-size:18px;line-height:1;cursor:pointer}' +
'.mbt-apps{display:grid;grid-template-columns:repeat(4,1fr);gap:20px 6px;padding:4px 0 8px}' +
'.mbt-app{position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;min-width:0;border:0;background:none;font-family:inherit;color:#2A1E1E;cursor:pointer}' +
'.mbt-app img,.mbt-app .rl{width:48px;height:48px;display:block;object-fit:contain;border-radius:14px}' +
'.mbt-app .rl{display:grid;place-items:center;background:#F6D9CB;color:#C2553A;font-size:19px;font-weight:800}' +
'.mbt-app span{display:block;max-width:100%;font-size:11.5px;font-weight:600;letter-spacing:-.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.mbt-app.off img,.mbt-app.off .rl{opacity:.45;filter:grayscale(.4)}' +
'.mbt-app.off i{position:absolute;top:-8px;left:calc(50% + 4px);font-style:normal;font-size:9px;font-weight:700;line-height:1;padding:3px 5px;border-radius:99px;background:#B9A9A2;color:#fff;white-space:nowrap}' +
'.mbt-empty{grid-column:1/-1;padding:30px 0;text-align:center;font-size:13px;color:#9C8F88}' +
'.mbt-toast{position:fixed;left:50%;bottom:calc(110px + env(safe-area-inset-bottom));z-index:70;max-width:80vw;padding:9px 14px;border-radius:12px;background:rgba(40,28,24,.86);color:#fff;font-size:13px;transform:translate(-50%,8px);opacity:0;transition:.25s;pointer-events:none}' +
'.mbt-toast.on{opacity:1;transform:translate(-50%,0)}' ;
var IC = {
calc : '<svg viewBox="0 0 24 24" aria-hidden="true"><rect class="tf" x="5" y="3.5" width="14" height="17" rx="2.8"/><path class="tl" d="M8.6 7.6h6.8M8.8 12h.01M12 12h.01M15.2 12h.01M8.8 15.6h.01M12 15.6h.01M15.2 15.6h.01" stroke-width="2.2"/></svg>' ,
qa : '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="tf" d="M4 6.6A2.6 2.6 0 0 1 6.6 4h10.8A2.6 2.6 0 0 1 20 6.6v7.8a2.6 2.6 0 0 1-2.6 2.6H11l-4.2 3.2c-.4.3-.9 0-.9-.4V17A2.6 2.6 0 0 1 4 14.4z"/><path class="tl" d="M10.2 8.9a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M12 14.2h.01"/></svg>'
} ;
var HOME = '<svg class="ts" viewBox="0 0 24 24" aria-hidden="true"><path class="tf" d="M4 10.4 12 4l8 6.4V18a2.4 2.4 0 0 1-2.4 2.4H6.4A2.4 2.4 0 0 1 4 18z"/><path class="tl" d="M9.8 20.2v-4.4a2.2 2.2 0 0 1 4.4 0v4.4"/></svg>' ;
function param ( n ) {
var m = location . search . match ( new RegExp ( '[?&]' + n + '=([^&]*)' ) ) || location . hash . match ( new RegExp ( '[#&]' + n + '=([^&]*)' ) ) ;
try { return m ? decodeURIComponent ( m [ 1 ] ) : '' ; } catch ( e ) { return '' ; }
}
function haptic ( ) { try { navigator . vibrate && navigator . vibrate ( 8 ) ; } catch ( e ) { } }
function esc ( s ) { return String ( s == null ? '' : s ) . replace ( /[&<>"']/g , function ( c ) { return { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } [ c ] ; } ) ; }
var realAv = function ( u ) { return ! ! u && ! /\/uploads\/system\// . test ( u ) ; } ;
function defAv ( oid ) { var h = 0 , s = String ( oid || '' ) ; for ( var i = 0 ; i < s . length ; i ++ ) h = ( h * 31 + s . charCodeAt ( i ) ) >>> 0 ; var n = ( h % 20 ) + 1 ; return 'https://mp.yizhezhe.com/_bridge/avatars/mom-' + ( n < 10 ? '0' : '' ) + n + '.png' ; }
function init ( o ) {
o = o || { } ;
var root = o . root || '../../' , mount = o . mount || document . body ;
if ( ! document . getElementById ( 'mbtCss' ) ) { var st = document . createElement ( 'style' ) ; st . id = 'mbtCss' ; st . textContent = CSS ; document . head . appendChild ( st ) ; }
var fade = document . createElement ( 'div' ) ; fade . className = 'mbt-fade' ; fade . setAttribute ( 'aria-hidden' , 'true' ) ;
var bar = document . createElement ( 'nav' ) ; bar . className = 'mbt' ;
var f = o . first || { t : '本页' , ic : 'qa' } ;
bar . innerHTML = '<button type="button" class="mbt-t on" data-k="first"><span class="mbt-i">' + ( IC [ f . ic ] || IC . qa ) + '</span><span class="mbt-x">' + esc ( f . t ) + '</span></button>' +
'<button type="button" class="mbt-t mbt-fab" data-k="rec"><span class="fab"><b class="mbt-w" aria-hidden="true"><i>记</i><em>一下</em><s>!</s></b></span><span class="mbt-x">记一下</span></button>' +
'<button type="button" class="mbt-t mbt-home" data-k="home"><span class="mbt-i">' + HOME + '</span><span class="mbt-x">返回首页</span></button>' ;
mount . appendChild ( fade ) ; mount . appendChild ( bar ) ;
var OPENID = param ( 'openid' ) ;
function homeURL ( ) {
var q = [ ] , oid = param ( 'openid' ) , uid = param ( 'unionid' ) ;
if ( oid ) q . push ( 'openid=' + encodeURIComponent ( oid ) ) ;
if ( uid ) q . push ( 'unionid=' + encodeURIComponent ( uid ) ) ;
return root + ( q . length ? '#' + q . join ( '&' ) : location . hash ) ;
}
function goApp ( url ) {
try { var u = new URL ( url , location . href ) ; if ( u . origin === location . origin || /(^|\.)yizhezhe\.com$/ . test ( u . hostname ) ) url = u . pathname + u . search ; } catch ( e ) { }
var q = [ ] , oid = param ( 'openid' ) , uid = param ( 'unionid' ) ;
if ( oid ) q . push ( 'openid=' + encodeURIComponent ( oid ) ) ;
if ( uid ) q . push ( 'unionid=' + encodeURIComponent ( uid ) ) ;
if ( ! oid && ! uid ) q . push ( '__no_identity=1' ) ;
var target = url + ( url . indexOf ( '?' ) >= 0 ? '&' : '?' ) + q . join ( '&' ) ;
try {
if ( oid ) {
var api = '/api/mp/public/last-app?openid=' + encodeURIComponent ( oid ) + '&url=' + encodeURIComponent ( location . origin + url ) ;
if ( ! ( navigator . sendBeacon && navigator . sendBeacon ( api ) ) && window . fetch ) fetch ( api , { method : 'POST' , keepalive : true } ) . catch ( function ( ) { } ) ;
}
if ( window . wxAuth && wxAuth . reportLocation ) wxAuth . reportLocation ( { href : target } ) ;
} catch ( e ) { }
location . href = target ;
}
var tt ;
function toast ( s ) {
var t = document . querySelector ( '.mbt-toast' ) ;
if ( ! t ) { t = document . createElement ( 'div' ) ; t . className = 'mbt-toast' ; document . body . appendChild ( t ) ; }
t . textContent = s ; t . classList . add ( 'on' ) ; clearTimeout ( tt ) ; tt = setTimeout ( function ( ) { t . classList . remove ( 'on' ) ; } , 2200 ) ;
}
/* 「记一下」:后台 App 表已上架、category=孕期工具(同首页 / 广场);数胎动 16 / 数宫缩 20 / 胎儿估重 15 周前灰掉 */
var GATE = { taidong : [ 16 , '数胎动' ] , gongsuopinlv : [ 20 , '数宫缩' ] , taierguzhong : [ 15 , '胎儿估重' ] } , apps = null , busy = false , mask , sh ;
var myWeek = ( function ( ) { try { var w = JSON . parse ( localStorage . getItem ( 'mmbox.gcwk.v1' ) || 'null' ) ; return w && ! w . pp && w . w ? + w . w : 0 ; } catch ( e ) { return 0 ; } } ) ( ) ;
function locked ( a ) { var x = GATE [ a . identifier ] ; return x && myWeek && myWeek < x [ 0 ] ? x : null ; }
function appsHTML ( ) {
if ( ! apps ) return '<div class="mbt-empty">加载中…</div>' ;
if ( ! apps . length ) return '<div class="mbt-empty">暂时没取到工具,稍后再试</div>' ;
return apps . filter ( function ( a ) { return ! locked ( a ) ; } ) . concat ( apps . filter ( locked ) ) . map ( function ( a ) {
var x = locked ( a ) , nm = a . name || a . identifier || '工具' , ic = a . logo || a . icon ;
return '<button type="button" class="mbt-app' + ( x ? ' off' : '' ) + '" data-rid="' + esc ( a . identifier || '' ) + '">' +
( ic ? '<img src="' + esc ( ic ) + '" alt="">' : '<b class="rl">' + esc ( nm . slice ( 0 , 1 ) ) + '</b>' ) + '<span>' + esc ( nm ) + '</span>' + ( x ? '<i>孕' + x [ 0 ] + '周开放</i>' : '' ) + '</button>' ;
} ) . join ( '' ) ;
}
function load ( ) {
if ( apps || busy ) return ; busy = true ;
fetch ( '/api/mp/apps?isPublished=1&pageSize=200' ) . then ( function ( r ) { return r . ok ? r . json ( ) : null ; } ) . then ( function ( d ) {
apps = ( ( d && d . result && d . result . items ) || [ ] ) . filter ( function ( a ) { return a . url && a . category === '孕期工具' ; } )
. sort ( function ( x , y ) { return ( y . seq || 0 ) - ( x . seq || 0 ) || ( y . no || 0 ) - ( x . no || 0 ) ; } ) ;
} ) . catch ( function ( ) { apps = [ ] ; } ) . then ( function ( ) { busy = false ; var b = sh && sh . querySelector ( '.mbt-apps' ) ; if ( b ) b . innerHTML = appsHTML ( ) ; } ) ;
}
function closeRec ( ) { if ( sh ) { sh . classList . remove ( 'on' ) ; mask . classList . remove ( 'on' ) ; } }
function openRec ( ) {
load ( ) ;
if ( ! sh ) {
mask = document . createElement ( 'div' ) ; mask . className = 'mbt-mask' ;
sh = document . createElement ( 'div' ) ; sh . className = 'mbt-sh' ; sh . setAttribute ( 'role' , 'dialog' ) ;
document . body . appendChild ( mask ) ; document . body . appendChild ( sh ) ;
mask . addEventListener ( 'click' , closeRec ) ;
sh . addEventListener ( 'click' , function ( e ) {
if ( e . target . closest ( '.mbt-rx' ) ) { closeRec ( ) ; return ; }
var b = e . target . closest ( '[data-rid]' ) ; if ( ! b || ! apps ) return ;
var a = apps . filter ( function ( x ) { return ( x . identifier || '' ) === b . dataset . rid ; } ) [ 0 ] ; if ( ! a ) return ;
var x = locked ( a ) ;
if ( x ) { haptic ( ) ; toast ( x [ 1 ] + '孕 ' + x [ 0 ] + ' 周以后开放,你现在孕 ' + myWeek + ' 周,还用不到哦' ) ; return ; }
haptic ( ) ; goApp ( a . url ) ;
} ) ;
}
sh . innerHTML = '<div class="mbt-rh"><b>记一下</b><span>兜爸心选的孕期工具</span><button type="button" class="mbt-rx" aria-label="关闭">× </button></div><div class="mbt-apps">' + appsHTML ( ) + '</div>' ;
requestAnimationFrame ( function ( ) { mask . classList . add ( 'on' ) ; sh . classList . add ( 'on' ) ; } ) ;
}
bar . addEventListener ( 'click' , function ( e ) {
var b = e . target . closest ( '.mbt-t' ) ; if ( ! b ) return ;
haptic ( ) ;
var k = b . dataset . k ;
if ( k === 'first' ) { if ( o . onFirst ) o . onFirst ( ) ; else { var s = o . scroller || document . getElementById ( 'scroll' ) || document . scrollingElement ; s && s . scrollTo ( { top : 0 , behavior : 'smooth' } ) ; } }
else if ( k === 'rec' ) { if ( o . onRec ) o . onRec ( ) ; else openRec ( ) ; }
else if ( o . onHome ) o . onHome ( ) ; else location . href = homeURL ( ) ;
} ) ;
/* 返回首页的头像( 同广场) : user-center/info 真头像 → hash 里的 avatar → 按 openid 固定默认头像;加载失败露回小房子 */
var ico = bar . querySelector ( '.mbt-home .mbt-i' ) ;
function paintAv ( url ) {
if ( ! url ) return ;
var im = ico . querySelector ( '.mbt-av' ) ;
if ( ! im ) {
im = document . createElement ( 'img' ) ; im . className = 'mbt-av' ; im . alt = '' ; ico . appendChild ( im ) ;
ico . insertAdjacentHTML ( 'beforeend' , '<i class="mbt-hm" aria-hidden="true"><svg viewBox="0 0 24 24" fill="#fff"><path d="M12 4 3.5 11H6v8.5h4.5v-5h3v5H18V11h2.5z"/></svg></i>' ) ;
}
im . onload = function ( ) { ico . classList . add ( 'has-av' ) ; } ;
im . onerror = function ( ) { if ( im . getAttribute ( 'src' ) !== defAv ( OPENID ) ) im . src = defAv ( OPENID ) ; else ico . classList . remove ( 'has-av' ) ; } ;
if ( im . getAttribute ( 'src' ) !== url ) im . src = url ;
}
function loadAv ( ) {
if ( ! OPENID || ! window . fetch ) return ;
fetch ( '/api/mp/public/user-center/info?openid=' + encodeURIComponent ( OPENID ) + ( param ( 'unionid' ) ? '&unionid=' + encodeURIComponent ( param ( 'unionid' ) ) : '' ) )
. then ( function ( r ) { return r . ok ? r . json ( ) : null ; } ) . then ( function ( d ) {
var av = d && d . result && d . result . found && d . result . data && d . result . data . avatar ;
if ( realAv ( av ) ) paintAv ( av ) ;
} ) . catch ( function ( ) { } ) ;
}
if ( OPENID ) { paintAv ( realAv ( param ( 'avatar' ) ) ? param ( 'avatar' ) : defAv ( OPENID ) ) ; loadAv ( ) ; }
window . addEventListener ( 'pageshow' , function ( e ) { if ( e . persisted ) loadAv ( ) ; } ) ;
return { bar : bar , fade : fade , openRec : openRec , closeRec : closeRec } ;
}
g . MBTab = { init : init } ;
} ) ( window ) ;