孕期首页 v26102101(预览):全部工具只出孕期工具;孕期→育儿过渡(37 周提示卡、过预产期每天问一次、登记出生、恭喜过渡页、已出生去育儿首页卡、我的页入口)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-10-21 01:22:50 +00:00
parent 75076f698e
commit dbdd37c95d
3 changed files with 262 additions and 3 deletions

257
js/birth-flow.js Normal file
View File

@@ -0,0 +1,257 @@
/* 孕期 → 育儿 过渡(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:400;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) {}
/* 等首页先渲染、开屏引导之类的弹层先走,再问 */
setTimeout(function () { if (!ov.classList.contains('on')) askSheet(over); }, 2500);
}
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();
})();