首页工具区下新增「我的孕期数据」:体重/血压/血糖自动读取 v26103104

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-10-31 11:46:08 +00:00
parent 750be14823
commit fc67dd8628
2 changed files with 158 additions and 3 deletions

View File

@@ -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');