diff --git a/index.html b/index.html
index e39e270..88fdf86 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
兜爸心选 · 孕育陪伴
-
+
@@ -1933,6 +1933,20 @@
+
+
+
确定退出登录?
diff --git a/js/app.js b/js/app.js
index 11bcce3..b5fc8f9 100644
--- a/js/app.js
+++ b/js/app.js
@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
- var VER = '26101012';
+ var VER = '26101013';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -337,9 +337,67 @@
if (!oid && !uid) q.push('__no_identity=1');
return q.length ? url + (url.indexOf('?') >= 0 ? '&' : '?') + q.join('&') : url;
}
+ /* ================= 孕周开放门槛(2026-10-11 用户)=================
+ 「数胎动 16 周、数宫缩 20 周、胎儿估重 15 周以后对外开放;选了没到的孕周(比如 10 周),
+ 这三个都不开放,点的时候告诉她几周后开放、现在还用不到,给一个定制感受。」
+ · 只在**已知孕周且没到门槛**时拦:访客 / 未建档 / 产后都照常放行(拿不准就别拦人)。
+ · 统一拦在 jumpApp() 里 —— 记一下宫格、首页工具卡、今日待办、盲盒任务……所有入口都走它,一处管全部。
+ · 按 identifier 认,认不到再按 URL 目录认(今日待办的上游 url 可能不带 identifier)。 */
+ const STAGE_GATE = [
+ { id: 'taidong', slug: '/taidong/', wk: 16, name: '数胎动',
+ why: nm => nm + '现在还小,胎动要到孕 16 周以后才慢慢感觉得到。到时候每天数一数,就能知道' + nm + '今天好不好。' },
+ { id: 'gongsuopinlv', slug: '/gongsuo-pinlv/', wk: 20, name: '数宫缩',
+ why: () => '宫缩计时主要用在孕中晚期,孕 20 周以后记下来才更有参考意义。现在先安心养着,不用急着用它。' },
+ { id: 'taierguzhong', slug: '/taier-guzhong/', wk: 15, name: '胎儿估重',
+ why: nm => '估重要用到 B 超量出来的头围、股骨长这些数据,孕 15 周以后算出来才靠谱。等下次 B 超拿到报告,就能看看' + nm + '有多重啦。' },
+ ];
+ function stageGateOf(url, identifier) {
+ if (identifier) { const g = STAGE_GATE.find(x => x.id === identifier); if (g) return g; }
+ let path = '';
+ try { path = new URL(url, location.href).pathname; } catch (e) { path = String(url || ''); }
+ return STAGE_GATE.find(x => path.indexOf(x.slug) === 0) || null;
+ }
+ /* 现在孕几天;拿不准(访客、未建档、产后)返回 null → 不拦 */
+ function knownGA() {
+ if (state.postpartum || state.pregEmpty || state.week == null) return null;
+ return state.week * 7 + (state.day || 0);
+ }
+ function stageLocked(g) { const ga = knownGA(); return !!g && ga != null && ga < g.wk * 7; }
+ function openStageGate(g) {
+ const ga = knownGA(), left = g.wk * 7 - ga, nm = state.babyName || '宝宝';
+ const app = APP_BY_ID[g.id], ico = app && (app.logo || app.icon);
+ $('#sgIco').innerHTML = ico ? '
' : '' + g.name.slice(0, 1) + '';
+ $('#sgT').textContent = g.name + ' · 孕 ' + g.wk + ' 周后开放';
+ $('#sgNow').textContent = '你现在 ' + gaTxt(ga).replace('孕', '孕 ').replace('周', ' 周');
+ $('#sgP').textContent = g.why(nm);
+ $('#sgFrom').textContent = gaTxt(ga);
+ $('#sgTo').textContent = '孕' + g.wk + '周';
+ $('#sgBar').style.width = Math.max(6, Math.min(100, ga / (g.wk * 7) * 100)).toFixed(1) + '%';
+ $('#sgLeft').textContent = (left >= 7 ? '还有 ' + Math.ceil(left / 7) + ' 周' : '还有 ' + left + ' 天') + ',大约 ' + mdTxt(gaToDate(g.wk * 7)) + ' 开放';
+ openModal('modalStageGate');
+ }
+ /* 宫格 / 首页工具卡上的小角标「16周+」:没到门槛的灰一点,到了自动去掉 */
+ function markStageLocks() {
+ $$('#appsGrid .app').forEach(b => {
+ const g = stageGateOf(decodeURIComponent(b.dataset.url || ''), decodeURIComponent(b.dataset.id || ''));
+ const on = stageLocked(g);
+ b.classList.toggle('is-locked', on);
+ if (on) b.dataset.lock = g.wk + '周+'; else delete b.dataset.lock;
+ });
+ [['kick', 'taidong'], ['curve', 'gongsuopinlv'], ['fweight', 'taierguzhong']].forEach(([slot, id]) => {
+ const el = $('.mini-card[data-mini="' + slot + '"]'), g = STAGE_GATE.find(x => x.id === id);
+ if (!el) return;
+ const on = stageLocked(g);
+ el.classList.toggle('is-locked', on);
+ if (on) el.dataset.lock = g.wk + '周+'; else delete el.dataset.lock;
+ });
+ }
+
/* 跳转到其它 H5 应用。identifier 可选(来自 App 表),用于 last_app 上报的服务端在架校验;
缺省时服务端按 URL 前缀反查,效果一致。 */
function jumpApp(url, identifier) {
+ const sg = stageGateOf(url, identifier);
+ if (stageLocked(sg)) { openStageGate(sg); return; } // 孕周没到:弹提示,不跳(见 STAGE_GATE)
haptic();
const target = withIdentity(url);
reportLastApp(url, identifier); // 净地址:恢复时身份按当前用户重新注入
@@ -372,6 +430,7 @@
cnt.textContent = '共 ' + apps.length + ' 个 ›';
cnt.dataset.toast = '共 ' + apps.length + ' 个孕期工具';
}
+ markStageLocks();
}
function loadPublishedApps() {
fetch('/api/mp/apps?isPublished=1&pageSize=200')
@@ -1362,6 +1421,7 @@
}
function syncPregnancy() {
Promise.resolve().then(pubPreg);
+ markStageLocks(); /* 孕周变了 → 「16周+」角标跟着变 */
/* 孕期小知识:只认真实孕周(自己判显隐,见 renderTips) */
renderTips();
nurseryRender();
@@ -4978,7 +5038,7 @@
if (title) title.style.display = (r && r.lead) ? '' : 'none';
if (!box) return;
box.innerHTML = '';
- ((r && r.unused) || []).slice(0, 4).forEach(u => {
+ ((r && r.unused) || []).filter(u => !stageLocked(stageGateOf(u.url, u.identifier))).slice(0, 4).forEach(u => {
const ident = u.identifier || '';
const app = ident ? APP_BY_ID[ident] : null;
/* ⚠️ 优先用上游带回的 `url`(2026-09-23 起它只推**我们已上架**的应用并把正式 url 带回来,
@@ -5029,6 +5089,7 @@
const done = $('#skDone');
let nx = null;
for (const u of unused) {
+ if (stageLocked(stageGateOf(u.url, u.identifier))) continue; // 孕周没到的不当「下一件」推
const ident = u.identifier || '', app = ident ? APP_BY_ID[ident] : null;
const href = u.url || (app && app.url) || '', name = u.appName || (app && app.name) || '';
if (href && name) { nx = { href, name, id: (app && app.identifier) || ident }; break; }
diff --git a/mama-box.css b/mama-box.css
index ffc4aa4..bdb8537 100644
--- a/mama-box.css
+++ b/mama-box.css
@@ -4750,3 +4750,30 @@ a.skip:focus{left:16px;top:16px}
.app 带 .ripple 是 overflow:hidden,勾原来 top:-4px 伸出格子外。格子上方留 6px,勾下移同样距离,位置相对图标不变 */
.apps-grid .app{padding-top:6px}
.apps-grid .app.is-done::after{top:2px}
+
+/* ================= 孕周开放门槛(2026-10-11)=================
+ 没到孕周的工具:宫格和首页工具卡上挂一枚「16周+」浅色角标,图标略淡;点了弹 #modalStageGate。
+ 配色只用现有的暖米 / 品牌橘,不另起颜色:角标是米底灰字,进度条是品牌橘渐变。 */
+.apps-grid .app.is-locked .a-ico{opacity:.5;filter:saturate(.6)}
+.apps-grid .app.is-locked .a-n{color:var(--muted)}
+.apps-grid .app.is-locked::after{content:attr(data-lock);position:absolute;top:-6px;left:calc(50% + 10px);
+ padding:1px 5px;border-radius:7px;background:#F3E7DF;color:#9A7E70;font-size:9px;font-weight:800;line-height:14px;
+ box-shadow:0 0 0 2px #FAF5F1;white-space:nowrap}
+.mini-card.is-locked::after{content:attr(data-lock);position:absolute;top:10px;right:10px;
+ padding:1px 6px;border-radius:8px;background:rgba(255,255,255,.72);color:#9A7E70;font-size:10px;font-weight:800;line-height:16px}
+.mini-card.is-locked .mc-art{opacity:.55}
+.mini-card.kick.is-locked .mc-btn{background:rgba(255,255,255,.7);color:#9A7E70;box-shadow:none}
+
+.sg-modal .sg-ico{width:64px;height:64px;margin:4px auto 10px;border-radius:20px;background:linear-gradient(150deg,#FFE9E1,#FFDDD3);
+ display:grid;place-items:center;box-shadow:0 8px 18px rgba(244,89,60,.14)}
+.sg-modal .sg-ico img{width:40px;height:40px;object-fit:contain}
+.sg-modal .sg-ico b{font-size:24px;font-weight:900;color:var(--brand)}
+.sg-modal .sg-now{display:inline-block;margin-top:8px;padding:3px 10px;border-radius:10px;background:rgba(244,89,60,.1);color:var(--brand-dk);font-size:12px;font-weight:800}
+.sg-modal p{margin:10px 6px 0;line-height:1.75}
+.sg-track{margin:16px 8px 0}
+.sg-rail{height:8px;border-radius:4px;background:#EFE5DE;overflow:hidden}
+.sg-rail i{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,#FFB49F,var(--brand));transition:width .5s var(--ease)}
+.sg-ends{display:flex;justify-content:space-between;margin-top:6px;font-size:11.5px;font-weight:700;color:var(--muted)}
+.sg-left{margin-top:10px;font-size:13px;font-weight:700;color:var(--ink-2)}
+.sg-modal .modal-foot{margin-top:18px}
+