Files
mama-box/js/birth-flow.js

436 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 孕期 → 育儿 过渡(2026-10-21 用户定,方案演示稿 /yunyu-preview/demo/guodu/)
① 孕 37 周起:首页常驻提示卡「宝宝随时可能来报到」+「宝宝出生了」按钮(不弹窗)
② 过了预产期:每天第一次打开问一次「宝宝出生了吗?」,点「还没有」当天不再问;不自动切换
③ 登记出生:出生日期必填(默认今天、不能晚于今天),小名沿用孕期胎名,性别可「先不说」
④ 恭喜过渡页:孕期记录都保留;首页换成育儿模式;点「进入育儿首页」带身份跳 /yunyu/(#from=birth)
⑤ 已登记出生、还停在孕期首页的人:首页一张「宝宝已出生 · 去育儿首页」卡
⑥「我的」页:孕期卡下面一行「宝宝出生了?切换到育儿模式」,任何孕周都能自己登记
写入和分流页 /yunyu/entry/ 选「宝宝已出生」走同一条路:PUT mama-box/profile {babyBirthDate}(box_app 推上游共享资料)
+ 小名 / 性别;本机同时记下分流页和育儿首页认的阶段键,下次打开小程序直接进育儿首页。
自成一体:自己读 /pregnancy、/profile,不碰 app.js 的状态;样式全在本文件里。
演示:地址 hash 带 bf_demo=37 / over / born,按该状态演示,**不写任何数据**,最后跳育儿预览站。 */
(function () {
'use strict';
var API = '/api/mp/public/';
var BABY_HOME = 'https://mp.yizhezhe.com/yunyu/';
var BABY_HOME_DEMO = 'https://mp.yizhezhe.com/yunyu-preview/';
var ENTRY_KEY = 'yy-entry.choice.v1'; /* 分流页 /yunyu/entry/ 认的本机阶段 */
var YUNYU_KEY = 'yunyu:yy.stage.v1'; /* 育儿首页 /yunyu/ 认的本机阶段(pending=true 时它会自动再交一次出生日期) */
var ASK_KEY = 'mamabox.birthAsk.v1'; /* 过预产期询问:记当天日期,同一天只问一次 */
function hashParam(name) {
var h = (location.hash.slice(1) || '') + '&' + (location.search.slice(1) || '');
var m = h.match(new RegExp('(?:^|&)' + name + '=([^&]*)'));
return m ? decodeURIComponent(m[1]) : '';
}
var openid = hashParam('openid');
var DEMO = hashParam('bf_demo'); /* 37 | over | born */
var $ = function (s, r) { return (r || document).querySelector(s); };
function pad(n) { return (n < 10 ? '0' : '') + n; }
function iso(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
function midnight(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
function parseDay(s) { var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || ''); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
function getJSON(u) { return fetch(u).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }
function esc(s) { return String(s || '').replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
var S = { week: null, day: null, due: null, postpartum: false, birth: '', babyName: '', babySex: 'unknown', since: null };
/* ---------- 样式 ---------- */
var css = '' +
'.bf-card{margin:12px 16px 4px;display:flex;gap:12px;align-items:center;padding:13px 14px;border-radius:18px;background:linear-gradient(120deg,#FFF1E8,#FFFDFB);border:1px solid #FFD9C7;box-shadow:0 4px 14px rgba(240,112,79,.08)}' +
'.bf-card .bf-ic{flex:none;width:44px;height:44px;border-radius:50%;background:#FFE0D1;display:flex;align-items:center;justify-content:center}' +
'.bf-card .bf-tx{flex:1;min-width:0;font-size:12.5px;color:#6B615B;line-height:1.5}' +
'.bf-card .bf-tx b{display:block;font-size:14.5px;color:#2B2320;margin-bottom:1px}' +
'.bf-card .bf-go{flex:none;font-size:12.5px;font-weight:700;color:#F4593C;border:1.5px solid #F4593C;border-radius:99px;padding:6px 11px;background:#fff;white-space:nowrap}' +
'.bf-card.born{background:linear-gradient(120deg,#E6F5EC,#FBFFFC);border-color:#BFE5CF}' +
'.bf-card.born .bf-ic{background:#CFEEDC}.bf-card.born .bf-go{color:#2F8A5E;border-color:#2F8A5E}' +
'.bf-me{margin-top:14px}.bf-me .li-ico svg *{fill:none}.bf-me.born .li-ico{background:#E2F2E8;color:#3A8A5A}.bf-me.born .li-v{color:#3A7A52}' +
'.bf-ov{position:fixed;inset:0;z-index:135;display:none;background:rgba(30,20,15,.45);font-family:inherit;color:#2B2320}' +
'.bf-ov.on{display:flex;align-items:flex-end}' +
'.bf-sheet{width:100%;background:#fff;border-radius:22px 22px 0 0;padding:22px 20px calc(22px + env(safe-area-inset-bottom));animation:bfUp .3s ease}' +
'@keyframes bfUp{from{transform:translateY(40px);opacity:0}to{transform:none;opacity:1}}' +
'.bf-sheet .bf-em{width:64px;height:64px;margin:0 auto 10px;border-radius:50%;background:#FFE7DC;display:flex;align-items:center;justify-content:center}' +
'.bf-sheet h2{text-align:center;font-size:20px;margin:0}' +
'.bf-sheet p{text-align:center;font-size:13.5px;color:#6B615B;line-height:1.6;margin:8px 0 18px}' +
'.bf-btn{display:block;width:100%;border:0;border-radius:99px;padding:13px;font-size:15px;font-weight:700;background:#F4593C;color:#fff;text-align:center;font-family:inherit}' +
'.bf-btn+.bf-btn{margin-top:8px}.bf-btn.ghost{background:transparent;color:#6B615B;font-weight:500}.bf-btn.baby{background:#3FA27E}' +
'.bf-btn[disabled]{opacity:.6}' +
'.bf-page{position:absolute;inset:0;background:#F8F3EF;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;animation:bfIn .25s ease}' +
'@keyframes bfIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}' +
/* ③ 登记页:海盐蓝(和育儿首页同色系 #3A7A52 / #5B86D0,配浅青),迎接宝宝的感觉 */
'.bf-reg{min-height:100%;background:linear-gradient(180deg,#CBEBD8 0%,#E8F4EC 34%,#F1F7F2 62%,#F6FAF7 100%)}' +
'.bf-reg .bf-foot{background:linear-gradient(180deg,rgba(246,250,247,0),#F6FAF7 35%)}' +
'.bf-reg .bf-sky{position:absolute;left:0;right:0;top:0;height:260px;pointer-events:none;overflow:hidden}' +
'.bf-reg .bf-sky i{position:absolute;border-radius:50%;background:#fff;opacity:.55;filter:blur(1px)}' +
'.bf-reg .bf-sky b{position:absolute;color:#fff;font-size:12px;opacity:.9;animation:bfTw 2.6s ease-in-out infinite alternate}' +
'@keyframes bfTw{from{opacity:.25;transform:scale(.8)}to{opacity:.95;transform:scale(1.1)}}' +
'.bf-hero{position:relative;padding:calc(26px + env(safe-area-inset-top)) 22px 6px;text-align:center}' +
'.bf-moon{width:104px;height:104px;margin:0 auto 12px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#FFFFFF,#E6F4EB 62%,#CDE8D7);box-shadow:0 10px 30px rgba(50,110,75,.25),inset 0 -6px 14px rgba(50,110,75,.12);display:flex;align-items:center;justify-content:center;animation:bfFloat 3.4s ease-in-out infinite alternate}' +
'@keyframes bfFloat{to{transform:translateY(-6px)}}' +
'.bf-hero h2{font-size:22px;margin:0;color:#24372B;letter-spacing:.5px}' +
'.bf-hero p{font-size:13px;color:#55695C;margin:8px 0 0;line-height:1.65}' +
'.bf-form{position:relative;padding:18px 16px 170px}' +
/* 2026-10-21 用户:点「宝宝出生了」从底部出大半弹窗,不换新页面 → .bf-rs 占屏 90%,中间滚、按钮钉底 */
'.bf-rs{position:relative;width:100%;height:90%;min-height:0;max-height:calc(100% - 36px);border-radius:24px 24px 0 0;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 -10px 30px rgba(20,40,28,.18);animation:bfRsUp .34s cubic-bezier(.2,.8,.2,1)}' +
'@keyframes bfRsUp{from{transform:translateY(100%)}to{transform:none}}' +
'.bf-rs-body{flex:1;min-height:0;position:relative;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}' +
'.bf-rs .bf-hero{padding-top:30px}.bf-rs .bf-moon{width:92px;height:92px}' +
'.bf-rs .bf-form{padding-bottom:8px}' +
'.bf-rs .bf-foot{position:static;flex:none;padding-top:10px;background:#F6FAF7;box-shadow:0 -6px 16px rgba(246,250,247,.95)}' +
'.bf-grab{position:absolute;z-index:3;top:8px;left:50%;width:40px;height:5px;margin-left:-20px;border-radius:3px;background:rgba(36,55,43,.18)}' +
'.bf-x{position:absolute;z-index:3;top:12px;right:12px;width:32px;height:32px;border:0;border-radius:50%;background:rgba(255,255,255,.7);color:#55695C;font-size:20px;line-height:32px;padding:0;font-family:inherit}' +
'.bf-card2{background:rgba(255,255,255,.92);border-radius:20px;padding:4px 16px;box-shadow:0 8px 26px rgba(50,110,75,.12);border:1px solid rgba(255,255,255,.9)}' +
'.bf-fld{display:flex;align-items:center;gap:12px;padding:15px 0;border-bottom:1px solid #E4F1E9;width:100%;background:none;border-left:0;border-right:0;border-top:0;text-align:left;font-family:inherit;color:inherit}' +
'.bf-fld:last-child{border-bottom:0}' +
'.bf-fld .bf-fi svg *{fill:none}.bf-fld .bf-fi svg circle[fill]{fill:#3A7A52}.bf-fld .bf-fi{flex:none;width:36px;height:36px;border-radius:12px;display:flex;align-items:center;justify-content:center;background:#D8EFE1}' +
'.bf-fld .bf-fb{flex:1;min-width:0}' +
'.bf-fld .bf-fl{display:block;font-size:12px;color:#6B8072;margin-bottom:3px}.bf-fld .bf-fl em{font-style:normal;color:#3A7A52;font-weight:700;margin-left:4px}' +
'.bf-fld .bf-fv{display:block;font-size:16.5px;font-weight:700;color:#24372B}.bf-fld .bf-fv small{font-size:12px;font-weight:500;color:#6B8072;margin-left:6px}' +
'.bf-fld .bf-fa{flex:none;color:#9DB8A6;font-size:20px}' +
'.bf-fld input{width:100%;border:0;outline:0;background:transparent;font-size:16.5px;font-weight:700;color:#24372B;font-family:inherit;padding:0;-webkit-appearance:none;appearance:none}' +
'.bf-fld input::placeholder{color:#B3C7B9;font-weight:500}' +
'.bf-sexs{display:flex;gap:10px;padding:4px 0 16px}' +
'.bf-sexs button{flex:1;border:1.5px solid #D7E8DD;background:#fff;border-radius:16px;padding:12px 0 10px;font-size:13.5px;color:#4A5F51;font-family:inherit;display:flex;flex-direction:column;align-items:center;gap:6px;transition:all .2s}' +
'.bf-sexs button i{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-style:normal;line-height:0}.bf-sexs button i svg{display:block;fill:none}' +
'.bf-sexs .s-boy i{background:#6FA3DD}.bf-sexs .s-girl i{background:#F29BB5}.bf-sexs .s-unknown i{background:#BDCDC2}' +
'.bf-sexs button.on{border-color:#3A7A52;background:#EAF5EE;color:#24372B;font-weight:700;box-shadow:0 4px 12px rgba(50,110,75,.18)}' +
'.bf-sexs .s-girl.on{border-color:#E57FA0;background:#FDF0F4}' +
'.bf-tip{display:flex;gap:8px;align-items:flex-start;margin:14px 4px 0;font-size:12px;color:#6B8072;line-height:1.6}' +
'.bf-err{font-size:12.5px;color:#D9442B;min-height:18px;margin:8px 4px 0}' +
'.bf-btn.green{background:linear-gradient(100deg,#6DB088,#3A7A52);box-shadow:0 8px 20px rgba(58,122,82,.3)}' +
'.bf-btn.ghost.b{color:#6B8072}' +
/* 底部日期滚轮 */
'.bf-dmask{position:fixed;inset:0;z-index:2;background:rgba(20,40,28,.42);display:flex;align-items:flex-end;animation:bfFade .2s ease}' +
'@keyframes bfFade{from{opacity:0}}' +
'.bf-dsheet{width:100%;background:#fff;border-radius:22px 22px 0 0;padding:6px 0 calc(16px + env(safe-area-inset-bottom));animation:bfUp .28s ease}' +
'.bf-dhd{display:flex;align-items:center;justify-content:space-between;padding:10px 18px 4px}' +
'.bf-dhd b{font-size:16px;color:#24372B}.bf-dhd button{border:0;background:none;font-size:15px;font-family:inherit;padding:6px 2px;color:#6B8072}' +
'.bf-dhd .ok{color:#3A7A52;font-weight:700}' +
'.bf-dquick{display:flex;gap:8px;padding:6px 18px 2px}.bf-dquick button{border:1px solid #D5E8DC;background:#F3F9F5;color:#4A5F51;border-radius:99px;padding:5px 12px;font-size:12.5px;font-family:inherit}' +
'.bf-wheels{position:relative;display:flex;height:220px;margin:6px 12px 0;overflow:hidden}' +
'.bf-wheels::before{content:"";position:absolute;left:0;right:0;top:88px;height:44px;border-radius:12px;background:#EAF5EE;pointer-events:none}' +
'.bf-wheels::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#fff 0%,rgba(255,255,255,0) 36%,rgba(255,255,255,0) 64%,#fff 100%)}' +
'.bf-wh{flex:1;position:relative;overflow-y:scroll;scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;overscroll-behavior:contain}' +
'.bf-wh::-webkit-scrollbar{display:none}' +
'.bf-wh div{height:44px;line-height:44px;text-align:center;font-size:17px;color:#98AE9F;scroll-snap-align:center}' +
'.bf-wh div.on{color:#24372B;font-weight:700;font-size:18px}' +
'.bf-wh div.off{color:#D3DED6}' +
'.bf-wh .pad{scroll-snap-align:none}' +
'.bf-foot{position:fixed;left:0;right:0;bottom:0;padding:12px 16px calc(12px + env(safe-area-inset-bottom));background:linear-gradient(180deg,rgba(248,243,239,0),#F8F3EF 35%)}' +
'.bf-cele{min-height:100%;background:linear-gradient(180deg,#FFE9DF 0%,#FFF7F2 45%,#F1FAF4 100%);padding:calc(30px + env(safe-area-inset-top)) 18px 130px;text-align:center;position:relative;overflow:hidden}' +
'.bf-conf{position:absolute;inset:0;pointer-events:none}.bf-conf i{position:absolute;top:-12px;width:8px;height:12px;border-radius:2px;animation:bfFall 3.2s linear infinite}' +
'@keyframes bfFall{to{transform:translateY(110vh) rotate(540deg)}}' +
'.bf-badge{width:96px;height:96px;margin:6px auto 12px;border-radius:50%;background:#fff;box-shadow:0 6px 20px rgba(244,89,60,.18);display:flex;align-items:center;justify-content:center}' +
'.bf-cele h1{font-size:24px;line-height:1.35;margin:0}.bf-cele .bf-who{font-size:14px;color:#6B615B;margin-top:6px}' +
'.bf-memo{text-align:left;margin-top:16px;background:#fff;border-radius:16px;padding:14px;position:relative}' +
'.bf-memo h3{font-size:14.5px;margin:0 0 8px}.bf-memo .bf-p{font-size:12.5px;color:#6B615B;line-height:1.65}' +
'.bf-memo .bf-big{font-size:28px;font-weight:800;color:#E04A2E}' +
'.bf-swap{display:flex;align-items:center;gap:8px;margin:2px 0 8px}.bf-swap div{flex:1;border-radius:12px;padding:9px 8px;font-size:12px;line-height:1.7}' +
'.bf-swap b{display:block;font-size:12.5px}.bf-swap .f{background:#FFF1EA;color:#8A4A35}.bf-swap .t{background:#EAF5EE;color:#2F6B4A}.bf-swap span{flex:none;color:#9A8E86}' +
'.bf-tiny{font-size:12px;color:#8A7E76;margin-top:12px}';
var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
var HEART = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F4593C" stroke-width="2" stroke-linecap="round"><path d="M12 21s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 11c0 5.6-7 10-7 10z"/></svg>';
var BABY = function (c, w) { return '<svg width="' + w + '" height="' + w + '" viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="1.7" stroke-linecap="round"><circle cx="12" cy="10" r="6"/><path d="M9.6 9.5h.01M14.4 9.5h.01M10 12.2c1.2.8 2.8.8 4 0M12 4c0-1.3 1-2 2-2"/></svg>'; };
/* 11-07 用户:「我的」里「宝宝出生了」的小图标要和下面几行(锁 / 星星…)匹配 → 同款 19px 线性图标(婴儿车),色块走 .li-ico 配色 */
var PRAM = '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M12.5 4.2a7.3 7.3 0 0 1 7.3 7.3h-7.3z"/><path d="M3.2 6.8l1.3 4.7h15.3a5.2 5.2 0 0 1-5.2 4.6H9.4a5 5 0 0 1-4.9-4.6"/>' +
'<circle cx="8.6" cy="19.3" r="1.6"/><circle cx="16.2" cy="19.3" r="1.6"/></svg>';
/* ---------- 浮层 ---------- */
var ov = document.createElement('div'); ov.className = 'bf-ov'; document.body.appendChild(ov);
/* 浮层里滑动不带动后面的首页(禁止下拉回弹规则) */
ov.addEventListener('touchmove', function (e) { if (!e.target.closest('.bf-page,.bf-rs-body,.bf-wh')) e.preventDefault(); }, { passive: false });
function show(html, cls) { ov.innerHTML = html; ov.className = 'bf-ov on' + (cls ? ' ' + cls : ''); }
function hide() { ov.className = 'bf-ov'; ov.innerHTML = ''; }
/* ② 过预产期询问 */
function askSheet(over) {
show('<div class="bf-sheet"><div class="bf-em">' + BABY('#F4593C', 34) + '</div><h2>宝宝出生了吗?</h2>' +
'<p>预产期已经过了 ' + over + ' 天。<br>宝宝到了的话告诉我们,帮你换成育儿模式。</p>' +
'<button class="bf-btn" data-bf="reg">出生了,去登记</button><button class="bf-btn ghost" data-bf="later">还没有,明天再问我</button></div>');
}
/* ③ 登记出生(2026-10-21 用户:出生日期点开从底部弹滚轮选;整页用育儿首页的海盐薄荷绿(同日改:蓝→绿),好看一点) */
var sex = 'unknown', birth = null;
var MOON = '<svg width="62" height="62" viewBox="0 0 64 64" fill="none"><circle cx="32" cy="34" r="17" fill="#FFE9DA"/><path d="M17 30c2-10 9-15 15-15s13 5 15 15c-4-3-9-5-15-5s-11 2-15 5z" fill="#86C6A0"/><circle cx="26" cy="35" r="1.8" fill="#24372B"/><circle cx="38" cy="35" r="1.8" fill="#24372B"/><path d="M28 41c2.4 1.8 5.6 1.8 8 0" stroke="#24372B" stroke-width="1.8" stroke-linecap="round"/><circle cx="22" cy="40" r="2.6" fill="#F7B6C2" opacity=".7"/><circle cx="42" cy="40" r="2.6" fill="#F7B6C2" opacity=".7"/><path d="M45 14l1.3 3.3 3.3 1.3-3.3 1.3L45 23.2l-1.3-3.3-3.3-1.3 3.3-1.3z" fill="#FFD25C"/></svg>';
var ICO = {
date: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3A7A52" stroke-width="1.9" stroke-linecap="round"><rect x="4" y="5.5" width="16" height="14.5" rx="3"/><path d="M4 10h16M9 3.5v4M15 3.5v4"/><circle cx="12" cy="15" r="1.4" fill="#3A7A52" stroke="none"/></svg>',
name: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3A7A52" stroke-width="1.9" stroke-linecap="round"><path d="M12 20s-6.5-4-6.5-9.2A3.8 3.8 0 0 1 12 8.3a3.8 3.8 0 0 1 6.5 2.5C18.5 16 12 20 12 20z"/></svg>',
sex: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3A7A52" stroke-width="1.9" stroke-linecap="round"><circle cx="12" cy="9" r="4.5"/><path d="M5.5 20c1.2-3.4 3.6-5 6.5-5s5.3 1.6 6.5 5"/></svg>'
};
/* 性别小标志用 SVG 画、按 24×24 几何居中(2026-10-21 用户:♂♀ 字符在圆里没居中——字体字形本身偏心) */
var SEX = {
boy: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="14" r="5.5"/><path d="M13.9 10.1L19.5 4.5M14.5 4.5h5v5"/></svg>',
girl: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="8" r="5.5"/><path d="M12 13.5v8M8.5 18h7"/></svg>',
unknown: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8.6 7.4a3.5 3.5 0 0 1 6.8 1.1c0 2.4-3.4 2.9-3.4 5"/><circle cx="12" cy="18" r=".6" fill="#fff"/></svg>'
};
function dateTxt(d) {
var t = midnight(new Date()), n = Math.round((t - d) / 864e5);
var tag = n === 0 ? '今天' : n === 1 ? '昨天' : n === 2 ? '前天' : '出生第 ' + (n + 1) + ' 天';
return d.getFullYear() + ' 年 ' + (d.getMonth() + 1) + ' 月 ' + d.getDate() + ' 日<small>' + tag + '</small>';
}
function regPage() {
birth = midnight(new Date());
sex = S.babySex === 'boy' || S.babySex === 'girl' ? S.babySex : 'unknown';
var stars = '';
[[12, 18, 6], [80, 12, 4], [64, 30, 5], [24, 44, 3], [90, 40, 6]].forEach(function (x, i) { stars += '<i style="left:' + x[0] + '%;top:' + x[1] + '%;width:' + x[2] * 6 + 'px;height:' + x[2] * 2.4 + 'px"></i>'; });
[[8, 30], [88, 22], [72, 8], [30, 12], [50, 46]].forEach(function (x, i) { stars += '<b style="left:' + x[0] + '%;top:' + x[1] + '%;animation-delay:' + i * .5 + 's"><svg width="12" height="12" viewBox="0 0 12 12"><path d="M6 0l1.4 4.6L12 6 7.4 7.4 6 12 4.6 7.4 0 6l4.6-1.4z" fill="#fff"/></svg></b>'; });
show('<div class="bf-rs bf-reg"><i class="bf-grab"></i><button class="bf-x" data-bf="close" aria-label="关闭">×</button><div class="bf-rs-body"><div class="bf-sky">' + stars + '</div>' +
'<div class="bf-hero"><div class="bf-moon">' + MOON + '</div><h2>欢迎宝宝来到这个世界</h2>' +
'<p>告诉我们宝宝的生日,首页就换成育儿模式<br>孕期的点点滴滴,都会好好留着</p></div>' +
'<div class="bf-form"><div class="bf-card2">' +
'<button class="bf-fld" data-bf="date"><span class="bf-fi">' + ICO.date + '</span><span class="bf-fb"><span class="bf-fl">出生日期<em>必填</em></span><span class="bf-fv" id="bfDateTxt">' + dateTxt(birth) + '</span></span><span class="bf-fa">›</span></button>' +
'<label class="bf-fld"><span class="bf-fi">' + ICO.name + '</span><span class="bf-fb"><span class="bf-fl">宝宝小名</span><input id="bfName" maxlength="12" placeholder="给宝宝起个小名吧" value="' + esc(S.babyName) + '"></span></label>' +
'<div class="bf-fld" style="padding-bottom:6px"><span class="bf-fi">' + ICO.sex + '</span><span class="bf-fb"><span class="bf-fl">宝宝性别</span></span></div>' +
'<div class="bf-sexs" id="bfSex">' +
[['boy', '男孩', SEX.boy], ['girl', '女孩', SEX.girl], ['unknown', '先不说', SEX.unknown]].map(function (p) { return '<button class="s-' + p[0] + (p[0] === sex ? ' on' : '') + '" data-sex="' + p[0] + '"><i>' + p[2] + '</i>' + p[1] + '</button>'; }).join('') +
'</div></div>' +
'<div class="bf-tip"><svg width="14" height="14" viewBox="0 0 24 24" style="flex:none;margin-top:2px"><path d="M12 21s-7.5-4.6-7.5-10.6A4.4 4.4 0 0 1 12 7.6a4.4 4.4 0 0 1 7.5 2.8C19.5 16.4 12 21 12 21z" fill="#86C6A0"/></svg><span>只有出生日期必填,小名和性别以后在育儿首页「我的」里也能改。</span></div>' +
'<div class="bf-err" id="bfErr"></div></div></div>' +
'<div class="bf-foot"><button class="bf-btn green" data-bf="submit" id="bfSubmit">完成,切换到育儿模式</button><button class="bf-btn ghost b" data-bf="close">先不切换</button></div></div>');
}
/* 底部日期滚轮:年 / 月 / 日,最晚今天、最早两年前;快捷「今天 / 昨天」 */
function dateSheet() {
var page = $('.bf-reg'); if (!page) return;
var t = midnight(new Date()), minY = t.getFullYear() - 2;
var cur = { y: birth.getFullYear(), m: birth.getMonth() + 1, d: birth.getDate() };
var dim = function (y, m) { return new Date(y, m, 0).getDate(); };
var box = document.createElement('div'); box.className = 'bf-dmask';
box.innerHTML = '<div class="bf-dsheet"><div class="bf-dhd"><button data-d="cancel">取消</button><b>宝宝的生日</b><button class="ok" data-d="ok">确定</button></div>' +
'<div class="bf-dquick"><button data-d="q0">今天</button><button data-d="q1">昨天</button><button data-d="q2">前天</button></div>' +
'<div class="bf-wheels"><div class="bf-wh" data-k="y"></div><div class="bf-wh" data-k="m"></div><div class="bf-wh" data-k="d"></div></div></div>';
ov.appendChild(box);
var wh = {}; [].forEach.call(box.querySelectorAll('.bf-wh'), function (w) { wh[w.dataset.k] = w; });
function fill(k, from, to, unit) {
var h = '<div class="pad"></div><div class="pad"></div>';
for (var v = from; v <= to; v++) h += '<div data-v="' + v + '">' + v + unit + '</div>';
wh[k].innerHTML = h + '<div class="pad"></div><div class="pad"></div>';
}
function mark() {
['y', 'm', 'd'].forEach(function (k) {
[].forEach.call(wh[k].querySelectorAll('[data-v]'), function (el) {
var v = +el.dataset.v, y = k === 'y' ? v : cur.y, m = k === 'm' ? v : cur.m, d = k === 'd' ? v : Math.min(cur.d, dim(y, m));
el.classList.toggle('on', v === cur[k]);
el.classList.toggle('off', new Date(y, m - 1, k === 'd' ? v : 1) > t); /* 晚于今天的灰掉 */
});
});
}
function scrollTo(k, smooth) {
var el = wh[k].querySelector('[data-v="' + cur[k] + '"]'); if (!el) return;
wh[k].scrollTo({ top: el.offsetTop - 88, behavior: smooth ? 'smooth' : 'auto' });
}
function clamp() {
if (cur.d > dim(cur.y, cur.m)) cur.d = dim(cur.y, cur.m);
if (new Date(cur.y, cur.m - 1, cur.d) > t) { cur.y = t.getFullYear(); cur.m = Math.min(cur.m, t.getMonth() + 1); if (new Date(cur.y, cur.m - 1, cur.d) > t) { cur.m = t.getMonth() + 1; cur.d = t.getDate(); } }
}
function rebuildDays() { fill('d', 1, dim(cur.y, cur.m), ' 日'); }
fill('y', minY, t.getFullYear(), ' 年'); fill('m', 1, 12, ' 月'); rebuildDays(); mark();
['y', 'm', 'd'].forEach(function (k) { scrollTo(k); });
var timers = {};
['y', 'm', 'd'].forEach(function (k) {
wh[k].addEventListener('scroll', function () {
clearTimeout(timers[k]);
timers[k] = setTimeout(function () {
var idx = Math.round(wh[k].scrollTop / 44), items = wh[k].querySelectorAll('[data-v]');
var el = items[Math.max(0, Math.min(items.length - 1, idx))]; if (!el) return;
var before = cur.y + '-' + cur.m + '-' + cur.d;
cur[k] = +el.dataset.v; clamp();
if (k !== 'd') { var keep = cur.d; rebuildDays(); cur.d = Math.min(keep, dim(cur.y, cur.m)); clamp(); }
mark();
if (before !== cur.y + '-' + cur.m + '-' + cur.d || +el.dataset.v !== cur[k]) ['y', 'm', 'd'].forEach(function (q) { scrollTo(q, true); });
}, 110);
}, { passive: true });
});
box.addEventListener('click', function (e) {
var b = e.target.closest('[data-d]');
if (!b) { if (e.target === box) box.remove(); return; }
var a = b.dataset.d;
if (a === 'cancel') box.remove();
else if (a === 'ok') { birth = new Date(cur.y, cur.m - 1, cur.d); $('#bfDateTxt').innerHTML = dateTxt(birth); box.remove(); }
else { var q = new Date(t - (+a.slice(1)) * 864e5); cur = { y: q.getFullYear(), m: q.getMonth() + 1, d: q.getDate() }; rebuildDays(); mark(); ['y', 'm', 'd'].forEach(function (k) { scrollTo(k, true); }); }
});
box.addEventListener('touchmove', function (e) { if (!e.target.closest('.bf-wh')) e.preventDefault(); }, { passive: false });
}
function submit() {
var d = birth, name = $('#bfName').value.trim();
var t = midnight(new Date());
if (!d) { $('#bfErr').textContent = '请选择出生日期'; return; }
if (d > t) { $('#bfErr').textContent = '出生日期不能晚于今天'; return; }
/* 不拿预产期卡出生日期:在分流页误选了「孕期」的育儿用户,预产期是随手填的,宝宝可能早就出生了(2026-10-21) */
var v = iso(d), btn = $('#bfSubmit');
btn.disabled = true; btn.textContent = '正在保存…'; $('#bfErr').textContent = '';
var put = function (body) {
return fetch(API + 'mama-box/profile?openid=' + encodeURIComponent(openid), {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
}).then(function (r) { return r.json(); }).catch(function () { return null; });
};
var done = (DEMO || !openid) ? Promise.resolve(!!DEMO)
: put({ babyBirthDate: v }).then(function (res) {
var ok = !!(res && res.code === 200);
var extra = {};
if (name && name !== S.babyName) extra.babyName = name;
if (sex !== S.babySex) extra.babySex = sex;
/* 2026-10-22(测试会话 150e0991):第二次 PUT(小名 / 性别)原来不看结果,失败也照跳育儿首页 →
小名性别静默丢了。现在失败就停在登记页提示重试;重试会把出生日期再 PUT 一次(同值,幂等)。
出生日期本身没存上仍按原设计放行(pending,育儿首页会自动再交),但那份补交不含小名性别,所以这里必须拦。 */
if (!Object.keys(extra).length) return ok;
return put(extra).then(function (r2) {
if (r2 && r2.code === 200) return ok;
btn.disabled = false; btn.textContent = '重试保存';
$('#bfErr').textContent = (r2 && r2.message ? r2.message + ',' : '') + '宝宝小名和性别没保存上,请重试';
return null;
});
});
done.then(function (ok) {
if (ok === null) return; /* 小名 / 性别保存失败,已提示,留在登记页 */
if (!openid && !DEMO) { btn.disabled = false; btn.textContent = '完成,切换到育儿模式'; $('#bfErr').textContent = '没有拿到你的身份信息,请从小程序里重新打开再试'; return; }
/* 本机记下阶段:分流页下次直接送育儿首页;没存上服务器时 pending=true,育儿首页会自动再交 */
if (!DEMO) try {
localStorage.setItem(ENTRY_KEY, JSON.stringify({ stage: 'baby', date: v, name: name, at: Date.now() }));
localStorage.setItem(YUNYU_KEY, JSON.stringify({ stage: 'baby', date: v, name: name, pending: !ok }));
} catch (e) {}
S.birth = v; S.babyName = name; S.babySex = sex;
celebrate(ok);
});
}
/* ④ 恭喜过渡页 */
function celebrate(saved) {
var b = parseDay(S.birth), age = b ? Math.round((midnight(new Date()) - b) / 864e5) + 1 : 1;
var who = [S.babyName || '宝宝', S.babySex === 'boy' ? '男孩' : S.babySex === 'girl' ? '女孩' : '', age <= 1 ? '今天出生' : '出生第 ' + age + ' 天'].filter(Boolean).join(' · ');
var days = S.since ? Math.max(1, Math.round((midnight(new Date()) - midnight(S.since)) / 864e5) + 1) : 0;
show('<div class="bf-page"><div class="bf-cele"><div class="bf-conf" id="bfConf"></div>' +
'<div class="bf-badge">' + BABY('#F4593C', 52) + '</div><h1>恭喜你,<br>正式升级当妈妈啦</h1><div class="bf-who">' + esc(who) + '</div>' +
'<div class="bf-memo"><h3>你的孕期记录都还在</h3>' +
(days > 1 ? '<div class="bf-p">兜爸心选已经陪你 <span class="bf-big">' + days + '</span> 天</div>' : '') +
'<div class="bf-p">体重、产检、胎动这些孕期记录一条都不删,以后想看随时都能翻。</div></div>' +
'<div class="bf-memo"><h3>首页会换成育儿模式</h3><div class="bf-swap"><div class="f"><b>孕期工具(收起)</b>数胎动 · 数宫缩<br>待产清单 · 孕期体重</div><span>→</span>' +
'<div class="t"><b>育儿工具(换上)</b>喂奶记录 · 月子餐<br>产后恢复 · 疫苗时间表</div></div>' +
'<div class="bf-p">昵称、头像、宝宝小名都带过去,不用重新填。</div></div>' +
(saved ? '' : '<div class="bf-tiny">出生日期还没存到服务器,进入育儿首页后会自动再存一次</div>') +
'<div class="bf-tiny">选错了?在育儿首页「我的」里可以改回孕期</div></div>' +
'<div class="bf-foot"><button class="bf-btn baby" data-bf="baby">进入育儿首页</button></div></div>');
var c = $('#bfConf'), col = ['#F4593C', '#FFB36B', '#5AB98C', '#8B6FE0', '#FFD25C'];
for (var i = 0; i < 26; i++) { var e = document.createElement('i'); e.style.left = Math.random() * 100 + '%'; e.style.background = col[i % 5]; e.style.animationDelay = Math.random() * 2.4 + 's'; e.style.animationDuration = 2.6 + Math.random() * 1.6 + 's'; c.appendChild(e); }
}
/* 带着原 hash / query(身份等宿主参数)跳育儿首页,加 from=birth 让它提示「已切换到育儿模式」 */
function goBaby() {
var h = location.hash.replace(/&?bf_demo=[^&]*/, '').replace(/^#&/, '#');
h = (h && h !== '#' ? h + '&' : '#') + 'from=birth';
location.replace((DEMO ? BABY_HOME_DEMO : BABY_HOME) + location.search + h);
}
ov.addEventListener('click', function (e) {
var a = e.target.closest('[data-bf]'), sx = e.target.closest('#bfSex button');
if (sx) { sex = sx.dataset.sex; [].forEach.call(sx.parentNode.children, function (x) { x.classList.toggle('on', x === sx); }); return; }
if (!a) { if (e.target === ov) hide(); return; }
var k = a.dataset.bf;
if (k === 'reg') regPage();
else if (k === 'date') dateSheet();
else if (k === 'later' || k === 'close') hide();
else if (k === 'submit') submit();
else if (k === 'baby') goBaby();
});
document.addEventListener('click', function (e) {
var a = e.target.closest('[data-bf-open]');
if (!a) return;
e.preventDefault(); e.stopPropagation();
if (a.dataset.bfOpen === 'baby') goBaby(); else regPage();
}, true);
/* ---------- ① ⑤ 首页卡片 / ⑥「我的」入口 ---------- */
function mountCards() {
var home = $('#screen-home .tool-dock'), old = $('#bfCard');
if (old) old.remove();
var card = null;
if (S.postpartum) {
card = '<div class="bf-card born" id="bfCard"><div class="bf-ic">' + BABY('#2F8A5E', 26) + '</div><div class="bf-tx"><b>宝宝已出生</b>育儿首页按宝宝月龄推荐工具,孕期记录也都在</div><button class="bf-go" data-bf-open="baby">去育儿首页</button></div>';
} else if (S.week != null && S.week >= 37) {
card = '<div class="bf-card" id="bfCard"><div class="bf-ic">' + HEART + '</div><div class="bf-tx"><b>宝宝快来报到啦</b>出生后点一下,首页换成育儿模式,孕期记录都保留</div><button class="bf-go" data-bf-open="reg">宝宝出生了</button></div>';
}
if (card && home) home.insertAdjacentHTML('beforebegin', card);
var pc = $('#screen-me .preg-card'), me = $('#bfMe');
if (me) me.remove();
/* 2026-10-22(测试会话 150e0991):产后用户「我的」里原来什么入口都没有 → 换成「宝宝已出生 · 去育儿首页」,走同一个 goBaby() */
/* 11-05 用户:「宝宝出生了」字号和图标要跟下面「我的收藏」「兜豆兑换」一致 → 直接用同一套 .list-card > .li(38px 图标块、16.5px 粗体) */
if (pc) pc.insertAdjacentHTML('afterend', S.postpartum
? '<div class="list-card bf-me born" id="bfMe"><button class="li ripple" data-bf-open="baby"><span class="li-ico">' + PRAM + '</span><span class="li-t">宝宝已出生</span><span class="li-v">去育儿首页</span><i class="chev">›</i></button></div>'
: '<div class="list-card bf-me" id="bfMe"><button class="li ripple" data-bf-open="reg"><span class="li-ico o">' + PRAM + '</span><span class="li-t">宝宝出生了?</span><span class="li-v">切换到育儿模式</span><i class="chev">›</i></button></div>');
}
function maybeAsk() {
if (S.postpartum || !S.due) return;
var over = Math.round((midnight(new Date()) - S.due) / 864e5);
if (over < 1) return;
var today = iso(new Date());
try { if (!DEMO && localStorage.getItem(ASK_KEY) === today) return; localStorage.setItem(ASK_KEY, today); } catch (e) {}
/* 等首页先渲染 5 秒;首页使用引导、弹窗、底部弹层开着就再等等,别叠在一起。
层级 135:高于弹窗(125),低于首页使用引导(140)——引导晚出来也会盖在上面,不会被挡 */
var tries = 0;
function wait() {
var busy = $('.mb-coach.show') || $('.modal.show') || $('#sheet.show') || document.hidden;
if (busy && ++tries < 90) return setTimeout(wait, 2000);
if (!ov.classList.contains('on')) askSheet(over);
}
setTimeout(wait, 5000);
}
function apply(pg, pf) {
if (pg) {
S.postpartum = !!pg.postpartum;
S.week = S.postpartum || pg.empty ? null : pg.week;
S.due = parseDay(pg.due);
S.birth = pg.babyBirthDate || '';
}
if (pf) {
S.babyName = pf.babyName || '';
S.babySex = pf.babySex || 'unknown';
S.since = pf.created_at ? new Date(pf.created_at) : null;
}
}
function load() {
if (DEMO) {
var t = midnight(new Date());
S.babyName = '柚子'; S.babySex = 'girl'; S.since = new Date(t - 230 * 864e5);
if (DEMO === 'born') { S.postpartum = true; S.birth = iso(new Date(t - 3 * 864e5)); }
else if (DEMO === 'over') { S.due = new Date(t - 3 * 864e5); S.week = 40; }
else { S.due = new Date(+t + 24 * 864e5); S.week = 37; }
mountCards(); maybeAsk(); return;
}
if (!openid) return;
Promise.all([
getJSON(API + 'mama-box/pregnancy?openid=' + encodeURIComponent(openid)),
getJSON(API + 'mama-box/profile?openid=' + encodeURIComponent(openid))
]).then(function (r) {
apply(r[0] && r[0].code === 200 ? r[0].result : null, r[1] && r[1].code === 200 ? r[1].result : null);
mountCards(); maybeAsk();
});
}
/* 2026-10-31 用户反馈:登记过出生的人在「孕期档案」保存了孕周,首页仍挂「宝宝已出生 · 去育儿首页」卡。
后台已把账号翻回孕期;这里把本机两个阶段键也改成孕期(否则分流页 / 育儿首页读到 baby 会直接进育儿,
育儿首页见 pending 还会把出生日期再交一次),再重读卡片。 */
window.addEventListener('mmbox:profile-saved', function (e) {
var d = (e && e.detail) || {};
/* 预产期在未来才算回到孕期(和后台同一条规则);过去的日期 = 只是改了下档案,不动阶段 */
if (DEMO || !d.dueDate || d.dueDate < iso(new Date()) || !(d.wasPostpartum || S.postpartum)) { if (!DEMO && openid) load(); return; }
S.postpartum = false; S.birth = '';
try {
localStorage.setItem(ENTRY_KEY, JSON.stringify({ stage: 'preg', date: d.dueDate, name: S.babyName || '', at: Date.now() }));
localStorage.setItem(YUNYU_KEY, JSON.stringify({ stage: 'preg', date: d.dueDate, name: S.babyName || '', pending: false }));
} catch (x) {}
mountCards();
if (openid) load();
});
/* 回到前台重读一次(别的应用可能刚登记了出生 / 改了预产期) */
document.addEventListener('visibilitychange', function () { if (!document.hidden && !DEMO && openid) load(); });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', load); else load();
})();