153 lines
8.7 KiB
JavaScript
153 lines
8.7 KiB
JavaScript
/* ================= 首页操作引导(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(); });
|
||
})();
|