/* 星空页 · 妈妈们的留言(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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 = '
妈妈们的留言上滑看看
' +
'
' +
'
' +
'
';
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 = '' + (SORT === 'wk' ? (window.MB_WH_WEEK ? '同孕周的妈妈还没留言,说一句吧' : '填好预产期,就能看同孕周妈妈的留言') : '还没有留言') + '
';
more.hidden = true; return;
}
add(20);
}
function item(c) {
var on = likedOf(c);
return ' + ')
' +
'
' + esc(c.n) + '' + (c.s ? '' + esc(c.s) + '' : '') + (c.ts ? '' + ago(c.ts) + '' : '') + '
' +
'
' + esc(c.t) + '
' + (c.st === 'pending' ? '
审核中 · 通过后大家才能看到
' : '') +
(c.star ? '
已挂上星星
' : '') +
(c.r.length ? '
' + c.r.map(function (x) { return '
' + esc(x.n) + ':' + esc(x.t) + '
'; }).join('') + '
' : '') + '
' +
(c.me ? '
' + fmt(likesOf(c)) + ' ' :
'');
}
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();
})();