首页:数据卡与编辑解耦(用过就显示,最多4张可展开);图标宫格由编辑决定最多8个,全部工具移到标题栏 v26110102

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-11-01 04:05:39 +00:00
parent c57546f9ba
commit cb1201df42
2 changed files with 102 additions and 77 deletions

View File

@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title>
<!-- V:26110101 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26110102 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<link rel="stylesheet" href="night.css">
<!-- 夜晚模式:首屏绘制前就按上次的选择加 class,避免先白后黑闪一下(开关在首页右上角 #nightBtn,逻辑见 app.js) -->
@@ -200,15 +200,17 @@
2026-10-01 用户要求:水印字从「妈妈」改成品牌名「兜爸」。 -->
<span class="wm" aria-hidden="true">兜爸</span>
<!-- 2026-11-01 用户:「我的孕期数据和上面那个工具结合在一起时,是一个模块……上面所有的工具,用户可以去选择放哪一个,放到什么位置上」
→ 「我的孕期数据」不再单独成块,和工具宫格合成一个模块「我的孕期工具」:
① 上面 2 列「数据卡」= 首页工具里排在最前的 4 个记录类工具(胎动 / 体重 / 血压 / 血糖 / 宫缩 / 估重),直接显示数据;
② 下面 4 列小图标宫格 = 其余工具 + 固定的「全部工具」出口。
右上角「编辑」打开 #htSheet 自选首页工具和顺序(app.js renderHomeTools / htEditor,存 localStorage mmbox.homeTools.v1)。
没自定义过 = 默认顺序 + 孕周没到换替补;自定义后以用户为准,没到孕周的显示「孕N周开放」、点了弹原提示。
数据卡没登录身份 / 产后时不出,全部按小图标排(同原来 #vitals 的显隐口径)。
同日二改(用户看过预览):数据卡和编辑完全解耦 ——
① #vitals 数据卡:只看「用过哪个记录类工具」(胎动 / 体重 / 血压 / 血糖 / 宫缩 / 估重),有真实记录就出,最近记录的排前;
最多直接露 4 张,多的折叠在「展开全部 N 项数据」里;一条真实记录都没有时出 胎动 / 体重 / 血压 / 血糖 4 张示例数卡(不写「示例」)。
访客 / 产后不出。逻辑在 app.js renderVitals。
② #htGrid 图标宫格:完全由「编辑」决定显示哪些、什么顺序,最多 8 个(4×2),记录类工具也以图标出现,胶囊里放简短状态。
「全部工具」不占格子,挪到标题右侧「全部 ›」,与「编辑」并排。编辑弹层 #htSheet(app.js htEditor),存 localStorage mmbox.homeTools.v1。
没自定义过 = 默认 8 个 + 孕周没到换替补;自定义后以用户为准,没到孕周的显示「孕N周开放」、点了弹原提示。
各 id(#kickTodayHome / #weightVal / #calNext / #vtK…)和 data-mini / data-vt 照旧,接口回填、示例数、跳转都沿用。 -->
<div class="dock-head">
<h2 class="dock-title" id="dockTitle">我的孕期工具</h2>
<button class="ht-all" type="button" data-sheet="open">全部<span aria-hidden="true"> ›</span></button>
<button class="ht-edit" id="htEdit" type="button" aria-label="编辑首页工具"><svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16z" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/><path d="M13.5 6.5l4 4" stroke="currentColor" stroke-width="2.2"/></svg>编辑</button>
</div>
<p class="dock-note"><svg viewBox="0 0 24 24" width="11" height="11" aria-hidden="true"><path d="M11 2.6l1.9 5.2 5.2 1.9-5.2 1.9L11 16.8l-1.9-5.2L3.9 9.7l5.2-1.9L11 2.6z" fill="currentColor"/><path d="M18.4 14.6l.8 2.1 2.2.8-2.2.8-.8 2.1-.8-2.1-2.2-.8 2.2-.8.8-2.1z" fill="currentColor" opacity=".6"/></svg>妈妈每记录一次,宝宝就多一段成长记忆</p>
@@ -217,52 +219,47 @@
<span class="vt-top"><i class="vt-ic"><img src="/taidong/logo/logo-foot.svg" alt="" aria-hidden="true"></i><span class="vt-l">数胎动</span></span>
<span class="vt-v"><b id="vtK">--</b><small class="vt-u">次</small></span>
<span class="vt-f"><span class="vt-s" id="vtKs">去数一数 ›</span><span class="vt-d" id="vtKd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-it vt-w ripple" data-vt="weight" data-tool="yunqitizhong">
<span class="vt-top"><i class="vt-ic"><img src="/upload/picture/a6d7e61edee2ff0395cf00587a7c4036.svg" alt="" aria-hidden="true"></i><span class="vt-l">孕期体重</span></span>
<span class="vt-v"><b id="vtW">--</b><small class="vt-u">kg</small></span>
<span class="vt-f"><span class="vt-s" id="vtWs">去记一笔 ›</span><span class="vt-d" id="vtWd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-it vt-p ripple" data-vt="bp" data-tool="yun">
<span class="vt-top"><i class="vt-ic"><img src="/upload/picture/816227e3933211145bb8fc774ee32e82.svg" alt="" aria-hidden="true"></i><span class="vt-l">孕期血压</span></span>
<span class="vt-v"><b id="vtP">--</b><small class="vt-u">mmHg</small></span>
<span class="vt-f"><span class="vt-s" id="vtPs">去记一笔 ›</span><span class="vt-d" id="vtPd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-it vt-g ripple" data-vt="glu" data-tool="yunqixuetang">
<span class="vt-top"><i class="vt-ic"><img src="/upload/picture/fde3c920fae453f2642731e4135b88f5.png" alt="" aria-hidden="true"></i><span class="vt-l">孕期血糖</span></span>
<span class="vt-v"><b id="vtG">--</b><small class="vt-u">mmol/L</small></span>
<span class="vt-f"><span class="vt-s" id="vtGs">去记一笔 ›</span><span class="vt-d" id="vtGd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-it vt-c ripple hide" data-vt="curve" data-tool="gongsuopinlv">
<span class="vt-top"><i class="vt-ic"><img src="/upload/picture/1c02d720e4d23b09c996e859703b8b5e.svg" alt="" aria-hidden="true"></i><span class="vt-l">数宫缩</span></span>
<span class="vt-v"><b id="vtC">--</b><small class="vt-u"></small></span>
<span class="vt-f"><span class="vt-s" id="vtCs">去记一笔 ›</span><span class="vt-d" id="vtCd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-it vt-e ripple hide" data-vt="fweight" data-tool="taierguzhong">
<span class="vt-top"><i class="vt-ic"><img src="/upload/picture/715333a69fb67d8812ab093f342fa00f.png" alt="" aria-hidden="true"></i><span class="vt-l">胎儿估重</span></span>
<span class="vt-v"><b id="vtE">--</b><small class="vt-u"></small></span>
<span class="vt-f"><span class="vt-s" id="vtEs">去测算 ›</span><span class="vt-d" id="vtEd"></span></span>
<span class="vt-lk"></span>
</button>
<button type="button" class="vt-more hide" id="vtMore" aria-expanded="false"></button>
</div>
<!-- 图标:App 表里各应用的正式 logo(app.js applyToolLogos),「全部工具」没有应用 logo,保留渐变底白图形。
data-tool = App 表 identifier(排序 / 显隐按它认);不是 data-mini 的普通应用格由 JS 按需生成(.tg-app)。 -->
<div class="tg-grid" id="htGrid">
<button class="tg kick ripple hide" data-mini="kick" data-tool="taidong">
<button class="tg kick ripple" data-mini="kick" data-tool="taidong">
<i class="tg-ic lg"><img src="/taidong/logo/logo-foot.svg" alt="" aria-hidden="true"></i>
<span class="tg-n">数胎动</span>
<!-- 默认中性占位,不写演示数字(真实值由卡片接口写入) -->
<span class="tg-s">今天 <b id="kickTodayHome">--</b> 次</span>
</button>
<button class="tg weight ripple hide" data-mini="weight" data-tool="yunqitizhong">
<button class="tg weight ripple" data-mini="weight" data-tool="yunqitizhong">
<i class="tg-ic lg"><img src="/upload/picture/a6d7e61edee2ff0395cf00587a7c4036.svg" alt="" aria-hidden="true"></i>
<span class="tg-n">孕期体重</span>
<span class="tg-s" id="weightVal">--</span>
<span class="tg-s" id="weightVal">记一笔</span>
</button>
<button class="tg cal ripple" data-mini="cal" data-tool="chanjianrili">
<i class="tg-ic lg"><img src="/chanjian-rili/logo/logo-7-dots.svg" alt="" aria-hidden="true"></i>
@@ -279,21 +276,16 @@
<span class="tg-n">数宫缩</span>
<span class="tg-s" data-wk="cn">—</span>
</button>
<button class="tg bp ripple hide" data-mini="bp" data-tool="yun">
<button class="tg bp ripple" data-mini="bp" data-tool="yun">
<i class="tg-ic lg"><img src="/upload/picture/816227e3933211145bb8fc774ee32e82.svg" alt="" aria-hidden="true"></i>
<span class="tg-n">孕期血压</span>
<span class="tg-s">量一量</span>
</button>
<button class="tg glu ripple hide" data-mini="glu" data-tool="yunqixuetang">
<button class="tg glu ripple" data-mini="glu" data-tool="yunqixuetang">
<i class="tg-ic lg"><img src="/upload/picture/fde3c920fae453f2642731e4135b88f5.png" alt="" aria-hidden="true"></i>
<span class="tg-n">孕期血糖</span>
<span class="tg-s">记一笔</span>
</button>
<button class="tg all ripple" data-sheet="open">
<i class="tg-ic"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="#fff" stroke-width="2.2"><rect x="4" y="4" width="6.5" height="6.5" rx="2"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="2"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="2"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="2"/></svg></i>
<span class="tg-n">全部工具</span>
<span class="tg-s">更多</span>
</button>
</div>
<style>
/* —— 工具宫格(仿育儿首页) —— */
@@ -315,7 +307,6 @@
.tg.curve{--tg-bg:linear-gradient(150deg,#B39BEA,#8366CC);--tg-pl:#EFE8FC;--tg-tx:#6F50BC;--tg-sh:rgba(131,102,204,.28)}
.tg.bp{--tg-bg:linear-gradient(150deg,#F58FB0,#DC5584);--tg-pl:#FDE6EE;--tg-tx:#C93F70;--tg-sh:rgba(220,85,132,.28)}
.tg.glu{--tg-bg:linear-gradient(150deg,#6FD0C2,#2FA192);--tg-pl:#DFF5F1;--tg-tx:#22887B;--tg-sh:rgba(47,161,146,.28)}
.tg.all{--tg-bg:linear-gradient(150deg,#F2C17E,#E19A4C);--tg-pl:#FBEEDC;--tg-tx:#B9772D;--tg-sh:rgba(225,154,76,.28)}
.tg.is-warn .tg-s{background:#FDE3E0;color:#D2453B}
html.night .tg-n{color:#F1E6EE}
html.night .tg-s{background:rgba(255,255,255,.1);color:#E9D6E2}
@@ -323,7 +314,9 @@
.tg.hide,.vt-it.hide,.vt.hide{display:none}
.tg-n{max-width:100%;overflow:hidden;text-overflow:ellipsis}
.tg-app{--tg-pl:#F7EEE8;--tg-tx:#9A6450;--tg-bg:linear-gradient(150deg,#F2C17E,#E19A4C)}
.ht-edit{margin-left:auto;display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 12px;border:1px solid #F1D9CF;border-radius:99px;background:#FFF6F2;font:inherit;font-size:12.5px;font-weight:700;color:#D9573A}
.ht-all{margin-left:auto;flex:none;padding:4px 2px;border:0;background:none;font:inherit;font-size:12.5px;font-weight:600;color:#8E7773;white-space:nowrap}
.ht-all:active{opacity:.5}
.ht-edit{flex:none;white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 12px;border:1px solid #F1D9CF;border-radius:99px;background:#FFF6F2;font:inherit;font-size:12.5px;font-weight:700;color:#D9573A}
.ht-edit:active{opacity:.6}
.dock-head .dock-all{display:none}
/* 孕周没到(用户自己选了放在首页):图标淡一点 + 右上角「孕N周开放」,点了弹原提示 */
@@ -357,11 +350,12 @@
.vt-s.lo{background:rgba(199,122,30,.14);color:#C77A1E}
.vt-d{font-size:11px;color:#6E5A57;white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}
.vt-d:empty{display:none}
.vt-lk{display:none}
.vt-it.is-locked:not([data-has="1"]) .vt-v,.vt-it.is-locked:not([data-has="1"]) .vt-f{display:none}
.vt-it.is-locked:not([data-has="1"]) .vt-lk{display:block;margin-top:4px;font-size:12px;line-height:1.5;color:#8E7773}
.vt-lk b{display:block;font-size:17px;font-weight:800;color:#9A8380}
.vt-it.is-locked .vt-ic{opacity:.55;filter:saturate(.6)}
/* 超过 4 项数据时的折叠开关(占满一行) */
.vt-more{grid-column:1/-1;display:flex;align-items:center;justify-content:center;gap:4px;height:32px;margin-top:-1px;border:0;border-radius:12px;background:#FBF4F1;font:inherit;font-size:12.5px;font-weight:700;color:#9A6450}
.vt-more.hide{display:none}
.vt-more svg{width:12px;height:12px;transition:transform .2s}
.vt-more[aria-expanded="true"] svg{transform:rotate(180deg)}
html.night .vt-more{background:rgba(255,255,255,.07);color:#E2C2B5}
/* 每格一层很淡的同色底(2026-10-31 用户:稍微加一点点背景色) */
.vt-k{--vt-c:#E9673F;--vt-bg:#FFF5F0}
.vt-w{--vt-c:#E5606A;--vt-bg:#FFF4F4}
@@ -1879,7 +1873,7 @@
<!-- ================= 编辑首页工具(2026-11-01,app.js htEditor) =================
上半「首页显示的工具」:长按整行(或按住右侧把手)拖动排序,↑↓ 兜底,「−」移除;
下半「更多孕期工具」:App 表里 category=孕期工具 的已上架应用,点「+」加入。上限 11 个 + 固定「全部工具」,下限 1 个。
下半「更多孕期工具」:App 表里 category=孕期工具 的已上架应用,点「+」加入。上限 8 个(4×2),下限 1 个;「全部工具」在模块标题右侧,不占格子。
关闭(完成 / 叉 / 点遮罩)即保存到 localStorage mmbox.homeTools.v1,首页立即重排;「恢复默认」= 删掉这个键,回到默认顺序 + 孕周替补。 -->
<div class="scrim hts-scrim" id="htScrim"></div>
<section class="hts" id="htSheet" role="dialog" aria-modal="true" aria-labelledby="htsT" aria-hidden="true">
@@ -1887,7 +1881,7 @@
<div class="hts-head">
<div>
<h2 id="htsT">编辑首页工具</h2>
<p id="htsSub">长按拖动排序,排在前面的记录工具会直接显示数据</p>
<p id="htsSub">长按拖动排序,首页最多放 8 个</p>
</div>
<button class="sheet-close" id="htsX" type="button" aria-label="关闭"><svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>
</div>
@@ -2322,7 +2316,7 @@
<!-- 2026-10-05 首页头图:宝宝时不时动一动小手小脚(独立文件,删掉这一行即下线) -->
<script src="js/baby-limbs.js?v=26101201"></script>
<script src="js/coach.js?v=26101211"></script>
<script src="js/birth-flow.js?v=26110101"></script>
<script src="js/birth-flow.js?v=26110102"></script>
<!-- 禁止下拉回弹(2026-10-11 统一规则,与孕期体重同一段):竖向拖动时从手指下往上找还能朝该方向滚的容器,找不到才拦;横滑、双指、可滚列表不受影响 -->
<script>

119
js/app.js
View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26110101';
var VER = '26110102';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -423,16 +423,21 @@
}
/* ================= 首页「我的孕期工具」:数据卡 + 图标宫格,用户自选(2026-11-01) =================
用户:「我的孕期数据和上面那个工具结合在一起时,是一个模块……上面所有的工具,用户可以去选择放哪一个,放到什么位置上。」
· 一个模块两层:#vitals 数据卡(2 列,列表里最靠前的 HT_CARDS 个记录类工具)+ #htGrid 小图标宫格(其余工具 + 固定「全部工具」)。
数据卡只在有身份、非产后时出现(原 #vitals 的口径);访客 / 产后全部按小图标排。
· 一个模块两层,二者完全解耦(同日二改,用户看过预览后定):
① #vitals 数据卡 = 「用过哪个记录类工具」:有真实记录就出,最近记录的在前,最多露 4 张、多的折叠(renderVitals);
一条真实记录都没有 → 胎动 / 体重 / 血压 / 血糖 4 张示例数卡。访客 / 产后不出。
② #htGrid 图标宫格 = 「编辑」列表:显示哪些、什么顺序,最多 HT_MAX=8 个(4×2),记录类工具也以图标出现。
「全部工具」不占格子,在标题右侧「全部 ›」。
· 顺序 = localStorage HT_KEY(identifier 数组)。没存过 = HT_DEFAULT + 孕周替补(原 swapHomeCards 规则:没到门槛的坑位按 SWAP_POOL 换);
存过 = 完全以用户为准,没到门槛的不替换,显示「孕N周开放」、点了由 jumpApp 弹原提示。
· 能放哪些工具:App 表 category=孕期工具 的已上架应用(APP_BY_ID)。应用列表回来前,用 HT_META_KEY 里上次缓存的名字 / logo / url 先画,
回来后下架的自动不出现(不在前端写名单)。
· 7 个老格子(data-mini)和 6 张数据卡的 DOM 一直都在,只挪位置 / 显隐 —— 卡片接口回填、此刻 tapMini 点它们照旧能用。 */
const HT_KEY = 'mmbox.homeTools.v1', HT_META_KEY = 'mmbox.homeTools.meta.v1';
const HT_MAX = 11, HT_MIN = 1, HT_CARDS = 4;
const HT_DEFAULT = ['taidong', 'yunqitizhong', 'yun', 'yunqixuetang', 'chanjianrili', 'taierguzhong', 'gongsuopinlv'];
const HT_MAX = 8, HT_MIN = 1, VT_SHOW = 4;
const HT_DEFAULT = ['taidong', 'yunqitizhong', 'chanjianrili', 'taierguzhong', 'gongsuopinlv', 'yun', 'yunqixuetang', 'yunqinengbuchi'];
const VT_ORDER = ['kick', 'weight', 'bp', 'glu', 'curve', 'fweight'];
const VT_TOOL = { kick: 'taidong', weight: 'yunqitizhong', bp: 'yun', glu: 'yunqixuetang', curve: 'gongsuopinlv', fweight: 'taierguzhong' }; /* 拿不到记录时间时的兜底顺序;前 4 个也是「没有任何记录」时的示例卡 */
const HT_DATA = { taidong: 'kick', yunqitizhong: 'weight', yun: 'bp', yunqixuetang: 'glu', gongsuopinlv: 'curve', taierguzhong: 'fweight' };
let htAppsLoaded = false;
let htMeta = {};
@@ -464,12 +469,15 @@
return null;
}
function htStore(a) { try { if (a) localStorage.setItem(HT_KEY, JSON.stringify(a)); else localStorage.removeItem(HT_KEY); } catch (e) {} }
/* 默认顺序 + 孕周替补(替补顺序 / 不重复的规则同原 swapHomeCards) */
/* 默认顺序 + 孕周替补(替补顺序 / 不重复的规则同原 swapHomeCards)。
SWAP_POOL 用完了(能不能吃已在默认里)再从 App 表里挑一个没放上来、也没被孕周锁住的孕期工具顶上,免得留个锁着的格子。 */
function htDefault() {
const A = htApps() || {}, used = new Set(HT_DEFAULT);
return HT_DEFAULT.map(id => {
if (!stageLocked(STAGE_GATE.find(x => x.id === id))) return id;
const sub = SWAP_POOL.find(x => !used.has(x.id) && A[x.id] && A[x.id].url) || SWAP_POOL.find(x => !used.has(x.id));
const sub = SWAP_POOL.find(x => !used.has(x.id) && A[x.id] && A[x.id].url) ||
Object.keys(A).map(k => ({ id: k })).find(x => !used.has(x.id) && A[x.id].url && !stageLocked(STAGE_GATE.find(g => g.id === x.id))) ||
SWAP_POOL.find(x => !used.has(x.id));
if (!sub) return id;
used.add(sub.id);
return sub.id;
@@ -483,7 +491,6 @@
}
function htCurrent() { return (htLoad() || htDefault()).filter(htKnown); }
function htVtOn() { return !!hashParam('openid') && !state.postpartum; }
function htCardIds(list) { return htVtOn() ? list.filter(id => HT_DATA[id]).slice(0, HT_CARDS) : []; }
function htRel(u) { /* 同源相对路径(AGENTS §13):本站或 mp 业务域名的绝对地址 → /<slug>/… */
try { const x = new URL(u, location.href); if (x.host === location.host || x.host === 'mp.yizhezhe.com') return x.pathname + x.search + x.hash; } catch (e) {}
return u;
@@ -524,42 +531,67 @@
const on = stageLocked(g);
el.classList.toggle('is-locked', on);
if (on) el.dataset.lock = '孕' + g.wk + '周开放'; else delete el.dataset.lock;
if (el.classList.contains('vt-it')) {
const lk = el.querySelector('.vt-lk');
if (lk) lk.innerHTML = on ? '<b>孕 ' + g.wk + ' 周开放</b>' + (function () { const left = g.wk * 7 - knownGA(); return left >= 7 ? '还有 ' + Math.ceil(left / 7) + ' 周' : '还有 ' + left + ' 天'; })() : '';
}
return on;
}
/* 图标宫格:完全按编辑列表(最多 8 个)排 */
function renderHomeTools() {
const grid = $('#htGrid'), cards = $('#vitals');
if (!grid || !cards) return;
const list = htCurrent(), cardIds = htCardIds(list);
const allBtn = grid.querySelector('.tg.all');
const want = new Set();
cardIds.forEach(id => { const it = cards.querySelector('.vt-it[data-tool="' + id + '"]'); if (it) { want.add(it); cards.appendChild(it); } });
$$('.vt-it[data-tool]', cards).forEach(it => it.classList.toggle('hide', !want.has(it)));
const vis = $$('.vt-it[data-tool]', cards).filter(it => want.has(it));
vis.forEach((it, i) => it.classList.toggle('span2', vis.length % 2 === 1 && i === vis.length - 1));
cards.classList.toggle('hide', !vis.length);
const grid = $('#htGrid');
if (!grid) return;
const list = htCurrent().slice(0, HT_MAX);
const shown = new Set();
list.forEach(id => {
if (cardIds.indexOf(id) >= 0) return;
const t = grid.querySelector('.tg[data-tool="' + id + '"]') || htMakeCell(id);
if (!t) return;
shown.add(t);
grid.insertBefore(t, allBtn);
grid.appendChild(t);
});
grid.appendChild(allBtn);
$$('.tg[data-tool]', grid).forEach(t => t.classList.toggle('hide', !shown.has(t)));
/* 孕周门槛角标:只有用户自己放上来的才会锁着出现(默认列表里锁着的已经换成替补) */
let anyLock = false;
$$('.tg[data-tool], .vt-it[data-tool]', grid.parentNode).forEach(el => {
$$('.tg[data-tool]', grid).forEach(el => {
const on = htLockMark(el, STAGE_GATE.find(x => x.id === el.dataset.tool));
if (on && !el.classList.contains('hide') && el.classList.contains('tg')) anyLock = true;
if (on && shown.has(el)) anyLock = true;
});
grid.classList.toggle('has-locked', anyLock);
renderVitals();
applyToolLogos();
}
/* 数据卡:只看「用过哪个记录类工具」,和编辑列表无关。
· 有真实记录(vtSet 写的 data-has)就出;按最近一次记录时间(data-ts,毫秒)新的在前,拿不到时间的排在有时间的后面、按 VT_ORDER。
· 直接露 VT_SHOW=4 张,多的折叠,「展开全部 N 项数据」点开(展开状态不记忆)。
· 一条真实记录都没有 → VT_ORDER 前 4 个(胎动 / 体重 / 血压 / 血糖)带示例数;孕周没到的工具没记录自然不出(示例卡里也去掉)。
· 访客 / 产后整块不出。 */
let vtOpen = false;
function renderVitals() {
const box = $('#vitals'), more = $('#vtMore');
if (!box) return;
if (!htVtOn()) { box.classList.add('hide'); return; }
const its = {};
$$('.vt-it[data-vt]', box).forEach(it => { its[it.dataset.vt] = it; });
const ord = k => VT_ORDER.indexOf(k);
let keys = VT_ORDER.filter(k => its[k] && its[k].dataset.has);
const real = keys.length > 0;
if (real) {
keys.sort((a, b) => {
const ta = +its[a].dataset.ts || 0, tb = +its[b].dataset.ts || 0;
return (tb - ta) || (ord(a) - ord(b));
});
} else {
keys = VT_ORDER.slice(0, 4).filter(k => !stageLocked(STAGE_GATE.find(g => g.id === VT_TOOL[k])));
}
const fold = real && keys.length > VT_SHOW;
const vis = fold && !vtOpen ? keys.slice(0, VT_SHOW) : keys;
keys.forEach(k => box.insertBefore(its[k], more));
Object.keys(its).forEach(k => its[k].classList.toggle('hide', vis.indexOf(k) < 0));
vis.forEach((k, i) => its[k].classList.toggle('span2', vis.length % 2 === 1 && i === vis.length - 1));
if (more) {
more.classList.toggle('hide', !fold);
more.setAttribute('aria-expanded', vtOpen ? 'true' : 'false');
more.innerHTML = (vtOpen ? '收起' : '展开全部 ' + keys.length + ' 项数据') + '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 4.5L6 8l3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
box.classList.toggle('hide', !vis.length);
}
(function () { const m = $('#vtMore'); if (m) m.addEventListener('click', e => { e.stopPropagation(); vtOpen = !vtOpen; renderVitals(); haptic(); }); })();
/* 原「首页工具卡按孕周换替补」入口(markStageLocks 调):现在替补并进 htDefault,这里只重排 */
function swapHomeCards() { renderHomeTools(); }
@@ -944,14 +976,13 @@
return ids;
}
function render() {
const cards = htCardIds(draft);
list.innerHTML = draft.map((id, i) => {
const f = htInfo(id), g = STAGE_GATE.find(x => x.id === id), lk = stageLocked(g);
const nm = f.name || f.short || id;
return '<li class="hts-row" data-id="' + esc(id) + '">' +
'<button type="button" class="hts-rm" data-act="rm" aria-label="从首页移除' + esc(nm) + '">' + ICO.rm + '</button>' +
icoHtml(f) +
'<span class="hts-n"><i>' + esc(nm) + '</i>' + (cards.indexOf(id) >= 0 ? '<em class="hts-tag">显示数据</em>' : '') + (lk ? '<em class="hts-tag lk">孕' + g.wk + '周开放</em>' : '') + '</span>' +
'<span class="hts-n"><i>' + esc(nm) + '</i>' + (lk ? '<em class="hts-tag lk">孕' + g.wk + '周开放</em>' : '') + '</span>' +
'<button type="button" class="hts-mv" data-act="up" aria-label="上移"' + (i === 0 ? ' disabled' : '') + '>' + ICO.up + '</button>' +
'<button type="button" class="hts-mv" data-act="dn" aria-label="下移"' + (i === draft.length - 1 ? ' disabled' : '') + '>' + ICO.dn + '</button>' +
'<span class="hts-hd" aria-hidden="true">' + ICO.hd + '</span></li>';
@@ -963,9 +994,7 @@
return '<button type="button" class="hts-add' + (full ? ' is-full' : '') + '" data-id="' + esc(id) + '" aria-label="加到首页:' + esc(f.name || f.short) + '">' + icoHtml(f) + '<span>' + esc(f.short || f.name) + '</span><b aria-hidden="true">+</b></button>';
}).join('') : '<div class="hts-empty">孕期工具都已经放在首页啦</div>';
$('#htsCnt').textContent = draft.length + ' / ' + HT_MAX;
$('#htsSub').textContent = htVtOn()
? '长按拖动排序,前 ' + HT_CARDS + ' 个记录类工具直接显示数据'
: '长按拖动排序,点「−」移出首页';
$('#htsSub').textContent = '长按拖动排序,首页最多放 ' + HT_MAX + ' 个';
}
function change(next) { draft = next; dirty = true; resetAsked = false; render(); }
function open() {
@@ -3980,7 +4009,7 @@
if (w) {
const big = $('#weightVal');
if (big) {
big.textContent = w.value || '--';
big.textContent = w.value || '记一笔'; /* 2026-11-01:没记过体重时胶囊给动作,不显示「--」 */
/* 单位单独小字;只在有值时补,避免出现「-- kg」 */
if (w.value && w.unit) {
const sm = document.createElement('small');
@@ -5542,11 +5571,12 @@
const VT_TG = { bp: '量一量', glu: '记一笔' }; /* 小图标格上的默认胶囊(同 index.html) */
/* 2026-10-31 用户:去掉「示例」两个字,没记过的格直接按正常数据的样子显示(值 / 胶囊 / 胶囊样式 / 时间) */
const VT_DEMO = { kick: ['12', '今天', '', '最近 14:20'], weight: ['58.2', '比孕前 +4.6', '', '今天记录'], bp: ['115/75', '正常', 'ok', '昨天记录'], glu: ['5.1', '达标', 'ok', '今天 空腹'] }; /* 这两格有记录才出现 */
function vtSet(key, val, pill, cls, when) {
function vtSet(key, val, pill, cls, when, ts) {
const it = $('.vt-it[data-vt="' + key + '"]'); if (!it) return;
const id = VT_ID[key];
it.classList.toggle('empty', !val);
it.dataset.has = val ? '1' : '';
it.dataset.ts = val && ts ? String(ts) : ''; /* 最近一次记录时间(毫秒),数据卡按它排先后 */
/* 2026-10-31 用户定:没记过的格放示例数(像育儿首页「宝宝的记录」那样标「示例」),一记就换成自己的 */
const dm = !val && VT_DEMO[key];
it.classList.toggle('demo', !!dm);
@@ -5558,22 +5588,23 @@
if (tgS) tgS.textContent = val || VT_TG[key];
vtLayout();
}
/* 2026-11-01 起数据卡出哪几张、什么顺序由首页工具列表决定(renderHomeTools):不再「记过的排前面 / 没记录就藏」,
用户放在哪就在哪;单数时最后一格占满一行也在那边算。 */
function vtLayout() { renderHomeTools(); }
/* 2026-11-01 起数据卡出哪几张、什么顺序、折叠都在 renderVitals(只看有没有真实记录 + 记录时间,和编辑列表无关)。 */
function vtLayout() { renderVitals(); }
if (location.pathname.indexOf('/mama-box-preview/') === 0) window.__mbVtSet = vtSet; /* 预览专用:截图 / 联调时手动喂数据卡,正式版不暴露 */
/* 胎动 / 数宫缩 / 胎儿估重 来自卡片接口(tool-cards),applyRemoteCards 收到就喂进来 */
function vtCards(r) {
const by = {}; ((r && r.cards) || []).forEach(c => { if (c && c.slot) by[c.slot] = c; });
const k = by.kick;
if (k) vtSet('kick', !k.empty && +k.value > 0 ? String(k.value) : '', '今天', '', String(k.sub || '').replace(/^.*·\s*/, ''));
const at = c => (c && c.at && Date.parse(c.at)) || 0; /* 卡片接口的最近记录时间 */
if (k) vtSet('kick', !k.empty && +k.value > 0 ? String(k.value) : '', '今天', '', String(k.sub || '').replace(/^.*·\s*/, ''), at(k));
const c = by.curve;
if (c) vtSet('curve', !c.empty && c.value ? c.value + (c.unit || '') : '', '最近一次', '', c.sub || '');
if (c) vtSet('curve', !c.empty && c.value ? c.value + (c.unit || '') : '', '最近一次', '', c.sub || '', at(c));
const f = by.fweight;
if (f) vtSet('fweight', !f.empty && (f.value || f.text) ? (f.value ? f.value + (f.unit || '') : f.text) : '', '', '', f.sub || '');
if (f) vtSet('fweight', !f.empty && (f.value || f.text) ? (f.value ? f.value + (f.unit || '') : f.text) : '', '', '', f.sub || '', at(f));
}
function loadVitals() {
if (!$('#vitals')) return;
renderHomeTools(); /* 数据卡显隐跟身份 / 产后走(htVtOn),这里按最新状态排一次 */
renderVitals(); /* 数据卡显隐跟身份 / 产后走(htVtOn),这里按最新状态排一次 */
if (!htVtOn()) return;
_vtAt = Date.now();
vtGet('yunqi-tizhong', '/state').then(j => {
@@ -5583,7 +5614,7 @@
if (!last) { vtSet('weight', ''); return; }
const pre = pw && !pw.demo && pw.profile && +pw.profile.preWeight;
const gain = pre ? Math.round((last.weight - pre) * 10) / 10 : null;
vtSet('weight', (+last.weight).toFixed(1), gain != null ? '比孕前 ' + (gain >= 0 ? '+' : '') + gain : '', '', vtDay(last.date) + '记录');
vtSet('weight', (+last.weight).toFixed(1), gain != null ? '比孕前 ' + (gain >= 0 ? '+' : '') + gain : '', '', vtDay(last.date) + '记录', Math.max(+last.updatedAt || 0, Date.parse(String(last.date || '').slice(0, 10) + 'T12:00:00') || 0));
});
vtGet('yunqi-xueya', '/xueya/records?limit=5').then(j => {
const rs = ((j && j.items) || []).filter(r => r && r.systolic && r.diastolic).sort((a, b) => String(b.measured_at).localeCompare(String(a.measured_at)));
@@ -5592,7 +5623,7 @@
const sy = +r.systolic, di = +r.diastolic;
/* 孕期口径:≥140/90 偏高(≥160/110 很高),<90/60 偏低 */
const st = (sy >= 160 || di >= 110) ? ['很高,请就医', 'hi'] : (sy >= 140 || di >= 90) ? ['偏高', 'hi'] : (sy < 90 || di < 60) ? ['偏低', 'lo'] : ['正常', 'ok'];
vtSet('bp', sy + '/' + di, st[0], st[1], vtDay(r.measured_at) + '记录');
vtSet('bp', sy + '/' + di, st[0], st[1], vtDay(r.measured_at) + '记录', Date.parse(r.measured_at) || 0);
});
vtGet('yunqi-xuetang', '/state').then(j => {
const tm = vtVal(j && j.items, 'tmrj.v1');
@@ -5601,7 +5632,7 @@
if (!r) { vtSet('glu', ''); return; }
const t = (tm.targets && tm.targets[r.slot]) || VT_TGT[r.slot] || [3.3, 7.8];
const st = r.value > t[1] ? ['偏高', 'hi'] : r.value < t[0] ? ['偏低', 'lo'] : ['达标', 'ok'];
vtSet('glu', (+r.value).toFixed(1), st[0], st[1], vtDay(r.ts) + (VT_SLOT[r.slot] ? ' ' + VT_SLOT[r.slot] : ''));
vtSet('glu', (+r.value).toFixed(1), st[0], st[1], vtDay(r.ts) + (VT_SLOT[r.slot] ? ' ' + VT_SLOT[r.slot] : ''), +r.ts || 0);
});
}
$$('.vt-it[data-vt]').forEach(b => b.addEventListener('click', () => {