Files
mama-box/js/coach.js

153 lines
8.7 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-12) =================
用户:「给首页加上引导操作步骤」。第一次进首页时,一步一步高亮讲 5 个地方:
① 头图孕周 ② 孕育工具 ③ 此刻 ④「记一下」圆钮 ⑤「我的」
· 只走一次:看完或点「跳过」都记 localStorage 'mmbox.coach.v1',以后不再出(同 onboarding-once-rule)。
· 调试 / 给人看:URL 带 ?coach=1 每次都出。
· 不抢别的弹层:授权、欢迎礼、孕期档案、记一下面板、详情页等开着时不计时,
全部关掉、首页安静 1.5 秒后才开始;引导进行中别的弹层冒出来 → 暂时收起,等它关了从当前步接着讲。
· 某一步的目标不在页面上(如「此刻」接口没数据整块隐藏)→ 自动跳过这一步,不留空框。
· 遮罩挂在 #phone 里(跟页面同一个坐标系),z-index 高于所有弹层;滚动交给首页 .screen,引导时锁住。 */
(function () {
'use strict';
const KEY = 'mmbox.coach.v1';
const FORCE = /[?&]coach=1\b/.test(location.search);
try { if (!FORCE && localStorage.getItem(KEY)) return; } catch (e) { return; }
const phone = document.getElementById('phone');
const scr = document.getElementById('screen-home');
if (!phone || !scr) return;
/* 样式随脚本走(不动 mama-box.css)。z-index 140:高于 modal(125) / pk-sheet(131)。 */
const st = document.createElement('style');
st.textContent =
'.mb-coach{position:absolute;inset:0;z-index:140;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s;-webkit-tap-highlight-color:transparent}' +
'.mb-coach.show{opacity:1;visibility:visible}' +
'.mbc-hole{position:absolute;border-radius:18px;box-shadow:0 0 0 2px rgba(255,255,255,.9),0 0 0 9999px rgba(40,22,16,.62);transition:left .3s,top .3s,width .3s,height .3s;pointer-events:none}' +
'.mb-coach.is-moving .mbc-hole,.mb-coach.is-moving .mbc-card{transition:none}' +
'.mbc-card{position:absolute;left:20px;right:20px;background:#fff;border-radius:18px;padding:16px 18px 12px;box-shadow:0 12px 32px rgba(0,0,0,.18);transition:top .3s}' +
'.mbc-n{display:block;font-size:12px;color:#B08A7C;margin-bottom:4px}' +
'.mbc-t{display:block;font-size:17px;color:var(--ink,#2B2320);margin-bottom:6px}' +
'.mbc-d{margin:0;font-size:14px;line-height:1.6;color:#6B5A53}' +
'.mbc-act{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}' +
'.mbc-act button{min-height:40px;min-width:64px;padding:0 16px;border:0;border-radius:20px;font-size:14px;font-weight:600}' +
'.mbc-skip{background:none;color:#9C8A83}' +
'.mbc-next{background:var(--brand,#F4593C);color:#fff}' +
'.screen.mbc-lock{overflow:hidden!important}' +
'@media (prefers-reduced-motion:reduce){.mbc-hole,.mbc-card,.mb-coach{transition:none}}';
document.head.appendChild(st);
const STEPS = [
{ sel: '.hero-pane', t: '这是你的孕周', d: '孕几周、离见面还有几天、宝宝现在有多大,每天打开都会更新。' },
{ sel: '.tool-dock', t: '常用孕育工具', d: '数胎动、孕检日历、体重这些常用工具都在这里,点卡片直接进。' },
/* 11-03「此刻」换成测一测 · 科普 · 计算器(js/kit.js);?kit=0 时 #kit 不显示,自动跳过 */
{ sel: '#kit', t: '测一测 · 科普 · 计算器', d: '按你的孕周,每天推荐一个小测试、一篇科普和实用计算器。' },
{ sel: '.tab-fab', t: '记一下', d: '想记点什么、找更多工具,点中间这颗按钮。' },
{ sel: '.tabbar [data-tab="me"]', t: '我的', d: '孕期档案、头像、兜豆和兑换记录都在这里。' }
];
const visible = el => {
if (!el) return false;
const r = el.getBoundingClientRect();
if (!r.width || !r.height) return false;
const cs = getComputedStyle(el);
return cs.display !== 'none' && cs.visibility !== 'hidden' && !el.classList.contains('hide');
};
// 有别的弹层 / 不在首页 / 页面在后台 → 先别讲
function busy() {
if (document.hidden || !scr.classList.contains('is-active')) return true;
if (document.querySelector('.modal.show,.sheet.show,.detail.show,.pk-sheet.show,.search-layer.show,.bx-reveal.show')) return true;
const wh = document.getElementById('whPage');
if (wh && wh.getAttribute('aria-hidden') === 'false') return true;
return false;
}
let root, hole, card, idx = -1, on = false, timer = 0;
function build() {
root = document.createElement('div');
root.className = 'mb-coach';
root.setAttribute('role', 'dialog');
root.setAttribute('aria-modal', 'true');
root.setAttribute('aria-label', '首页使用引导');
root.innerHTML = '<i class="mbc-hole" aria-hidden="true"></i>' +
'<div class="mbc-card"><span class="mbc-n"></span><b class="mbc-t"></b><p class="mbc-d"></p>' +
'<div class="mbc-act"><button type="button" class="mbc-skip">跳过</button><button type="button" class="mbc-next">下一步</button></div></div>';
phone.appendChild(root);
hole = root.querySelector('.mbc-hole');
card = root.querySelector('.mbc-card');
root.querySelector('.mbc-skip').addEventListener('click', e => { e.stopPropagation(); done(); });
root.querySelector('.mbc-next').addEventListener('click', e => { e.stopPropagation(); go(idx + 1); });
// 点遮罩任意处也算下一步(大靶子);引导时不让页面跟着滑
root.addEventListener('click', () => go(idx + 1));
root.addEventListener('touchmove', e => { if (e.cancelable) e.preventDefault(); }, { passive: false });
root.addEventListener('wheel', e => e.preventDefault(), { passive: false });
}
function place() {
const s = STEPS[idx], el = s && document.querySelector(s.sel);
if (!el || !visible(el)) return false;
const p = phone.getBoundingClientRect(), r = el.getBoundingClientRect(), pad = 6;
const x = r.left - p.left - pad, y = r.top - p.top - pad, w = r.width + pad * 2, h = r.height + pad * 2;
Object.assign(hole.style, { left: x + 'px', top: y + 'px', width: w + 'px', height: h + 'px' });
hole.style.borderRadius = getComputedStyle(el).borderRadius === '0px' ? '16px' : 'calc(' + getComputedStyle(el).borderRadius + ' + 6px)';
const live = STEPS.filter(t => visible(document.querySelector(t.sel)));
root.querySelector('.mbc-n').textContent = (live.indexOf(s) + 1) + ' / ' + live.length;
root.querySelector('.mbc-t').textContent = s.t;
root.querySelector('.mbc-d').textContent = s.d;
root.querySelector('.mbc-next').textContent = live.indexOf(s) === live.length - 1 ? '知道了' : '下一步';
// 卡片放在目标下方,放不下就放上方
const ch = card.offsetHeight || 140, below = y + h + 12, ph = p.height;
card.style.top = (below + ch < ph - 16 ? below : Math.max(16, y - ch - 12)) + 'px';
return true;
}
function go(i) {
while (i < STEPS.length) {
const el = document.querySelector(STEPS[i].sel);
if (visible(el)) break;
i++;
}
if (i >= STEPS.length) return done();
idx = i;
const el = document.querySelector(STEPS[i].sel);
// 首页里的目标先滚到视口中间(底栏不在 .screen 里,不用滚)
if (scr.contains(el)) {
const sr = scr.getBoundingClientRect(), r = el.getBoundingClientRect();
const want = scr.scrollTop + (r.top - sr.top) - Math.max(80, (sr.height - r.height) / 2 - 60);
scr.scrollTop = Math.max(0, want);
}
root.classList.add('is-moving');
requestAnimationFrame(() => requestAnimationFrame(() => { place(); root.classList.remove('is-moving'); }));
}
function show() {
if (!root) build();
on = true;
root.classList.add('show');
scr.classList.add('mbc-lock');
go(idx < 0 ? 0 : idx);
}
function hideTmp() { if (root) root.classList.remove('show'); scr.classList.remove('mbc-lock'); on = false; }
function done() {
try { localStorage.setItem(KEY, String(Date.now())); } catch (e) { /* 存不了就只是下次还会出 */ }
clearInterval(timer);
if (root) { root.classList.remove('show'); setTimeout(() => root.remove(), 300); }
scr.classList.remove('mbc-lock');
idx = STEPS.length;
}
// 每 0.5 秒看一眼:首页安静满 1.5 秒才开讲;讲到一半有弹层冒出来就先收起
let quiet = 0;
function tick() {
if (idx >= STEPS.length) return;
if (busy()) { quiet = 0; if (on) hideTmp(); return; }
if (on) return;
if ((quiet += 500) >= 1500) show();
}
// 等孕周数据回来(头图有真实数字)再开始;最多等 4 秒
let started = false;
const start = () => { if (started) return; started = true; timer = setInterval(tick, 500); };
addEventListener('mb-preg', () => setTimeout(start, 600));
setTimeout(start, 4000);
addEventListener('resize', () => { if (on) place(); });
})();