/* 兜爸心选 · 页面访问记录(2026-11-07) 推送效果看板要看「推送打开后用户看了哪些页面」,用户同意做。后端 box_app:POST /api/mp/public/mama-box/pv(公开上报), 看板走 POST /api/mp/public/mama-box/pageviews/query(X-Api-Key)。说明见 box_app services/mama_box_pageview_serv.py。 一次「看一页」= 一个 visitId:进来 0.8 秒后报一条(停留 0),之后每 15 秒、切到后台、离开时报一次停留时长(只算页面在前台的时间)。 同一页里换内容也算新的一页:广场切笔记 / 话题 / 分类(pushState)、首页开关星空页(replaceState ?wh=1)—— 这里接管 history 自动识别,页面不用改。 首页 + 孕妈广场 + 测一测 + 专题页 / 计算器都引本文件(和 js/last-sub.js 放一起);新子页面记得加 script。 预览站 / 正式站由后端按路径分(/mama-box-preview/ → preview),这里不管。 11-07 同时报一份给数胎动后台(taidong.douba.top/admin「当天用户 / 全部用户」看每人逛了哪些页,375797cf 接的,app_id=mama-box): openid-login 拿令牌(存 sessionStorage)→ events/open 报一次进站(visit_id 同一会话共用,30 分钟没动静算新一次) → 每离开一页 / 切到后台报一条 events(path /mama-box/…、中文 title、这段前台停留);每次离开页面再报一条不带 title 的 「整次停留」(累计值,对方取最新)。只在有 openid、没退出登录时报;预览站不报(不混进正式数据)。 */ (function () { 'use strict'; if (window.__mbPV) return; window.__mbPV = 1; var API = '/api/mp/public/mama-box/pv'; var BEAT = 15000; var CAT_NAME = { family: '家庭婚姻', work: '职场方向', baby: '孕育经验' }; function param(k) { var m = ((location.hash || '') + '&' + (location.search || '')).match(new RegExp('[#?&]' + k + '=([^&]*)')); try { return m ? decodeURIComponent(m[1]).split('?')[0] : ''; } catch (e) { return ''; } } function who() { if (window.mbGate && window.mbGate.out) return { openid: '', unionid: '' }; /* 11-07 退出登录 = 访客 */ var o = param('openid'), u = param('unionid'); if (!o) { try { var c = JSON.parse(sessionStorage.getItem('mmbox.identity') || 'null'); if (c && c.openid && (!c.at || Date.now() - c.at < 432e5)) { o = c.openid; u = u || c.unionid || ''; } } catch (e) {} } return { openid: o, unionid: u }; } /* 当前地址 → 是哪一页。base = 站点根(/mama-box-v2/ 或 /mama-box-preview/),rel = 之后的部分 */ function where() { var p = location.pathname.replace(/index\.html$/, ''); var m = p.match(/^(\/[^/]+\/)(.*)$/) || [0, '/', '']; var rel = m[2], q = new URLSearchParams(location.search), r = { group: 'other', page: rel || 'home', title: document.title || '' }; var T = function (v) { return String(v || '').replace(/\s*·\s*(测一测|孕妈广场)$/, ''); }; if (!rel) { if (q.get('wh') === '1') { r.group = 'whisper'; r.page = 'whisper'; r.title = '孕妈悄悄话·星空页'; } else { r.group = 'home'; r.page = 'home'; r.title = '妈妈盒子首页'; } } else if (rel === 'guangchang/ceshi/') { var id = q.get('id') || '', t = (window.GC_TESTS || []).filter(function (x) { return x.id === id; })[0]; r.group = 'ceshi'; r.page = id ? 'ceshi/test' : 'ceshi/list'; r.testId = id; r.testTitle = t ? t.title : (id ? T(document.title) : ''); r.title = id ? '测一测·' + (r.testTitle || id) : '测一测·列表'; } else if (rel === 'guangchang/mine/') { r.group = 'plaza'; r.page = 'plaza/mine'; r.title = '孕妈广场·我的投稿'; } else if (rel === 'guangchang/') { var n = q.get('n') || '', tp = q.get('t') || '', c = q.get('c') || ''; var tid = tp || (n ? n.replace(/-\d+$/, '') : ''); var topic = (window.GC_TOPICS || []).filter(function (x) { return x.id === tid; })[0]; r.group = 'plaza'; if (tid) { r.topicId = tid; r.topicTitle = topic ? topic.name : ''; } if (n) { var d = window.GC_DATA && GC_DATA[tid], i = +(n.match(/-(\d+)$/) || [0, 0])[1]; r.page = 'plaza/note'; r.noteId = n; r.noteTitle = d && d.notes && d.notes[i - 1] ? d.notes[i - 1].title : ''; r.title = '孕妈广场·笔记详情'; } else if (tp) { r.page = 'plaza/topic'; r.title = '孕妈广场·话题'; } else if (c === 'ceshi') { r.group = 'ceshi'; r.page = 'ceshi/list'; r.title = '测一测·列表'; } else if (c) { r.page = 'plaza/cat'; r.title = '孕妈广场·' + (CAT_NAME[c] || c); } else if (q.get('write') === '1') { r.page = 'plaza/write'; r.title = '孕妈广场·写笔记'; } else if (q.get('fav') === '1') { r.page = 'plaza/fav'; r.title = '孕妈广场·我的收藏'; } else if (q.get('mine') === '1') { r.page = 'plaza/mine'; r.title = '孕妈广场·我的投稿'; } else { r.page = 'plaza/home'; r.title = '孕妈广场'; } } else if (/^topic\/[a-z0-9_-]+\/$/.test(rel)) { r.group = 'topic'; r.page = rel.replace(/\/$/, ''); r.title = T(document.title) || r.page; } r.key = r.page + '|' + (r.noteId || '') + '|' + (r.topicId || '') + '|' + (r.testId || ''); /* 净地址:不带身份(openid/unionid 在 hash,不记) */ r.path = location.pathname + (location.search || '').replace(/([?&])(openid|unionid)=[^&]*/g, '$1').replace(/[?&]+$/, ''); return r; } function send(o) { var s = JSON.stringify(o); try { if (navigator.sendBeacon && navigator.sendBeacon(API, new Blob([s], { type: 'text/plain' }))) return; } catch (e) {} try { fetch(API, { method: 'POST', body: s, keepalive: true, headers: { 'Content-Type': 'text/plain' } }).catch(function () {}); } catch (e) {} } function vid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6); } /* ---- 数胎动后台 ---- */ var TD = '/taidong/api/', TD_ON = /^\/mama-box-v2\//.test(location.pathname) || (function () { try { return localStorage.getItem('mmbox.tdtest') === '1'; } catch (e) { return false; } })() /* 预览自测:本机手动置 mmbox.tdtest=1 才报 */, TDK = 'mmbox.td.v1'; function tdState() { var st = null; try { st = JSON.parse(sessionStorage.getItem(TDK) || 'null'); } catch (e) {} var now = Date.now(); if (!st || now - (st.seen || 0) > 30 * 60000) st = { vid: 'mb-' + now + '-' + Math.random().toString(36).slice(2, 8), start: now, acc: 0, opened: 0, tok: st && st.tok, oid: st && st.oid }; st.seen = now; return st; } function tdSave(st) { try { sessionStorage.setItem(TDK, JSON.stringify(st)); } catch (e) {} } var tdTok = null; function tdToken() { if (!TD_ON) return Promise.resolve(''); var w = who(); if (!w.openid) return Promise.resolve(''); var st = tdState(); if (st.tok && st.oid === w.openid) return Promise.resolve(st.tok); if (tdTok) return tdTok; tdTok = fetch(TD + 'auth/openid-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app_id: 'mama-box', openid: w.openid, unionid: w.unionid || undefined }) }) .then(function (r) { return r.json(); }).then(function (j) { var t = j && (j.token || (j.data && j.data.token)) || ''; var s2 = tdState(); s2.tok = t; s2.oid = w.openid; tdSave(s2); return t; }).catch(function () { return ''; }).then(function (t) { tdTok = null; return t; }); return tdTok; } function tdPost(path, body, tok) { if (!tok) return; try { fetch(TD + path, { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + tok }, body: JSON.stringify(body) }).catch(function () {}); } catch (e) {} } function tdTitle(r) { var t = r.page === 'home' ? '首页' : r.page === 'whisper' ? '孕妈悄悄话 · 星空页' : r.page === 'plaza/note' ? '帖子详情 · ' + (r.noteTitle || r.noteId) : r.page === 'plaza/topic' ? '话题 · ' + (r.topicTitle || r.topicId) : r.page === 'ceshi/test' ? '测一测 · ' + (r.testTitle || r.testId) : r.group === 'topic' ? '专题 · ' + r.title : String(r.title || '').replace('·', ' · '); return t.slice(0, 40); } function tdOpen() { tdToken().then(function (tok) { var st = tdState(); if (!tok || st.opened) { tdSave(st); return; } st.opened = 1; tdSave(st); tdPost('events/open', { visit_id: st.vid, page: '/mama-box/', src: param('src') }, tok); }); } /* 一段前台停留结束(离开这一页 / 切到后台):报这一页 + 整次累计 */ function tdSeg(c, ms, leaving) { if (!TD_ON || !c || ms < 300) return; var st = tdState(); st.acc += ms; tdSave(st); var r = c.full || c.r, path = '/mama-box/' + (r.page === 'home' ? '' : r.page === 'whisper' ? 'whisper' : location.pathname.replace(/^\/[^/]+\//, '').replace(/index\.html$/, '').replace(/\/$/, '')); var ev = { visit_id: st.vid, path: path.replace(/\/$/, '') || '/mama-box/', title: tdTitle(r), duration_ms: ms, entered_at: c.segAt }; if (ev.path === '/mama-box') ev.path = '/mama-box/'; var site = { visit_id: st.vid, path: '/mama-box/', duration_ms: st.acc }; tdToken().then(function (tok) { tdPost('events', ev, tok); if (leaving) tdPost('events', site, tok); }); } var cur = null, timer = 0; function dur() { if (!cur) return 0; return cur.acc + (cur.shownAt ? Date.now() - cur.shownAt : 0); } function beat() { if (!cur || !cur.sent) return; var d = dur(); if (d - cur.lastDur < 1000) return; cur.lastDur = d; var w = who(); send({ visitId: cur.id, durationMs: d, openid: w.openid, unionid: w.unionid }); } function first(c) { if (c !== cur || c.sent) return; var r = where(); /* 晚 0.8 秒再取:专题页 / 测试页的标题、广场数据那时才就绪 */ if (r.key !== c.r.key) r = c.r; var w = who(); c.sent = true; c.full = r; send({ visitId: c.id, openid: w.openid, unionid: w.unionid, page: r.page, pageGroup: r.group, pageTitle: r.title, noteId: r.noteId || '', noteTitle: r.noteTitle || '', topicId: r.topicId || '', topicTitle: r.topicTitle || '', testId: r.testId || '', testTitle: r.testTitle || '', path: c.r.path, enteredAt: c.at, durationMs: dur() }); } function close() { if (!cur) return; if (cur.shownAt) tdSeg(cur, Date.now() - cur.segAt, true); if (cur.sent) beat(); else if (dur() >= 800) first(cur); cur = null; } function open() { var r = where(); if (cur && cur.r.key === r.key) return; close(); var c = cur = { id: vid(), r: r, at: Date.now(), segAt: Date.now(), acc: 0, shownAt: document.hidden ? 0 : Date.now(), sent: false, lastDur: 0 }; setTimeout(function () { first(c); }, 800); } ['pushState', 'replaceState'].forEach(function (k) { var f = history[k]; if (typeof f !== 'function') return; history[k] = function () { var x = f.apply(this, arguments); try { setTimeout(open, 0); } catch (e) {} return x; }; }); window.addEventListener('popstate', function () { setTimeout(open, 0); }); document.addEventListener('visibilitychange', function () { if (!cur) return; if (document.hidden) { if (cur.shownAt) { cur.acc += Date.now() - cur.shownAt; tdSeg(cur, Date.now() - cur.segAt, true); cur.shownAt = 0; } beat(); } else if (!cur.shownAt) { cur.shownAt = cur.segAt = Date.now(); tdOpen(); } }); window.addEventListener('pagehide', close); window.addEventListener('pageshow', function (e) { if (e.persisted) open(); }); /* 从后退缓存回来 = 又看一次 */ timer = setInterval(function () { if (!document.hidden) beat(); }, BEAT); open(); if (TD_ON) tdOpen(); })();