首页工具区下新增「我的孕期数据」:体重/血压/血糖自动读取 v26103104
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
62
index.html
62
index.html
@@ -7,7 +7,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>兜爸心选 · 孕育陪伴</title>
|
||||
<!-- V:26103103 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<!-- V:26103104 缓存版本标记:改 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) -->
|
||||
@@ -285,6 +285,64 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2026-10-31 用户定:工具宫格下面加「我的孕期数据」——体重 / 血压 / 血糖最近一次的数值,
|
||||
妈妈在对应工具里填完,回到首页自动刷新(app.js loadVitals:各工具自己的云端存储,只读、只认非示例记录)。
|
||||
有身份 + 孕期才显示;产后 / 访客整块隐藏。点哪格就进哪个工具。 -->
|
||||
<div class="vt hide" id="vitals">
|
||||
<div class="vt-h"><b>我的孕期数据</b><span>在工具里记一笔,这里自动更新</span></div>
|
||||
<div class="vt-row">
|
||||
<button type="button" class="vt-it vt-w ripple" data-vt="weight">
|
||||
<span class="vt-top"><i class="vt-ic"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="4" width="17" height="16" rx="5" fill="currentColor" opacity=".18"/><rect x="3.5" y="4" width="17" height="16" rx="5" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8.5 10.5a3.5 3.5 0 017 0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M12 10.5l1.6-1.8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></i><span class="vt-l">体重<small>kg</small></span></span>
|
||||
<span class="vt-v"><b id="vtW">--</b></span>
|
||||
<span class="vt-s" id="vtWs">去记一笔</span>
|
||||
<span class="vt-d" id="vtWd"></span>
|
||||
</button>
|
||||
<button type="button" class="vt-it vt-p ripple" data-vt="bp">
|
||||
<span class="vt-top"><i class="vt-ic"><svg viewBox="0 0 24 24" aria-hidden="true"><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" fill="currentColor" opacity=".18"/><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" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6.5 12.5h3l1.3-2.3 2 4.3 1.3-2h3.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></i><span class="vt-l">血压<small>mmHg</small></span></span>
|
||||
<span class="vt-v"><b id="vtP">--</b></span>
|
||||
<span class="vt-s" id="vtPs">去记一笔</span>
|
||||
<span class="vt-d" id="vtPd"></span>
|
||||
</button>
|
||||
<button type="button" class="vt-it vt-g ripple" data-vt="glu">
|
||||
<span class="vt-top"><i class="vt-ic"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5s6 6.6 6 10.9a6 6 0 01-12 0C6 10.1 12 3.5 12 3.5z" fill="currentColor" opacity=".18"/><path d="M12 3.5s6 6.6 6 10.9a6 6 0 01-12 0C6 10.1 12 3.5 12 3.5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M9.3 14.6a2.8 2.8 0 002.7 2.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></i><span class="vt-l">血糖<small>mmol/L</small></span></span>
|
||||
<span class="vt-v"><b id="vtG">--</b></span>
|
||||
<span class="vt-s" id="vtGs">去记一笔</span>
|
||||
<span class="vt-d" id="vtGd"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.vt{margin-top:12px;padding:12px;border-radius:20px;background:linear-gradient(180deg,#FFF3EE,#FFFDFB);border:1px solid #F7E0D7}
|
||||
.vt.hide{display:none}
|
||||
.vt-h{display:flex;align-items:baseline;gap:8px;margin:0 2px 10px}
|
||||
.vt-h b{font-size:15.5px;color:#3B2A2A}
|
||||
.vt-h span{font-size:11.5px;color:#A88E89}
|
||||
.vt-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
||||
.vt-it{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:10px 10px 10px;border-radius:16px;background:#fff;box-shadow:0 4px 12px rgba(201,120,96,.09);text-align:left;min-width:0;border:0;font:inherit;color:inherit}
|
||||
.vt-top{display:flex;align-items:center;gap:5px;width:100%}
|
||||
.vt-ic{flex:none;width:24px;height:24px;border-radius:8px;display:grid;place-items:center}
|
||||
.vt-ic svg{width:17px;height:17px}
|
||||
.vt-l{display:flex;flex-direction:column;font-size:12.5px;line-height:1.15;color:#6E5A57;font-weight:600;white-space:nowrap;min-width:0}
|
||||
.vt-l small{font-size:9.5px;line-height:1.2;color:#B7A29D;font-weight:500}
|
||||
.vt-v{margin-top:2px;white-space:nowrap}
|
||||
.vt-v b{font-size:23px;line-height:1.15;font-weight:800;letter-spacing:-.5px;color:#3B2A2A;font-variant-numeric:tabular-nums}
|
||||
.vt-it.empty .vt-v b{color:#DCCBC6}
|
||||
.vt-s{font-size:11px;line-height:1.35;padding:1px 8px;border-radius:99px;background:#FFEDE6;color:#D9573A;white-space:nowrap}
|
||||
.vt-s:empty{display:none}
|
||||
.vt-it.empty .vt-s{background:none;padding:1px 0;color:#E0775C;font-weight:600}
|
||||
.vt-s.ok{background:#E6F5EC;color:#2F8A5E}
|
||||
.vt-s.hi{background:#FDE7E5;color:#D2453B}
|
||||
.vt-s.lo{background:#FFF1DC;color:#C77A1E}
|
||||
.vt-d{font-size:10.5px;color:#B09C98;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
||||
.vt-d:empty{display:none}
|
||||
.vt-w .vt-ic{background:#FFE9E1;color:#F4593C}
|
||||
.vt-p .vt-ic{background:#F1EAFB;color:#8A63C9}
|
||||
.vt-g .vt-ic{background:#E4F1FC;color:#2F86C9}
|
||||
html.night .vt{background:#2C2433;border-color:#3D3346}
|
||||
html.night .vt-it{background:#372E40;box-shadow:none}
|
||||
html.night .vt-h b,html.night .vt-v b{color:#F3E8F0}
|
||||
html.night .vt-l{color:#CDBBC8}
|
||||
</style>
|
||||
<!-- 此刻(2026-10-01 需求文档 v1.0 第二节;同日两次改版)。数据 GET /api/mp/public/mama-box/pulse,渲染在 app.js pulseCard()。
|
||||
⚠️ 模拟数据是产品负责人 2026-10-01 拍板的例外(AGENTS.md §4.12),只限这一条。
|
||||
用户 2026-10-01:「把此刻跟上面的孕育工具结合到一起……此刻相当于上面那个卡片的一个轻量化的展示」
|
||||
@@ -2068,7 +2126,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=26103103"></script>
|
||||
<script src="js/birth-flow.js?v=26103104"></script>
|
||||
|
||||
<!-- 禁止下拉回弹(2026-10-11 统一规则,与孕期体重同一段):竖向拖动时从手指下往上找还能朝该方向滚的容器,找不到才拦;横滑、双指、可滚列表不受影响 -->
|
||||
<script>
|
||||
|
||||
99
js/app.js
99
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26103103';
|
||||
var VER = '26103104';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -3355,6 +3355,7 @@
|
||||
applyRemoteAll();
|
||||
applyRemoteFootprint();
|
||||
applyRemoteCardsFetch();
|
||||
loadVitals();
|
||||
syncLaixinReminder();
|
||||
loadBlindbox().then(() => { // 孕期盲盒:要等 ② 的预产期落地(服务端按同一份上游视图算孕周)
|
||||
/* 2026-10-07 用户要盲盒直达链接:地址带 open=box(# 或 ? 里都行)→ 数据到了直接弹盲盒面板。
|
||||
@@ -5148,6 +5149,102 @@
|
||||
],
|
||||
} : null;
|
||||
|
||||
/* ---------- 我的孕期数据(体重 / 血压 / 血糖,2026-10-31 用户定) ----------
|
||||
上游 /user/app-cards 只聚合了五张工具卡,没有血压、血糖 → 这里直接读三个工具自己的数据(只读):
|
||||
· 体重 yunqi-tizhong:GET /taidong/api/state → pw_data_v1.weights(跳过 demo:true 示例记录)
|
||||
· 血糖 yunqi-xuetang:GET /taidong/api/state → tmrj.v1.readings(跳过 demo:1 与老种子 id 如 r29fasting)
|
||||
· 血压 yunqi-xueya :GET /taidong/api/xueya/records(服务端只存真实记录)
|
||||
每个工具单独 openid-login 换 token(state 按 app_id 分区),token 在 sessionStorage 缓存 20 分钟。
|
||||
回到前台 / bfcache 恢复时重拉:妈妈在工具里刚填完,回首页就看到新数。 */
|
||||
const VT_API = '/taidong/api';
|
||||
const VT_SLOT = { fasting: '空腹', b1: '早餐后1h', b2: '早餐后2h', l1: '午餐后1h', l2: '午餐后2h', d1: '晚餐后1h', d2: '晚餐后2h', bed: '睡前', night: '夜间' };
|
||||
const VT_TGT = { fasting: [3.3, 5.3], b1: [3.3, 7.8], l1: [3.3, 7.8], d1: [3.3, 7.8], b2: [3.3, 6.7], l2: [3.3, 6.7], d2: [3.3, 6.7], bed: [4.4, 6.7], night: [3.3, 6.7] };
|
||||
let _vtAt = 0;
|
||||
function vtToken(app) {
|
||||
const openid = hashParam('openid'), unionid = hashParam('unionid');
|
||||
const ck = 'mmbox.vt.tok.' + app;
|
||||
try { const c = JSON.parse(sessionStorage.getItem(ck) || 'null'); if (c && c.o === openid && Date.now() - c.t < 20 * 60e3) return Promise.resolve(c.k); } catch (e) {}
|
||||
return fetch(VT_API + '/auth/openid-login', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ openid: openid, unionid: unionid || null, app_id: app })
|
||||
}).then(r => (r.ok ? r.json() : null)).then(j => {
|
||||
const k = j && (j.token || (j.data && j.data.token)); if (!k) return null;
|
||||
try { sessionStorage.setItem(ck, JSON.stringify({ o: openid, k: k, t: Date.now() })); } catch (e) {}
|
||||
return k;
|
||||
});
|
||||
}
|
||||
function vtGet(app, path) {
|
||||
return vtToken(app).then(k => k ? fetch(VT_API + path, { headers: { Authorization: 'Bearer ' + k } }).then(r => (r.ok ? r.json() : null)) : null).catch(() => null);
|
||||
}
|
||||
function vtVal(items, key) { /* state 条目可能是 {value:"…json…"} 或直接值 */
|
||||
let v = items && items[key];
|
||||
if (v && typeof v === 'object' && 'value' in v) v = v.value;
|
||||
if (typeof v === 'string') { try { v = JSON.parse(v); } catch (e) { v = null; } }
|
||||
return v && typeof v === 'object' ? v : null;
|
||||
}
|
||||
function vtDay(d) { /* 'YYYY-MM-DD…' 或时间戳 → 今天 / 昨天 / M月D日 */
|
||||
const t = typeof d === 'number' ? new Date(d) : parseDay(String(d || '').slice(0, 10));
|
||||
if (!t || isNaN(t)) return '';
|
||||
const a = new Date(); a.setHours(0, 0, 0, 0);
|
||||
const b = new Date(t); b.setHours(0, 0, 0, 0);
|
||||
const n = Math.round((a - b) / DAY_MS);
|
||||
return n === 0 ? '今天' : n === 1 ? '昨天' : (b.getMonth() + 1) + '月' + b.getDate() + '日';
|
||||
}
|
||||
function vtSet(key, val, pill, cls, when) {
|
||||
const it = $('.vt-it[data-vt="' + key + '"]'); if (!it) return;
|
||||
const id = { weight: 'vtW', bp: 'vtP', glu: 'vtG' }[key];
|
||||
it.classList.toggle('empty', !val);
|
||||
$('#' + id).textContent = val || '--';
|
||||
const s = $('#' + id + 's'); s.textContent = val ? (pill || '') : '去记一笔 ›'; s.className = 'vt-s' + (val && cls ? ' ' + cls : '');
|
||||
$('#' + id + 'd').textContent = val ? (when || '') : '';
|
||||
}
|
||||
function loadVitals() {
|
||||
const box = $('#vitals'); if (!box) return;
|
||||
if (!hashParam('openid') || state.postpartum) { box.classList.add('hide'); return; }
|
||||
box.classList.remove('hide');
|
||||
_vtAt = Date.now();
|
||||
vtGet('yunqi-tizhong', '/state').then(j => {
|
||||
const pw = vtVal(j && j.items, 'pw_data_v1');
|
||||
const ws = ((pw && pw.weights) || []).filter(w => w && !w.demo && +w.weight > 0).sort((a, b) => String(a.date).localeCompare(String(b.date)) || (a.updatedAt || 0) - (b.updatedAt || 0));
|
||||
const last = ws[ws.length - 1];
|
||||
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) + '记录');
|
||||
});
|
||||
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)));
|
||||
const r = rs[0];
|
||||
if (!r) { vtSet('bp', ''); return; }
|
||||
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) + '记录');
|
||||
});
|
||||
vtGet('yunqi-xuetang', '/state').then(j => {
|
||||
const tm = vtVal(j && j.items, 'tmrj.v1');
|
||||
const rs = ((tm && tm.readings) || []).filter(r => r && !r.demo && !/^r\d{1,3}[a-z]/.test(String(r.id || '')) && +r.value > 0).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
||||
const r = rs[rs.length - 1];
|
||||
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] : ''));
|
||||
});
|
||||
}
|
||||
$$('.vt-it[data-vt]').forEach(b => b.addEventListener('click', () => {
|
||||
const k = b.dataset.vt;
|
||||
if (k === 'weight') { const c = $('.mini-card[data-mini="weight"]'); if (c) { c.click(); return; } }
|
||||
const id = { weight: 'yunqitizhong', bp: 'yun', glu: 'yunqixuetang' }[k];
|
||||
const app = APP_BY_ID[id];
|
||||
if (app && app.url) { jumpApp(app.url, app.identifier); return; }
|
||||
const path = { weight: '/yunqi-tizhong/', bp: '/yunqi-xueya/', glu: '/yunqi-xuetang/' }[k];
|
||||
haptic(); location.href = withIdentity(location.origin + path);
|
||||
}));
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible' && _vtAt && Date.now() - _vtAt > 3000) loadVitals();
|
||||
});
|
||||
window.addEventListener('pageshow', function (e) { if (e.persisted && _vtAt) loadVitals(); });
|
||||
|
||||
function applyRemoteCardsFetch() {
|
||||
if (DEMO_CARDS) { applyRemoteCards(DEMO_CARDS); return; }
|
||||
const openid = hashParam('openid');
|
||||
|
||||
Reference in New Issue
Block a user