首页新增操作引导(孕周/孕育工具/此刻/记一下/我的,只走一次)v26101204

Co-Authored-By: AI 助手
This commit is contained in:
karlcarlo
2026-10-12 06:44:46 +00:00
parent 3a4ab3be4d
commit 62b36aab8d
3 changed files with 154 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26101203';
var VER = '26101204';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));

151
js/coach.js Normal file
View File

@@ -0,0 +1,151 @@
/* ================= 首页操作引导(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: '数胎动、孕检日历、体重这些常用工具都在这里,点卡片直接进。' },
{ sel: '#pulse', 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(); });
})();