悄悄话全屏页四改:一屏一句竖滑沉浸式、逐字亮起、光晕随句变色、双击抱抱、全部抽屉 (v26100168)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
174
js/app.js
174
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26100167';
|
||||
var VER = '26100168';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -3341,101 +3341,137 @@
|
||||
return '<button class="whp-hug' + (it.hugged ? ' on' : '') + '" type="button" data-hug="' + esc(it.id) + '" aria-label="给她一个抱抱">' +
|
||||
HEART + '<b>' + (it.hugs || '') + '</b></button>';
|
||||
}
|
||||
/* 星图 + 聚焦卡 + 清单(三改)。星星位置按 id 哈希定在网格里再抖一抖,同一句每次出现在同一处 */
|
||||
let fi = 0, focT = null, focHold = false;
|
||||
/* 四改:一屏一句的竖滑沉浸页。slide 0 = 封面(标题 + 星座),slide k = 第 k 句。 */
|
||||
const RM = !!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||
const AURA = [['#6B4AA8', '#C2667A'], ['#3F4FA6', '#8E5BB0'], ['#9A4F86', '#E0907A'], ['#365C9E', '#7A58AE'], ['#7D3F8C', '#D27A9A']];
|
||||
const hsh = s => { let h = 7; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) >>> 0; return h; };
|
||||
function paintSky(l) {
|
||||
let cur = 0, io = null, vl = [];
|
||||
/* 沉浸页先看别人的,自己说的放最后(金色收尾) */
|
||||
const vlist = () => list().filter(x => !x.mine).concat(list().filter(x => x.mine));
|
||||
const whv = $('#whv');
|
||||
function skySvg(l) {
|
||||
const n = l.length, cols = n > 12 ? 5 : 4, rows = Math.max(2, Math.ceil(n / cols));
|
||||
const pts = l.map((it, i) => {
|
||||
const h = hsh(it.id), c = i % cols, r = Math.floor(i / cols);
|
||||
/* 蛇形排列,连线不来回横穿 */
|
||||
const cc = r % 2 ? cols - 1 - c : c;
|
||||
const x = ((cc + .5 + ((h % 100) / 100 - .5) * .62) / cols) * 100;
|
||||
const y = ((r + .5 + (((h >> 8) % 100) / 100 - .5) * .55) / rows) * 100;
|
||||
const sz = it.mine ? 13 : Math.round(5 + Math.min(it.hugs || 0, 40) / 40 * 11);
|
||||
return { x, y, sz, it, i };
|
||||
const h = hsh(it.id), c = i % cols, r = Math.floor(i / cols), cc = r % 2 ? cols - 1 - c : c;
|
||||
return { i, it, x: ((cc + .5 + ((h % 100) / 100 - .5) * .62) / cols) * 100, y: ((r + .5 + (((h >> 8) % 100) / 100 - .5) * .55) / rows) * 100,
|
||||
sz: it.mine ? 12 : Math.round(5 + Math.min(it.hugs || 0, 40) / 40 * 10) };
|
||||
});
|
||||
$('#whpSky').innerHTML =
|
||||
'<svg class="whs-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"><polyline points="' +
|
||||
return '<svg class="whs-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"><polyline points="' +
|
||||
pts.map(p => p.x.toFixed(1) + ',' + p.y.toFixed(1)).join(' ') + '"/></svg>' +
|
||||
pts.map(p => '<button type="button" class="whs-star' + (p.it.mine ? ' mine' : '') + (p.i === fi ? ' on' : '') +
|
||||
'" data-fi="' + p.i + '" style="left:' + p.x.toFixed(1) + '%;top:' + p.y.toFixed(1) + '%;--s:' + p.sz + 'px;--tw:' + ((hsh(p.it.id) % 30) / 10).toFixed(1) + 's" aria-label="' +
|
||||
esc(p.it.text) + '"><i></i></button>').join('');
|
||||
pts.map(p => '<button type="button" class="whs-star' + (p.it.mine ? ' mine' : '') + '" data-go="' + (p.i + 1) + '" style="left:' + p.x.toFixed(1) + '%;top:' + p.y.toFixed(1) +
|
||||
'%;--s:' + p.sz + 'px;--tw:' + ((hsh(p.it.id) % 30) / 10).toFixed(1) + 's" aria-label="' + esc(p.it.text) + '"><i></i></button>').join('');
|
||||
}
|
||||
function paintFocus(anim) {
|
||||
const l = list(), it = l[fi];
|
||||
const box = $('#whpFocus');
|
||||
if (!it) { box.innerHTML = ''; return; }
|
||||
box.innerHTML =
|
||||
'<div class="whf-top"><span class="whf-no">' + (it.mine ? '<em>我说的</em>' : '') + 'No.' + (fi + 1) + ' / ' + l.length + '</span>' +
|
||||
'<span class="whf-nav"><button type="button" data-fn="-1" aria-label="上一句">‹</button><button type="button" data-fn="1" aria-label="下一句">›</button></span></div>' +
|
||||
'<p class="whf-q' + (anim ? ' in' : '') + '">' + esc(it.text) + '</p>' +
|
||||
'<div class="whp-meta"><span>孕 ' + it.week + ' 周 · ' + esc(it.ago) + '</span>' + hugBtn(it) + '</div>';
|
||||
$$('.whs-star', $('#whpSky')).forEach(s => s.classList.toggle('on', +s.dataset.fi === fi));
|
||||
}
|
||||
function focusTo(i, user) {
|
||||
const n = list().length;
|
||||
if (!n) return;
|
||||
fi = (i + n) % n;
|
||||
if (user) focHold = true; // 用户自己点过就不再自动轮播,免得正要抱抱时换走
|
||||
paintFocus(true);
|
||||
function slide(it, i, n) {
|
||||
const t = String(it.text || ''), len = [...t].length;
|
||||
const fs = len <= 9 ? 'xl' : len <= 18 ? 'l' : 'm';
|
||||
const chars = [...t].map((c, k) => '<span style="--c:' + k + '">' + esc(c) + '</span>').join('');
|
||||
const rail = it.mine
|
||||
? '<div class="whv-got">' + HEART + '<b>' + (it.hugs || 0) + '</b><small>收到的抱抱</small></div>'
|
||||
: '<button class="whv-hug whp-hug' + (it.hugged ? ' on' : '') + '" type="button" data-hug="' + esc(it.id) + '" aria-label="给她一个抱抱">' + HEART + '<b>' + (it.hugs || '') + '</b></button><small class="whv-hl">抱抱</small>';
|
||||
return '<article class="whv-s' + (it.mine ? ' mine' : '') + '" data-i="' + (i + 1) + '">' +
|
||||
'<div class="whv-in">' +
|
||||
'<div class="whv-no"><b>' + String(i + 1).padStart(2, '0') + '</b><span>/ ' + String(n).padStart(2, '0') + '</span>' + (it.mine ? '<em>我说的</em>' : '') + '</div>' +
|
||||
'<p class="whv-q ' + fs + '">' + chars + '</p>' +
|
||||
'<div class="whv-meta">孕 ' + it.week + ' 周的妈妈 · ' + esc(it.ago) + '</div>' +
|
||||
(i === 0 && !it.mine ? '<div class="whv-tip">双击屏幕,也能给她一个抱抱</div>' : '') +
|
||||
'</div><div class="whv-rail">' + rail + '</div></article>';
|
||||
}
|
||||
function paintList(l) {
|
||||
$('#whpCount').textContent = l.length + ' 句 · 最新在上';
|
||||
$('#whpCount').textContent = l.length + ' 句 · 我说的在最后';
|
||||
$('#whpList').innerHTML = l.map((it, i) => {
|
||||
const lv = it.mine ? 'mine' : it.hugs >= 25 ? 'l3' : it.hugs >= 10 ? 'l2' : 'l1';
|
||||
return '<div class="whl ' + lv + '"><button type="button" class="whl-t" data-fi="' + i + '">' +
|
||||
return '<div class="whl ' + lv + '"><button type="button" class="whl-t" data-go="' + (i + 1) + '">' +
|
||||
(it.mine ? '<em>我说的</em>' : '') + esc(it.text) + '</button>' +
|
||||
'<div class="whp-meta"><span>孕 ' + it.week + ' 周 · ' + esc(it.ago) + '</span>' + hugBtn(it) + '</div></div>';
|
||||
}).join('');
|
||||
}
|
||||
function setCur(i) {
|
||||
cur = i;
|
||||
const n = list().length;
|
||||
$$('.whv-bars i', page).forEach((b, k) => { b.classList.toggle('done', k < i - 1); b.classList.toggle('on', k === i - 1); });
|
||||
$('#whvBars').classList.toggle('hide', i === 0);
|
||||
const it = vl[i - 1];
|
||||
const pal = it && it.mine ? ['#A8723A', '#E0A86A'] : i === 0 ? ['#5A3B72', '#4A2C55'] : AURA[(i - 1) % AURA.length];
|
||||
const au = $$('#whvAura i');
|
||||
au[0].style.backgroundColor = pal[0]; au[1].style.backgroundColor = pal[1];
|
||||
$$('.whs-star', page).forEach(s => s.classList.toggle('on', +s.dataset.go === i));
|
||||
}
|
||||
function paintPage() {
|
||||
const l = list();
|
||||
$('#whpTag').textContent = data ? '孕 ' + data.week + ' 周 · 前后一周的妈妈' : '';
|
||||
/* 北京时间 18 点后 / 5 点前叫「今晚」,其余「今天」 */
|
||||
const l = vl = vlist(), n = l.length;
|
||||
const bh = (new Date().getUTCHours() + 8) % 24, night = bh >= 18 || bh < 5;
|
||||
$('#whpTitle').innerHTML = (night ? '今晚' : '今天') + ',<b>' + l.length + '</b> 句悄悄话';
|
||||
$('#whpEnd').textContent = night ? '— 都在这里了,晚安 —' : '— 都在这里了 —';
|
||||
/* 第一次聚焦:抱抱最多的那句(不选自己的) */
|
||||
fi = 0;
|
||||
l.forEach((x, i) => { if (!x.mine && x.hugs > (l[fi].mine ? -1 : l[fi].hugs)) fi = i; });
|
||||
paintSky(l);
|
||||
paintFocus(false);
|
||||
whv.innerHTML =
|
||||
'<article class="whv-s whv-cover" data-i="0"><div class="whv-in">' +
|
||||
'<span class="whp-tag">' + (data ? '孕 ' + data.week + ' 周 · 前后一周的妈妈' : '') + '</span>' +
|
||||
'<div class="whv-ct">' + (night ? '今晚' : '今天') + ',<br><b>' + n + '</b> 句悄悄话</div>' +
|
||||
'<p class="whp-sub">不留名字,只留一句话。听到了,就给她一个抱抱。</p>' +
|
||||
'<div class="whp-sky2" id="whpSky" role="group" aria-label="今晚的星星,每颗是一句悄悄话">' + skySvg(l) + '</div>' +
|
||||
'<div class="whv-down" aria-hidden="true"><i></i>上滑,听她们说</div></div></article>' +
|
||||
l.map((it, i) => slide(it, i, n)).join('') +
|
||||
'<article class="whv-s whv-end" data-i="' + (n + 1) + '"><div class="whv-in"><div class="whv-ct sm">' +
|
||||
(night ? '都在这里了,<br>晚安。' : '都在这里了。') + '</div><p class="whp-sub">你也说一句吧,她们会听到的。</p></div></article>';
|
||||
$('#whvBars').innerHTML = l.map(() => '<i></i>').join('');
|
||||
paintList(l);
|
||||
$('#whpLeft').textContent = !data ? '' : !oid() || !data.canPost ? '填好预产期,就能说一句' : data.left ? '今天还能说 ' + data.left + ' 句' : '今天的 3 句说完啦,明天再来';
|
||||
if (io) io.disconnect();
|
||||
if ('IntersectionObserver' in window) {
|
||||
io = new IntersectionObserver(es => es.forEach(e => {
|
||||
e.target.classList.toggle('on', e.isIntersecting);
|
||||
if (e.isIntersecting) setCur(+e.target.dataset.i);
|
||||
}), { root: whv, threshold: .6 });
|
||||
$$('.whv-s', whv).forEach(s => io.observe(s));
|
||||
} else $$('.whv-s', whv).forEach(s => s.classList.add('on'));
|
||||
setCur(0);
|
||||
}
|
||||
function goTo(i, smooth) {
|
||||
const s = $('.whv-s[data-i="' + i + '"]', whv);
|
||||
if (s) whv.scrollTo({ top: s.offsetTop, behavior: smooth && !RM ? 'smooth' : 'auto' });
|
||||
}
|
||||
function drawer(on) {
|
||||
$('#whDrawer').classList.toggle('show', on);
|
||||
$('#whDrawer').setAttribute('aria-hidden', on ? 'false' : 'true');
|
||||
}
|
||||
$('#whvAll').addEventListener('click', () => { haptic(); drawer(true); });
|
||||
$('#whdX').addEventListener('click', () => drawer(false));
|
||||
$('#whDrawer').addEventListener('click', e => { if (e.target.id === 'whDrawer') drawer(false); });
|
||||
page.addEventListener('click', e => {
|
||||
const st = e.target.closest('[data-fi]');
|
||||
if (st) {
|
||||
haptic();
|
||||
focusTo(+st.dataset.fi, true);
|
||||
if (st.classList.contains('whl-t')) $('#whpScroll').scrollTo({ top: $('#whpSky').offsetTop - 70, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
const nb = e.target.closest('[data-fn]');
|
||||
if (nb) { haptic(); focusTo(fi + +nb.dataset.fn, true); }
|
||||
const g = e.target.closest('[data-go]');
|
||||
if (!g) return;
|
||||
haptic();
|
||||
drawer(false);
|
||||
goTo(+g.dataset.go, true);
|
||||
});
|
||||
/* 聚焦卡左右滑切换 */
|
||||
let tx = null;
|
||||
$('#whpFocus').addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
|
||||
$('#whpFocus').addEventListener('touchend', e => {
|
||||
if (tx == null) return;
|
||||
const dx = e.changedTouches[0].clientX - tx; tx = null;
|
||||
if (Math.abs(dx) > 40) focusTo(fi + (dx < 0 ? 1 : -1), true);
|
||||
/* 双击屏幕 = 抱抱:大爱心在手指处炸开,再走下面同一条抱抱逻辑 */
|
||||
let lastTap = 0;
|
||||
whv.addEventListener('click', e => {
|
||||
if (e.target.closest('button')) return;
|
||||
const now = Date.now();
|
||||
if (now - lastTap > 320) { lastTap = now; return; }
|
||||
lastTap = 0;
|
||||
const s = e.target.closest('.whv-s');
|
||||
const hb = s && $('.whv-hug', s);
|
||||
if (!hb) return;
|
||||
if (!RM) {
|
||||
const big = document.createElement('i');
|
||||
big.className = 'whv-big';
|
||||
big.innerHTML = HEART;
|
||||
const r = page.getBoundingClientRect();
|
||||
big.style.left = (e.clientX - r.left) + 'px';
|
||||
big.style.top = (e.clientY - r.top) + 'px';
|
||||
page.appendChild(big);
|
||||
setTimeout(() => big.remove(), 900);
|
||||
}
|
||||
if (!hb.classList.contains('on')) hb.click();
|
||||
});
|
||||
function openPage() {
|
||||
paintPage();
|
||||
page.classList.add('show');
|
||||
page.setAttribute('aria-hidden', 'false');
|
||||
$('#whpScroll').scrollTop = 0;
|
||||
whv.scrollTop = 0;
|
||||
drawer(false);
|
||||
haptic();
|
||||
focHold = false;
|
||||
clearInterval(focT);
|
||||
if (!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches))
|
||||
focT = setInterval(() => { if (!focHold && !document.hidden && list().length > 1) focusTo(fi + 1); }, 6000);
|
||||
}
|
||||
function closePage() {
|
||||
clearInterval(focT);
|
||||
drawer(false);
|
||||
page.classList.remove('show');
|
||||
page.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
@@ -3491,7 +3527,6 @@
|
||||
const o = oid();
|
||||
if (!o) { toast('登录后就能给她抱抱啦'); return; }
|
||||
if (b.classList.contains('on')) { toast('你已经抱过她啦'); return; }
|
||||
focHold = true;
|
||||
const id = b.dataset.hug;
|
||||
const it = list().find(x => x.id === id);
|
||||
haptic();
|
||||
@@ -3556,8 +3591,7 @@
|
||||
paintEntry();
|
||||
paintPage();
|
||||
closeModal();
|
||||
fi = 0; focHold = true; paintFocus(true);
|
||||
$('#whpScroll').scrollTo({ top: $('#whpSky').offsetTop - 70, behavior: 'smooth' });
|
||||
setTimeout(() => goTo(vl.findIndex(x => x.id === it.id) + 1, true), 80);
|
||||
toast('发出去啦,同孕周妈妈会给你抱抱的');
|
||||
})
|
||||
.catch(() => toast('网络不太好,再试一次'))
|
||||
|
||||
Reference in New Issue
Block a user