悄悄话全屏页四改:一屏一句竖滑沉浸式、逐字亮起、光晕随句变色、双击抱抱、全部抽屉 (v26100168)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 06:54:02 +00:00
parent a9581b64a1
commit e70e4cb91b
3 changed files with 198 additions and 88 deletions

174
js/app.js
View File

@@ -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('网络不太好,再试一次'))