249 lines
18 KiB
JavaScript
249 lines
18 KiB
JavaScript
/* 11-02 首页「孕妈广场」效果稿 V4 · 故事卡堆(翻一翻)
|
||
一叠微微错开的真实照片卡,左右拖/轻扫/点箭头翻页,最后一张是「轮到你了」投稿卡;下方一行 #话题 入口。
|
||
只在 ?gcv=4 时由 index.html 加载;笔记是从 guangchang/data.js 摘的精选(占位示例)。 */
|
||
(function () {
|
||
var sec = document.getElementById('gcHome'), T = window.GC_TOPICS;
|
||
if (!sec || !Array.isArray(T) || !T.length) return;
|
||
function init() {
|
||
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
|
||
var kfmt = function (n) { return n >= 10000 ? (n / 10000).toFixed(1).replace(/\.0$/, '') + '万' : n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : String(n); };
|
||
var ava = function (n) { return 'assets/img/mom/' + n + '.jpg'; };
|
||
var reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var CAT_CN = { family: '家庭婚姻', work: '职场', baby: '经验分享' }; /* 11-03 卡上话题标签同广场:经验 → 经验分享 */
|
||
var TOP = {}; T.forEach(function (t) { TOP[t.id] = t; });
|
||
/* 11-02 用户:广场帖子跟「其他妈妈也在搜」一样按孕周 6 段走 —— 多数是同阶段孕妈的帖子,穿插过来人(已生完)的。
|
||
帖子清单在 guangchang/feed.js(由 data.js 生成的轻量索引 + 排序函数 GC_ORDER),孕周取 app.js 广播的 window.__mbPreg。 */
|
||
var MOMS = ['m03', 'p02', 'm07', 'p05', 'm11', 'p09', 'm14', 'm16', 'p12', 'm19', 'm21', 'p07'];
|
||
function userBand() {
|
||
var pg = window.__mbPreg || {};
|
||
return pg.postpartum ? 'pp' : (pg.week && window.GC_BAND ? window.GC_BAND('孕' + pg.week + '周') : -1);
|
||
}
|
||
function notes(cat) {
|
||
if (!window.GC_ORDER) return [];
|
||
var F = window.GC_FEED || [], ub = userBand();
|
||
var l = window.GC_ORDER(F.filter(function (n) { return n.photo && (!cat || (TOP[n.t] || {}).cat === cat); }), ub);
|
||
if (!cat) { /* 「全部」:同一分类最多 3 张,三类穿插着来 */
|
||
var cnt = {}, out = [];
|
||
l.forEach(function (n) { var c = (TOP[n.t] || {}).cat; if (out.length < 7 && (cnt[c] || 0) < 3) { cnt[c] = (cnt[c] || 0) + 1; out.push(n); } });
|
||
l = out;
|
||
}
|
||
return l.slice(0, 7).map(function (n, i) { return { t: n.t, id: n.id, title: n.title, author: n.author, stage: n.stage, likes: n.likes, photo: n.photo, av: MOMS[(i * 5 + (cat ? cat.length : 0)) % MOMS.length] }; });
|
||
}
|
||
function topics(cat) {
|
||
var l = T.filter(function (t) { return !cat || t.cat === cat; });
|
||
var hot = l.filter(function (t) { return t.hot; });
|
||
return (cat ? hot : hot.filter(function (_, i) { return i % 3 === 0; })).concat(l.filter(function (t) { return !t.hot; })).slice(0, 3);
|
||
}
|
||
/* 11-02 用户:「进广场首页太深,只能点详情再返回」→ 点帖子只带 ?n=(返回落在广场首页,不落在单个话题);话题标签才带 ?t= */
|
||
function go(id, nid) {
|
||
location.href = 'guangchang/' + (nid ? '?n=' + encodeURIComponent(nid) : id ? '?t=' + encodeURIComponent(id) : '') + location.hash;
|
||
}
|
||
|
||
sec.className = sec.className.replace(/\bgv\d\b/g, '') + ' gv4';
|
||
sec.innerHTML =
|
||
'<div class="g4-head"><h2 class="g4-title"><span>孕妈广场</span></h2>' +
|
||
/* 11-03 用户:首页孕妈广场右上角的分类(全部 / 家庭婚姻 / 职场 / 经验)取消,换一句小 slogan */
|
||
'<span class="g4-slg"><i></i>孕期路上,有人陪你</span></div>' +
|
||
/* 11-02 用户:「逛广场」单独挂在右边突兀 → 整行做成入口条(浅粉底,整条可点),右侧实心「进广场」 */
|
||
/* 11-04 用户:「那一排头像去掉,重新设计得更协调,引导用户点进广场」→ 左边图标块 + 两行字(主句 + 小字说有什么),右边「进广场」箭头轻推;不写篇数(11-03 规则) */
|
||
'<div class="g4-sub" role="button" tabindex="0"><i class="g4-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 6.5A2.5 2.5 0 0 1 6.5 4h8A2.5 2.5 0 0 1 17 6.5v5a2.5 2.5 0 0 1-2.5 2.5H10l-3.6 2.8c-.5.4-1.2 0-1.2-.6V14H6.5A2.5 2.5 0 0 1 4 11.5z" fill="currentColor"/><path d="M19 9.5c.6.4 1 1.1 1 1.9v4.1a2 2 0 0 1-2 2h-.6v1.7c0 .5-.6.8-1 .5L13.6 17.5H12" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" opacity=".55"/></svg></i>' +
|
||
'<span class="g4-st"><b>看看其他孕妈的真实故事</b><small>孕期、家庭、职场,天天有新笔记</small></span><span class="g4-in">进广场<i>›</i></span></div>' +
|
||
'<div class="g4-stage"><div class="g4-deck" aria-live="polite"></div>' +
|
||
/* 11-03 用户:左右翻的按钮去掉,「1/8」挪到原来按钮的位置(卡堆右下) —— 翻页靠左右滑 / 点后面露出的卡角 */
|
||
'<div class="g4-rail"><span class="g4-pg"><b>1</b><i>/8</i></span></div></div>' +
|
||
'<div class="g4-tags"></div>' +
|
||
'<div class="g4-foot"><button type="button" class="g4-all">进孕妈广场 ›</button><span class="g4-mark">效果稿 V4</span></div>';
|
||
sec.classList.remove('hide');
|
||
|
||
var deck = sec.querySelector('.g4-deck'), pgB = sec.querySelector('.g4-pg b'), pgI = sec.querySelector('.g4-pg i'), tags = sec.querySelector('.g4-tags');
|
||
var cards = [], idx = 0, cur = '';
|
||
|
||
function storyCard(n) {
|
||
var tp = TOP[n.t] || {}, cat = tp.cat || 'family';
|
||
return '<div class="g4-card st c-' + cat + '" data-t="' + esc(n.t) + '" data-n="' + esc(n.id) + '" role="button" tabindex="0">' +
|
||
'<img class="g4-ph" src="' + esc(n.photo) + '" alt="" loading="lazy" decoding="async" draggable="false">' +
|
||
'<span class="g4-k">' + CAT_CN[cat] + '</span>' +
|
||
'<span class="g4-bot"><b class="g4-tt">' + esc(n.title) + '</b>' +
|
||
'<span class="g4-by"><img src="' + ava(n.av) + '" alt="" loading="lazy" draggable="false"><em>' + esc(n.author) + '</em><i>' + esc(n.stage) + '</i>' +
|
||
'<span class="g4-like"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5s-7.5-4.6-7.5-10.2A4.3 4.3 0 0112 7.6a4.3 4.3 0 017.5 2.7c0 5.6-7.5 10.2-7.5 10.2z"/></svg>' + kfmt(n.likes) + '</span></span></span></div>';
|
||
}
|
||
function submitCard() {
|
||
return '<div class="g4-card sub" data-sub="1" role="button" tabindex="0">' +
|
||
'<span class="g4-s-k">兜爸心选 · 官方征集</span>' + /* 11-02 品牌露出 */
|
||
'<b class="g4-s-t">轮到你了</b>' +
|
||
'<span class="g4-s-d">你的孕期故事,正是别的妈妈想听的</span>' +
|
||
'<span class="g4-s-r"><span><b>¥30</b><i>现金</i></span><em>+</em><span><b>500</b><i>兜豆</i></span></span>' +
|
||
'<span class="g4-s-n">被选中即可获得</span>' +
|
||
'<span class="g4-s-go">去投稿 ›</span>' +
|
||
'<span class="g4-s-avs" aria-hidden="true">' + ['m02', 'p04', 'm09', 'p13', 'm18'].map(function (n) { return '<img src="' + ava(n) + '" alt="" loading="lazy" draggable="false">'; }).join('') + '<i>本周已有 326 位妈妈投稿</i></span></div>';
|
||
}
|
||
function paint(cat, first) {
|
||
cur = cat; idx = 0;
|
||
deck.innerHTML = notes(cat).map(storyCard).join('') + submitCard();
|
||
cards = [].slice.call(deck.children);
|
||
if (!first && !reduce) {
|
||
deck.classList.add('deal');
|
||
cards.forEach(function (c, i) { c.style.transitionDelay = (Math.min(i, 3) * 60) + 'ms'; });
|
||
layout();
|
||
requestAnimationFrame(function () { requestAnimationFrame(function () { deck.classList.remove('deal'); layout(); setTimeout(function () { cards.forEach(function (c) { c.style.transitionDelay = ''; }); }, 500); }); });
|
||
} else layout();
|
||
tags.innerHTML = '<span class="g4-tl">大家在聊</span>' + topics(cat).map(function (t) { return '<button type="button" data-t="' + esc(t.id) + '">#' + esc(t.name) + '#</button>'; }).join('');
|
||
tags.scrollLeft = 0;
|
||
}
|
||
/* depth:0 顶层;1、2 在后面错开;>2 藏在后面;<0 已翻走(左侧飞出) */
|
||
function layout() {
|
||
cards.forEach(function (c, i) {
|
||
var d = i - idx;
|
||
c.setAttribute('data-d', d < 0 ? 'out' : d > 3 ? 3 : d);
|
||
c.style.zIndex = 20 - Math.abs(d);
|
||
c.style.transform = '';
|
||
c.setAttribute('aria-hidden', d === 0 ? 'false' : 'true');
|
||
c.tabIndex = d === 0 ? 0 : -1;
|
||
});
|
||
pgB.textContent = idx + 1; pgI.textContent = '/' + cards.length;
|
||
/* 11-02 用户:滑到哪张图,右上角分类小滑块跟着跳到它的归属类(只在「全部」里跳;投稿卡回到「全部」;选了具体分类时不动) */
|
||
if (cur === '') {
|
||
var cc = cards[idx] && (cards[idx].className.match(/\bc-(\w+)/) || [])[1] || '';
|
||
[].forEach.call(sec.querySelectorAll('.g4-cat button'), function (x) { x.classList.toggle('on', x.dataset.cat === cc); });
|
||
}
|
||
}
|
||
function next() {
|
||
if (idx < cards.length - 1) { idx++; layout(); return; }
|
||
/* 最后一张再往后 → 整叠收回,从第 1 张重新开始 */
|
||
idx = 0; deck.classList.add('reset'); layout();
|
||
setTimeout(function () { deck.classList.remove('reset'); }, 380);
|
||
}
|
||
function prev() { if (idx > 0) { idx--; layout(); } else bounce(1); }
|
||
function bounce(dir) {
|
||
var c = cards[idx]; if (!c || reduce) return;
|
||
c.style.transform = 'translateX(' + dir * 14 + 'px) rotate(' + dir * 1.5 + 'deg)';
|
||
setTimeout(function () { c.style.transform = ''; }, 160);
|
||
}
|
||
|
||
/* 拖动:方向锁 —— 先判断横 / 竖,竖向直接放行给页面滚动,横向才拦截(touchmove passive:false 才能 preventDefault) */
|
||
var drag = null, moved = false;
|
||
function start(x, y) { var c = cards[idx]; if (!c) return; drag = { x: x, y: y, dx: 0, lock: '', t: Date.now(), c: c }; moved = false; }
|
||
function move(x, y, e) {
|
||
if (!drag) return;
|
||
var dx = x - drag.x, dy = y - drag.y;
|
||
if (!drag.lock) {
|
||
if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return;
|
||
drag.lock = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
|
||
if (drag.lock === 'y') { drag = null; return; }
|
||
drag.c.classList.add('drag'); deck.classList.add('dragging');
|
||
}
|
||
if (e && e.cancelable) e.preventDefault();
|
||
if (e) e.stopPropagation();
|
||
moved = true;
|
||
if ((idx === 0 && dx > 0)) dx = dx * 0.3; // 第一张往右拖:阻尼
|
||
drag.dx = dx;
|
||
drag.c.style.transform = 'translateX(' + dx + 'px) translateY(' + Math.abs(dx) * 0.04 + 'px) rotate(' + dx / 18 + 'deg)';
|
||
}
|
||
function end() {
|
||
if (!drag) return false;
|
||
var d = drag; drag = null;
|
||
d.c.classList.remove('drag'); deck.classList.remove('dragging');
|
||
if (!d.lock) return false;
|
||
var v = Math.abs(d.dx) / Math.max(1, Date.now() - d.t);
|
||
if (d.dx < -70 || (d.dx < -24 && v > 0.45)) next();
|
||
else if ((d.dx > 70 || (d.dx > 24 && v > 0.45)) && idx > 0) prev();
|
||
else d.c.style.transform = '';
|
||
return true;
|
||
}
|
||
deck.addEventListener('touchstart', function (e) { if (e.touches.length === 1) start(e.touches[0].clientX, e.touches[0].clientY); else drag = null; }, { passive: true });
|
||
deck.addEventListener('touchmove', function (e) { if (drag && e.touches.length === 1) move(e.touches[0].clientX, e.touches[0].clientY, e); }, { passive: false });
|
||
/* 首页在 document 上有「左右滑切底部 tab」的手势(app.js touchend):卡堆横拖结束时截住 touchend,别让它把页面切走 */
|
||
deck.addEventListener('touchend', function (e) { if (end()) e.stopPropagation(); });
|
||
deck.addEventListener('touchcancel', end);
|
||
deck.addEventListener('mousedown', function (e) {
|
||
if (e.button) return; start(e.clientX, e.clientY); e.preventDefault();
|
||
var mm = function (ev) { move(ev.clientX, ev.clientY, ev); }, mu = function () { end(); document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu); };
|
||
document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu);
|
||
});
|
||
deck.addEventListener('click', function (e) {
|
||
if (moved) { moved = false; return; }
|
||
var c = e.target.closest('.g4-card');
|
||
if (!c) return;
|
||
if (c !== cards[idx]) { next(); return; } // 点到后面露出的卡角 → 翻到下一张
|
||
go(c.dataset.sub ? '' : c.dataset.t, c.dataset.sub ? '' : c.dataset.n);
|
||
});
|
||
deck.addEventListener('keydown', function (e) {
|
||
if (e.key === 'ArrowRight') next(); else if (e.key === 'ArrowLeft') prev();
|
||
else if (e.key === 'Enter') { var c = cards[idx]; if (c) go(c.dataset.sub ? '' : c.dataset.t, c.dataset.sub ? '' : c.dataset.n); }
|
||
});
|
||
tags.addEventListener('click', function (e) { var b = e.target.closest('button[data-t]'); if (b) go(b.dataset.t); });
|
||
sec.querySelector('.g4-all').addEventListener('click', function () { go(''); });
|
||
sec.querySelector('.g4-sub').addEventListener('click', function () { go(''); });
|
||
/* 孕周变了(档案里改了 / 孕期数据晚到)→ 段变了才重排,免得翻到一半被打回第 1 张 */
|
||
var lastUB = userBand();
|
||
window.addEventListener('mb-preg', function () { var u = userBand(); if (u !== lastUB) { lastUB = u; paint(cur, true); } });
|
||
paint('', true);
|
||
|
||
/* 11-03 用户:「首次进入首页时,孕妈广场下面的图片给一个左右滑动的动效,提示用户可以滑」
|
||
→ 卡堆第一次露出大半屏时:顶上那张往左探 2 下再回来 + 小手 + 「左右滑动看更多」;只在第一次进首页演一次(mmbox.gc4.hint.v1),
|
||
用户自己滑过 / 点过卡堆就不再演;演的过程中一碰就停。系统开了「减少动态效果」不演。 */
|
||
var HK = 'mmbox.gc4.hint.v1', hinted = false;
|
||
try { hinted = !!localStorage.getItem(HK); } catch (e) {}
|
||
function hintDone() { hinted = true; try { localStorage.setItem(HK, '1'); } catch (e) {} }
|
||
if (!hinted && !reduce && window.IntersectionObserver && Element.prototype.animate) {
|
||
var stage = sec.querySelector('.g4-stage'), hintEl = null, anims = [], hintT = 0;
|
||
var stopHint = function () {
|
||
clearTimeout(hintT); anims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); anims = [];
|
||
if (hintEl) { var h = hintEl; hintEl = null; h.classList.add('off'); setTimeout(function () { h.remove(); }, 260); }
|
||
};
|
||
['touchstart', 'mousedown'].forEach(function (ev) { deck.addEventListener(ev, function () { if (!hinted) hintDone(); stopHint(); }, { passive: true }); });
|
||
/* 卡堆露出 60% 以上(扣掉底部 tab 栏那条)且首页引导 coach.js 的遮罩已关、页面在前台,停 0.5 秒再演;引导还开着就等它关 */
|
||
var seen = false, waitT = 0;
|
||
function tryPlay() {
|
||
clearTimeout(waitT);
|
||
if (hinted || !seen) return;
|
||
if (document.querySelector('.mb-coach') || document.hidden) { waitT = setTimeout(tryPlay, 700); return; }
|
||
hintT = setTimeout(function () {
|
||
if (!seen || hinted || document.querySelector('.mb-coach')) { tryPlay(); return; }
|
||
io.disconnect();
|
||
var c = cards[idx]; if (!c || hinted) return;
|
||
hintDone();
|
||
stage.insertAdjacentHTML('beforeend', '<div class="g4-hint" aria-hidden="true"><span class="g4-hand"><svg viewBox="0 0 24 24"><path d="M9.5 11.2V5.3a1.5 1.5 0 013 0v5.2m0-.7V9a1.5 1.5 0 013 0v1.6m0-.4a1.5 1.5 0 013 0v4.3c0 3.4-2.4 6-5.7 6h-.9c-1.8 0-3.2-.8-4.3-2.2l-3-4a1.5 1.5 0 012.3-1.9l1.6 1.7V11.2"/></svg></span><b>左右滑动 看更多</b></div>');
|
||
hintEl = stage.lastElementChild;
|
||
var ez = { duration: 2600, easing: 'ease-in-out', iterations: 1 };
|
||
var kf = [
|
||
{ transform: 'none', offset: 0 }, { transform: 'translateX(-46px) rotate(-5deg)', offset: .2 }, { transform: 'none', offset: .38 },
|
||
{ transform: 'translateX(-46px) rotate(-5deg)', offset: .6 }, { transform: 'none', offset: .78 }, { transform: 'none', offset: 1 }
|
||
];
|
||
anims.push(c.animate(kf, ez));
|
||
var n1 = cards[idx + 1]; /* 后面那张跟着往前凑一点,像要接上来 */
|
||
if (n1) anims.push(n1.animate([
|
||
{ transform: 'translate(12px,4px) rotate(4deg) scale(.95)', offset: 0 }, { transform: 'translate(4px,2px) rotate(1.5deg) scale(.97)', offset: .2 },
|
||
{ transform: 'translate(12px,4px) rotate(4deg) scale(.95)', offset: .38 }, { transform: 'translate(4px,2px) rotate(1.5deg) scale(.97)', offset: .6 },
|
||
{ transform: 'translate(12px,4px) rotate(4deg) scale(.95)', offset: .78 }, { transform: 'translate(12px,4px) rotate(4deg) scale(.95)', offset: 1 }
|
||
], ez));
|
||
var hd = hintEl.querySelector('.g4-hand');
|
||
anims.push(hd.animate([
|
||
{ transform: 'translateX(0)', opacity: 0, offset: 0 }, { transform: 'translateX(0)', opacity: 1, offset: .04 }, { transform: 'translateX(-60px)', opacity: 1, offset: .2 },
|
||
{ transform: 'translateX(0)', opacity: 1, offset: .38 }, { transform: 'translateX(-60px)', opacity: 1, offset: .6 }, { transform: 'translateX(0)', opacity: 1, offset: .78 },
|
||
{ transform: 'translateX(0)', opacity: 0, offset: 1 }
|
||
], ez));
|
||
hintT = setTimeout(stopHint, 2900);
|
||
}, 500);
|
||
}
|
||
var io = new IntersectionObserver(function (es) { seen = es[0].isIntersecting; if (seen) tryPlay(); }, { threshold: .6, rootMargin: '0px 0px -110px 0px' });
|
||
io.observe(deck);
|
||
}
|
||
/* 11-04 用户:「8 张卡自己也轮播,5 秒左右一次;手滑照样能翻」→ 卡堆在屏幕里、页面在前台、没在拖、首次滑动提示演完才自动翻;
|
||
手碰过卡堆后停 8 秒再接着轮。系统开了「减少动态效果」不自动翻。 */
|
||
if (!reduce && 'IntersectionObserver' in window) {
|
||
var apOn = false, apHold = 0;
|
||
new IntersectionObserver(function (es) { apOn = es[0].isIntersecting; }, { threshold: .5 }).observe(deck);
|
||
['touchstart', 'mousedown'].forEach(function (ev) { deck.addEventListener(ev, function () { apHold = Date.now() + 8000; }, { passive: true }); });
|
||
setInterval(function () {
|
||
if (!apOn || document.hidden || drag || Date.now() < apHold || document.querySelector('.mb-coach') || sec.querySelector('.g4-hint:not(.off)') || cards.length < 2) return;
|
||
next();
|
||
}, 5000);
|
||
}
|
||
sec.dispatchEvent(new Event('gc4-ready')); /* V6 合并版等这个再往里插「其他妈妈也在搜」 */
|
||
}
|
||
/* feed.js 首页默认不加载(index.html 只引了 topics.js),这里按需拉一次 */
|
||
if (window.GC_FEED) init();
|
||
else { var fs = document.createElement('script'); fs.src = 'guangchang/feed.js?v=26110412'; fs.onload = fs.onerror = init; document.body.appendChild(fs); }
|
||
})();
|