上正式:悄悄话星空页+留言抽屉+变星(合并 preview 8b84526)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-11-07 12:46:25 +08:00
15 changed files with 1131 additions and 59 deletions

View File

@@ -115,6 +115,7 @@ nginx:`root /opt/app` + `Cache-Control: no-cache` + `etag on` —— 缓存靠
- 决策人:产品负责人(用户本人),2026-11-02 拍板「准备 1000 个头像和名字,每篇评论至少 20 条以上,也会有作者回复」;已书面告知虚假宣传风险(《反不正当竞争法》第 8 条),且比模拟数字更敏感。
- 期限:**最晚 2026-12-31** 复核;真实评论上线后,有真实评论的帖子要先撤掉模拟评论,不得混排冒充。
- 2026-11-02 扩展:104 个专题页底部「大家怎么说」的预置评论 `topic/_shared/cm/<slug>.json`(每页 10~14 条 + 楼中楼,共约 1200 条,人设取自同一批 1000 个),由 `topic/_shared/tools.js` 渲染;用户自己发的评论只存本机 `mmbox.tp.v1`,不上传。同一期限复核。
- 2026-11-07 扩展:孕妈广场「全部」信息流的置顶笔记「孕妈悄悄话」(id `wh-20260928`,按每周话题一篇)下 100 条模拟祝福评论 + 18 条楼中楼,存在同一个 `cm.json`,由 `tools/whcm/build.py` 从 `lines.txt` 生成(人设取同一批,主评论限孕 24 周以上)。用户 11-07 原话「评论里边还是按照正常评论都给我放上 100 个」。模拟评论**不得**挂上星空(挂星只认真实且审核通过的)。同一期限复核。
- 边界:只用于占位帖子的详情页评论区;真实投稿(`n.real`)一律不配模拟评论;这 1000 个人设不得出现在投稿审核、排行榜、通知等任何其他地方。
13. **跳其它 H5 应用 / 调第三方接口一律同源:只写相对路径 `/<slug>/`,不写域名**(2026-10-09 用户定)

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<!-- 11-04 UGC 总开关关着时本页不展示,直接回首页(js/ugc.js) -->
<script src="../js/login-gate.js?v=26110605"></script>
<script src="../js/login-gate.js?v=26110610"></script>
<script src="../js/ugc.js?v=26110464" data-guard></script>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
@@ -66,6 +66,40 @@ button{font:inherit;color:inherit;border:0;background:none;padding:0;cursor:poin
.qf{position:relative;display:block;width:calc(100% - 24px);height:148px;margin:2px 12px 0;border-radius:20px;overflow:hidden;text-align:left;
background:radial-gradient(120% 140% at 82% 100%,#3B3A86 0%,#252A63 38%,#161B3F 75%);box-shadow:0 12px 26px -14px rgba(30,30,90,.7)}
.qf:active{transform:scale(.985)}
/* 11-07 置顶笔记「孕妈悄悄话」(用户:首页不放,在广场内容里发一条置顶笔记,点了进星空页):
右列第一张,样子是一张笔记卡 —— 封面换成夜空(星星一闪一闪 + 偶尔一道流星)+ 本周话题大字 + 一句轮播的悄悄话 */
.nc.pin .pcv,.dcv .pcv{position:relative;height:calc(var(--cw, 175px) * 1.25);overflow:hidden;color:#fff;
background:radial-gradient(120% 70% at 85% 0%,#5A3B72 0%,transparent 60%),radial-gradient(90% 60% at 0% 100%,#4A2C55 0%,transparent 70%),linear-gradient(180deg,#191634 0%,#241D44 55%,#2F2148 100%)}
.pcv .psk{position:absolute;inset:0;pointer-events:none}
.pcv .psk i{position:absolute;width:6px;height:6px;margin:-3px 0 0 -3px;opacity:.85;filter:drop-shadow(0 0 2px rgba(255,255,255,.7));
background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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' fill='%23FFFFFF'/%3E%3C/svg%3E") center/contain no-repeat;animation:pTw 2.6s ease-in-out infinite}
.pcv .psk i.b{width:11px;height:11px;margin:-5.5px 0 0 -5.5px;opacity:1;filter:drop-shadow(0 0 4px rgba(255,255,255,.9))}
@keyframes pTw{0%,100%{opacity:.9;transform:scale(1)}50%{opacity:.2;transform:scale(.55)}}
.pcv .pmt{position:absolute;top:8%;left:78%;width:80px;height:2px;border-radius:2px;opacity:0;transform-origin:0 50%;
background:linear-gradient(90deg,#fff,rgba(255,255,255,.7) 30%,rgba(255,255,255,0));box-shadow:0 0 8px 1px rgba(255,255,255,.8);animation:pMt 6s linear infinite 1.2s}
.pcv .pmt::before{content:"";position:absolute;left:-2px;top:-1.5px;width:5px;height:5px;border-radius:50%;background:#fff;box-shadow:0 0 6px 2px #fff}
.pcv .pmt.m2{top:30%;left:96%;width:60px;animation-duration:9s;animation-delay:4.2s}
@keyframes pMt{0%{opacity:0;transform:translate(0,0) rotate(-28deg)}2%{opacity:1}12%{opacity:0;transform:translate(-130px,70px) rotate(-28deg)}100%{opacity:0;transform:translate(-130px,70px) rotate(-28deg)}}
@media (prefers-reduced-motion:reduce){.pcv .psk i,.pcv .pmt{animation:none}}
.pcv .ptop{position:absolute;left:8px;top:8px;z-index:1;height:19px;padding:0 7px;border-radius:6px;background:linear-gradient(120deg,#FFE2B8,#FFB98F);color:#4A2E1E;font-size:10.5px;font-style:normal;font-weight:900;line-height:19px;letter-spacing:.5px}
.pcv .pbig{position:absolute;left:12px;right:12px;bottom:40px;z-index:1}
.pcv .pbig small{display:block;font-size:10.5px;font-weight:700;letter-spacing:1px;color:rgba(255,231,199,.75)}
.pcv .pbig b{display:block;margin-top:4px;font-size:20px;font-weight:900;line-height:1.3;letter-spacing:.5px;color:#FFE7C7;text-shadow:0 2px 10px rgba(20,10,40,.6)}
.pcv .pq{position:absolute;left:12px;right:12px;bottom:12px;z-index:1;margin:0;font-size:11.5px;color:rgba(255,244,232,.8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .26s,transform .26s}
.pcv .pq.out{opacity:0;transform:translateY(5px)}
.nb .ntg.k-whs{background:#F0EBFA;color:#7A5BB8}
.nc.pin .nf .pav{flex:none;display:grid;place-items:center;width:18px;height:18px;border-radius:50%;background:linear-gradient(135deg,#3E2F5E,#6B4AA8);color:#FFE2B8;font-size:10px;font-style:normal}
/* 置顶笔记详情页:夜空封面 + 「去星空看看」;不要点赞/收藏/关注(这篇是官方置顶,不是某个妈妈的帖子) */
.dcv .pcv{height:min(78vw,340px)}
.dcv .pcv .pbig{bottom:auto;top:30%}
.dcv .pcv .pbig small{font-size:12px}
.dcv .pcv .pbig b{font-size:28px}
.dcv .pcv .pq{bottom:64px;font-size:13px}
.dcv .pcv .ptop{left:14px;top:14px}
.pstar{position:absolute;left:14px;bottom:16px;z-index:2;display:inline-flex;align-items:center;gap:4px;height:36px;padding:0 16px;border:0;border-radius:99px;font:inherit;font-size:14px;font-weight:800;color:#4A2E1E;background:linear-gradient(120deg,#FFE2B8,#FFB98F);box-shadow:0 6px 16px -6px rgba(255,185,143,.8)}
.pstar:active{transform:scale(.97)}
#dt.is-pin #blk,#dt.is-pin #bfv,#dt.is-pin #dfo{display:none}
.nc.pin .nf .pgo{margin-left:auto;flex:none;font-size:11px;font-weight:800;color:#7A5BB8}
.qf-st{position:absolute;inset:0;pointer-events:none;opacity:.9;
background-image:radial-gradient(1.4px 1.4px at 12% 20%,#fff,transparent),radial-gradient(1px 1px at 28% 70%,#fff,transparent),radial-gradient(1.2px 1.2px at 46% 14%,#FFE9B0,transparent),
radial-gradient(1px 1px at 54% 82%,#fff,transparent),radial-gradient(1.5px 1.5px at 62% 30%,#fff,transparent),radial-gradient(1px 1px at 8% 88%,#FFE9B0,transparent),radial-gradient(1.2px 1.2px at 40% 46%,#fff,transparent);
@@ -450,6 +484,19 @@ button{font:inherit;color:inherit;border:0;background:none;padding:0;cursor:poin
/* 11-05 整合:投稿奖励一行(原在热门话题列表头)+ 选中标签后的热门话题 */
.rww{margin:14px 12px 0}
.rww .rwl{margin:0}
/* 11-07 用户:祈福树下面「投稿被选中得 ¥30…」一条 → 改成轮播「哪些妈妈的笔记上了广场」:真实投稿(审核通过)排前,
不够时用广场里点赞高的笔记补;只说「上了广场 / 入选精选」,不写某人领了多少钱。右边「我也投稿」= 原来的规则弹层 */
.rwc{display:flex;align-items:center;gap:8px;height:40px;padding:0 12px;border-radius:12px;background:linear-gradient(100deg,#FFF1E8,#FFE6E6)}
.rwc>.ic{flex:none;width:16px;height:16px;color:#F2553A}
.rwv{position:relative;flex:1;min-width:0;height:40px;overflow:hidden}
.rwi{position:absolute;left:0;right:0;top:0;display:flex;align-items:center;gap:6px;height:40px;border:0;background:none;padding:0;font:inherit;text-align:left;color:#8A4B36;font-size:12px;
transition:transform .4s ease,opacity .4s ease}
.rwi.in{transform:translateY(100%);opacity:0}.rwi.out{transform:translateY(-100%);opacity:0}
.rwi img{flex:none;width:20px;height:20px;border-radius:50%;object-fit:cover;background:#FFE3D3}
.rwi span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rwi span b{font-weight:800;color:#5A3226}
.rwi span em{font-style:normal;font-weight:800;color:#F2394A;margin:0 3px}
.rwb{flex:none;height:26px;padding:0 10px;border:0;border-radius:99px;background:linear-gradient(135deg,#FF8A65,#F2553A);color:#fff;font:inherit;font-size:11.5px;font-weight:800}
.gtp{display:flex;flex-direction:column;align-items:flex-start;gap:6px;margin:2px 14px 4px;padding:10px 12px;border-radius:14px;background:#fff;box-shadow:0 6px 16px -14px rgba(120,60,40,.35)}
.gtp[hidden]{display:none}
.gtp-h{font-size:11.5px;font-weight:800;color:#F2553A}
@@ -931,6 +978,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
<div id="sheet" role="dialog" aria-modal="true"><i class="sg"></i><div class="sbd" id="sbd"></div></div>
<div class="toast" id="toast"></div>
<script src="../js/wh-themes.js?v=26110609"></script> <!-- 11-07 悄悄话每周话题(与首页共用) -->
<script src="topics.js?v=26110487"></script>
<script src="data.js?v=26110487"></script>
<script src="feed.js?v=26110487"></script>
@@ -1005,7 +1053,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
var CMD = null, CMERR = false;
/* 头像编码:数字 = av/NNNN.webp(旧代码头像);'av:c012' = av/c012.webp(卡通/可爱卡通);'/…' = 站内绝对路径(内置头像库 /_bridge/avatars/);其余 = assets/img/mom/xxx.jpg */
var avP = function (a) { return typeof a === 'number' ? 'av/' + ('000' + a).slice(-4) + '.webp' : a.indexOf('av:') === 0 ? 'av/' + a.slice(3) + '.webp' : a.charAt(0) === '/' ? a : '../assets/img/mom/' + a + '.jpg'; };
fetch('cm.json?v=26110487').then(function (r) { return r.json(); }).then(function (j) {
fetch('cm.json?v=26110631').then(function (r) { return r.json(); }).then(function (j) {
CMD = j; if (openId && NBY[openId]) { renderComments(NBY[openId]); syncBar(NBY[openId]); }
}).catch(function () { CMERR = true; if (openId && NBY[openId]) renderComments(NBY[openId]); });
function commentsOf(n) {
@@ -1031,9 +1079,10 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
/* 11-03 用户:评论要后台审核通过才对外公开。服务器 /plaza/comments:list = 别人已通过的;mine = 我在这篇下发的(含审核中 / 未通过)。
ME.cm 里没有 id 的是 11-03 以前只存本机的旧评论,从没公开过,照旧只给自己看(标「仅自己可见」)。 */
var RC = {};
/* 11-07 用户:评论、回复一定要审核通过才显示,自己也不显示「审核中」—— 只留已通过的;本机旧评论(从没审过)也不再显示 */
var myCm = function (id) {
var old = (ME.cm[id] || []).filter(function (c) { return !c.id; }).map(function (c) { return { t: c.t, ts: c.ts, s: c.s, st: 'local' }; });
var srv = RC[id] ? RC[id].mine.map(function (c) { return { id: c.id, t: c.text, ts: c.at, s: c.stage, st: c.status, why: c.reason, parent: c.parent, to: c.parentName, img: c.images }; }) : [];
var old = [];
var srv = RC[id] ? RC[id].mine.filter(function (c) { return c.status === 'approved'; }).map(function (c) { return { id: c.id, t: c.text, ts: c.at, s: c.stage, st: c.status, why: c.reason, parent: c.parent, to: c.parentName, img: c.images }; }) : [];
return old.concat(srv.reverse());
};
var realCm = function (id) { return RC[id] ? RC[id].list : []; };
@@ -1172,7 +1221,51 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
'<span class="qf-fly" aria-hidden="true"><i>福</i><i>安</i><i>福</i><i>康</i></span>' +
'<span class="qf-tx"><small><i></i>宝宝祈福树</small><b><i>#</i>为宝宝挂一张福牌<i>#</i></b><em>集齐五福,愿宝宝平平安安</em>' +
'<span class="qf-go">去祈福<svg class="ic"><use href="#i-right"/></svg></span></span></button>';
} /* 11-07 用户:「首页就不放了,在内容页里放,发一个笔记置顶,点击就进到这个页面」→ 「全部」信息流右列第一张置顶笔记卡。
有孕周(首页写的 mmbox.gcwk.v1,≥4 周、没生)且没退出登录才出;数据同首页 GET /whisper?openid=&week=,回来后卡里轮播真实的悄悄话,
拉不到就只显示话题,不造内容。星空全屏页在首页 → 点了跳首页带 ?wh=1,首页直接打开星空页,关掉回到这里。 */
var PIN_WK = (function () { try { var g = JSON.parse(localStorage.getItem('mmbox.gcwk.v1') || 'null'); return g && !g.pp && g.w ? +g.w : 0; } catch (e) { return 0; } })();
var PIN_ON = PIN_WK >= 4 && !(window.mbGate && window.mbGate.out);
var WHD = null, whRi = 0, whT = null;
/* 置顶笔记本身:按每周话题一篇(id = wh-话题起始日),留言走广场评论那套(先审后发,后台「评论审核」页签);
模拟祝福评论在 cm.json 的 c['wh-20260928'](100 条,tools/whcm/,AGENTS.md 12c 扩展) */
var PIN_TH = window.MB_WH_THEME ? window.MB_WH_THEME() : null;
var PIN_ID = 'wh-' + (PIN_TH ? PIN_TH.from.replace(/-/g, '') : '0');
var PIN = NBY[PIN_ID] = { id: PIN_ID, pin: 1, title: '孕妈悄悄话 · ' + (PIN_TH ? PIN_TH.name : ''), author: '孕妈悄悄话', stage: '', likes: 0 };
function pinCoverHTML(th, l, sky, big) {
return '<div class="pcv"><span class="psk" aria-hidden="true">' + sky + '<b class="pmt"></b><b class="pmt m2"></b></span><i class="ptop">置顶</i>' +
'<div class="pbig"><small>' + (th ? '本周话题' : '孕妈悄悄话') + '</small><b>' + (th ? (th.lines || [th.name]).map(esc).join('<br>') : '说给宝宝听的悄悄话') + '</b></div>' +
'<p class="pq" id="' + (big ? 'pinRl2' : 'pinRl') + '">' + (l.length ? '“' + esc(l[0].text) + '”' : '点一颗星星,看看她说了啥') + '</p>' +
(big ? '<button type="button" class="pstar" data-star="1">去星空看看<svg class="ic" style="width:14px;height:14px"><use href="#i-right"/></svg></button>' : '') + '</div>';
}
function pinHTML() {
var th = (WHD && WHD.theme && WHD.theme.name) ? WHD.theme : (window.MB_WH_THEME ? window.MB_WH_THEME() : null);
var l = WHD ? (WHD.list || []).filter(function (x) { return !x.mine; }) : [];
var sky = pinSky();
return '<button type="button" class="nc pin" id="whPin" data-id="' + PIN_ID + '" aria-label="置顶:孕妈悄悄话' + (th ? ',本周话题' + esc(th.name) : '') + '"><div class="cvw">' + pinCoverHTML(th, l, sky) + '</div>' +
'<div class="nb"><h3>每颗星星,都是一个妈妈说的话</h3><span class="ntg k-whs">#孕妈悄悄话</span>' +
'<div class="nf"><i class="pav" aria-hidden="true"><svg viewBox="0 0 20 20" width="10" height="10"><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" fill="currentColor"/></svg></i><span class="nm">孕妈悄悄话</span><span class="pgo">去看看 ›</span></div></div></button>';
}
var PIN_SKY = [[14,14,0,-1.2,2.4],[38,8,0,-.3,2],[62,20,1,-2.4,2.8],[86,12,0,-3.1,2.2],[24,34,1,-3.7,2.6],[50,40,0,-1.6,3.2],[80,32,0,-2.2,2.6],[10,52,0,-.8,3],[70,50,1,-1.9,2.4],[90,58,0,-2.8,3]];
function pinSky() { return PIN_SKY.map(function (k) { return '<i' + (k[2] ? ' class="b"' : '') + ' style="left:' + k[0] + '%;top:' + k[1] + '%;animation-delay:' + k[3] + 's;animation-duration:' + k[4] + 's"></i>'; }).join(''); }
function whRoll() {
clearInterval(whT); whRi = 0;
var els = ['pinRl', 'pinRl2'].map(function (x) { return document.getElementById(x); }).filter(Boolean), l = WHD ? (WHD.list || []).filter(function (x) { return !x.mine; }) : [];
if (!els.length || !l.length) return;
els.forEach(function (el) { el.textContent = '“' + l[0].text + '”'; });
if (l.length < 2) return;
whT = setInterval(function () {
var es = ['pinRl', 'pinRl2'].map(function (x) { return document.getElementById(x); }).filter(Boolean);
if (!es.length) { clearInterval(whT); return; } if (document.hidden) return;
whRi = (whRi + 1) % l.length; es.forEach(function (e2) { e2.classList.add('out'); });
setTimeout(function () { es.forEach(function (e2) { e2.textContent = '“' + l[whRi].text + '”'; e2.classList.remove('out'); }); }, 260);
}, 4000);
}
if (PIN_ON) fetch('/api/mp/public/mama-box/whisper?openid=' + encodeURIComponent(OPENID || '') + '&week=' + PIN_WK)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (j) { var r = j && j.code === 200 && j.result; if (r && r.show && (r.list || []).length) { WHD = r; whRoll(); } })
.catch(function () {});
/* 分类小 banner:家庭婚姻 / 职场 / 经验 点了 = 参与本类最热话题(投稿规则弹层);测一测 点了 = 进最热的那个测试 */
var CBN = {
family: ['家庭婚姻', '孕期的家,也需要用心经营', '和老公、婆婆、爸妈的那些事', '我也来聊聊'],
@@ -1573,6 +1666,33 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
bx.hidden = !tl.length;
bx.innerHTML = '<span class="gtp-h">热门话题</span>' + tl.map(function (x) { return '<button type="button" data-t="' + x.id + '">#' + esc(x.name) + '#</button>'; }).join('');
}
/* ---------- 「谁的笔记上了广场」轮播(11-07) ---------- */
var rwI = 0, rwT = null;
function rwList() {
var real = NOTES.filter(function (n) { return n.real; }).slice(0, 10);
var pick = NOTES.filter(function (n) { return !n.real && n.photo; }).sort(function (a, b) { return b.likes - a.likes; });
var seen = {}, mock = [];
pick.forEach(function (n) { if (mock.length < 12 && !seen[n.author]) { seen[n.author] = 1; mock.push(n); } });
return real.concat(mock);
}
function rwItem(k) {
var L = rwList(); if (!L.length) return '';
var n = L[k % L.length], tt = n.title.length > 14 ? n.title.slice(0, 13) + '…' : n.title;
return '<button type="button" class="rwi" data-id="' + esc(n.id) + '"><img src="' + esc(avOf(n)) + '" alt="" loading="lazy"><span><b>' + esc(n.author) + '</b>' +
(n.real ? '<em>投稿上了广场</em>' : '<em>入选精选</em>') + '《' + esc(tt) + '》</span></button>';
}
function rwRun() {
clearInterval(rwT); rwI = 0;
if (rwList().length < 2) return;
rwT = setInterval(function () {
var box = document.getElementById('rwv'); if (!box) { clearInterval(rwT); return; }
if (document.hidden || openId) return;
var old = box.firstElementChild; rwI++;
box.insertAdjacentHTML('beforeend', rwItem(rwI));
var nw = box.lastElementChild; nw.classList.add('in'); void nw.offsetWidth;
nw.classList.remove('in'); if (old) { old.classList.add('out'); setTimeout(function () { if (old.parentNode) old.remove(); }, 420); }
}, 3200);
}
function renderHead() {
var t = TBY[cur], el = $('#head'), fh = $('#fh');
var fl0 = document.getElementById('luckFl'); if (fl0) fl0.classList.remove('on');
@@ -1582,7 +1702,8 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
标签作为唯一入口(末尾加「全部话题 ›」);选中标签时标签下露出这一类的 2~3 个热门话题(用户同意);投稿奖励一行挪到「大家在聊」上面。
话题本身不变:投稿选话题、帖子卡上的 #话题#、话题详情页照旧。分类视图(catF)仍是原来的热门话题列表。 */
el.innerHTML = qifuHTML() +
'<div class="rww"><button type="button" class="rwl" data-rule="1"><svg class="ic"><use href="#i-gift"/></svg><span>投稿被选中得 ' + REWARD + '</span><b>怎么参与 ›</b></button></div>';
'<div class="rww"><div class="rwc"><div class="rwv" id="rwv">' + rwItem(0) + '</div></div></div>'; /* 11-07 用户:去掉礼物图标和「我也投稿」 */
rwRun();
fh.className = 'shtg';
fh.innerHTML = '<div class="sh"><h2>大家在聊</h2><small>孕妈们的分享</small></div>' +
'<div class="gtg"><button type="button" data-tag=""' + (tagF ? '' : ' class="on"') + '>全部</button>' +
@@ -1660,7 +1781,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
11-03 用户:「笔记里测一测减少一些,已经有测一测模块了」→ 原来第 3 位起每 8 张插 1 张(约 20 张),改成第 6 位起每 30 张插 1 张、最多 3 张 */
var csq = CS_MIX.concat(byHot(TM).map(function (x) { return x.id; }).filter(function (id) { return CS_MIX.indexOf(id) < 0; }));
list.forEach(function (n, k) { if (k >= 6 && (k - 6) % 30 === 0 && ci < 3) { var tid = csq[ci++ % csq.length]; if (TMBY[tid]) mixed.push({ test: TMBY[tid] }); } mixed.push(n); });
list = [{ mine: 1 }].concat(mixed);
list = [{ mine: 1 }].concat(PIN_ON ? [{ pin: 1 }] : [], mixed); /* 11-07 置顶笔记「孕妈悄悄话」 */
}
/* 11-03 用户:切分类有点卡。原来每张卡塞进列里再 offsetHeight 量高,每量一次整页重排;85 张卡在中端机上卡 0.5 秒以上,正好卡在滑入动画中间。
现在:帖子卡高度直接算(封面 = 列宽 × 比例,底部 = 标题 1/2 行 + 作者行,和 .nc 样式对应),只有「我的投稿 / 收藏 / 测试卡」这几种
@@ -1672,6 +1793,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
list.forEach(function (n) {
if (n.mine) specials.push(myCardHTML(), favCardHTML());
else if (n.test) specials.push(testCardHTML(n.test, dn));
else if (n.pin) specials.push(pinHTML());
});
var spH = [];
if (specials.length) {
@@ -1686,6 +1808,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
function add(html, c, h) { out[c].push(anim(html, k2++)); hs[c] += h + 8; }
list.forEach(function (n) {
if (n.mine) { add(specials[si], 0, spH[si++]); add(specials[si], 0, spH[si++]); return; }
if (n.pin) { add(specials[si], 1, spH[si++]); lastTx[1] = false; return; } /* 右列第一张 */
if (n.test) { var tc0 = hs[0] <= hs[1] ? 0 : 1; add(specials[si], tc0, spH[si++]); lastTx[tc0] = false; return; }
nk++;
var c = hs[0] <= hs[1] ? 0 : 1, sty = !lastTx[c] && n.photo ? (txN[c]++ % 2 ? 'tg' : 'ov') : '';
@@ -1693,6 +1816,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
var r = n.ratio; if (n.photo && sty === 'ov') r = Math.max(r, 1.33); else if (n.photo && sty === 'tg') r = Math.max(r, 1.2);
add(cardHTML(n, !cur, sty), c, estH(n, r));
});
if (PIN_ON) setTimeout(whRoll, 0);
var FIRST = 14, gen = renderFeed.gen = (renderFeed.gen || 0) + 1;
cols[0].innerHTML = out[0].slice(0, FIRST).join(''); cols[1].innerHTML = out[1].slice(0, FIRST).join('');
if (out[0].length > FIRST || out[1].length > FIRST) setTimeout(function () {
@@ -1821,6 +1945,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
var dc = e.target.closest('#csvGo'); if (dc) { csdRoll(dc); return; }
var j = e.target.closest('[data-join]'); if (j) { haptic(); openRule(j.dataset.join); return; }
if (e.target.closest('[data-rule]')) { openRule(''); return; }
var rw = e.target.closest('.rwi[data-id]'); if (rw) { haptic(); openNote(rw.dataset.id, true); return; }
if (e.target.closest('[data-all]')) { openTopics(); return; }
var b = e.target.closest('[data-t]'); if (b) { haptic(); selectTopic(b.dataset.t); }
});
@@ -1942,8 +2067,17 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
$('#bcm span').textContent = cfmt(cmN(n));
if (popK) { var b = popK === 'lv' ? lv : fv; b.classList.remove('pop'); void b.offsetWidth; b.classList.add('pop'); }
}
/* 先把本页地址换成不带 ?n= 的广场列表,星空页关掉 history.back() 回到列表,不会再弹回星空 */
function goStar() {
var q = new URLSearchParams(location.search); ['t', 'n', 'c'].forEach(function (k) { q.delete(k); });
var qs = q.toString();
try { history.replaceState(null, '', location.pathname + (qs ? '?' + qs : '') + location.hash); } catch (e) {}
q.set('wh', '1'); location.href = '../?' + q + location.hash;
}
function openNote(id, push) {
var n = NBY[id]; if (!n) return;
if (n.pin) return goStar(); /* 11-07 用户:点置顶笔记直接进星空页,留言在星空页下面(openPin 详情页不再用) */
$('#dt').classList.remove('is-pin');
var t = TBY[n.t];
openId = id;
$('#dav').src = avOf(n);
@@ -1969,6 +2103,31 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
if (push) { history.pushState({ gcNote: id }, '', setQuery(cur, id)); pushed = true; }
else history.replaceState(history.state, '', setQuery(cur, id));
}
/* 11-07 置顶笔记「孕妈悄悄话」详情:夜空封面(点「去星空看看」进首页星空页)+ 说明 + 留言区(同普通笔记,先审后发) */
var PIN_PH = '对宝宝说一句悄悄话…';
function openPin(n, push) {
openId = n.id;
var th = PIN_TH, l = WHD ? (WHD.list || []).filter(function (x) { return !x.mine; }) : [];
$('#dt').classList.add('is-pin');
$('#dav').src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#3E2F5E"/><stop offset="1" stop-color="#6B4AA8"/></linearGradient></defs><circle cx="20" cy="20" r="20" fill="url(#g)"/><path transform="translate(10 10)" 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" fill="#FFE2B8"/></svg>');
$('#dnm').textContent = '孕妈悄悄话';
$('#dst').textContent = th ? '本周话题 · ' + th.name : '';
$('#dsc').innerHTML = '<div class="dcv">' + pinCoverHTML(th, l, pinSky(), true) + '</div>' +
'<div class="dbody"><h2>每颗星星,都是一个妈妈说的话</h2><div class="tx">' +
'<p>这周的话题是「' + esc(th ? th.name : '说给宝宝听的悄悄话') + '」。' + esc(th && th.tip ? th.tip + '。' : '') + '</p>' +
'<p>在下面留言区写一句,审核通过后大家都能看到。点赞多的悄悄话会挂上星空,变成一颗星星,同孕周的妈妈点一下,就能看到它像流星一样划过。</p>' +
'<p>一天最多说 3 句,每一句都会被认真对待。</p></div>' +
'<div class="tm">孕妈悄悄话 · 置顶</div></div>' +
'<div class="cm" id="cmw"><div class="cm-h" id="cmh"></div>' +
'<div class="cm-in"><span class="me">我</span><button type="button" id="cmgo">' + PIN_PH + '</button></div>' +
'<div id="cml"></div><div class="cm-end">— 没有更多悄悄话了 —</div></div>';
renderComments(n); syncBar(n); loadRC(n); whRoll();
$('#cin').value = ''; $('#cin').placeholder = PIN_PH; if (CP) CP.clear(); $('#bar').classList.remove('typing'); $('#csend').disabled = true;
$('#dsc').scrollTop = 0; $('#dnav').classList.remove('on');
var dt = $('#dt'); dt.classList.add('show'); dt.setAttribute('aria-hidden', 'false');
if (push) { history.pushState({ gcNote: n.id }, '', setQuery(cur, n.id)); pushed = true; }
else history.replaceState(history.state, '', setQuery(cur, n.id));
}
function hideNote() {
var dt = $('#dt'); dt.classList.remove('show'); dt.setAttribute('aria-hidden', 'true');
$('#cin').blur();
@@ -2010,7 +2169,8 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
}
var rr = e.target.closest('[data-rk]');
if (rr) { REPLY = { k: rr.dataset.rk, n: rr.dataset.rn }; cin.placeholder = '回复 ' + REPLY.n + ':'; cin.focus(); return; }
if (e.target.closest('#cmgo')) { REPLY = null; cin.placeholder = '说点什么…'; $('#cin').focus(); }
if (e.target.closest('#cmgo')) { REPLY = null; cin.placeholder = (NBY[openId] || {}).pin ? PIN_PH : '说点什么…'; $('#cin').focus(); }
if (e.target.closest('[data-star]')) { haptic(); goStar(); }
});
$('#blk').addEventListener('click', function () {
var n = NBY[openId]; if (!n) return;
@@ -2033,7 +2193,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
if (CP) { $('#cemo').innerHTML = MBCmPics.EMO_ICON; $('#cemo').addEventListener('mousedown', function (e) { e.preventDefault(); }); $('#cemo').addEventListener('click', function (e) { e.preventDefault(); $('#bar').classList.add('typing'); CP.toggleEmo(); }); }
else { $('#cpic').remove(); $('#cemo').remove(); }
cin.addEventListener('focus', function () { $('#bar').classList.add('typing'); });
cin.addEventListener('blur', function () { setTimeout(function () { if (!canSend() && !(CP && CP.emoOpen())) { $('#bar').classList.remove('typing'); REPLY = null; cin.placeholder = '说点什么…'; } }, 120); });
cin.addEventListener('blur', function () { setTimeout(function () { if (!canSend() && !(CP && CP.emoOpen())) { $('#bar').classList.remove('typing'); REPLY = null; cin.placeholder = (NBY[openId] || {}).pin ? PIN_PH : '说点什么…'; } }, 120); });
cin.addEventListener('input', function () { $('#csend').disabled = !canSend(); });
cin.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); send(); } });
$('#csend').addEventListener('click', send);
@@ -2052,12 +2212,12 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
send.busy = false;
if (!j || j.code !== 200) { $('#csend').disabled = !canSend(); toast((j && j.message) || '没发出去,稍后再试'); return; }
var rc = RC[n.id] || (RC[n.id] = { list: [], mine: [] }); rc.mine.unshift(j.result);
var wasReply = !!REPLY; REPLY = null; cin.placeholder = '说点什么…';
var wasReply = !!REPLY; REPLY = null; cin.placeholder = (NBY[openId] || {}).pin ? PIN_PH : '说点什么…';
haptic();
cin.value = ''; if (CP) CP.clear(); $('#csend').disabled = true; cin.blur(); $('#bar').classList.remove('typing');
if (openId === n.id) { renderComments(n); syncBar(n); }
if (!wasReply) { var w = $('#cmw'); if (w) $('#dsc').scrollTo({ top: w.offsetTop - 50, behavior: 'smooth' }); }
toast(wasReply ? '回复已提交,审核通过后大家就能看到' : '已提交,审核通过后大家就能看到');
toast(wasReply ? '回复已提交,审核通过后才会显示' : '已提交,审核通过后才会显示');
}).catch(function () { send.busy = false; $('#csend').disabled = !canSend(); toast('网络不太好,稍后再试'); });
}
@@ -2401,6 +2561,7 @@ html.night .gtg button.on{background:#F2725B;border-color:#F2725B;color:#fff}
/* 11-04 微信转发图:跟着地址走 —— ?n= 笔记(g/<id>.jpg,真实投稿用广场图)、?t= 话题(g/t-<id>.jpg)、?c=ceshi 测试列表、其余广场首页;见 js/share-card.js */
if (window.mbShareWatch) window.mbShareWatch(function () {
var q = new URLSearchParams(location.search), n = NBY[q.get('n')], t = TBY[q.get('t')];
if (n && n.pin) return ['../assets/share/g/home.jpg', '孕妈悄悄话|每颗星星,都是一个妈妈想对宝宝说的话']; /* 11-07 置顶笔记暂用广场首页图 */
if (n) return n.real ? ['../assets/share/g/home.jpg', n.title] : ['../assets/share/g/' + n.id + '.jpg', n.title];
if (t) return ['../assets/share/g/t-' + t.id + '.jpg', '#' + t.name + '#'];
if (q.get('c') === 'ceshi') return ['../assets/share/g/ceshi.jpg', '趣味小测试|30 个孕育小测试,1 分钟测出你是哪种孕妈'];

View File

@@ -9,16 +9,17 @@
<title>兜爸心选 · 孕育陪伴</title>
<!-- 11-04 UGC 总开关(孕妈广场 + 测一测,后台 /box_bm/plaza 顶部切换),见 js/ugc.js -->
<script src="js/ugc.js?v=26110487"></script>
<!-- V:26110605 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26110631 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<link rel="stylesheet" href="night.css?v=26110605">
<link rel="stylesheet" href="night.css?v=26110608">
<link rel="stylesheet" href="js/wh-cm.css?v=26110631">
<!-- 11-04 用户:「顶部图片上的倒计时先删除掉」→ 不加载 hero-cd.css / hero-cd.js(文件留着,加回这两行即恢复),头图回到原样式 -->
<!-- 夜晚模式:首屏绘制前就按上次的选择加 class,避免先白后黑闪一下(开关在首页右上角 #nightBtn,逻辑见 app.js) -->
<script>try{if(localStorage.getItem('mmbox.night.v1')==='1'){document.documentElement.classList.add('night');document.querySelector('meta[name=theme-color]').content='#2A2340'}}catch(e){}</script>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
<script src="/common/wx-auth.js?v=26102601"></script>
<script src="js/login-gate.js?v=26110605"></script>
<script src="js/login-gate.js?v=26110618"></script>
<!-- 11-05 用户:「消消乐的推广 banner 图,不要有外面白色的边框」→ 去掉 .game-promo 外层白底(mama-box.css 里和其他模块共用一条白底规则,这里单独盖掉;夜间同) -->
<style>.game-promo,html.night .game-promo{background:none!important;box-shadow:none!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
/* 11-05 用户:「消消乐 banner 左右要顶到两边,跟上下模块的距离再缩小几个像素,保持协调一致」→ 去掉外层内边距:左右和其他模块一样贴满(0~屏宽),上下只留模块间统一的 16px 缝(原来多出 8px);圆角跟其他模块一致 26px;同日三改:用户嫌圆角太大 → 16px */
@@ -2475,7 +2476,7 @@ html.night .rs-pan .rp-card{background:rgba(255,255,255,.06);border-color:rgba(2
<button class="whv-all" id="whvAll" type="button">全部</button>
</header>
<div class="wsk-title" id="wskTitle"></div>
<p class="wsk-tip" id="wskTip"><i></i>点一下小星星,听听她们说了什么</p>
<p class="wsk-tip" id="wskTip"><i></i>点一下星星,看看她说了啥</p>
<div class="wsk-done" id="wskDone"><p>星星都落下来啦<br><span>她们的话,你都听到了</span></p><button type="button" id="wskBack">让星星回来</button></div>
<div class="wsc" id="wsc" aria-live="polite"></div>
<h2 class="sr-only" id="whpTitle">同孕周悄悄话</h2>
@@ -2678,6 +2679,8 @@ html.night .rs-pan .rp-card{background:rgba(255,255,255,.06);border-color:rgba(2
<script src="js/zodiac-art.js"></script>
<script src="guangchang/topics.js?v=26110487"></script>
<script src="js/ava-pool.js?v=26110487"></script> <!-- 11-05 装饰头像分配表(各模块互不重复) -->
<script src="js/wh-themes.js?v=26110609"></script>
<script src="js/wh-cm.js?v=26110631" defer></script>
<script src="js/app.js"></script>
<script src="js/kit.js?v=26110605"></script>
<!-- 11-05 用户:孕期百问加上跟趣味小测试一样的底部 Tab(孕期百问 / 记一下 / 返回首页)。挂在 #detailQA 里,面板关着时跟着滑出屏外;

137
js/app.js
View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26110605';
var VER = '26110631';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -4453,25 +4453,10 @@
const oid = () => hashParam('openid');
const list = () => (data && data.list) || [];
/* ---------- 每周话题(2026-10-02 用户:「一周一个主题,要么大家也不知道说什么」)----------
一条 = 从 from(北京时间那周的周一)起生效,直到下一条的 from;所以表里永远取「from ≤ 今天」的最后一条,
没配新话题时沿用上一周的,不会空着。换话题 = 往表尾追加一条。
后端将来若在 /whisper 返回 theme(同结构),以后端为准。
presets 只是「不知道说什么?点一句」的示范句,发出去时不在服务端预设库里 → 照常走微信内容安全审核。 */
const WH_THEMES = [
/* 2026-10-02 用户:本周话题由「对宝宝说的话」改成「对老公说的话」;2026-11-01 用户:改成「对未来宝宝说的话」 */
{ from: '2026-09-28', name: '对未来宝宝说的话',
tip: '想对还没见面的宝宝说的,悄悄放进星空',
say: '对未来宝宝说一句悄悄话', ph: '宝宝,妈妈想跟你说……',
presets: ['宝宝,妈妈每天都在等你', '不用多优秀,健康快乐就好', '今天你踢了我好多下,妈妈都记着呢',
'等你来了,带你去看海', '谢谢你选中我做你的妈妈', '我们很快就见面啦'] },
];
/* ---------- 每周话题:表在 js/wh-themes.js(2026-11-07 抽出,孕妈广场也要用)---------- */
function theme() {
if (data && data.theme && data.theme.name) return data.theme;
const d = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10);
let t = null;
WH_THEMES.forEach(x => { if (x.from <= d) t = x; });
return t;
return window.MB_WH_THEME ? window.MB_WH_THEME() : null;
}
/* ---------- 首页入口:一句轮播 ---------- */
@@ -4485,8 +4470,8 @@
}
function paintEntry() {
const l = list().filter(x => !x.mine);
/* 用户:「同孕周悄悄话改成 30 周孕妈悄悄话」—— 周数跟着她自己的孕周走 */
const nm = data && data.week ? data.week + '周孕妈悄悄话' : '孕妈悄悄话';
/* 11-07 用户:名字去掉孕周,只叫「孕妈悄悄话」(之前是「30 周孕妈悄悄话」) */
const nm = '孕妈悄悄话';
$('#weName').textContent = nm;
const th = theme();
$('#weTopic').textContent = th ? th.name : '';
@@ -4535,21 +4520,26 @@
}
/* 亮星落在夜空中上部偏右(流星往左下走,右边起步才划得够长);抖动网格,不会叠在一起 */
function paintSky(l) {
const n = l.length, cols = n > 16 ? 5 : 4, rows = Math.max(2, Math.ceil(n / cols));
/* 11-07 用户:「星星要均匀地挂在星空中,不要密集地放在一个地方」→ 话题牌下面到「说一句」上面整片夜空
(横 8–92%、竖 19–79%)切成差不多正方的格子,n 颗星按等间隔挑格子,每格里小幅抖动 */
const n = l.length, X0 = 8, XW = 84, Y0 = 19, YH = 60;
const pw = page.clientWidth || 390, phh = page.clientHeight || 844;
const cols = Math.max(3, Math.round(Math.sqrt(n * (XW * pw) / (YH * phh)))), rows = Math.max(2, Math.ceil(n / cols));
const cell = l.map((it, i) => Math.floor(i * cols * rows / n));
const fresh = l.findIndex(x => !x.mine && !seen.has(x.id));
const xs = l.map((it, i) => 36 + ((i % cols + .5 + ((hsh(it.id) % 100) / 100 - .5) * .7) / cols) * 58);
const xs = l.map((it, i) => X0 + ((cell[i] % cols + .5 + ((hsh(it.id) % 100) / 100 - .5) * .6) / cols) * XW);
/* 十三改(用户:「每点一下都是一个音符,每一个星星代表不同的音符」):
按横向位置从左到右排成一架竖琴——越靠左越低、越靠右越高,五声音阶(C4 起),连着点就是一段旋律 */
const rank = xs.map((x, i) => i).sort((p, q) => xs[p] - xs[q]);
const note = [];
rank.forEach((i, k) => { note[i] = PENTA[k % PENTA.length]; });
$('#wskBright').innerHTML = l.map((it, i) => {
const h = hsh(it.id), r = Math.floor(i / cols);
const h = hsh(it.id), r = Math.floor(cell[i] / cols);
const x = xs[i];
const y = 21 + ((r + .5 + (((h >> 8) % 100) / 100 - .5) * .6) / rows) * 38;
const y = Y0 + ((r + .5 + (((h >> 8) % 100) / 100 - .5) * .5) / rows) * YH;
const sz = it.mine ? 26 : Math.round(15 + Math.min(it.hugs || 0, 30) / 30 * 11);
return '<button type="button" class="wsk-st' + (it.mine ? ' mine' : '') + (seen.has(it.id) ? ' used' : '') + (i === fresh && !seen.size ? ' hint' : '') +
'" data-go="' + (i + 1) + '" data-note="' + note[i].toFixed(1) + '" style="left:' + x.toFixed(1) + '%;top:' + y.toFixed(1) + '%;--s:' + sz + 'px;--tw:' + (2 + (h % 30) / 10).toFixed(1) + 's" aria-label="' + esc(it.text) + '"><i></i></button>';
'" data-go="' + (i + 1) + '" data-hx="' + x.toFixed(1) + '" data-hy="' + y.toFixed(1) + '" data-note="' + note[i].toFixed(1) + '" style="left:' + x.toFixed(1) + '%;top:' + y.toFixed(1) + '%;--s:' + sz + 'px;--tw:' + (2 + (h % 30) / 10).toFixed(1) + 's" aria-label="' + esc(it.text) + '"><i></i></button>';
}).join('');
}
function paintList(l) {
@@ -4568,6 +4558,9 @@
card.classList.remove('show');
void card.offsetWidth;
card.classList.add('show');
/* 11-07 用户:这句话呈现出来之后,过几秒钟就让它消失 */
clearTimeout(paintCard.t);
paintCard.t = setTimeout(() => card.classList.remove('show'), 6500);
const pal = it.mine ? ['#A8723A', '#E0A86A'] : AURA[(i - 1) % AURA.length];
const au = $$('#whvAura i');
au[0].style.backgroundColor = pal[0]; au[1].style.backgroundColor = pal[1];
@@ -4580,6 +4573,7 @@
m.el.remove();
m.btn.classList.remove('flare', 'gone');
m.btn.classList.add('used');
reborn(m.btn);
mts.delete(m.i);
if (done) paintCard(m.it, m.i);
checkDone();
@@ -4590,8 +4584,21 @@
· 声音用 WebAudio 现合成,不加载音频文件:点星 = 一声随机五声音阶的「叮」(连点几颗像在弹琴),
唰出去 = 一道由低到高的风声;右上角喇叭可关,记在本地。 */
function checkDone() {
const all = $$('.wsk-st', page);
$('#wskDone').classList.toggle('show', all.length > 0 && all.every(b => b.classList.contains('used')) && !mts.size);
/* 11-07 用户:点完不要整片星空都空掉 —— 星星会再冒出来(reborn),「星星都落下来啦」不再出现 */
$('#wskDone').classList.remove('show');
}
/* 11-07 用户:「星星跑掉了,过一会儿还有其他星星出来,相当于其他妈妈说的话能出来,而不是都点完之后整个星空就不能点了」
划走的星 8~16 秒后换个位置重新亮起来(同一句话;整片夜空一直有星可点) */
function reborn(btn) {
setTimeout(() => {
if (!btn.isConnected || !btn.classList.contains('used')) return;
/* 回到自己那一格(小幅挪一点),整片星空保持均匀 */
btn.style.left = (+btn.dataset.hx + (Math.random() - .5) * 4).toFixed(1) + '%';
btn.style.top = (+btn.dataset.hy + (Math.random() - .5) * 3).toFixed(1) + '%';
btn.classList.remove('used', 'cur', 'flare', 'gone');
btn.classList.add('born');
setTimeout(() => btn.classList.remove('born'), 1500);
}, 8000 + Math.random() * 8000);
}
const SND = 'mmbox.whsnd.v1';
let actx = null, sndOn = true;
@@ -4701,7 +4708,7 @@
const btn = $('.wsk-st[data-go="' + i + '"]', page);
if (!btn) { paintCard(it, i); return; }
btn.classList.add('cur');
if (RM) { btn.classList.add('used'); paintCard(it, i); checkDone(); return; }
if (RM) { btn.classList.add('used'); reborn(btn); paintCard(it, i); checkDone(); return; }
const pr = page.getBoundingClientRect(), br = btn.getBoundingClientRect();
const x0 = br.left + br.width / 2 - pr.left, y0 = br.top + br.height / 2 - pr.top;
/* 瞄准左下(屏幕左边外、六成高处):越靠左起步角度越陡,保证在屏幕里拖得够长 */
@@ -4768,6 +4775,29 @@
mts.set(i, m);
m.raf = requestAnimationFrame(frame);
}
/* 11-07 用户:「背后的一些小星星也可以点」—— 点到装饰小星附近,借一颗还没在飞的亮星挪到那儿划出去
(话按顺序轮着来,所以背景星点不完;小星本身闪一下) */
$('#wsk').addEventListener('click', e => {
if (e.target.closest('.wsk-st')) return;
const l = list(); if (!l.length) return;
let best = null, bd = 24 * 24;
$$('#wskStars i').forEach(d => {
const r = d.getBoundingClientRect(), dx = r.left + r.width / 2 - e.clientX, dy = r.top + r.height / 2 - e.clientY, dd = dx * dx + dy * dy;
if (dd < bd) { bd = dd; best = d; }
});
if (!best) return;
let i = 0;
for (let k = 1; k <= l.length; k++) { const j = (deco.n + k) % l.length; if (!mts.has(j + 1)) { i = j + 1; break; } }
if (!i) return;
deco.n = i - 1 + 1;
const btn = $('.wsk-st[data-go="' + i + '"]', page); if (!btn) return;
btn.style.left = best.style.left; btn.style.top = best.style.top;
btn.classList.remove('used', 'born');
best.classList.add('pk'); setTimeout(() => best.classList.remove('pk'), 900);
haptic(); drawer(false);
launch(i);
});
const deco = { n: 0 };
function nextStar() {
const l = list(), n = l.length;
for (let k = 1; k <= n; k++) {
@@ -4783,14 +4813,16 @@
/* 2026-10-02 用户:「每个星星都是孕妈的一句话,类似这个感觉,替换掉今晚多少句悄悄话」→ 标题不再报条数 */
/* 2026-10-02 用户:上半部分文字太占地方、挡住星星 → 标题收成一行,话题做成一行小胶囊;副标题去掉,话题说明挪进写悄悄话弹窗 */
const th = theme();
$('#wskTitle').innerHTML = '<div class="wsk-t">每一颗星星,都是<b>一位孕妈</b>的一句话</div>' +
$('#wskTitle').innerHTML = '<div class="wsk-t">每颗星星,都是<b>一个妈妈</b>说的话</div>' +
(th ? '<div class="wsk-topic"><em>本周话题</em><b>' + esc(th.name) + '</b></div>' : '<p>' + (night ? '今晚' : '今天') + ',她们和你一样在等宝宝来</p>');
$('#whSay').firstElementChild.textContent = th && th.say ? th.say : '说一句悄悄话';
paintDeco();
paintSky(l);
paintList(l);
if (cur && list()[cur - 1]) $$('.wsk-st[data-go="' + cur + '"]', page).forEach(x => x.classList.add('cur'));
$('#whpLeft').textContent = !data ? '' : !oid() || !data.canPost ? '填好预产期,就能说一句' : data.left ? '今天还能说 ' + data.left + ' 句' : '今天的 3 句说完啦,明天再来';
$('#whpLeft').textContent = !data ? '' : !oid() || !data.canPost ? '填好预产期,就能说一句' :
window.MBWhCm ? '先审核再公开,被赞 ' + MBWhCm.starLikes() + ' 次就变成星星' : /* 11-07 悄悄话第 2 步:说一句 = 发进下面的留言区 */
data.left ? '今天还能说 ' + data.left + ' 句' : '今天的 3 句说完啦,明天再来';
}
function drawer(on) {
$('#whDrawer').classList.toggle('show', on);
@@ -4821,6 +4853,7 @@
function closePage() {
stopAll();
drawer(false);
if (fromPlaza) { fromPlaza = false; history.back(); return; } /* 从广场置顶笔记来的:回广场 */
page.classList.remove('show');
page.setAttribute('aria-hidden', 'true');
}
@@ -4837,12 +4870,13 @@
function load(w, silent) {
wk = w;
if (!w) { data = null; show(false); return; }
fetch(API + '?openid=' + encodeURIComponent(oid() || '') + '&week=' + w)
fetch(API + '?openid=' + encodeURIComponent(oid() || '') + '&week=' + w + '&more=1') /* 11-07 用户:星星再多一些 */
.then(r => (r.ok ? r.json() : null))
.then(j => {
const r = j && j.code === 200 && j.result;
if (!r || !r.show || !(r.list || []).length) { data = null; show(false); return; }
if (!r || !r.show || !(r.list || []).length) { data = null; show(false); autoFail(); return; }
data = r;
window.MB_WH_WEEK = r.week; /* 留言区「同孕周」用(js/wh-cm.js) */
const ns = r.list.map(x => x.id + ':' + x.hugs + (x.hugged ? 'h' : '')).join('|');
if (ns !== sig) {
sig = ns;
@@ -4851,8 +4885,33 @@
if (!page.classList.contains('show')) paintPage();
}
show(true);
if (autoOpen) { autoOpen = false; openPage(); }
})
.catch(() => { if (!silent) show(false); });
.catch(() => { if (!silent) show(false); autoFail(); });
}
/* 2026-11-07 孕妈广场信息流的置顶笔记「孕妈悄悄话」点了跳到首页带 ?wh=1 → 直接打开星空页:
· 星空页先空着盖上来(不露首页),孕周取广场也在用的 mmbox.gcwk.v1,马上拉数据,不等「此刻」;
· 参数用完就去掉,刷新不再弹;从广场来的,关掉星空页 = 浏览器返回,回到广场;
· 拉不到 → 提示一句再回广场。 */
let autoOpen = /[?&]wh=1(?:&|$)/.test(location.search), fromPlaza = false;
if (autoOpen) {
fromPlaza = /\/guangchang\//.test(document.referrer || '') && history.length > 1;
if (history.replaceState) {
const q = new URLSearchParams(location.search); q.delete('wh');
history.replaceState(history.state, '', location.pathname + (q.toString() ? '?' + q : '') + location.hash);
}
page.classList.add('show');
page.setAttribute('aria-hidden', 'false');
let gw = 0;
try { const g = JSON.parse(localStorage.getItem('mmbox.gcwk.v1') || 'null'); gw = g && !g.pp && g.w ? +g.w : 0; } catch (e) {}
if (gw < 4 && /[?&]wc=/.test(location.search)) gw = 20; /* 别人转发来请你点赞的:没建档也打开(孕周以服务端档案为准,没有就随便取一周的星空) */
if (gw >= 4) setTimeout(() => load(gw), 0); else setTimeout(autoFail, 0);
}
function autoFail() {
if (!autoOpen) return;
autoOpen = false;
toast('悄悄话暂时打不开,稍后再试');
setTimeout(closePage, 1500);
}
whLoad = w => load(w, w === wk); /* 同一孕周的重拉(「此刻」每 60 秒一次)按静默刷新走:失败不收入口 */
setInterval(() => { if (wk && !document.hidden && !busy) load(wk, true); }, 300000);
@@ -4904,7 +4963,7 @@
$('#whSay').addEventListener('click', () => {
if (!data) return;
if (!oid() || !data.canPost) { toast('填好预产期,就能和同孕周妈妈说悄悄话啦'); return; }
if (!data.left) { toast('今天已经说了 3 句啦,明天再来'); return; }
if (!data.left && !window.MBWhCm) { toast('今天已经说了 3 句啦,明天再来'); return; }
ta.value = '';
ta.maxLength = data.maxLen || 40;
cnt();
@@ -4914,7 +4973,8 @@
$('#whMTopic').style.display = th ? '' : 'none';
ta.placeholder = th && th.ph ? th.ph : '今天想对谁说点什么?';
$('#whPresets').innerHTML = ((th && th.presets && th.presets.length) ? th.presets : (data.presets || [])).map(t => '<button class="wh-pre" type="button">' + esc(t) + '</button>').join('');
$('#whLeft').textContent = '今天还能说 ' + data.left + ' 句';
$('#whLeft').textContent = window.MBWhCm ? '' : '今天还能说 ' + data.left + ' 句';
if (window.MBWhCm && MBWhCm.fillModal) MBWhCm.fillModal(); /* 11-07 弹窗重新设计(规则三步走 + 星空样式),见 js/wh-cm.js */
openModal('modalWhisper');
});
$('#whPresets').addEventListener('click', e => {
@@ -4932,6 +4992,13 @@
const btn = $('#whSend');
btn.disabled = true;
btn.textContent = '正在发出…';
/* 11-07 悄悄话第 2 步(用户定):星空页「说一句」并入留言区 —— 发成置顶笔记下的留言(先审后发),赞够门槛自动挂星 */
if (window.MBWhCm) {
MBWhCm.post(t, data.week)
.then(m => { if (m) { toast(m); return; } closeModal(); toast('已提交,审核通过后才会显示'); })
.then(() => { busy = false; btn.disabled = false; btn.textContent = '悄悄发出'; });
return;
}
fetch(API + '?openid=' + encodeURIComponent(oid()), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: t })
})

View File

@@ -108,11 +108,11 @@
var REC = ['taidong', 'yunqitizhong', 'yun', 'yunqixuetang', 'gongsuopinlv', 'taierguzhong', 'chanjianrili', 'bushui']
.map(function (t) { return '#htGrid .tg[data-tool="' + t + '"]'; }).join(',');
var SEL = {
home: REC + ',#vitals .vt-it,.tab-fab,.fab,.fab-w,#htEdit,.tg-add,#homeExchMod,#meExchCard,#meExchGo,#meExchOrders,#bbBanner,[data-modal="modalProfile"],[data-modal="modalWhisper"],#bfMe,.preg-card,#whSay,[data-hug]',
home: REC + ',#vitals .vt-it,.tab-fab,.fab,.fab-w,#htEdit,.tg-add,#homeExchMod,#meExchCard,#meExchGo,#meExchOrders,#bbBanner,[data-modal="modalProfile"],[data-modal="modalWhisper"],#bfMe,.preg-card,#whSay,[data-hug],[data-wcl],[data-wrp],#wcsSend',
plaza: '#blk,#bfv,#csend,#cpic,#cemo,#cpOk,[data-like],[data-cl],[data-rk],#cmgo,[data-write],[data-mywrite],#tabRec',
topic: '.tc-lk,.tc-go,.tc-in .pic-b,.tc-in .emo-b'
};
var IN = { plaza: '#cin', topic: '.tc-in textarea' };
var IN = { home: '#wcsIn', plaza: '#cin', topic: '.tc-in textarea' };
function page() {
if (document.getElementById('homeBody')) return 'home';
if (document.getElementById('wf')) return 'plaza';

160
js/wh-cm.css Normal file
View File

@@ -0,0 +1,160 @@
/* 星空页 · 妈妈们的留言(2026-11-07,js/wh-cm.js)
二改:从「整页上滑盖住星空」改成半屏面板 —— 最高 68%,上面始终露出星空;深靛紫玻璃,跟星空一个色系 */
/* 三改(11-07):去掉「看看大家说了啥」入口 → 抽屉平时在底部露一截(--wpk 由 wh-cm.js 写),「说一句」和流星内容卡往上让 */
.wh-page .whp-left{display:none}
.wh-page .whp-bar{bottom:var(--wpk,58px);padding-bottom:12px}
.wh-page .wsc{bottom:calc(var(--wpk,58px) + 104px)}
/* 原来的「全部」抽屉不用了 */
.wh-page .whd{display:none}
/* 半屏面板 */
.wcs{position:absolute;inset:0;z-index:8;background:rgba(10,6,24,0);pointer-events:none;transition:background .3s}
.wcs.show{pointer-events:auto;background:rgba(10,6,24,.32)}
.wcs-p{position:absolute;left:0;right:0;bottom:0;height:58px;max-height:76%;pointer-events:auto;overflow:hidden;display:flex;flex-direction:column;border-radius:22px 22px 0 0;color:#F4ECFF;
background:linear-gradient(#2A1F48,#1E1634);box-shadow:0 -10px 40px rgba(0,0,0,.4);transform:translateY(110%);transition:transform .35s cubic-bezier(.2,.8,.2,1),height .3s cubic-bezier(.2,.8,.2,1)}
.wh-page.show .wcs-p{transform:none}
.wcs-hd{flex:none;padding:8px 0 6px;touch-action:none}
.wcs-grab{display:block;width:38px;height:4px;margin:0 auto 8px;border-radius:2px;background:rgba(255,255,255,.28)}
.wcs-tt{display:flex;align-items:baseline;gap:8px;padding:0 16px 0 18px}
.wcs-tt b{font-size:16px;font-weight:800;letter-spacing:.5px}
.wcs-tt span{font-size:12px;color:rgba(232,222,255,.5)}
.wcs-up{margin-left:auto;font-style:normal;font-size:12px;color:#FFD9B8;animation:wcmUp 1.6s ease-in-out infinite}
@keyframes wcmUp{50%{transform:translateY(-3px)}}
.wcs.show .wcs-up{display:none}
.wcs:not(.show) .wcs-rule,.wcs:not(.show) .wcm-tb{opacity:0}
.wcs-x{display:none!important}
.wcs.show .wcs-x{display:grid!important;margin-left:auto;align-self:center;display:grid;place-items:center;width:28px;height:28px;border:0;border-radius:50%;background:rgba(255,255,255,.1);color:#fff}
.wcm-tb{display:flex;align-items:center;gap:6px;margin:10px 18px 2px}
.wcm-tb button{height:26px;padding:0 12px;border-radius:99px;border:0;background:rgba(255,255,255,.07);color:rgba(232,222,255,.62);font:inherit;font-size:12.5px}
.wcm-tb button.on{background:linear-gradient(135deg,rgba(255,214,170,.92),rgba(255,170,160,.92));color:#3A2312;font-weight:800}
/* 重点:被赞 N 次就变成星星 */
.wcs-rule{display:flex;align-items:center;gap:8px;margin:10px 14px 0;padding:9px 12px;border-radius:14px;font-size:13px;color:#FFF3E2;
background:linear-gradient(110deg,rgba(255,200,150,.22),rgba(150,110,255,.22));box-shadow:inset 0 0 0 1px rgba(255,214,170,.3),0 0 18px rgba(255,200,150,.12)}
.wcs-rule svg{flex:none;width:18px;height:18px;fill:#FFD9A0;filter:drop-shadow(0 0 4px rgba(255,214,160,.9));animation:wstTw 2.2s ease-in-out infinite}
.wcs-rule b{font-size:16px;color:#FFD08A;margin:0 1px}
@keyframes wstTw{50%{transform:scale(.8) rotate(20deg);opacity:.7}}
.wcs-ls{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:0 12px 14px}
.wcm-e{margin:24px 0;text-align:center;font-size:12.5px;color:rgba(232,222,255,.45)}
.wcm-it{display:flex;gap:10px;margin-top:8px;padding:12px 10px 10px 12px;border-radius:16px;
background:linear-gradient(140deg,rgba(255,255,255,.075),rgba(255,255,255,.03));box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}
.wcm-av{flex:none;width:32px;height:32px;border-radius:50%;object-fit:cover;background:#3A2F5E;box-shadow:0 0 0 1.5px rgba(255,220,180,.35)}
.wcm-bd{flex:1;min-width:0}
.wcm-nm{display:flex;align-items:baseline;gap:6px;margin:0;font-size:12px;color:rgba(232,222,255,.45)}
.wcm-nm b{font-size:12.5px;font-weight:700;color:#FFD9B8;max-width:9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wcm-tx{margin:4px 0 0;font-size:14px;line-height:1.6;color:#F2ECFA;word-break:break-word}
.wcm-st{margin:4px 0 0;font-size:11.5px;color:#FFC07A}
.wcm-rp{margin-top:8px;padding:7px 10px;border-radius:10px;background:rgba(255,255,255,.05)}
.wcm-rp p{margin:0;font-size:12.5px;line-height:1.55;color:rgba(242,236,250,.78)}
.wcm-rp p+p{margin-top:4px}
.wcm-rp b{font-weight:700;color:#C9B8FF}
.wcm-rp p.me b:first-child{color:#FFD9B8}
.wcm-re{margin-top:6px;padding:2px 0;border:0;background:none;font:inherit;font-size:12px;color:rgba(232,222,255,.5)}
.wcm-star{display:flex;width:fit-content;align-items:center;gap:4px;margin:6px 0 0;padding:2px 8px 2px 6px;border-radius:99px;font-size:11px;font-weight:700;color:#FFE2B8;
background:linear-gradient(135deg,rgba(122,79,214,.55),rgba(90,59,170,.35));box-shadow:0 0 10px rgba(255,214,170,.25)}
.wcm-star svg{width:10px;height:10px;fill:currentColor}
.wcm-lk{flex:none;align-self:flex-start;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:32px;padding:2px 0;border:0;background:none;font:inherit;color:rgba(232,222,255,.5)}
.wcm-lk svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;transition:transform .2s}
.wcm-lk span{font-size:11px}
.wcm-lk.on{color:#FF8FA6}.wcm-lk.on svg{fill:currentColor;transform:scale(1.12)}
.wcm-lk.me{opacity:.7}
.wcm-lk.pop svg{animation:wcmPop .45s ease}
@keyframes wcmPop{40%{transform:scale(1.45)}100%{transform:scale(1.12)}}
.wcm-more{display:block;margin:14px auto 0;height:34px;padding:0 22px;border-radius:99px;border:1px solid rgba(255,220,180,.35);background:rgba(255,255,255,.05);color:#FFD9B8;font:inherit;font-size:13px}
.wcm-more[hidden]{display:none}
/* 底部输入框:发新留言 / 回复某一楼 */
.wcs-in{flex:none;display:flex;align-items:center;gap:8px;padding:8px 12px calc(8px + env(safe-area-inset-bottom));border-top:1px solid rgba(255,255,255,.08);background:#1E1634}
.wcs-to{flex:none;max-width:7em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;color:#FFD9B8}
.wcs-to[hidden]{display:none}
.wcs-in input{flex:1;min-width:0;height:36px;padding:0 14px;border:0;border-radius:99px;background:rgba(255,255,255,.09);color:#fff;font:inherit;font-size:14px;outline:none}
.wcs-in input::placeholder{color:rgba(232,222,255,.4)}
.wcs-in button{flex:none;height:34px;padding:0 14px;border:0;border-radius:99px;background:linear-gradient(135deg,#FFD6AA,#FFAAA0);color:#3A2312;font:inherit;font-size:13.5px;font-weight:800}
.wcs-in button:disabled{opacity:.5}
/* 星星点完会再冒出来(app.js reborn) */
.wsk-st.born i{animation:wskBorn 1.4s ease-out both}
@keyframes wskBorn{0%{opacity:0;transform:scale(.2)}60%{opacity:1;transform:scale(1.35)}100%{opacity:1;transform:none}}
/* 背景小星被点到:闪一下(app.js 把一颗亮星挪过去) */
.wsk-deco i.pk{animation:wskPk .6s ease-out}
@keyframes wskPk{50%{transform:scale(2.4);opacity:1}}
/* 自己的留言:还差几个赞 + 找人帮我点赞 */
.wcm-mf{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;margin:8px 0 0;font-size:11.5px;color:rgba(232,222,255,.62)}
p.wcm-mf{display:block;color:#FFC07A}
.wcm-pg{position:relative;width:54px;height:5px;border-radius:3px;background:rgba(255,255,255,.12);overflow:hidden}
.wcm-pg i{position:absolute;left:0;top:0;bottom:0;border-radius:3px;background:linear-gradient(90deg,#FFD6AA,#FF9FB0)}
.wcm-share{flex-basis:100%;text-align:center;height:32px;margin-top:2px;border:0;border-radius:99px;font:inherit;font-size:13px;font-weight:800;color:#3A2312;
background:linear-gradient(135deg,#FFE0B5,#FFAAA0);box-shadow:0 4px 16px -4px rgba(255,170,150,.6)}
.wcm-share:active{transform:scale(.97)}
.wcm-it.hl{animation:wcmHl 2.4s ease}
@keyframes wcmHl{0%,60%{box-shadow:inset 0 0 0 1.5px #FFD9A0,0 0 22px rgba(255,214,160,.45)}}
/* 转发引导 */
.wsh-g{position:absolute;inset:0;z-index:20;background:rgba(8,5,20,.72);animation:wshIn .25s ease}
@keyframes wshIn{from{opacity:0}}
.wsh-arrow{position:absolute;right:26px;top:calc(10px + env(safe-area-inset-top));width:60px;height:70px;
background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 70' fill='none' stroke='%23FFD9A0' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M8 66C12 36 28 18 48 8' stroke-dasharray='6 6'/%3E%3Cpath d='M36 6l13 2-4 12'/%3E%3C/svg%3E") center/contain no-repeat;animation:wshA 1.2s ease-in-out infinite}
@keyframes wshA{50%{transform:translate(4px,-4px)}}
.wsh-c{position:absolute;left:28px;right:28px;top:120px;padding:22px 20px 18px;border-radius:22px;text-align:center;color:#F4ECFF;
background:linear-gradient(#2E2350,#1F1738);box-shadow:inset 0 0 0 1px rgba(255,214,170,.25),0 20px 50px rgba(0,0,0,.5)}
.wsh-c b{font-size:18px;color:#FFD9A0}
.wsh-c p{margin:8px 0 0;font-size:13.5px;line-height:1.6;color:rgba(232,222,255,.8)}
.wsh-c p.q{margin-top:12px;padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.07);color:#fff}
.wsh-c p.s{font-size:12.5px;color:#FFC9A0}
.wsh-c button{margin-top:14px;height:38px;padding:0 34px;border:0;border-radius:99px;font:inherit;font-size:14px;font-weight:800;color:#3A2312;background:linear-gradient(135deg,#FFE0B5,#FFAAA0)}
/* 变成星星的特效:一颗星从底下升上去,炸开碎星,亮出那句话 */
.wst-fx{position:absolute;inset:0;z-index:21;background:radial-gradient(60% 40% at 50% 34%,rgba(120,80,200,.35),rgba(8,5,20,.78));animation:wshIn .4s ease;overflow:hidden}
.wst-fx.out{opacity:0;transition:opacity .5s}
.wst-star,.wst-ring,.wst-sp{position:absolute;left:50%;top:30%}
.wst-star{width:64px;height:64px;margin:-32px 0 0 -32px;
background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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' fill='%23FFF6E4'/%3E%3C/svg%3E") center/contain no-repeat;
filter:drop-shadow(0 0 10px #FFD9A0) drop-shadow(0 0 26px rgba(255,200,140,.8));animation:wstUp 1.3s cubic-bezier(.3,.7,.3,1) both,wstGlow 1.8s 1.3s ease-in-out infinite}
@keyframes wstUp{0%{transform:translateY(60vh) scale(.3);opacity:0}20%{opacity:1}80%{transform:translateY(-6px) scale(1.15)}100%{transform:none}}
@keyframes wstGlow{50%{transform:scale(1.18) rotate(12deg)}}
.wst-ring{width:20px;height:20px;margin:-10px 0 0 -10px;border-radius:50%;box-shadow:0 0 0 2px rgba(255,220,170,.9);opacity:0;animation:wstRing 1s 1.2s ease-out both}
@keyframes wstRing{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(12)}}
.wst-sp{width:6px;height:6px;margin:-3px 0 0 -3px;border-radius:50%;background:#FFE7C2;box-shadow:0 0 8px #FFD9A0;opacity:0;animation:wstSp 1s ease-out both}
@keyframes wstSp{0%{opacity:1;transform:rotate(var(--a)) translateX(0)}100%{opacity:0;transform:rotate(var(--a)) translateX(var(--d)) scale(.4)}}
.wst-tx{position:absolute;left:30px;right:30px;top:calc(30% + 60px);text-align:center;animation:wstTx .6s 1.4s ease-out both}
@keyframes wstTx{from{opacity:0;transform:translateY(14px)}}
.wst-tx b{display:block;font-size:22px;font-weight:900;letter-spacing:1px;color:#FFE2B0;text-shadow:0 0 18px rgba(255,200,140,.7)}
.wst-tx p{margin:12px 0 0;font-size:15px;line-height:1.65;color:#fff}
.wst-tx span{display:block;margin-top:10px;font-size:12.5px;color:rgba(232,222,255,.65)}
@media (prefers-reduced-motion:reduce){.wst-star,.wst-sp,.wst-ring,.wst-tx,.wcs-up,.wsh-arrow{animation:none!important}.wst-sp{display:none}}
/* 「说一句」弹窗 · 星空版(wh-cm.js fillModal 给 #modalWhisper 加 .wh-star) */
#modalWhisper.wh-star{background:linear-gradient(170deg,#33265A 0%,#221A40 55%,#1B1533 100%);color:#F4ECFF;box-shadow:0 -14px 50px rgba(20,10,50,.6),inset 0 1px 0 rgba(255,255,255,.12);overflow-x:hidden}
.wms-sky{display:none}
#modalWhisper.wh-star .wms-sky{display:block;position:absolute;left:0;right:0;top:0;height:150px;pointer-events:none;opacity:.9;
background:
radial-gradient(1.5px 1.5px at 12% 22%,#fff,transparent),radial-gradient(1px 1px at 28% 60%,#fff,transparent),radial-gradient(1.5px 1.5px at 46% 14%,#FFE7C2,transparent),
radial-gradient(1px 1px at 63% 40%,#fff,transparent),radial-gradient(2px 2px at 78% 18%,#FFE7C2,transparent),radial-gradient(1px 1px at 90% 52%,#fff,transparent),
radial-gradient(1px 1px at 36% 84%,#fff,transparent),radial-gradient(1.5px 1.5px at 70% 76%,#fff,transparent),
radial-gradient(80% 100% at 85% 0%,rgba(255,180,150,.18),transparent 70%)}
#modalWhisper.wh-star .modal-head,#modalWhisper.wh-star>*{position:relative}
#modalWhisper.wh-star .modal-head h3{color:#FFF1DE;font-size:21px;letter-spacing:.5px;text-shadow:0 0 16px rgba(255,200,150,.35)}
#modalWhisper.wh-star .modal-x{background:rgba(255,255,255,.1);color:#E8DEFF}
#modalWhisper.wh-star .wh-m-topic{margin-top:6px;color:#FFC9A0}
#modalWhisper.wh-star .wh-m-topic span{color:rgba(232,222,255,.55)}
#modalWhisper.wh-star .wh-m-lead{display:flex;gap:6px;margin-top:12px}
.wms{flex:1;display:flex;align-items:center;gap:14px;padding:14px 18px;border-radius:18px;text-align:left}
.wms.hi{font-size:16px;font-weight:800;line-height:1.45;color:#FFF3E2;
background:linear-gradient(120deg,rgba(255,200,150,.32),rgba(150,110,255,.3));box-shadow:inset 0 0 0 1.5px rgba(255,214,170,.55),0 0 26px rgba(255,200,150,.25)}
.wms.hi svg{flex:none;width:38px;height:38px;fill:#FFE2B0;filter:drop-shadow(0 0 6px #FFD9A0) drop-shadow(0 0 16px rgba(255,200,140,.7));animation:wstTw 2.2s ease-in-out infinite}
.wms.hi b{font-size:24px;color:#FFD08A;margin:0 2px;text-shadow:0 0 12px rgba(255,200,140,.6)}
#modalWhisper.wh-star .wh-ta{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.14);color:#fff}
#modalWhisper.wh-star .wh-ta::placeholder{color:rgba(232,222,255,.4)}
#modalWhisper.wh-star .wh-ta:focus{border-color:rgba(255,214,170,.6);box-shadow:0 0 0 3px rgba(255,214,170,.12)}
#modalWhisper.wh-star .wh-cnt{color:rgba(232,222,255,.4)}
#modalWhisper.wh-star .wh-m-sub{color:rgba(232,222,255,.6);font-weight:600}
#modalWhisper.wh-star .wh-pre{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.1);color:rgba(242,236,250,.85)}
#modalWhisper.wh-star .wh-pre.on{background:rgba(255,214,170,.18);border-color:rgba(255,214,170,.6);color:#FFE2B8}
#modalWhisper.wh-star .dl-btn.main{background:linear-gradient(135deg,#FFE0B5,#FFAAA0);color:#3A2312;box-shadow:0 10px 26px -8px rgba(255,170,150,.7)}
#modalWhisper.wh-star .wh-left{color:rgba(232,222,255,.5)}
/* 11-07 用户:「本周话题 · 对未来宝宝说的话」字大一点,和上面 23px 的标题协调(原 10.5 / 13.5px) */
.wh-page .wsk-topic{gap:10px;margin-top:12px;padding:6px 18px 6px 6px}
.wh-page .wsk-topic em{padding:3px 11px;font-size:13px}
.wh-page .wsk-topic b{font-size:18px;letter-spacing:1px}

400
js/wh-cm.js Normal file
View File

@@ -0,0 +1,400 @@
/* 星空页 · 妈妈们的留言(2026-11-07)
· 二改(用户:「往下滑评论就挡住整个背景,不好;改成点『全部』那种半弹窗,最多拉到 2/3,一定留出一块星空;
首页要有个明显入口,别只放右上角『全部』」):
「说一句」按钮下面一条入口 → 半屏面板(先开到半屏,拖把手 / 往上划能拉到 2/3,再往上就不动了;往下拖关掉)。
面板里自己滚动看全部留言。右上角「全部」也打开这个面板(原来那个只列悄悄话的抽屉不用了)。
· 数据:广场置顶笔记 wh-<话题 from> 的评论 = cm.json 模拟(AGENTS.md 12c,不得挂星)+ /plaza/comments 已审核通过的真实评论
+ 我自己发的(11-07 起只显示已通过的)。打开星空页才拉 cm.json(版本号跟广场 index.html 引的一致,共用缓存)。
· 真实留言的赞走服务端(POST /plaza/comment/like,每人每条一次、不能赞自己),赞够 starLikes(后台可改,先 3)自动挂星;
模拟留言的赞只记本机 mmbox.whcm.v1。最热 / 最新 / 同孕周 三种看法。
· 三改(用户:「我不能给别人的留言回复」):点留言上的「回复」→ 底部输入框回复那一楼(先审后发);
底部输入框不点「回复」时就是发一条新留言。楼的 key 同广场:模拟 = wh-xxx#序号,真实 = r:<id>。
· 星空页「说一句」发的话也进这里(window.MBWhCm.post,app.js 调)。
· 四改(用户 11-07):去掉「看看大家说了啥」那条入口 → 抽屉平时在屏幕底部露一截(把手 + 标题),一滑就拉开,最高再往上一点(76%);
「被赞 3 次就变成星星」做重点;自己的留言能转发出去请人帮点赞(宿主转发卡,链接 ?wh=1&wc=<评论id>,对方打开直接定位到这条);
自己的话变成星星时放一段特效(每条只放一次,记在 mmbox.whstar.v1);说一句的弹窗换成星空样式(fillModal)。 */
(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 HEART = '<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>';
var WC = (location.search.match(/[?&]wc=([A-Za-z0-9]{1,40})/) || [])[1] || ''; /* 别人转发来请我点赞的那条 */
/* 半屏面板 */
var sh = document.createElement('div');
sh.className = 'wcs'; sh.id = 'wcs'; sh.setAttribute('aria-hidden', 'true');
sh.innerHTML = '<div class="wcs-p" role="dialog" aria-label="妈妈们的留言">' +
'<div class="wcs-hd"><i class="wcs-grab" aria-hidden="true"></i><div class="wcs-tt"><b>妈妈们的留言</b><span id="wcmN"></span><i class="wcs-up">上滑看看</i>' +
'<button type="button" class="wcs-x" aria-label="收起"><svg viewBox="0 0 24 24" width="14" height="14"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg></button></div>' +
'<div class="wcs-rule"><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><span>留言被赞 <b id="wcmRuleN">6</b> 次,就会变成一颗星星挂上去</span></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></div></div>' +
'<div class="wcs-ls" id="wcsLs"><div id="wcmLs"><p class="wcm-e">留言加载中…</p></div><button type="button" class="wcm-more" id="wcmMore" hidden>再看看</button></div>' +
'<div class="wcs-in"><span class="wcs-to" id="wcsTo" hidden></span><input id="wcsIn" type="text" maxlength="200" enterkeyhint="send" placeholder="说点什么…"><button type="button" id="wcsSend">发送</button></div>' +
'</div>';
page.appendChild(sh);
var P = sh.querySelector('.wcs-p'), lsBox = sh.querySelector('#wcsLs'), ls = sh.querySelector('#wcmLs'), more = sh.querySelector('#wcmMore');
var inp = sh.querySelector('#wcsIn'), toEl = sh.querySelector('#wcsTo'), sendB = sh.querySelector('#wcsSend');
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 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); }
/* ---------- 数据 ---------- */
var SIM = null, REAL = null, RREP = [], ALL = [], shown = 0, loading = false, SORT = 'hot', STAR_N = 6; // 11-07 用户改门槛为 6(以服务端 starLikes 为准)
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: [] };
}
function repOf(c, me) { return { p: c.parent, n: me ? '我' : (c.nickname || '孕妈'), me: me, to: c.parentName, t: c.text, ts: c.at, st: me ? c.status : '' }; }
function load() {
if (loading || SIM) return; loading = true;
fetch('guangchang/cm.json?v=26110631').then(function (r) { return r.json(); }).then(function (j) {
var PP = j.p;
SIM = (j.c[PID] || []).map(function (c, k) {
var p = PP[c[0]];
return { key: PID + '#' + k, n: p[0], av: avP(p[1]), s: p[2], t: c[1], likes: c[2], st0: !!c[4],
r: c[3].map(function (x) { var q = x[0] < 0 ? null : PP[x[0]]; return { n: q ? q[0] : '孕妈悄悄话', t: x[1] }; }) };
});
build(); paintGo(); focusWc();
}).catch(function () { SIM = []; build(); focusWc(); });
fetchReal();
}
function fetchReal() {
return 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;
/* 11-07 用户:留言、回复一定要审核通过才显示,自己也不显示「审核中」 */
var mine = (R.mine || []).filter(function (c) { return c.status === 'approved'; });
REAL = (R.list || []).filter(top).map(realOf).concat(mine.filter(top).map(function (c) { var o = realOf(c); o.n = '我'; o.me = 1; o.st = c.status; return o; }));
RREP = (R.list || []).filter(function (c) { return !top(c); }).map(function (c) { return repOf(c, 0); })
.concat(mine.filter(function (c) { return !top(c); }).map(function (c) { return repOf(c, 1); }));
build(); paintGo();
celebrate(); focusWc();
}).catch(function () { if (!REAL) { REAL = []; build(); } });
}
/* ---------- 我的话变成星星:特效(每条只放一次) ---------- */
var SK = 'mmbox.whstar.v1';
function celebrate() {
var done = {}; try { done = JSON.parse(localStorage.getItem(SK) || '{}') || {}; } catch (e) {}
var c = (REAL || []).filter(function (x) { return x.me && x.star && !done[x.id]; })[0];
if (!c || celebrate.on) return;
/* 等星空里的星星画出来再放(拉星星要几秒),放了才记下,没放成下次进来还会放 */
celebrate.on = 1; var n = 0;
(function wait() {
if (++n > 40) { celebrate.on = 0; return; }
if (!page.classList.contains('show') || !page.querySelector('.wsk-st')) { setTimeout(wait, 500); return; }
setTimeout(function () {
celebrate.on = 0;
if (!page.classList.contains('show')) return;
done[c.id] = 1; try { localStorage.setItem(SK, JSON.stringify(done)); } catch (e) {}
starFx(c.t, true);
}, 1200);
})();
}
function starFx(text, mine) {
snap('closed');
var fx = document.createElement('div');
fx.className = 'wst-fx';
fx.innerHTML = '<i class="wst-ring"></i><i class="wst-star"></i>' + Array.apply(null, Array(14)).map(function (_, k) {
return '<i class="wst-sp" style="--a:' + (k * 360 / 14) + 'deg;--d:' + (60 + Math.random() * 70).toFixed(0) + 'px;animation-delay:' + (1.25 + Math.random() * .2).toFixed(2) + 's"></i>'; }).join('') +
'<div class="wst-tx"><b>' + (mine ? '你的话变成星星啦' : '这句变成星星啦') + '</b><p>“' + esc(text.length > 40 ? text.slice(0, 39) + '…' : text) + '”</p><span>' +
(mine ? '同孕周的妈妈点开星空,就能看到你对宝宝说的话' : '谢谢你帮她点亮') + '</span></div>';
page.appendChild(fx);
if (navigator.vibrate) navigator.vibrate([10, 60, 20]);
fx.addEventListener('click', function () { fx.classList.add('out'); setTimeout(function () { fx.remove(); }, 500); });
setTimeout(function () { if (fx.isConnected) { fx.classList.add('out'); setTimeout(function () { fx.remove(); }, 500); } }, 5200);
}
/* ---------- 转发求赞 ---------- */
function shareMine(id) {
var c = (REAL || []).filter(function (x) { return x.id === id; })[0]; if (!c) return;
var href = location.origin + location.pathname + '?wh=1&wc=' + encodeURIComponent(id);
var title = '帮我点个赞吧!被赞 ' + STAR_N + ' 次,我对宝宝说的话就能变成一颗星星';
var img = new URL('assets/share/g/home.jpg', document.baseURI).href;
if (window.wxAuth && wxAuth.reportLocation) wxAuth.reportLocation({ href: href, title: title, imageUrl: img });
var g = document.createElement('div');
g.className = 'wsh-g';
g.innerHTML = '<i class="wsh-arrow"></i><div class="wsh-c"><b>点右上角「···」</b><p>选「发送给朋友」或发到群里,<br>请她们帮你点个赞</p><p class="q">“' + esc(c.t.length > 30 ? c.t.slice(0, 29) + '…' : c.t) + '”</p>' +
'<p class="s">还差 ' + Math.max(0, STAR_N - c.likes) + ' 个赞,就变成星星了</p><button type="button">知道了</button></div>';
page.appendChild(g);
g.addEventListener('click', function (e) { if (e.target === g || e.target.closest('button')) g.remove(); });
}
/* 打开的是别人转发来的链接:拉开抽屉、把那条放最前并闪一下 */
function focusWc() {
if (!WC || !REAL || !SIM) return;
var w = WC; WC = '';
try { var q = new URLSearchParams(location.search); q.delete('wc'); history.replaceState(history.state, '', location.pathname + (q.toString() ? '?' + q : '') + location.hash); } catch (e) {}
var c = REAL.filter(function (x) { return x.id === w; })[0];
if (!c) { toast('这条留言还在审核,或者已经找不到了'); return; }
REAL = [c].concat(REAL.filter(function (x) { return x !== c; }));
c.pin = 1; build();
setTimeout(function () {
snap('full');
var el = ls.querySelector('.wcm-it[data-k="' + CSS.escape(c.key) + '"]');
if (el) { el.classList.add('hl'); lsBox.scrollTop = 0; }
toast(c.star ? '这句已经变成星星啦' : (c.me ? '这是你的留言' : '帮她点个赞,让这句话变成星星'));
}, 600);
}
var wkOf = function (c) { var x = /孕\s*(\d+)\s*周/.exec(c.s || ''); return x ? +x[1] : 0; };
/* 11-07 方案 A:模拟留言 = cm.json 标了已变星的 + 赞数(含我本机这一赞)够 STAR_N 的 */
function simStar(c) { c.star = c.st0 || likesOf(c) >= STAR_N; }
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 pin = REAL.filter(function (c) { return c.pin; });
var mine = REAL.filter(function (c) { return c.me && !c.pin; }), rest = REAL.filter(function (c) { return !c.me && !c.pin; }).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 pin.concat(mine.sort(function (a, b) { return b.ts - a.ts; }), rest);
}
function total() { return (REAL ? REAL.length : 0) + (SIM ? SIM.length : 0); }
function paintGo() {
document.getElementById('wcmN').textContent = total() ? total() + ' 条' : '';
document.getElementById('wcmRuleN').textContent = STAR_N;
}
function build() {
if (!SIM || !REAL) return;
SIM.forEach(simStar);
ALL = sorted(); shown = 0; ls.innerHTML = '';
paintGo();
if (!ALL.length) {
ls.innerHTML = '<p class="wcm-e">' + (SORT === 'wk' ? (window.MB_WH_WEEK ? '同孕周的妈妈还没留言,你先说一句吧' : '填好预产期,就能看同孕周妈妈说了啥') : '还没有人留言') + '</p>';
more.hidden = true; return;
}
add(20);
}
function repsOf(c) {
var x = c.r.slice();
RREP.forEach(function (r) { if (r.p === c.key) x.push(r); });
return x;
}
function item(c) {
var on = likedOf(c), rp = repsOf(c);
return '<div class="wcm-it" data-k="' + esc(c.key) + '"><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.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>' : '') +
(rp.length ? '<div class="wcm-rp">' + rp.map(function (x) {
return '<p' + (x.me ? ' class="me"' : '') + '><b>' + esc(x.n) + '</b>' + (x.to && x.to !== c.n ? ' 回复 <b>' + esc(x.to) + '</b>' : '') + ':' + esc(x.t) + '</p>';
}).join('') + '</div>' : '') +
(c.me ? mineFoot(c) : '<button type="button" class="wcm-re" data-wrp="' + esc(c.key) + '">回复</button>') + '</div>' +
(c.me ? '<span class="wcm-lk me">' + HEART + '<span>' + fmt(likesOf(c)) + '</span></span></div>' :
'<button type="button" class="wcm-lk' + (on ? ' on' : '') + '" data-wcl="' + esc(c.key) + '" aria-label="赞">' + HEART + '<span>' + fmt(likesOf(c)) + '</span></button></div>');
}
/* 自己的留言底下:还差几个赞 + 找人帮我点赞(审核通过才能转) */
function mineFoot(c) {
if (c.star) return '';
if (c.st !== 'approved') return '<p class="wcm-mf">审核通过后,就能转给朋友帮你点赞</p>';
var n = Math.min(c.likes, STAR_N);
return '<div class="wcm-mf"><span class="wcm-pg"><i style="width:' + Math.round(n / STAR_N * 100) + '%"></i></span><span>' + n + '/' + STAR_N + ' 个赞,还差 ' + (STAR_N - n) + ' 个就变成星星</span>' +
'<button type="button" class="wcm-share" data-wsh="' + esc(c.id) + '">找人帮我点赞</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;
}
function rerender(c) { var el = ls.querySelector('.wcm-it[data-k="' + CSS.escape(c.key) + '"]'); if (el) el.outerHTML = item(c); }
more.addEventListener('click', function () { add(30); });
/* ---------- 点赞 / 回复 ---------- */
function paintLk(b, c) { var on = likedOf(c); b.classList.toggle('on', on); b.querySelector('span').textContent = fmt(likesOf(c)); }
var REPLY = null;
function setReply(c) {
REPLY = c;
toEl.hidden = !c; toEl.textContent = c ? '回复 ' + c.n : '';
inp.placeholder = c ? '回复 ' + c.n + '…' : '说点什么…';
}
ls.addEventListener('click', function (e) {
var sb = e.target.closest('[data-wsh]');
if (sb) { shareMine(sb.dataset.wsh); return; }
var r = e.target.closest('[data-wrp]');
if (r) {
var rc = ALL.filter(function (x) { return x.key === r.dataset.wrp; })[0]; if (!rc) return;
setReply(rc); snap('full'); inp.focus(); return;
}
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) {
/* 跟真实留言一样:赞了就不能取消;这一赞让它够了 STAR_N → 变星特效 */
if (LIKE[k]) { toast('你已经赞过啦'); return; }
LIKE[k] = 1;
try { localStorage.setItem(LK, JSON.stringify(LIKE)); } catch (e2) {}
var was0 = c.star; simStar(c);
paintLk(b, c); b.classList.add('pop'); setTimeout(function () { b.classList.remove('pop'); }, 500);
if (navigator.vibrate) navigator.vibrate(8);
if (c.star && !was0) { rerender(c); starFx(c.t, false); }
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 (x) { return x.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) { rerender(c); starFx(c.t, false); }
}).catch(function (j) { c.liked = false; c.likes--; paintLk(b, c); toast((j && j.message) || '网络不太好,再点一次试试'); });
});
sh.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(); lsBox.scrollTop = 0;
});
function post(text, week, reply) {
if (!OID) return Promise.resolve('登录后就能留言啦');
var body = { kind: 'post', target: PID, title: '孕妈悄悄话 · ' + TH.name, text: text, stage: week ? '孕 ' + week + ' 周' : '' };
if (reply) { body.parent = reply.key; body.parentName = reply.n; }
return fetch('/api/mp/public/mama-box/plaza/comment?openid=' + encodeURIComponent(OID), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
}).then(function (r) { return r.json(); }).then(function (j) {
if (!j || j.code !== 200 || !j.result) return (j && j.message) || '没发出去,再试一次';
if (j.result.status !== 'approved') return null; /* 审核中的不放进列表,通过后才显示 */
if (reply) { RREP.push(repOf(j.result, 1)); rerender(reply); }
else {
var o = realOf(j.result); o.n = '我'; o.me = 1; o.st = j.result.status;
REAL = [o].concat(REAL || []);
if (SIM) { build(); lsBox.scrollTop = 0; }
paintGo();
}
return null;
}).catch(function () { return '网络不太好,再试一次'; });
}
var busy = false;
function send() {
var t = inp.value.trim();
if (t.length < 2) { toast('多写一两个字吧'); return; }
if (busy) return; busy = true; sendB.disabled = true;
var REPLY0 = REPLY;
post(t, +window.MB_WH_WEEK || 0, REPLY).then(function (msg) {
busy = false; sendB.disabled = false;
if (msg) { toast(msg); return; }
inp.value = ''; inp.blur(); setReply(null);
toast(REPLY0 ? '回复已提交,审核通过后才会显示' : '已提交,审核通过后才会显示');
});
}
sendB.addEventListener('click', send);
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); send(); } });
/* ---------- 半屏面板:开 / 关 / 拖 ----------
按高度伸缩(底部输入框一直露着):closed = 底部露一截(把手 + 标题,PEEK px),half = 55%,full = 76%(上面留出一块星空),再往上拉不动 */
var state = 'closed', PEEK = 58;
(function () { /* 底部露一截要加上 iPhone 底部安全区 */
var pr = document.createElement('i'); pr.style.cssText = 'position:absolute;height:env(safe-area-inset-bottom);visibility:hidden';
page.appendChild(pr); PEEK += pr.offsetHeight || 0; pr.remove();
page.style.setProperty('--wpk', PEEK + 'px');
})();
function ph() { return page.clientHeight; }
function hFor(s) { return s === 'closed' ? PEEK : ph() * (s === 'full' ? .76 : .55); }
function setH(h, anim) { P.style.transition = anim ? '' : 'none'; P.style.height = Math.round(h) + 'px'; }
function snap(s) {
state = s;
sh.classList.toggle('show', s !== 'closed');
sh.setAttribute('aria-hidden', s === 'closed' ? 'true' : 'false');
setH(hFor(s), true);
if (s === 'closed') { inp.blur(); setReply(null); lsBox.scrollTop = 0; }
}
function open() { load(); snap('half'); }
sh.querySelector('.wcs-hd').addEventListener('click', function (e) { if (state === 'closed' && !e.target.closest('.wcs-x')) open(); });
sh.querySelector('.wcs-x').addEventListener('click', function () { snap('closed'); });
sh.addEventListener('click', function (e) { if (e.target === sh) snap('closed'); });
/* 右上角「全部」也开这个面板(原来那个只列悄悄话的抽屉不再用) */
var all = document.getElementById('whvAll');
if (all) all.addEventListener('click', function (e) { e.stopImmediatePropagation(); open(); }, true);
/* 拖:抓把手 / 标题拖;列表在顶上时往下拖 = 拖面板;半屏时在列表上往上划 = 先把面板拉满 */
var d0 = null;
P.addEventListener('touchstart', function (e) {
if (e.touches.length !== 1 || e.target.closest('.wcs-in')) return;
d0 = { y: e.touches[0].clientY, base: P.offsetHeight, hd: !!e.target.closest('.wcs-hd'), drag: false, t: Date.now() };
}, { passive: true });
P.addEventListener('touchmove', function (e) {
if (!d0) return;
var dy = e.touches[0].clientY - d0.y;
if (!d0.drag) {
if (Math.abs(dy) < 6) return;
var canDown = dy > 0 && (d0.hd || lsBox.scrollTop <= 0);
var canUp = dy < 0 && (d0.hd || state !== 'full');
if (!canDown && !canUp) { d0 = null; return; }
d0.drag = true;
}
if (e.cancelable) e.preventDefault();
if (state === 'closed') { load(); sh.classList.add('show'); }
setH(Math.max(PEEK, Math.min(hFor('full'), d0.base - dy)), false);
}, { passive: false });
P.addEventListener('touchend', function (e) {
if (!d0) return;
var dd = d0; d0 = null;
if (!dd.drag) return;
/* 11-07 用户:「滑了一下又缩回去,要很大力气才上得去」→ 按方向判断:往上动过 24px(或甩一下)就往上一档,
拖过半屏再往上就直接拉满;往下同理;只有几像素的小抖动才回原位 */
var dy = e.changedTouches[0].clientY - dd.y, h = dd.base - dy, v = dy / Math.max(1, Date.now() - dd.t);
if (dy < -24 || v < -.25) snap(state !== 'closed' || h > hFor('half') + 40 ? 'full' : 'half');
else if (dy > 24 || v > .25) snap(state !== 'full' || h < hFor('half') - 40 ? 'closed' : 'half');
else snap(state);
});
/* 在「说一句」按钮一带往上划,也能把抽屉拉上来 */
var by = null;
page.addEventListener('touchstart', function (e) {
by = state === 'closed' && !P.contains(e.target) && e.touches[0].clientY > page.getBoundingClientRect().bottom - 170 ? e.touches[0].clientY : null;
}, { passive: true });
page.addEventListener('touchmove', function (e) { if (by != null && by - e.touches[0].clientY > 30) { by = null; open(); } }, { passive: true });
/* app.js 先画了「今天还能说 N 句」(那时本脚本还没加载)—— 说一句现在发进留言区,不再按每天 3 句算,换成留言规则 */
function fixLeft() {
var el = document.getElementById('whpLeft');
if (el && /^今天/.test(el.textContent)) el.textContent = '先审核再公开,被赞 ' + STAR_N + ' 次就变成星星';
}
new MutationObserver(fixLeft).observe(document.getElementById('whpLeft'), { childList: true, characterData: true, subtree: true });
/* 关掉星空页时面板一起收 */
new MutationObserver(function () {
fixLeft();
if (page.classList.contains('show')) { load(); setH(PEEK, false); } else snap('closed');
}).observe(page, { attributes: true, attributeFilter: ['class'] });
if (page.classList.contains('show')) load();
fixLeft();
/* 星空页「说一句」→ 发成留言(app.js 的 #whSend 调)。返回 Promise:resolve(null) = 成功,resolve('提示') = 没发出去 */
/* 说一句的弹窗(11-07 用户:重新设计,有点设计感;「今天还能说」这类说明放到弹窗里) */
var M = document.getElementById('modalWhisper');
function fillModal() {
if (!M) return;
M.classList.add('wh-star');
var ld = document.getElementById('whMLead');
/* 11-07 用户:只留「被赞 3 次变成星星」,明显一点 */
ld.innerHTML = '<span class="wms hi"><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>' +
'<span>被赞 <b>' + STAR_N + '</b> 次,<br>你的话就变成一颗星星</span></span>';
var lf = document.getElementById('whLeft'); if (lf) lf.textContent = '审核通过后,可以转给朋友帮你点赞';
}
if (M && !M.querySelector('.wms-sky')) M.insertAdjacentHTML('afterbegin', '<i class="wms-sky" aria-hidden="true"></i>');
window.MBWhCm = {
fillModal: fillModal,
starLikes: function () { return STAR_N; },
post: function (text, week) {
return post(text, week, null).then(function (msg) { if (!msg) setTimeout(function () { snap('half'); }, 400); return msg; });
},
open: open
};
})();

17
js/wh-themes.js Normal file
View File

@@ -0,0 +1,17 @@
/* 孕妈悄悄话 · 每周话题表(2026-11-07 从 app.js 抽出来,首页和孕妈广场共用)。
一条 = 从 from(北京时间那周的周一)起生效,直到下一条的 from;取「from ≤ 今天」的最后一条,没配新话题时沿用上一周的。
换话题 = 往表尾追加一条。后端将来若在 /whisper 返回 theme(同结构),以后端为准。
presets 只是「不知道说什么?点一句」的示范句,发出去照常走微信内容安全审核。 */
window.MB_WH_THEMES = [
/* 2026-10-02 用户:本周话题由「对宝宝说的话」改成「对老公说的话」;2026-11-01 用户:改成「对未来宝宝说的话」 */
{ from: '2026-09-28', name: '对未来宝宝说的话', lines: ['对未来宝宝', '说的话'], /* lines:广场置顶笔记封面大字的分行 */
tip: '想对还没见面的宝宝说的,悄悄放进星空',
say: '对未来宝宝说一句悄悄话', ph: '宝宝,妈妈想跟你说……',
presets: ['宝宝,妈妈每天都在等你', '不用多优秀,健康快乐就好', '今天你踢了我好多下,妈妈都记着呢',
'等你来了,带你去看海', '谢谢你选中我做你的妈妈', '我们很快就见面啦'] },
];
window.MB_WH_THEME = function () {
var d = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10), t = null;
window.MB_WH_THEMES.forEach(function (x) { if (x.from <= d) t = x; });
return t;
};

View File

@@ -1102,9 +1102,6 @@ html.night .wsk-done button{color:#fff;background:rgba(255,255,255,.12);box-shad
html.night .wmt-l::before{background:linear-gradient(90deg,#2D2730 0,rgba(45,39,48,0.7) 8%,rgba(45,39,48,0.32) 22%,rgba(35,29,45,0.22) 55%,rgba(255,255,255,.06) 90%,transparent)}
html.night .wmt.mine .wmt-l::before{background:linear-gradient(90deg,#2F291E 0,rgba(36,32,23,0.7) 8%,rgba(36,31,23,0.32) 22%,rgba(255,210,150,.22) 55%,rgba(255,220,170,.06) 90%,transparent)}
html.night .we-sky i{background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cdefs%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23FFF4E6' stop-opacity='.85'/%3E%3Cstop offset='1' stop-color='%23FFF4E6' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='10' cy='10' r='10' fill='url(%23g)' opacity='.45'/%3E%3Cpath 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' fill='%23FFFFFF'/%3E%3C/svg%3E") center/contain no-repeat}
html.night .we-mt{background:linear-gradient(90deg,#2D2730,rgba(45,39,48,0.5) 30%,rgba(255,255,255,0));box-shadow:0 0 6px rgba(45,39,48,0.7)}
html.night .we-mt::before{background:#2D2730;box-shadow:0 0 6px 2px #2D2730}
html.night .we-mt{background:linear-gradient(90deg,#2D2730,rgba(45,39,48,0.75) 30%,rgba(255,255,255,0));box-shadow:0 0 8px 1px rgba(45,39,48,0.8)}
html.night .sk-today .sk-go{color:#fff;background:linear-gradient(135deg,#F09A80,#E07560);box-shadow:0 4px 10px rgba(224,117,96,.28)}
html.night .modal:not(.show),html.night .sheet:not(.show){box-shadow:none!important}
html.night .pl-feed3 .pl-row .pr-ava{box-shadow:0 0 0 1.5px #2D2730}

115
tools/whcm/build.py Normal file
View File

@@ -0,0 +1,115 @@
"""孕妈悄悄话置顶笔记的 100 条模拟祝福评论(11-07 用户:「评论里按正常评论放上 100 个」)。
lines.txt 一行一条,「||」后面是楼中楼回复(其他孕妈/宝妈)。人设从 cm.json 的 1000 人里挑:主评论用孕期人设、回复混宝妈;
尽量挑在别的帖子下出现少的人。确定性(固定种子),重跑结果不变;只追加/覆盖 c[TARGET],不动其他帖子。"""
import json, random
from collections import Counter
TARGET = 'wh-20260928'
P = 'guangchang/cm.json'
d = json.load(open(P, encoding='utf-8'))
used = Counter(c[0] for k, L in d['c'].items() if k != TARGET for c in L)
rnd = random.Random(20261107)
import re
def wk(p):
m = re.match(r'孕 ?(\d+)', p[2]); return int(m.group(1)) if m else 0
# 内容多是胎动、产检、待产(孕中晚期的事)→ 主评论只用孕 24 周以上的人设,免得「孕 6 周」说「头朝下了」
preg = sorted([i for i, p in enumerate(d['p']) if wk(p) >= 24], key=lambda i: (used[i], i))
anyp = sorted(range(len(d['p'])), key=lambda i: (used[i], i))
# 11-07 用户看评论:名字套路太像(4 个「在挑名字」、4 个「今天也开心」、3 个「水獭」、虎虎的老母亲/虎虎妈妈…)→ 同一帖里前两个字、后两个字都不撞
SIG = set()
def fresh(i):
n = d['p'][i][0]; ks = {'h:' + n[:2], 't:' + n[-2:]}
return not (ks & SIG)
def take(pool, k):
out = []
for i in pool:
if len(out) >= k: break
if fresh(i):
n = d['p'][i][0]; SIG.update({'h:' + n[:2], 't:' + n[-2:]}); out.append(i)
for i in pool: # 不够就只要求后两个字不撞
if len(out) >= k: break
n = d['p'][i][0]
if i not in out and 't:' + n[-2:] not in SIG: SIG.add('t:' + n[-2:]); out.append(i)
for i in pool: # 还不够就随它
if len(out) >= k: break
if i not in out: out.append(i)
return out
preg = take(preg, 160)
mains = preg[:200]; rnd.shuffle(mains)
anyp = [i for i in anyp if i not in set(preg) and not re.match(r'准爸爸', d['p'][i][2])] # 回复里不放「准爸爸」(还没娃,说不了打针)
reps = take(anyp, 31); rnd.shuffle(reps)
lines = [l.strip() for l in open('tools/whcm/lines.txt', encoding='utf-8') if l.strip()]
assert len(lines) == 100, len(lines)
out, ri = [], 0
# 内容要和孕周对得上:按关键词给每条定孕周范围,从打乱后的人设里挑第一个落在范围内的
RULES = [(r'糖耐', 24, 28), (r'预产期在月底', 37, 39), (r'过年', 25, 28),
(r'待产包|入盆|无痛|胎位|头朝下|小床|产假|月子餐|育儿嫂|报纸|满月|儿科医生|插座|标签|绘本|包被', 32, 39),
(r'踢的位置越来越低|孕妇照|羊水|吸奶器|数胎动|数够十下|产褥垫|剪指甲|第一批小衣服|婴儿车|疫苗时间表', 28, 39), (r'四维', 24, 30)]
def rng(t):
for pat, lo, hi in RULES:
if re.search(pat, t): return lo, hi
return 24, 39
left = list(mains)
def pick(t):
lo, hi = rng(t)
for i in left:
if lo <= wk(d['p'][i]) <= hi: left.remove(i); return i
raise SystemExit('没有合适人设:' + t)
for k, l in enumerate(lines):
parts = l.split('||')
likes = max(3, int(520 * (0.965 ** k)) + rnd.randint(-18, 18))
rr = []
for t in parts[1:]:
rr.append([reps[ri], t]); ri += 1
out.append([pick(parts[0]), parts[0], likes, rr])
# 11-07 用户:「评论里也加一些孕早期的孕妈,看到别人有胎动也有期待,也有自己的话对孩子说;都要真人感」
# early.txt = 孕 6~13 周人设的留言(回复用孕中晚期/宝妈人设,像过来人);均匀插进前 100 条中间,不全堆在后面
# early_rep.txt = 「原留言开头|早孕妈妈的回复」,挂在已有留言楼里(「看你们说胎动好羡慕」这类)
early_p = take(sorted([i for i, p in enumerate(d['p']) if 6 <= wk(p) <= 13 and i not in set(reps) and not re.search(r'待产|宝妈|老母亲|养娃|学抱娃|拼尿布|麻麻|大宝', p[0])], key=lambda i: (used[i], i)), 51)
er = random.Random(20261108); er.shuffle(early_p)
ei = 0
for t in [l.strip() for l in open('tools/whcm/early_rep.txt', encoding='utf-8') if l.strip()]:
head, txt = t.split('|', 1)
hit = [c for c in out if c[1].startswith(head)]
assert len(hit) == 1, head
hit[0][3].append([early_p[ei], txt]); ei += 1
ERULES = [(r'两道杠|还没告诉老公|还没告诉你爸|验孕棒|小黑圈|胎心胎芽', 6, 8), (r'孕酮|查血|见了一点血', 6, 10), (r'约了NT', 10, 12),
(r'满三个月', 8, 12), (r'才两个月', 8, 9), (r'葡萄|裤子有点紧|肚子还平', 10, 13)]
eleft = early_p[ei:]
def epick(t):
lo, hi = next(((a, b) for pat, a, b in ERULES if re.search(pat, t)), (6, 13))
for i in eleft:
if lo <= wk(d['p'][i]) <= hi: eleft.remove(i); return i
raise SystemExit('没有合适早孕人设:' + t)
early = [l.strip() for l in open('tools/whcm/early.txt', encoding='utf-8') if l.strip()]
eo = []
for k, l in enumerate(early):
parts = l.split('||')
rr = []
for t in parts[1:]:
rr.append([reps[ri], t]); ri += 1
eo.append([epick(parts[0]), parts[0], 0, rr])
# 定赞数:从第 3 条起每隔约 2~3 条放一条早孕的,赞数取前后两条之间
merged = []
step = len(out) / len(eo)
j = 0
for k, c in enumerate(out):
merged.append(c)
while j < len(eo) and k >= 2 + j * step * .96:
e = eo[j]; nxt = out[k + 1][2] if k + 1 < len(out) else 3
e[2] = max(3, (c[2] + nxt) // 2 + rnd.randint(-4, 4)); merged.append(e); j += 1
for e in eo[j:]: e[2] = 3
# 追加在最后(模拟留言的 key 是「帖子#下标」,插在中间会让原来那 100 条下标错位,真实回复/本机点赞挂错楼);
# 赞数已按插入位置取好,「最热」排序时自然穿插在中间
out = out + eo
# 11-07 用户定(方案 A):「被赞 3 次变星星」要和赞数对得上 → 赞数最高的 26 条标成已变星(第 5 位 = 1),
# 赞数压到几十~个位(不再几百);其余 0~5 个赞(门槛 6),看着就是「还差一点」,有人帮着点一下就能变星(前端本机算)
order = sorted(range(len(out)), key=lambda k: -out[k][2])
NS = 26
for r, k in enumerate(order):
if r < NS:
out[k][2] = max(6, int(168 * (0.88 ** r)) + rnd.randint(-5, 5)); out[k].append(1)
else: # 11-07 用户:门槛改 6 个赞 → 没变星的 0~5 个
out[k][2] = rnd.choice([0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5])
d['c'][TARGET] = out
json.dump(d, open(P, 'w', encoding='utf-8'), ensure_ascii=False, separators=(',', ':'))
print('ok', len(out), 'replies', ri, 'early', len(eo), 'early-replies', ei)

39
tools/whcm/early.txt Normal file
View File

@@ -0,0 +1,39 @@
早上测出两道杠,妈妈在厕所蹲了十分钟没敢出来,又拆了一根测,真的是你啊||恭喜恭喜!我当时测了三根才敢信
还没告诉你爸,打算他生日那天把验孕棒包起来送他,你先帮妈妈保密
第一次B超你就一个小黑圈,医生说这就是你,妈妈盯着看了半天也没看懂||一模一样,我拿着单子在走廊坐了好久
今天听到你的胎心了,一百六,跑得跟小火车似的,妈妈在诊室没忍住哭了||第一次听胎心真的会哭,我也是
刷牙吐,闻到电梯里的香水也吐,现在出门都戴口罩
看大家都在说胎动,好羡慕,你还是颗小豆子,妈妈啥也感觉不到,就是天天想吐||别急,到四五个月就有了,第一下像小鱼吐泡泡
宝宝,妈妈之前胎停过一次,这次每回去医院前一晚都睡不着,你要好好的||抱抱你,我之前也胎停过,后来这个上个月生了,好好的,你也会
妈妈开会困得直点头,领导看了好几眼,还不敢说有你了,等满三个月再讲
你的孕酮比上周涨了,妈妈出了医院买了根烤肠奖励自己
老家说满三个月才能往外讲,现在全世界就我和你爸知道你,憋得慌
宝宝,妈妈以前一天两杯冰美式,现在闻到咖啡味就想吐,你是不是不喜欢咖啡
昨晚梦到你是个圆脸小姑娘,跟你爸说了,他非说你是儿子||我怀孕的时候也老做梦,后来生出来真的跟梦里有点像
叶酸又差点忘吃,干脆定了个闹钟
今天去医院查血抽了五管,妈妈晕针,你爸在旁边握着我手,他手比我还凉
听说四维能看到你的脸,妈妈还得等好几个月
肚子还平平的,妈妈已经忍不住给你买了一双小袜子,就一双
你来得有点突然,本来计划明年的,妈妈慌了两天,现在挺好||意外来的宝宝都是来报恩的,我家这个也是
宝宝,妈妈吐到啥也吃不下,只有苏打饼干能咽,一周吃了四包,你记着
约了NT,妈妈有点紧张,你乖乖的,让医生量个好数
你爸已经开始研究婴儿车了,我说你才两个月他急啥
妈妈在备忘录里给你写了七八个名字,划掉五个,剩下的等你爸挑
这两天突然不怎么吐了,妈妈反而慌了,宝你要是还好好的就让我再吐一下||哈哈这心态太真实了,不吐担心吐了难受
看大家说宝宝在肚子里打嗝伸懒腰,好想快点到那时候
地铁上闻到韭菜包子,下车就吐了,你这鼻子比妈妈还灵
外婆知道有你以后,每天一个电话问我吃了啥,比我上学那会儿还上心
妈妈以前加班到半夜,有了你八点就困得睁不开眼
你才葡萄那么大,妈妈已经在想你上幼儿园会不会哭了||当妈都这样,我现在已经在想初中选哪个学校了
产检单上写着“宫内早孕,可见胎心胎芽”,妈妈拍照发给你爸,他回了一排感叹号
还不敢跟同事说有你了,午饭偷偷吃话梅,被问是不是失恋了
宝宝,妈妈现在看个广告都能哭,你爸说是激素,我说是你
每天来这看看大家说的话,感觉没那么难熬了
今天见了一点血,妈妈连夜去急诊,医生说你没事,回家路上哭了一路||能理解那种害怕,我也有过,后来都好好的,你也会
你爸说等你会动了要第一个摸,外婆说她已经预约了
手机游戏全删了,就想早点睡
宝宝,下次B超个头长一点好不好,别再让医生说偏小
裤子有点紧了,应该不是你,是妈妈吃多了
还在吐,还在困,还在担心,一想到再过几个月就能感觉到你动,又觉得还行
看大家都在收待产包了,妈妈连孕妇装都还没买,你慢慢长||不着急,日子快得很,一眨眼就到了
你爸贴着肚子跟你说了半天话,我说你现在还没耳朵呢,他说先练着

12
tools/whcm/early_rep.txt Normal file
View File

@@ -0,0 +1,12 @@
你爸第一次摸到你动,手一|看得好羡慕,我才两个月,还只能隔着肚皮瞎猜
宝宝你是在打嗝吗,一跳一|等我也能感觉到宝宝打嗝那天,一定要录下来
四维看到你的脸了,鼻子随|好期待四维,我现在B超上还是一颗小豆子
你啊,凌晨两点准时开踢,|看完好想快点被踢醒,现在只有被孕吐吐醒
前三个月你把妈妈折腾掉六|我正在吐的阶段,看到能吃下一碗饭了就有盼头了
你爸拿洋娃娃练换尿布练了|我老公也说要练,我说先把我的孕吐伺候好再说
今天听胎心你一直躲,医生|我连胎心都是上周才第一次听到的,听完在诊室里哭了
偷偷把你的B超照设成手机|我也设了!虽然只是个小黑圈
给你收拾待产包,发现产褥|收藏了,等我到那时候照着买
你的小床到了,你爸装了一|我们才三个月,已经在看小床了哈哈
睡不着就刷别人家宝宝的视|我也是,现在每天睡前刷别人家宝宝
医生让多走走,今天带你在|我现在连下楼都懒,只想躺着,等过了前三个月我也去走

100
tools/whcm/lines.txt Normal file
View File

@@ -0,0 +1,100 @@
你啊,凌晨两点准时开踢,妈妈直接被你踢醒,白天上班困成狗,咖啡还不让喝||同,两点准时开工,比闹钟还准
今天产检排了三个小时队,轮到我了你倒好,一动不动,医生按了半天肚子,吓死我了,结果你在睡觉
你爸第一次摸到你动,手一下缩回去了,说“真有人啊??”你说他是不是傻||哈哈哈我老公也是这反应
崽 商量个事 出来以后晚上让我睡满四个小时行不行 就四个
为了你把糖耐那瓶糖水硬灌下去了,齁得要命,抽了三次血,还好过了,你可得给我好好长
宝宝你是不是又长大了,妈妈腰疼得翻身都要喊你爸推,跟翻咸鱼似的
又偷吃了半个西瓜,测血糖前心虚得要死,还好没超,宝你别告诉医生||我也是管不住嘴,测完才敢松口气
你外婆给你寄了一箱小衣服,全是她拿旧秋衣改的,说纯棉软和,我说现在都买新的,她不听。到时候先给你穿她的
你可千万别学你爸挑食,三十多岁了还不吃胡萝卜
今天地铁上第一次有人给咱俩让座,一个大学生姐姐,妈妈有点不好意思,还是坐了
小家伙,你把妈妈的脚都压肿了,鞋穿不进去,你爸给买了双大一码的拖鞋,丑死了
宝宝你是在打嗝吗,一跳一跳的,妈妈手放上去数了二十多下才停
宝宝,妈妈已经在看你的疫苗时间表了,第一针出生就打,到时候哭了妈妈抱||第一针真的心疼,打完赶紧喂奶就好了
这个月给你买东西花了快两千,你爸说比给他买的多十倍,嗯,他说得对
偷偷把你的B超照设成手机屏保了,被同事看见,现在全办公室都知道你了
医生说你偏小一周,妈妈这几天每顿多吃一个鸡蛋,你争点气,下回追上||我家也偏小,多吃牛肉,下次产检就追上了
宝宝,妈妈怕疼,决定打无痛了,你别嫌我
你爸拿洋娃娃练换尿布练了一个月,现在一分钟能换好,天天让我验收,你出来给他打个分
给你收拾待产包,发现产褥垫买少了,又下单两包,你可别来太早
前三个月你把妈妈折腾掉六斤,现在终于能吃下一碗饭了,谢谢你高抬贵手
睡不着就刷别人家宝宝的视频,越刷越想看看你长啥样
宝宝,奶奶说你是男孩,外婆说是女孩,俩人赌了一顿火锅,你看着办||我家也赌了,赌注是一只金镯子哈哈
今天听胎心你一直躲,医生让我吃块巧克力,吃完你立马动了,小馋鬼
你的小床到了,你爸装了一晚上,最后多出来两颗螺丝,妈妈没敢问
宝宝,妈妈这两天动不动就哭,你爸问我怎么了我也说不上来,不关你的事啊
四维看到你的脸了,鼻子随你爸,嘴巴随我,还行,不丑
你预产期在过年那几天,红包妈妈先替你收着了(不是||红包存起来+1,我准备给宝宝开个账户
宝宝,妈妈产假一共一百五十八天,还没开始休,已经在算能陪你多少天了
医生让多走走,今天带你在楼下转了四十分钟,妈妈腿都酸了,你倒是睡得香
你爸为了你戒烟五个月了,一根没抽,这个你出来得夸夸他
最近踢的位置越来越低了,是要入盆了吗,有没有懂的姐妹
给你洗了第一批小衣服,晾了一阳台,邻居阿姨问妈妈是不是快生了,我说还早呢
你的名字吵了一个月了,你爸起的太土,我起的他说太文艺,要不你出来自己挑吧||我家也是吵了三个月,最后抓阄定的
宝宝,妈妈怀你胖了十八斤,医生说正常,那我就当真了
今天感觉你动得少,妈妈喝了口冰水,你马上踢我一脚,行,知道你在
以后不给你报一堆兴趣班,你喜欢啥学啥,妈妈小时候被逼学琴学到吐
半夜腿抽筋疼醒,你爸迷迷糊糊给我掰脚,掰完倒头就睡,呼噜都没停,你说这人
今天产检前面四十多个人,妈妈揣着你站了一上午,腰要断了||产检那天真的累,记得带个小板凳
你外公不会用微信,这两天专门学了视频通话,说等你出来天天看你
宝宝,妈妈今天把钥匙放冰箱里了,找了半小时,你爸说是你把我脑子借走了
妈妈发誓以后不在朋友圈天天晒你。争取吧||争取两个字太真实了哈哈
宝宝,家里有只猫叫可乐,比你先来的,以后你俩别打架
产检一切正常,出了医院妈妈就去吃了碗牛肉面,加俩蛋,你也吃到了吧
这两天你动得少了点,妈妈晚饭后躺着数你,数够十下才敢睡
你的第一套衣服是妈妈在拼多多九块九抢的,质量居然还行,别嫌弃
你爸说等你出来教你打游戏,妈妈已经驳回了,上小学之前想都别想||驳回得好,我家那位已经在挑游戏机了
宝宝,预产期在月底,那会儿挺冷了,包被给你买了两条,不知道够不够
妈妈开会坐了俩小时,你在里面踢了一个半小时,对会议内容有意见是吧
不图你多聪明,能吃能睡就行,别学妈妈天天失眠
上班时你在里面一拱一拱的,妈妈在工位上偷偷乐,同事以为我在摸鱼看视频
宝宝,奶奶把月子餐菜单写好了,三十天不重样,妈妈看完有点感动,又有点怕
等你会走路了带你去楼下喂鸽子,那边的鸽子特别肥
今天带你去拍了孕妇照,摄影师说妈妈是他最近拍过最圆的一个,都怪你||孕妇照一定要拍,以后翻出来很有意思
你爸每晚对着肚子给你讲三国,已经讲到赤壁了,你听不听得懂不知道,我快会背了
宝宝,等你出来,妈妈第一件事肯定是数你手指脚趾,别笑我
今天走快了点肚子就发紧,吓得赶紧找地方坐,以后再也不赶地铁了
以前妈妈一顿两碗饭,现在半碗就顶到胃,你挤得我好难受
你小姨给你买了个会唱歌的摇铃,她自己先玩了一下午
宝宝,妈妈最近特别想吃酸的,你爸念叨酸儿辣女,我说这是迷信,你是啥都行
今天在医院看到个刚出生的宝宝,皱巴巴红通通的,你刚出来是不是也这样
宝宝,妈妈打算喂你母乳到一岁,能不能坚持住不知道,尽量吧
抓到你爸偷偷搜怎么给新生儿剪指甲,他说是工作需要,呵
家里插座全给你装了保护盖,你爸说你还早着呢,妈妈说早装早安心
你的待产包收好放门口了,妈妈每次出门都要看一眼||我直接放车后备箱了,省得到时候手忙脚乱
生完想带你回老家住一阵,有院子,空气好,就是离医院远了点
你今天在超市踢到妈妈膀胱,差点没憋住,一路小跑找厕所,你可真行
你爸给你开了个奶粉钱账户,每月自动扣,他说这叫仪式感
想买份宝宝出生那天的报纸留着,问了一圈,报亭都没了
胎位查了,你头朝下了!宝,就这样,千万别再转了||我家当时也是臀位,做了两周膝胸卧位才转过来
外婆说她生妈妈疼了一天一夜,听完我更坚定要打无痛了,你别怪妈妈怕疼
你爸开始学做月子餐,鲫鱼汤第一锅糊了,第二锅还能喝,你以后可别学他做饭
小区门口那个滑滑梯每天都排队,等你会走了咱们也去排
又被你踢醒,一看手机三点十二,你这作息以后可咋整||三点的我也在,一起数羊
托人打听了个儿科医生,说看病很仔细,妈妈先把号码存上了,希望用不上
奶奶今天送来一只老母鸡,说给我补补,我懂,是给你补
听说宝宝哭了抱着转圈就好,妈妈已经做好在客厅转一千圈的准备了||我家已经买了个摇椅,替我转
今天试着给你读了本好饿的毛毛虫,你听没听见不知道,妈妈自己读得挺开心
你的小名定了,叫豆豆,大名还在吵,先豆豆叫着||豆豆好可爱,我家叫团团
妊娠纹还是长了,油涂了一罐也没用,算了随它吧||说得好,我也不涂了
宝宝,妈妈准备请三个月育儿嫂,贵是贵,但妈妈想睡觉
同事凑钱送了个婴儿背带,挺意外的,平时也没多熟
你的满月酒,你爸说摆两桌,妈妈说一桌够了,还没吵出结果
今天忘数胎动了,晚上补数了一个小时,你还挺配合
你一天一个样,妈妈的孕妇裤都快穿不下了
以前妈妈怕打针怕得要死,怀你打了十几针,现在完全无感了
明年夏天带你去海边,妈妈自己都没去过几回
医生说羊水偏少,让多喝水,今天喝了三大杯,你在里面泡舒服了没
老公在母婴店推婴儿车推了半小时,来回试,店员都看笑了
宝宝,生你之前妈妈想再吃一次火锅,微辣,就一次,你批准不||吃!微辣问题不大,我上周刚吃
最近老梦到你会说话了,一开口叫妈,醒了还挺失落
宝宝,吸奶器还没用上,妈妈教程已经看了十几个,到时候你配合点
今天你在肚子里翻了个大身,肚子整个歪到一边,你爸说像外星人
妈妈以前天天熬到两点,怀你以后十一点前就睡了,你出来别让我破功
你爸在家族群发了你的B超照,全家都说像他,B超能看出个啥
宝宝,妈妈也不知道自己能不能当好妈妈,边当边学吧,你多担待
你的小衣服标签妈妈挨个剪了,怕扎你,剪了一个多小时
希望你出生那天是晴天,你爸说下雨也行,反正都高兴
以后跟你说你在肚子里有多能踢,你肯定不信
尿蛋白有点高,要复查,妈妈有点慌,你在里面好好的就行||别太慌,我上次也是,复查就正常了
给你攒了一箱绘本,都是闲鱼收的二手,九成新,你别嫌弃

View File

@@ -64,7 +64,7 @@
/* 11-04 微信转发图:每个专题一张 assets/share/t/<slug>.jpg(核心词大字 + logo),见 js/share-card.js */
if (SLUG) (function () { var s = document.createElement('script'); s.src = '../../js/share-card.js?v=26110464'; s.onload = function () { window.mbShare('../../assets/share/t/' + SLUG + '.jpg'); }; document.head.appendChild(s); })();
/* 11-07 退出登录后只看不写(js/login-gate.js):点赞、评论、发图都弹「登录后才能使用」 */
if (!window.mbGate) (function () { var s = document.createElement('script'); s.src = '../../js/login-gate.js?v=26110605'; document.head.appendChild(s); })();
if (!window.mbGate) (function () { var s = document.createElement('script'); s.src = '../../js/login-gate.js?v=26110610'; document.head.appendChild(s); })();
var SK = 'mmbox.tp.v1', UK = (function () { var m = ((location.hash || '') + '&' + (location.search || '')).match(/[#?&]openid=([^&]*)/); return m ? decodeURIComponent(m[1]) : '_guest'; })();
function loadMe() { try { var a = JSON.parse(localStorage.getItem(SK) || '{}') || {}; var m = a[UK] || {}; m.cm = m.cm || {}; m.lk = m.lk || {}; return { a: a, m: m }; } catch (e) { return { a: {}, m: { cm: {}, lk: {} } }; } }
function saveMe(st) { try { st.a[UK] = st.m; localStorage.setItem(SK, JSON.stringify(st.a)); } catch (e) {} }
@@ -98,8 +98,8 @@
var st = loadMe(), L = [], open = false, RC = { list: [], mine: [] }, OID = UK === '_guest' ? '' : UK;
var API = '/api/mp/public/mama-box/plaza';
function mine() {
var old = (st.m.cm[SLUG] || []).map(function (c) { return { t: c.t, ts: c.ts, s: c.s, st: 'local' }; });
return old.concat(RC.mine.slice().reverse().map(function (c) { return { t: c.text, ts: c.at, s: c.stage, st: c.status, why: c.reason, id: c.id, parent: c.parent, to: c.parentName, img: c.images }; }));
/* 11-07 用户:评论、回复一定要审核通过才显示,自己也不显示「审核中」—— 只留已通过的;本机旧评论(从没审过)也不再显示 */
return RC.mine.filter(function (c) { return c.status === 'approved'; }).reverse().map(function (c) { return { t: c.text, ts: c.at, s: c.stage, st: c.status, why: c.reason, id: c.id, parent: c.parent, to: c.parentName, img: c.images }; });
}
function real() { return RC.list.filter(function (c) { return !c.parent; }).map(function (c) { return [c.nickname || '孕妈', c.avatar || '', c.stage || '', c.text, 0, [], 'r:' + c.id, c.images]; }); }
var cpH = function (a) { return window.MBCmPics ? MBCmPics.html(a) : ''; }; /* 11-05 评论里的图 */
@@ -191,7 +191,7 @@
var wasReply = !!REPLY; setReply(null);
ta.value = ''; if (CP) CP.clear(); ta.style.height = '24px'; bt.disabled = true; ta.blur(); paint();
if (!wasReply) box.scrollIntoView({ block: 'start', behavior: 'smooth' });
toast(wasReply ? '回复已提交,审核通过后大家就能看到' : '已提交,审核通过后大家就能看到');
toast(wasReply ? '回复已提交,审核通过后才会显示' : '已提交,审核通过后才会显示');
}).catch(function () { bt.busy = false; bt.disabled = !canSend(); toast('网络不太好,稍后再试'); });
});
function toast(m) {