feat(盲盒): 价值感改造一期前端 —— 待解锁任务 / 宝宝周记 / 集齐奖励 / 下周预告 (v26100107)

- 待解锁:错过的周 + 三个工具任务(进度、去完成跳应用、解锁按钮),卡册格子 🔒
- 翻卡层加「宝宝周记」;卡册点开带周记与宝宝的话
- 卡册顶部集齐奖励:三段 + 全集,差 N 张 / 待发放 / 已发放 / 填收货信息
- 集齐庆祝弹层 + 收货信息表单(POST /blindbox/claim)
- 本周已开时显示下周卡片剪影 + 倒计时;横幅优先提示可解锁 / 离 XX 大奖还差 N 张

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 03:15:16 +00:00
parent ec87d36db2
commit 555012f248
3 changed files with 252 additions and 9 deletions

134
js/app.js
View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26100106';
var VER = '26100107';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -3281,11 +3281,24 @@
return bbApi('blindbox').then(j => {
if (!j || j.code !== 200 || !j.result) return;
BB = j.result;
(BB.newRewards || []).forEach(g => bxPrizeQ.push(g));
renderBanner();
if (BB.backfilled > 0) setTimeout(() => toast('错过的 ' + BB.backfilled + ' 张宝宝卡已补给你,去卡册看看吧'), 1800);
if ($('#detailBox').classList.contains('show')) renderBox();
if ($('#detailBox').classList.contains('show')) {
renderBox();
if (bxPrizeQ.length && !bbBusy && !$('#bxReveal').classList.contains('show')) setTimeout(showPrize, 400);
}
}).catch(() => { /* 静默:横幅保持现状 */ });
}
/* 最近的一个集齐目标:先看当前孕周所在的那段,集齐了再看全集。用于「离孕中期大奖还差 3 张」 */
function bxGoal() {
const ss = (BB && BB.stages) || [];
const cur = ss.find(x => x.key !== 'all' && BB.week >= x.from && BB.week <= x.to && !x.complete);
const g = cur || ss.find(x => x.key === 'all' && !x.complete);
return g && g.left > 0 ? g : null;
}
const BX_TASK_ICO = { kick3: '👶', weight: '⚖️', tool: '📅' };
let bxLockSel = null, bxPrizeQ = [];
function renderBanner() {
const ban = $('#bbBanner');
const set = (t, s, cta, ready, calm) => {
@@ -3309,8 +3322,13 @@
if (op.length > 1) { badge.textContent = op.length; badge.classList.remove('hide'); }
if (BB.ended) set('我的孕期卡册', got + ' 张宝宝卡,永久保留', '去看看', false, true);
else if (!BB.currentOpened && op.indexOf(BB.week) >= 0) set('孕 ' + BB.week + ' 周的盲盒到了', got + ' · 免费开启', '去开启', true);
else if (op.length) set('还有 ' + op.length + ' 个盲盒没开', '错过的周也能补开 · ' + got, '去补开', true);
else set(BB.nextIn ? '下一个盲盒 ' + BB.nextIn + ' 天后到' : '本周盲盒已开启', got + (BB.flash ? ' · 闪卡 <b>' + BB.flash + '</b> 张' : ''), '看卡册', false, true);
else if (op.length) set('还有 ' + op.length + ' 个盲盒没开', '已解锁,随时补开 · ' + got, '去补开', true);
else if ((BB.locked || []).length && BB.credits) set('可以解锁 ' + BB.credits + ' 个错过的盲盒', '你完成了孕期小任务 · ' + got, '去解锁', true);
else {
const g = bxGoal();
set(BB.nextIn ? '下一个盲盒 ' + BB.nextIn + ' 天后到' : '本周盲盒已开启',
g ? '离' + g.name + '大奖还差 <b>' + g.left + '</b> 张' : got + (BB.flash ? ' · 闪卡 <b>' + BB.flash + '</b> 张' : ''), '看卡册', false, true);
}
}
/* 盲盒入口:首页横幅 + 「我的」页卡册行(#meBoxCard)走同一条路 */
function openBoxEntry() {
@@ -3319,6 +3337,7 @@
bbSel = null;
renderBox();
openPanel('detailBox');
if (bxPrizeQ.length) setTimeout(showPrize, 600);
}
$('#bbBanner').addEventListener('click', openBoxEntry);
$('#meBoxCard').addEventListener('click', openBoxEntry);
@@ -3349,6 +3368,43 @@
btn.textContent = '看看这张卡';
}
$('#bxMisc').textContent = (BB.weekOpeners >= 50) ? '本周已有 ' + fmtN(BB.weekOpeners) + ' 位妈妈开了盲盒' : '每周一个 · 完全免费';
/* 下周预告:剪影 + 倒计时(本周已开时才露出来,免得和「开启盲盒」抢视线) */
const nx = BB.next, nxEl = $('#bxNext');
nxEl.classList.toggle('hide', !(nx && !bbSel && !BB.ended));
if (nx && !bbSel && !BB.ended) {
$('#bxNextImg').innerHTML = cardImgHTML(nx.week);
$('#bxNextW').textContent = '孕 ' + nx.week + ' 周 · 下周预告';
$('#bxNextS').textContent = nx.size ? '宝宝会长到' + nx.size.name + '那么大' : '';
$('#bxNextD').textContent = nx.inDays;
}
/* 待解锁 + 任务 */
const lk = BB.locked || [], lkEl = $('#bxLock');
lkEl.classList.toggle('hide', !lk.length);
if (lk.length) {
if (lk.indexOf(bxLockSel) < 0) bxLockSel = lk[lk.length - 1];
$('#bxLockN').textContent = lk.length;
$('#bxLockChips').innerHTML = lk.map(w => '<button class="bx-chip lk' + (w === bxLockSel ? ' on' : '') + '" data-w="' + w + '" type="button">🔒 孕 ' + w + ' 周</button>').join('');
$('#bxTasks').innerHTML = (BB.tasks || []).map(t => {
const btn = t.ready ? '<button class="bx-tk-b go" data-unlock="' + t.id + '" type="button">解锁孕 ' + bxLockSel + ' 周</button>'
: t.used ? '<span class="bx-tk-b done">' + (t.period === 'week' ? '本周已用' : '今天已用') + '</span>'
: '<button class="bx-tk-b" data-app="' + bbEsc(t.identifier) + '" type="button">去完成</button>';
const prog = t.need > 1 ? '<i class="bx-tk-p"><s style="width:' + Math.round(t.progress / t.need * 100) + '%"></s></i><em>' + t.progress + '/' + t.need + '</em>' : '';
return '<div class="bx-tk' + (t.ready ? ' is-ready' : '') + '"><span class="bx-tk-i" aria-hidden="true">' + (BX_TASK_ICO[t.id] || '✨') + '</span>' +
'<div class="bx-tk-m"><b>' + bbEsc(t.title) + '</b><span>' + bbEsc(t.desc) + '</span>' + (prog ? '<div class="bx-tk-pr">' + prog + '</div>' : '') + '</div>' + btn + '</div>';
}).join('');
}
/* 集齐奖励 */
$('#bxGoal').innerHTML = (BB.stages || []).map(g => {
const e = g.earned, pct = Math.round(g.got / g.total * 100);
const prize = (g.doudou ? g.doudou + ' 兜豆' : '') + (g.gift ? (g.doudou ? ' + ' : '') + '实物纪念礼' : '');
let tail;
if (e) tail = e.needAddr ? '<button class="bx-gl-b" data-addr="1" type="button">填收货信息</button>'
: '<span class="bx-gl-s' + (e.status === 'sent' ? ' sent' : '') + '">' + (e.status === 'sent' ? '已发放' : '待发放') + '</span>';
else if (g.complete) tail = '<span class="bx-gl-s off">补发集齐</span>';
else tail = '<span class="bx-gl-l">差 <b>' + g.left + '</b> 张</span>';
return '<div class="bx-gl' + (g.key === 'all' ? ' all' : '') + (e ? ' got' : '') + '"><div class="bx-gl-h"><b>' + g.name + '</b><em>' + prize + '</em>' + tail + '</div>' +
'<i class="bx-gl-bar"><s style="width:' + pct + '%"></s></i></div>';
}).join('');
/* 待补开 */
const pend = $('#bxPend');
pend.classList.toggle('hide', op.length < 2);
@@ -3361,7 +3417,8 @@
$('#bxAll').textContent = BB.totalWeeks;
requestAnimationFrame(() => { $('#bxBar').style.width = Math.round(BB.collected / BB.totalWeeks * 100) + '%'; });
const nBack = (BB.album || []).filter(a => a.src === 'backfill').length;
$('#bxAnote').textContent = (BB.flash ? '其中闪卡 ' + BB.flash + ' 张。' : '') + (nBack ? '标「补」的是来之前的孕周,已自动补发普通款。' : '');
$('#bxAnote').textContent = (BB.flash ? '其中闪卡 ' + BB.flash + ' 张。' : '') + (nBack ? '标「补」的是来之前的孕周,已自动补发普通款。' : '') +
(lk.length ? '标 🔒 的是错过的周,完成小任务即可解锁。' : '');
const byW = {};
(BB.album || []).forEach(a => { byW[a.week] = a; });
$('#bxGroups').innerHTML = BX_GROUPS.map(g => {
@@ -3373,6 +3430,8 @@
cells += '<button class="bx-t got' + (a.v === 'flash' ? ' flash' : '') + (bbNew === w ? ' is-new' : '') + '" data-w="' + w + '" data-v="' + (a.v || 'normal') + '" type="button">' +
(CARD_IMG[w] ? '<img src="assets/img/' + CARD_IMG[w] + '.jpg" alt="" loading="lazy">' : '<span class="fb">' + babySize(w).emoji + '</span>') +
(a.src === 'backfill' ? '<span class="bf">补</span>' : '') + '<span class="wk">' + w + ' 周</span></button>';
} else if (lk.indexOf(w) >= 0) {
cells += '<button class="bx-t lockd" data-lk="' + w + '" type="button"><span class="li" aria-hidden="true">🔒</span><span class="wk">' + w + ' 周</span></button>';
} else if (op.indexOf(w) >= 0) {
cells += '<button class="bx-t can" data-op="' + w + '" type="button">' + giftSVG() + '<span class="wk">' + w + ' 周</span></button>';
} else {
@@ -3402,13 +3461,69 @@
const t = e.target.closest('.bx-t');
if (!t || bbBusy) return;
if (t.dataset.op) { bbSel = +t.dataset.op; renderBox(); $('#detailBox .detail-body').scrollTo({ top: 0, behavior: 'smooth' }); return; }
if (t.dataset.lk) { bxLockSel = +t.dataset.lk; renderBox(); $('#bxLock').scrollIntoView({ behavior: 'smooth', block: 'center' }); return; }
const w = +t.dataset.w;
if (!w) return;
/* 卡册里点开一张:大小来自本地 SIZE 表,发育说明取孕期小知识的「宝宝这周」 */
const c = { week: w, v: t.dataset.v, size: babySize(w), say: '' };
/* 卡册里点开一张:大小来自本地 SIZE 表,发育说明取孕期小知识的「宝宝这周」;周记 / 宝宝的话由接口 album 带回 */
const a = (BB.album || []).find(x => x.week === w) || {};
const c = { week: w, v: t.dataset.v, size: babySize(w), say: a.say || '', diary: a.diary || [] };
showReveal(c, null, false);
loadTips().then(() => { const x = TIPS && TIPS[w]; if (x && x.baby && $('#bxReveal').classList.contains('show')) $('#bxDev').textContent = x.baby; });
});
$('#bxLockChips').addEventListener('click', e => {
const b = e.target.closest('.bx-chip');
if (!b || bbBusy) return;
bxLockSel = +b.dataset.w; haptic(); renderBox();
});
$('#bxTasks').addEventListener('click', e => {
const go = e.target.closest('[data-app]');
if (go) {
const app = APP_BY_ID[go.dataset.app];
if (app && app.url) jumpApp(app.url, app.identifier); else toast('这个工具暂时打不开,换一个任务试试');
return;
}
const u = e.target.closest('[data-unlock]');
if (!u || bbBusy || !bxLockSel) return;
bbBusy = true; u.disabled = true; u.textContent = '解锁中…';
const w = bxLockSel;
bbApi('blindbox/unlock?week=' + w + '&task=' + u.dataset.unlock, 'POST').catch(() => null).then(j => {
bbBusy = false;
if (!j || j.code !== 200) { toast((j && j.message) || '网络开小差了,再试一次'); loadBlindbox(); return; }
if (navigator.vibrate) navigator.vibrate([10, 30, 14]);
toast('孕 ' + w + ' 周的盲盒解锁啦,快打开看看');
bbSel = w;
loadBlindbox().then(() => { bbSel = w; renderBox(); $('#detailBox .detail-body').scrollTo({ top: 0, behavior: 'smooth' }); });
});
});
$('#bxGoal').addEventListener('click', e => { if (e.target.closest('[data-addr]')) openModal('modalBxAddr'); });
$('#bxAddrSave').addEventListener('click', () => {
const v = id => $('#' + id).value.trim();
const body = { name: v('bxAddrName'), phone: v('bxAddrPhone'), addr: v('bxAddrAddr') };
if (!body.name || !body.phone || !body.addr) { toast('请把收货人、电话、地址填完整'); return; }
if (!/^1\d{10}$/.test(body.phone)) { toast('手机号好像不对,再检查一下'); return; }
fetch('/api/mp/public/mama-box/blindbox/claim?openid=' + encodeURIComponent(hashParam('openid')), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
}).then(r => r.json()).catch(() => null).then(j => {
if (!j || j.code !== 200) { toast((j && j.message) || '提交失败,再试一次'); return; }
closeModal(); toast('收到啦,工作人员会尽快寄出'); loadBlindbox();
});
});
/* 集齐庆祝:开盒 / 刷新时接口带回 newRewards(服务端只返回一次),排队逐个弹 */
function showPrize() {
const g = bxPrizeQ.shift();
if (!g) return;
$('#bxPrizeT').textContent = g.key === 'all' ? '37 张宝宝卡全部集齐!' : '集齐' + g.name + '!';
$('#bxPrizeP').textContent = (g.doudou ? g.doudou + ' 兜豆' : '') + (g.gift ? (g.doudou ? '和' : '') + g.gift : '') + '已为你记下,工作人员会在 7 个工作日内发放' +
(g.gift ? ',先填一下收货信息吧。' : '。');
$('#bxPrizeOk').textContent = g.gift ? '去填收货信息' : '太好了';
$('#bxPrizeOk').dataset.addr = g.gift ? '1' : '';
openModal('modalBxPrize');
}
$('#bxPrizeOk').addEventListener('click', () => {
const addr = $('#bxPrizeOk').dataset.addr;
closeModal();
if (addr) openModal('modalBxAddr'); else setTimeout(showPrize, 250);
});
$('#bxRuleBtn').addEventListener('click', () => {
const d = $('#bxRules'); d.open = true;
d.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -3437,6 +3552,7 @@
st.classList.add('is-popping');
if (navigator.vibrate) navigator.vibrate([12, 40, 18]);
const r = j.result;
(r.newRewards || []).forEach(g => bxPrizeQ.push(g));
bbNew = r.card.week;
setTimeout(() => { showReveal(r.card, r.reward, true); bbBusy = false; }, 420);
loadBlindbox(); // 后台刷新横幅与卡册(翻卡层盖着,用户看不到重绘)
@@ -3462,6 +3578,8 @@
if (!$('#bxBack').firstChild) $('#bxBack').innerHTML = giftSVG() + '<b>兜爸心选 · 孕期盲盒</b>';
$('#bxTag').textContent = fresh ? (flash ? '✦ 哇!开出了闪卡 ✦' : '新卡 · 孕 ' + card.week + ' 周') : (flash ? '✦ 闪卡 · 孕 ' + card.week + ' 周' : '孕 ' + card.week + ' 周');
$('#bxDev').textContent = card.dev || '';
const dl = card.diary || [];
$('#bxDiary').innerHTML = dl.length ? '<div class="bx-dy-h">📖 宝宝周记 · 孕 ' + card.week + ' 周</div>' + dl.map(x => '<p>' + bbEsc(x) + '</p>').join('') : '';
let rw = '';
if (reward) {
const tj = reward.type === 'taijiao';
@@ -3493,7 +3611,7 @@
rv.classList.remove('show', 'is-done', 'is-burst');
rv.setAttribute('aria-hidden', 'true');
}
$('#bxKeep').addEventListener('click', () => { hideReveal(); renderBox(); });
$('#bxKeep').addEventListener('click', () => { hideReveal(); renderBox(); if (bxPrizeQ.length) setTimeout(showPrize, 350); });
$('#detailBox .dt-back').addEventListener('click', hideReveal);
/* ================= 孕期小知识(2026-09-30) =================