feat(今日脚印): AI 气泡改说真话 —— 文案取接口 digest,引导取 lead+unused (v26092217)
配合 box_app 的 B 方案(今日脚印换源到数胎动用户面 /user/daily/footprint)。
接口现在一次给全:事件 + digest(气泡文案)+ lead/unused(引导),于是首页不用再编数据。
- **AI 气泡**:文案由 fpRender() 从 `digest.text` 写入(真实文案,宝宝口气的 AI 句子),
`digest.warn` 为真时整句标红(.ai-warn)。取不到 digest 就整颗气泡收起。
⚠️ index.html 里那句写死的「妈妈,今天你数到我动了 3 次…」已删除 —— 它对任何用户都显示,
而且把 today 的假次数和真实时间轴摆在同一屏。现在这条规则是 **data-driven**(有文案才显示),
不再靠"有没有事件"猜。
- **引导占位**:文案用接口的 `lead`(如「宝妈,先从这几件事记起吧。」),应用用 `unused[]` ——
能对上 App 表的渲染成**可点芯片**(点击 jumpApp 进该应用),对不上(如不在 App 表的宝宝来信)只显示名字。
- **三种显隐模式**(fpRender 是模块显隐的唯一 owner):
rows 拿到事件 → 时间轴;guide 一条都没有(上游查无此人 / kind=fallback 今天没活动 /
只浏览没产出记录)→ 引导;hide 接口读不到 / degraded → 整块不显示。
原来「有档案但今天没记」的「还没有足迹」空态并入 guide(上游的 lead + 推荐应用比一句空态有用)。
- 删掉不再使用的 emptyFootprintStep()。
本地验证(桩后端,五种返回 × 资料延迟):rows → 时间轴 + 气泡显示**真实 digest 文案**;
guide → 上游 lead + 芯片(数胎动/胎儿估重可点、宝宝来信纯文本);linked=false → 引导兜底标题;
degraded / 接口 500 → 整块不显示。全程无 JS 异常。
This commit is contained in:
26
index.html
26
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:26092216 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<!-- V:26092217 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<link rel="stylesheet" href="mama-box.css">
|
||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
|
||||
@@ -316,10 +316,14 @@
|
||||
aria-label="宝宝的今日脚印,查看完整档案">
|
||||
<div class="step"><span class="st-time">今天</span><span class="st-dot"></span><span class="st-txt">正在获取今天的脚印…</span><span class="st-from"></span></div>
|
||||
</div>
|
||||
<!-- 无档案时的引导占位(fpApply('guide') 才显示;此时 #tlSteps 收起) -->
|
||||
<!-- 引导占位(fpApply('guide') 才显示,此时 #tlSteps 收起):
|
||||
文案与应用**都来自接口** —— lead 是上游自己的引导语(如「宝妈,先从这几件事记起吧。」),
|
||||
unused[] 是今天没用过的应用,能对上 App 表的会渲染成可点芯片(见 js 的 renderGuide)。
|
||||
这里的默认文案只在接口没给 lead 时兜底。 -->
|
||||
<div class="tl-guide" id="tlGuide" style="display:none">
|
||||
<div class="tl-guide-t">还没有你的脚印</div>
|
||||
<div class="tl-guide-s">去「数胎动」记一笔,这里就会长出你今天的脚印</div>
|
||||
<div class="tl-guide-t" id="tlGuideTitle">还没有你的脚印</div>
|
||||
<div class="tl-guide-s" id="tlGuideSub">去上面的工具记一笔,这里就会长出你今天的脚印</div>
|
||||
<div class="tl-guide-apps" id="tlGuideApps"></div>
|
||||
</div>
|
||||
<!-- 这一天的收尾节点。三个直接子元素刻意和 .step 一样排
|
||||
(42px 的左格 + 圆点 + 剩下的),点才落得进同一条竖线里。
|
||||
@@ -335,11 +339,15 @@
|
||||
<!-- 这句是 app.js 里 AI_LOGS[0].txt 的手写副本(这里要 <b> 和 <br> 排版,
|
||||
没法直接共用那个字符串)。那边改口吻,这里必须跟着改,
|
||||
否则首页气泡说「小家伙」、点进去小结说「我」,像两个人在写。
|
||||
⚠️ 它是**演示文案**(数字写死),所以只有拿到真实事件的当天才显示:
|
||||
.today-line 挂上 .no-ai 时整颗气泡收起(见 JS 的 fpApply)。
|
||||
待办:把「3 次」接到真实次数(卡片接口 kick.value / events 里数胎动那条),
|
||||
并给「今天没记」定一句不说假话的文案 —— 产品确认口径后再改。 -->
|
||||
<span class="ab-say">妈妈,今天你数到我动了 <b>3</b> 次,<br>我今天精神特别好。</span>
|
||||
✅ 2026-09-23 已接真源:句子的数据来自 box_app 的 /footprint/today
|
||||
(上游数胎动平台的用户面接口 /user/daily/footprint 的 `digest.text`),
|
||||
由 fpRender() 写入;`digest.warn` 为真时整句标红(.ai-warn)。
|
||||
⚠️ 但 AI 小结面板(详情页 renderAI → AI_LOGS)里那份仍是**演示文案**,
|
||||
与这里是两套数据 —— 面板要一并用真数据,得先有 mood/stats/建议的字段。 -->
|
||||
<!-- ⚠️ 文案由 js 的 fpRender() 从接口 `digest.text` 写入(今日脚印接口给的真文案)。
|
||||
原来这里写死「妈妈,今天你数到我动了 3 次…」,任何用户都看到 —— 已删。
|
||||
接口没给 digest(今天没活动 / 读不到)时整颗气泡由 .no-ai 收起,不留空壳。 -->
|
||||
<span class="ab-say"></span>
|
||||
</span>
|
||||
<!-- 「AI自动汇总你每日记录」按用户要求删了,这行现在只剩右边的入口。
|
||||
✅ 位置不用额外处理:.ab-foot em 本来就写着 margin-left:auto,
|
||||
|
||||
106
js/app.js
106
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26092216';
|
||||
var VER = '26092217';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -3105,44 +3105,70 @@
|
||||
⚠️ 竞态:本函数与 applyRemoteProfile 并行。profile 回来若判 pregEmpty 会
|
||||
隐藏时间轴,所以这里落笔前再看一眼 state.pregEmpty,别把刚藏好的放出来。 */
|
||||
var _footprintApplied = false;
|
||||
/* 今日脚印的显示规则(2026-09-22 产品口径)—— **四种模式,全部由 fpApply 一处决定**:
|
||||
rows 有档案 + 拿到事件 → 渲染真实事件
|
||||
empty 有档案 + 今天没记 → 「还没有足迹」
|
||||
guide **无档案**(上游查无此人 / 本地未关联)→ 引导占位 #tlGuide,收起时间轴
|
||||
hide 有档案但**接口读不到**(网络 / 非 200 / 上游故障且无旧值)→ **整块不显示**
|
||||
⚠️ 原来「读不到」是回退到 4 条**写死的演示行**(胎动 12 次 / 体重 58.2kg…)——
|
||||
上游一抖页面就开始编数据。那 4 行已从 index.html 删掉,换成一行为中性「正在获取…」。
|
||||
⚠️ 除「已建档 + rows」外一律给 .today-line 挂 .no-ai:AI 气泡里的次数是写死的演示文案,
|
||||
没有真实事件(或还没建档)时显示它等于说假话(见 index.html 里那段注释与待办)。
|
||||
⚠️ **未建档(pregEmpty)不再整块隐藏**:照接口结论显示(无档案→引导),
|
||||
所以新用户首页也能看到「去「数胎动」记一笔」的引导。 */
|
||||
/* 当前模式 + 事件缓存在这里 —— fpRender() 是**模块显隐的唯一 owner**。
|
||||
⚠️ 之前 syncPregnancy 也直接改 .today-line 的 display,两个 owner 会撞竞态:
|
||||
接口先判「不显示」(hide),profile 后回来又把模块显示回来 → degraded 场景就是这么漏出来的。 */
|
||||
let _fpMode = 'pending'; // pending(加载中) | rows | empty | guide | hide
|
||||
let _fpEvents = [];
|
||||
function fpApply(mode, events) { _fpMode = mode; _fpEvents = events || []; fpRender(); }
|
||||
/* 今日脚印(2026-09-23,B 方案:数据全部来自 box_app 的 /footprint/today,
|
||||
它的上游是数胎动平台的**用户面**接口 /user/daily/footprint):
|
||||
rows 拿到事件 → 时间轴;AI 气泡用接口给的 digest.text(**真文案**)
|
||||
guide 今天没有任何活动 / 上游查无此人 → 引导占位:文案用接口的 lead、应用用 unused[],
|
||||
对得上 App 表的还能直接点进该应用
|
||||
hide 接口读不到(网络 / 非 200 / 上游故障且无旧值)→ **整块不显示**
|
||||
⚠️ AI 气泡不再挂写死的那句「今天你数到我动了 3 次」:文案一律取 digest.text,取不到就收起气泡
|
||||
—— **data-driven**,不靠"有没有事件"猜(那句演示文案已从 index.html 删掉)。
|
||||
⚠️ 未建档(pregEmpty)不再整块隐藏:照接口结论显示,新用户也能看到引导。
|
||||
⚠️ 模块显隐只有 fpRender 一个 owner(原来 syncPregnancy 也直接改 display,会撞竞态)。 */
|
||||
let _fpMode = 'pending'; // pending(加载中) | rows | guide | hide
|
||||
let _fpData = null; // 接口 result(events / digest / lead / unused…)
|
||||
function fpApply(mode, r) { _fpMode = mode; _fpData = r || null; fpRender(); }
|
||||
function fpRender() {
|
||||
const tl = document.querySelector('.today-line'), steps = $('#tlSteps'), guide = $('#tlGuide');
|
||||
if (!tl || !steps) return;
|
||||
/* ⚠️ **未建档不再整块隐藏**(2026-09-22 产品口径):模块照接口给的结论显示 ——
|
||||
无档案→引导、有档案→事件/空态、读不到→不显示。原来那道 pregEmpty/postpartum 硬门槛已去掉。 */
|
||||
const mode = _fpMode;
|
||||
const mode = _fpMode, r = _fpData || {};
|
||||
if (mode === 'hide') { tl.style.display = 'none'; return; }
|
||||
tl.style.display = '';
|
||||
/* ⚠️ AI 气泡里的次数是**写死的演示文案**,所以只有「已建档 + 拿到真实事件」才显示它:
|
||||
未建档(还没孕产期,页面本来就全是占位)/ 今天没记 / 无档案 / 读不到 都不显示,
|
||||
否则同一屏上会出现「还没有足迹」与「动了 3 次」并存这种自相矛盾。
|
||||
(数字接真实次数是另一件事,等产品定文案,见 index.html 那段注释与待办。) */
|
||||
tl.classList.toggle('no-ai', mode !== 'rows' || state.pregEmpty);
|
||||
/* AI 气泡:文案来自接口 digest.text;没有就整体收起。上游文案是**纯文本**,用 textContent 写。 */
|
||||
const say = document.querySelector('.ab-say');
|
||||
const txt = (r.digest && r.digest.text) || '';
|
||||
if (say) say.textContent = txt;
|
||||
tl.classList.toggle('no-ai', !txt);
|
||||
tl.classList.toggle('ai-warn', !!(r.digest && r.digest.warn));
|
||||
if (guide) guide.style.display = (mode === 'guide') ? '' : 'none';
|
||||
if (mode === 'guide') { steps.style.display = 'none'; steps.innerHTML = ''; return; }
|
||||
if (mode === 'guide') { steps.style.display = 'none'; steps.innerHTML = ''; renderGuide(r); return; }
|
||||
steps.style.display = '';
|
||||
steps.innerHTML = '';
|
||||
if (mode === 'empty') { steps.append(emptyFootprintStep()); return; }
|
||||
if (mode === 'pending') { steps.append(pendingFootprintStep()); return; }
|
||||
_fpEvents.forEach(e => steps.append(buildFootprintStep(e)));
|
||||
(r.events || []).forEach(e => steps.append(buildFootprintStep(e)));
|
||||
}
|
||||
|
||||
/* 引导占位:文案与应用都来自上游 —— lead 是上游自己的引导语,unused[] 是今天没用过的应用。
|
||||
能对上 App 表的(identifier 由 box_app 按 url 路径解析)渲染成可点芯片,其余只显示名字。 */
|
||||
function renderGuide(r) {
|
||||
const title = $('#tlGuideTitle'), sub = $('#tlGuideSub'), box = $('#tlGuideApps');
|
||||
if (title) title.textContent = (r && r.lead) || '还没有你的脚印';
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
((r && r.unused) || []).slice(0, 5).forEach(u => {
|
||||
const ident = u.identifier || '';
|
||||
const app = ident ? APP_BY_ID[ident] : null;
|
||||
if (app && app.url) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'tl-chip ripple';
|
||||
b.textContent = u.appName || app.name || ident;
|
||||
b.addEventListener('click', () => jumpApp(app.url, app.identifier));
|
||||
box.appendChild(b);
|
||||
} else if (u.appName) {
|
||||
const sp = document.createElement('span');
|
||||
sp.className = 'tl-chip is-plain';
|
||||
sp.textContent = u.appName;
|
||||
box.appendChild(sp);
|
||||
}
|
||||
});
|
||||
if (sub) {
|
||||
sub.textContent = box.childElementCount
|
||||
? '从下面这些记一笔,今天的脚印就会长出来'
|
||||
: '去上面的工具记一笔,这里就会长出你今天的脚印';
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载中的中性占位(不是假数据):与 index.html 里那条首屏占位同构 */
|
||||
function pendingFootprintStep() {
|
||||
const d = document.createElement('div'); d.className = 'step';
|
||||
@@ -3168,12 +3194,12 @@
|
||||
.then(j => {
|
||||
const r = (j && j.code === 200 && j.result) ? j.result : null;
|
||||
if (!r) { fpApply('hide'); return; } // 读不到 → 不显示(不再编数据)
|
||||
/* 未建档不再拦(2026-09-22 产品口径):照接口结论走,未建档也能看到引导占位 */
|
||||
if (!r.linked) { fpApply('guide'); return; } // 无档案 → 引导占位
|
||||
if (r.degraded) { fpApply('hide'); return; } // 有档案、上游故障且无旧值 → 不显示
|
||||
if (r.degraded) { fpApply('hide'); return; } // 上游故障且无旧值 → 不显示
|
||||
const events = (r.events || []).slice(-8); // 升序取最近 8 条
|
||||
if (!events.length) { fpApply('empty'); return; } // 有档案、今天没记 → 「还没有足迹」
|
||||
fpApply('rows', events); // 有档案 + 有事件(含 stale 旧值)→ 真数据
|
||||
/* 有事件 → 时间轴;一条都没有(上游查无此人 linked=false、今天 kind=fallback 没活动、
|
||||
或只浏览没产出记录)→ 统一走引导:文案与应用名都由接口给,不再自己编。 */
|
||||
if (events.length) { fpApply('rows', r); return; }
|
||||
fpApply('guide', r);
|
||||
})
|
||||
.catch(function () { fpApply('hide'); }); // 读不到 → 不显示
|
||||
}
|
||||
@@ -3202,18 +3228,6 @@
|
||||
}
|
||||
|
||||
/* 无足迹占位行:同构的一行,读屏和眼睛都不会把空卡当成坏卡。 */
|
||||
function emptyFootprintStep() {
|
||||
const d = document.createElement('div'); d.className = 'step';
|
||||
const time = document.createElement('span'); time.className = 'st-time';
|
||||
time.textContent = '今天';
|
||||
const dot = document.createElement('span'); dot.className = 'st-dot';
|
||||
const txt = document.createElement('span'); txt.className = 'st-txt';
|
||||
txt.textContent = '还没有足迹';
|
||||
const from = document.createElement('span'); from.className = 'st-from';
|
||||
from.textContent = '去逛逛上面的工具吧';
|
||||
d.append(time, dot, txt, from);
|
||||
return d;
|
||||
}
|
||||
|
||||
/* ---------- 进商城(web-view 内跳商城原生页) ----------
|
||||
兜爸心选 H5 由小程序 web-view 承载;点「商城」跳回商城原生页。
|
||||
|
||||
@@ -643,6 +643,14 @@ a.skip:focus{left:16px;top:16px}
|
||||
.tl-guide{background:#fff;border-radius:22px;padding:20px 18px;box-shadow:var(--sh);text-align:center}
|
||||
.tl-guide-t{font-size:15px;font-weight:800;color:var(--ink)}
|
||||
.tl-guide-s{font-size:12.5px;color:var(--ink-2);margin-top:6px;line-height:1.6}
|
||||
/* 引导里可点的应用芯片(名字来自接口 unused[];identifier 是 box_app 按 App 表 url 路径解析的) */
|
||||
.tl-guide-apps{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:12px}
|
||||
.tl-guide-apps:empty{display:none}
|
||||
.tl-chip{font:inherit;font-size:12.5px;font-weight:700;color:var(--brand-dk);
|
||||
background:#FFF1EC;border:1px solid #FFDFD5;border-radius:999px;padding:7px 13px;line-height:1}
|
||||
.tl-chip.is-plain{color:var(--muted);background:#F6F1ED;border-color:var(--line)}
|
||||
/* 接口说这句"需要标红"时(digest.warn)整句变告警色 */
|
||||
.today-line.ai-warn .ab-say{color:var(--alert)}
|
||||
/* 没有真实事件的当天不显示 AI 气泡:那句话里的次数是**写死的**(见 index.html 的注释),
|
||||
今天没记 / 未关联时显示它等于编数据。拿到真实事件(fpApply('rows'))时才摘掉这个类。 */
|
||||
.today-line.no-ai .ai-bubble{display:none}
|
||||
|
||||
Reference in New Issue
Block a user