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

265 lines
20 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{display:flex;align-items:center;gap:10px;width:100%;margin-top:12px;padding:14px 16px;border:0;border-radius:18px;background:#fff;text-align:left;font-size:15px;color:#2B2320;box-shadow:0 2px 10px rgba(80,40,20,.05)}' +
'.bf-me i{font-style:normal}.bf-me .bf-me-ic{flex:none;width:30px;height:30px;border-radius:10px;background:#FFE0D1;display:flex;align-items:center;justify-content:center}' +
'.bf-me .bf-me-t{flex:1}.bf-me .bf-me-v{font-size:12.5px;color:#9A8E86}.bf-me .bf-me-a{color:#C9BEB6}' +
'.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}}' +
'.bf-form{padding:calc(22px + env(safe-area-inset-top)) 18px 150px}' +
'.bf-form h2{font-size:21px;margin:0}.bf-form .bf-sub{font-size:13px;color:#6B615B;margin:6px 0 18px;line-height:1.6}' +
'.bf-row{background:#fff;border-radius:14px;padding:12px 14px;margin-bottom:10px}' +
'.bf-row label{display:block;font-size:12.5px;color:#8A7E76;margin-bottom:6px}.bf-row label b{color:#F4593C}' +
'.bf-row input{width:100%;border:0;outline:0;background:transparent;font-size:16px;font-weight:600;color:#2B2320;font-family:inherit;padding:2px 0;-webkit-appearance:none;appearance:none;min-height:26px}' +
'.bf-seg{display:flex;gap:8px}.bf-seg button{flex:1;border:1.5px solid #EFE7E1;background:#fff;border-radius:10px;padding:9px 0;font-size:14px;color:#2B2320;font-family:inherit}' +
'.bf-seg button.on{border-color:#F4593C;color:#F4593C;background:#FFF4EF;font-weight:700}' +
'.bf-err{font-size:12.5px;color:#D9442B;min-height:18px;margin-top:2px}' +
'.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>'; };
/* ---------- 浮层 ---------- */
var ov = document.createElement('div'); ov.className = 'bf-ov'; document.body.appendChild(ov);
/* 浮层里滑动不带动后面的首页(禁止下拉回弹规则) */
ov.addEventListener('touchmove', function (e) { if (!e.target.closest('.bf-page')) 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>');
}
/* ③ 登记出生 */
var sex = 'unknown';
function regPage() {
var today = iso(new Date());
sex = S.babySex === 'boy' || S.babySex === 'girl' ? S.babySex : 'unknown';
show('<div class="bf-page"><div class="bf-form"><h2>欢迎宝宝来到这个世界</h2>' +
'<div class="bf-sub">填好就能切换到育儿模式。只有出生日期必填,其它以后在育儿首页「我的」里也能改。</div>' +
'<div class="bf-row"><label>出生日期 <b>必填</b></label><input type="date" id="bfDate" value="' + today + '" max="' + today + '"></div>' +
'<div class="bf-row"><label>宝宝小名</label><input id="bfName" maxlength="12" placeholder="比如:柚子" value="' + esc(S.babyName) + '"></div>' +
'<div class="bf-row"><label>宝宝性别</label><div class="bf-seg" id="bfSex">' +
['boy:男孩', 'girl:女孩', 'unknown:先不说'].map(function (x) { var p = x.split(':'); return '<button data-sex="' + p[0] + '"' + (p[0] === sex ? ' class="on"' : '') + '>' + p[1] + '</button>'; }).join('') +
'</div></div><div class="bf-err" id="bfErr"></div></div>' +
'<div class="bf-foot"><button class="bf-btn" data-bf="submit" id="bfSubmit">完成,切换到育儿模式</button><button class="bf-btn ghost" data-bf="close">先不切换</button></div></div>');
}
function submit() {
var d = parseDay($('#bfDate').value), name = $('#bfName').value.trim();
var t = midnight(new Date());
if (!d) { $('#bfErr').textContent = '请选择出生日期'; return; }
if (d > t) { $('#bfErr').textContent = '出生日期不能晚于今天'; return; }
if (S.due && (S.due - d) / 864e5 > 140) { $('#bfErr').textContent = '出生日期比预产期早太多了,请再确认一下'; return; }
var v = iso(d), btn = $('#bfSubmit');
btn.disabled = true; btn.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;
return (Object.keys(extra).length ? put(extra) : Promise.resolve()).then(function () { return ok; });
});
done.then(function (ok) {
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 === '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();
if (pc && !S.postpartum) pc.insertAdjacentHTML('afterend',
'<button class="bf-me ripple" id="bfMe" data-bf-open="reg"><span class="bf-me-ic">' + BABY('#F4593C', 18) + '</span><span class="bf-me-t">宝宝出生了?</span><span class="bf-me-v">切换到育儿模式</span><i class="bf-me-a">›</i></button>');
}
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();
});
}
/* 回到前台重读一次(别的应用可能刚登记了出生 / 改了预产期) */
document.addEventListener('visibilitychange', function () { if (!document.hidden && !DEMO && openid) load(); });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', load); else load();
})();