Files
mama-box/js/wh-cm.js

170 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 星空页 · 妈妈们的留言(2026-11-07 用户:广场点「孕妈悄悄话」直接进星空页,留言自带在「说一句悄悄话」下面,
样式跟星空搭,不要白底)。
· 版式:#wcm 盖满星空页、自己滚动;上面一段透明垫高,下面是留言面板,
把原来的 .whp-bar(说一句悄悄话)挪进面板顶上,往上滑留言就盖上来。
· 数据:广场置顶笔记 wh-<话题 from> 的评论 = cm.json 模拟(AGENTS.md 12c,不得挂星)+ /plaza/comments 已审核通过的真实评论
+ 我自己发的(含审核中)。进星空页才拉 cm.json(600K+,gzip 后小很多;版本号跟广场 index.html 引的一致,共用缓存)。
· 11-07 第 2 步:真实留言的赞走服务端(POST /plaza/comment/like,每人每条一次、不能赞自己),赞够 starLikes(后台可改,先 3)
自动挂到星空,挂上的标「已挂上星星」;模拟留言的赞只记本机 mmbox.whcm.v1(模拟的永远不挂星)。
最热 / 最新 / 同孕周 三种看法;星空页「说一句」发的话也进这里(window.MBWhCm.post,app.js 调),先审后发。 */
(function () {
var page = document.getElementById('whPage'), bar = page && page.querySelector('.whp-bar');
if (!page || !bar || !window.MB_WH_THEME) return;
var TH = MB_WH_THEME(), PID = 'wh-' + TH.from.replace(/-/g, '');
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); };
var m = ((location.hash || '') + '&').match(/[#&]openid=([^&]*)/), OID = m ? decodeURIComponent(m[1]) : '';
var box = document.createElement('div');
box.className = 'wcm'; box.id = 'wcm';
box.innerHTML = '<div class="wcm-sp" aria-hidden="true"></div><div class="wcm-in"><div class="wcm-hd"><b>妈妈们的留言</b><span id="wcmN"></span><i class="wcm-up">上滑看看</i></div>' +
'<div class="wcm-tb" role="tablist"><button type="button" class="on" data-wso="hot">最热</button><button type="button" data-wso="new">最新</button><button type="button" data-wso="wk">同孕周</button>' +
'<span class="wcm-rule" id="wcmRule"></span></div>' +
'<div class="wcm-ls" id="wcmLs"><p class="wcm-e">留言加载中…</p></div><button type="button" class="wcm-more" id="wcmMore" hidden>再看看</button></div>';
box.querySelector('.wcm-in').insertBefore(bar, box.querySelector('.wcm-hd'));
page.appendChild(box);
var ls = box.querySelector('#wcmLs'), more = box.querySelector('#wcmMore');
var LK = 'mmbox.whcm.v1', LIKE = {};
try { LIKE = JSON.parse(localStorage.getItem(LK) || '{}') || {}; } catch (e) {}
var avP = function (a) { return typeof a === 'number' ? 'guangchang/av/' + ('000' + a).slice(-4) + '.webp' : a.indexOf('av:') === 0 ? 'guangchang/av/' + a.slice(3) + '.webp' : a.charAt(0) === '/' ? a : 'assets/img/mom/' + a + '.jpg'; };
var fmt = function (n) { return n >= 10000 ? (n / 10000).toFixed(1) + 'w' : String(n); };
var ago = function (ts) {
var d = (Date.now() - ts) / 1000;
if (d < 3600) return Math.max(1, Math.round(d / 60)) + ' 分钟前';
if (d < 86400) return Math.round(d / 3600) + ' 小时前';
var t = new Date(ts + 8 * 3600e3); return (t.getUTCMonth() + 1) + '-' + t.getUTCDate();
};
var SIM = null, REAL = null, ALL = [], shown = 0, loading = false, SORT = 'hot', STAR_N = 3;
function load() {
if (loading || SIM) return; loading = true;
fetch('guangchang/cm.json?v=26110612').then(function (r) { return r.json(); }).then(function (j) {
var P = j.p;
SIM = (j.c[PID] || []).map(function (c, k) {
var p = P[c[0]];
return { key: 's' + k, n: p[0], av: avP(p[1]), s: p[2], t: c[1], likes: c[2],
r: c[3].map(function (x) { var q = x[0] < 0 ? null : P[x[0]]; return { n: q ? q[0] : '孕妈悄悄话', t: x[1] }; }) };
});
build();
}).catch(function () { SIM = []; build(); });
fetch('/api/mp/public/mama-box/plaza/comments?kind=post&target=' + encodeURIComponent(PID) + (OID ? '&openid=' + encodeURIComponent(OID) : ''))
.then(function (r) { return r.json(); }).then(function (j) {
var R = j && j.code === 200 && j.result || {};
if (R.starLikes) STAR_N = R.starLikes;
REAL = (R.list || []).filter(top).map(realOf).concat((R.mine || []).filter(function (c) { return top(c) && c.status !== 'rejected'; }).map(function (c) {
var o = realOf(c); o.n = '我'; o.me = 1; o.st = c.status; return o; }));
build();
}).catch(function () { REAL = []; build(); });
}
var top = function (c) { return !c.parent; };
function realOf(c) {
return { key: 'r' + c.id, id: c.id, real: 1, n: c.nickname || '孕妈', av: c.avatar || 'assets/img/mom/k01.jpg', s: c.stage, t: c.text,
likes: c.likes || 0, liked: !!c.liked, star: !!c.star, ts: c.at, r: [] };
}
var wkOf = function (c) { var m = /孕\s*(\d+)\s*周/.exec(c.s || ''); return m ? +m[1] : 0; };
var likesOf = function (c) { return c.real ? c.likes : c.likes + (LIKE[c.key] ? 1 : 0); };
var likedOf = function (c) { return c.real ? c.liked : !!LIKE[c.key]; };
/* 最热:赞多的在前(挂了星的真实留言再往前提);最新:真实留言按时间,模拟的接在后面;同孕周:和我差 ±1 周的,按最热 */
function sorted() {
var mine = REAL.filter(function (c) { return c.me; }), rest = REAL.filter(function (c) { return !c.me; }).concat(SIM);
var hot = function (a, b) { return (!!b.star - !!a.star) || (likesOf(b) - likesOf(a)); };
if (SORT === 'new') rest = rest.filter(function (c) { return c.real; }).sort(function (a, b) { return b.ts - a.ts; }).concat(SIM);
else if (SORT === 'wk') {
var w = +window.MB_WH_WEEK || 0;
rest = w ? rest.filter(function (c) { var k = wkOf(c); return k && Math.abs(k - w) <= 1; }) : [];
rest.sort(hot);
} else rest.sort(hot);
return mine.sort(function (a, b) { return b.ts - a.ts; }).concat(rest);
}
function build() {
if (!SIM || !REAL) return;
ALL = sorted(); shown = 0; ls.innerHTML = '';
document.getElementById('wcmN').textContent = (REAL.length + SIM.length) ? (REAL.length + SIM.length) + ' 条' : '';
document.getElementById('wcmRule').textContent = '赞够 ' + STAR_N + ' 个挂上星星';
if (!ALL.length) {
ls.innerHTML = '<p class="wcm-e">' + (SORT === 'wk' ? (window.MB_WH_WEEK ? '同孕周的妈妈还没留言,说一句吧' : '填好预产期,就能看同孕周妈妈的留言') : '还没有留言') + '</p>';
more.hidden = true; return;
}
add(20);
}
function item(c) {
var on = likedOf(c);
return '<div class="wcm-it"><img class="wcm-av" src="' + esc(c.av) + '" alt="" loading="lazy" onerror="this.style.visibility=\'hidden\'">' +
'<div class="wcm-bd"><p class="wcm-nm"><b>' + esc(c.n) + '</b>' + (c.s ? '<span>' + esc(c.s) + '</span>' : '') + (c.ts ? '<span>' + ago(c.ts) + '</span>' : '') + '</p>' +
'<p class="wcm-tx">' + esc(c.t) + '</p>' + (c.st === 'pending' ? '<p class="wcm-st">审核中 · 通过后大家才能看到</p>' : '') +
(c.star ? '<p class="wcm-star"><svg viewBox="0 0 20 20"><path d="M10 .5C10.9 7.4 12.6 9.1 19.5 10 12.6 10.9 10.9 12.6 10 19.5 9.1 12.6 7.4 10.9.5 10 7.4 9.1 9.1 7.4 10 .5Z"/></svg>已挂上星星</p>' : '') +
(c.r.length ? '<div class="wcm-rp">' + c.r.map(function (x) { return '<p><b>' + esc(x.n) + '</b>:' + esc(x.t) + '</p>'; }).join('') + '</div>' : '') + '</div>' +
(c.me ? '<span class="wcm-lk me"><svg viewBox="0 0 24 24"><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><span>' + fmt(likesOf(c)) + '</span></span></div>' :
'<button type="button" class="wcm-lk' + (on ? ' on' : '') + '" data-wcl="' + esc(c.key) + '" aria-label="赞"><svg viewBox="0 0 24 24"><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><span>' + fmt(likesOf(c)) + '</span></button></div>');
}
function add(k) {
ls.insertAdjacentHTML('beforeend', ALL.slice(shown, shown + k).map(item).join(''));
shown = Math.min(ALL.length, shown + k); more.hidden = shown >= ALL.length;
}
more.addEventListener('click', function () { add(30); });
/* 用首页同一个 #toast(app.js 的 toast 在闭包里拿不到,这里照它的写法) */
var tT;
function toast(t) { var x = document.getElementById('toast'); if (!x) return; x.textContent = t; x.classList.add('show'); clearTimeout(tT); tT = setTimeout(function () { x.classList.remove('show'); }, 1800); }
function paintLk(b, c) { var on = likedOf(c); b.classList.toggle('on', on); b.querySelector('span').textContent = fmt(likesOf(c)); }
ls.addEventListener('click', function (e) {
var b = e.target.closest('[data-wcl]'); if (!b) return;
var k = b.dataset.wcl, c = ALL.filter(function (x) { return x.key === k; })[0]; if (!c) return;
if (!c.real) {
if (LIKE[k]) delete LIKE[k]; else LIKE[k] = 1;
try { localStorage.setItem(LK, JSON.stringify(LIKE)); } catch (e2) {}
paintLk(b, c); if (LIKE[k] && navigator.vibrate) navigator.vibrate(8);
return;
}
/* 真实留言:服务端计数,赞了就不能取消(同星星的抱抱) */
if (!OID) { toast('登录后就能点赞啦'); return; }
if (c.liked) { toast('你已经赞过啦'); return; }
c.liked = true; c.likes++; paintLk(b, c); b.classList.add('pop'); setTimeout(function () { b.classList.remove('pop'); }, 500);
if (navigator.vibrate) navigator.vibrate(8);
fetch('/api/mp/public/mama-box/plaza/comment/like?openid=' + encodeURIComponent(OID) + '&id=' + encodeURIComponent(c.id), { method: 'POST' })
.then(function (r) { return r.json(); }).then(function (j) {
if (!j || j.code !== 200) throw j;
c.likes = j.result.likes; var was = c.star; c.star = !!j.result.star; paintLk(b, c);
if (c.star && !was) { toast('这句被赞够 ' + STAR_N + ' 次,挂上星星啦'); var it = b.closest('.wcm-it'); if (it) it.outerHTML = item(c); }
}).catch(function (j) { c.liked = false; c.likes--; paintLk(b, c); toast((j && j.message) || '网络不太好,再点一次试试'); });
});
box.querySelector('.wcm-tb').addEventListener('click', function (e) {
var b = e.target.closest('[data-wso]'); if (!b || b.dataset.wso === SORT) return;
SORT = b.dataset.wso;
[].forEach.call(this.querySelectorAll('[data-wso]'), function (x) { x.classList.toggle('on', x === b); });
build();
});
/* 星空页「说一句」→ 发成留言(app.js 的 #whSend 调)。返回 Promise:resolve(null) = 成功,resolve('提示') = 没发出去 */
window.MBWhCm = {
starLikes: function () { return STAR_N; },
post: function (text, week) {
if (!OID) return Promise.resolve('登录后就能说一句啦');
return fetch('/api/mp/public/mama-box/plaza/comment?openid=' + encodeURIComponent(OID), {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 'post', target: PID, title: '孕妈悄悄话 · ' + TH.name, text: text, stage: week ? '孕 ' + week + ' 周' : '' })
}).then(function (r) { return r.json(); }).then(function (j) {
if (!j || j.code !== 200 || !j.result) return (j && j.message) || '没发出去,再试一次';
var o = realOf(j.result); o.n = '我'; o.me = 1; o.st = j.result.status;
REAL = [o].concat(REAL || []);
if (SIM) { build(); box.scrollTo({ top: box.querySelector('.wcm-hd').offsetTop + box.querySelector('.wcm-sp').offsetHeight - 80, behavior: 'smooth' }); }
return null;
}).catch(function () { return '网络不太好,再试一次'; });
}
};
/* 11-07 用户:页面滑不动 —— 原来垫高层 pointer-events:none,手机上按在星空上滑,滚动不归 #wcm。
现在整层都接触摸(按哪都能滑),点在垫高层上就把点击转给底下的星星 / 顶栏按钮 / 内容卡 */
var sp = box.querySelector('.wcm-sp');
sp.addEventListener('click', function (e) {
box.style.pointerEvents = 'none';
var el = document.elementFromPoint(e.clientX, e.clientY);
box.style.pointerEvents = '';
if (el && el !== sp && page.contains(el)) el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: e.clientX, clientY: e.clientY }));
});
/* 上滑过半:面板压暗星空的那层渐变加深,「上滑看看」收起 */
box.addEventListener('scroll', function () { box.classList.toggle('up', box.scrollTop > 40); }, { passive: true });
/* 进星空页才拉数据;关掉回到顶部 */
new MutationObserver(function () {
if (page.classList.contains('show')) load(); else { box.scrollTop = 0; box.classList.remove('up'); }
}).observe(page, { attributes: true, attributeFilter: ['class'] });
if (page.classList.contains('show')) load();
})();