feat(首页): 新增「孕期小知识」和「妈妈的树洞」两个模块 (v26093013)
孕期小知识(工具区下方): - 按真实孕周 + 天数显示「今日小知识」大卡,右上角本周水果 emoji 缓慢浮动; 底部 7 个圆点 = 本周第 1–7 天,今天高亮呼吸、以前可回看、以后锁着(明天见)。 - 四张马卡龙小卡:宝宝这周(带大小比喻)/ 妈妈身体 / 吃点什么 / 这周留心,进入视口依次浮上来。 - 内容 assets/data/preg-tips.json:孕 4–40 周,每周 4 句 + 每天 1 条,共 259 条,按需 fetch。 - 只在 /pregnancy 确认真实孕周后显示(pregEmpty === false 且非产后),不拿默认孕周配内容。 妈妈的树洞(今日脚印下方): - 心事只存本机 localStorage(mmbox.treehole.v1,最多 200 条),不上传;按用户选定不做回应, 只有纸团飞进树洞 + 树冠晃动 + 「已收好」动画;可展开「我的心事」回看、删除。 - 出现明显轻生字眼时,在「已收好」下加一行 12356 心理援助热线(兜底);卡底常驻一行求助提示。 动效在 prefers-reduced-motion 下关闭。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1
assets/data/preg-tips.json
Normal file
1
assets/data/preg-tips.json
Normal file
File diff suppressed because one or more lines are too long
75
index.html
75
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:26093012 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<!-- V:26093013 缓存版本标记:改 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 第四节) -->
|
||||
@@ -304,6 +304,37 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 孕期小知识(2026-09-30)。用户原话:「按她填写的孕周,比如孕 20 周 5 天,给她列出几个小模块,
|
||||
设计一定要漂亮、匹配母婴用户,最好是动态的;小知识匹配每周的每一天」。
|
||||
内容:assets/data/preg-tips.json(孕 4–40 周,每周 宝宝/妈妈/饮食/注意 四句 + 每天一条小知识,共 7 条),
|
||||
首次需要时才 fetch,不进首屏。渲染在 app.js renderTips()。
|
||||
⚠️ 默认 .hide:只有拿到**真实孕周**(state.pregEmpty === false 且非产后)才显示,
|
||||
内置默认孕周(DEFAULT_DUE 推出来的 27 周上下)是假的,不能拿它配知识。
|
||||
⚠️ 一周 7 个圆点 = 这一周的第 1–7 天。今天及以前可以点开回看;以后的几天锁着,
|
||||
写「明天见」—— 让每天回来看一条,而不是一口气翻完。 -->
|
||||
<section class="preg-tips hide" id="pregTips">
|
||||
<div class="pt-head">
|
||||
<h2 class="pt-title">孕期小知识</h2>
|
||||
<span class="pt-when" id="ptWhen"></span>
|
||||
</div>
|
||||
<div class="pt-today" id="ptToday">
|
||||
<span class="pt-float" id="ptFloat" aria-hidden="true"></span>
|
||||
<span class="pt-badge" id="ptBadge">今日小知识</span>
|
||||
<div class="pt-body" id="ptBody">
|
||||
<div class="pt-t" id="ptT"></div>
|
||||
<div class="pt-d" id="ptD"></div>
|
||||
</div>
|
||||
<div class="pt-days" id="ptDays" role="tablist" aria-label="本周每天的小知识"></div>
|
||||
</div>
|
||||
<div class="pt-grid" id="ptGrid">
|
||||
<div class="pt-card pc-baby"><span class="pt-ico" id="ptBabyIco" aria-hidden="true">👶</span><b>宝宝这周</b><p id="ptBaby"></p></div>
|
||||
<div class="pt-card pc-mom"><span class="pt-ico" aria-hidden="true">🤰</span><b>妈妈身体</b><p id="ptMom"></p></div>
|
||||
<div class="pt-card pc-eat"><span class="pt-ico" aria-hidden="true">🥗</span><b>吃点什么</b><p id="ptEat"></p></div>
|
||||
<div class="pt-card pc-care"><span class="pt-ico" aria-hidden="true">📋</span><b>这周留心</b><p id="ptCare"></p></div>
|
||||
</div>
|
||||
<p class="pt-foot">仅供参考,身体有任何不适请以医生意见为准</p>
|
||||
</section>
|
||||
|
||||
<!-- 模块二 · 今天的动线。
|
||||
上一版这里是两个各自独立的模块:「AI今日小结」一张渐变卡,
|
||||
下面「今日成长脚印」一张白卡装的时间轴。可 AI 那句话总结的
|
||||
@@ -397,6 +428,48 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- 树洞(2026-09-30)。用户原话:「宝妈们有任何情绪上的问题,都可以在这个树洞里扔进去」。
|
||||
用户选定:**只存在她自己手机上**(localStorage 'mmbox.treehole.v1',不上传、后台也看不到),
|
||||
扔进去后**只有「已收好」的动画,不做回应**。
|
||||
⚠️ 唯一的例外是安全提示:内容里出现明显的轻生字眼时,收好之后在卡内显示一行援助热线
|
||||
(12356 全国心理援助热线),这不是「回应心事」,是兜底,别删。
|
||||
⚠️ 纸团飞进树洞的动画靠 Element.animate;不支持的老内核直接跳到「已收好」,不影响收存。 -->
|
||||
<section class="tree-hole" id="treeHole">
|
||||
<div class="th-head">
|
||||
<h2 class="th-title">妈妈的树洞</h2>
|
||||
<span class="th-lock"><svg viewBox="0 0 24 24" width="11" height="11" aria-hidden="true"><path d="M7 10V7.5a5 5 0 0 1 10 0V10" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><rect x="4.5" y="10" width="15" height="11" rx="3" fill="currentColor"/></svg>只存在你的手机里</span>
|
||||
</div>
|
||||
<div class="th-stage">
|
||||
<p class="th-lead">烦心、委屈、害怕、说不出口的话,<br>都可以扔进来。树洞替你收着,<br>谁也看不到。</p>
|
||||
<svg class="th-tree" id="thTree" viewBox="0 0 120 124" aria-hidden="true">
|
||||
<g class="th-crown">
|
||||
<circle cx="40" cy="42" r="26" fill="#BFE0B4"/>
|
||||
<circle cx="78" cy="38" r="28" fill="#A9D59E"/>
|
||||
<circle cx="60" cy="22" r="22" fill="#C9E7BF"/>
|
||||
<circle cx="94" cy="58" r="17" fill="#B4DBA9"/>
|
||||
<circle cx="26" cy="62" r="15" fill="#B4DBA9"/>
|
||||
<circle cx="46" cy="28" r="3.2" fill="#F7B8A6"/><circle cx="84" cy="24" r="2.8" fill="#F9C9B8"/>
|
||||
<circle cx="96" cy="50" r="2.6" fill="#F7B8A6"/><circle cx="30" cy="52" r="2.6" fill="#F9C9B8"/>
|
||||
<circle cx="66" cy="44" r="2.4" fill="#F7B8A6"/>
|
||||
</g>
|
||||
<path d="M50 124c3-18 3-34 1-52h20c-2 18-2 34 1 52z" fill="#D4A27C"/>
|
||||
<path d="M56 80c-6-6-14-8-20-7M66 80c5-6 12-8 18-7" stroke="#D4A27C" stroke-width="5" stroke-linecap="round" fill="none"/>
|
||||
<ellipse id="thHole" class="th-hole" cx="61" cy="98" rx="6.5" ry="9" fill="#7A5238"/>
|
||||
<ellipse cx="61" cy="99.5" rx="4" ry="6.2" fill="#5A3A26"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="th-form">
|
||||
<textarea id="thText" maxlength="500" rows="3" placeholder="今天想说点什么……"></textarea>
|
||||
<div class="th-row">
|
||||
<button class="th-mine" id="thMine" type="button">我的心事 <b id="thCount">0</b> ›</button>
|
||||
<button class="th-send" id="thSend" type="button">扔进树洞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th-done" id="thDone" aria-live="polite"></div>
|
||||
<ul class="th-list hide" id="thList"></ul>
|
||||
<p class="th-help">如果持续情绪低落、睡不好,或有伤害自己的念头,请告诉家人或医生,也可拨打心理援助热线 <a href="tel:12356">12356</a>。</p>
|
||||
</section>
|
||||
|
||||
<!-- 模块三 · 书架卡带。
|
||||
上一版是「一张横排大卡 + 下面两张小卡」的老三段,和上面两个模块
|
||||
一样都是往下堆。这里改成一条横向可滑的书架:三张等宽等高的**竖版**书
|
||||
|
||||
178
js/app.js
178
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26093012';
|
||||
var VER = '26093013';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -1050,6 +1050,8 @@
|
||||
function syncPregnancy() {
|
||||
/* 孕期每日小打卡:产后整块收起;其它状态都显示(标签默认是中性提示,不依赖孕周) */
|
||||
const _dc = $('#dailyCheck'); if (_dc) _dc.classList.toggle('hide', !!state.postpartum);
|
||||
/* 孕期小知识:只认真实孕周(自己判显隐,见 renderTips) */
|
||||
renderTips();
|
||||
/* ---- 产后态:宝宝已出生 → 一律不显示孕周数字(2026-09-22) ----
|
||||
⚠️ 必须放在 pregEmpty 之前:产后返回体的 week 也是 null,
|
||||
落到空态分支会给**已经生产**的用户弹「完善孕期信息」引导。
|
||||
@@ -3107,6 +3109,180 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 孕期小知识(2026-09-30) =================
|
||||
内容 assets/data/preg-tips.json:[{week, baby, mom, eat, care, days:[{t,d}×7]}],孕 4–40 周。
|
||||
第几天 = state.day(0–6),和首页「孕 N 周 D 天」同一口径。
|
||||
⚠️ 只在 state.pregEmpty === false(/pregnancy 已确认有孕周)且非产后时显示:
|
||||
pregEmpty 初值是 undefined —— 档案没回来之前 state.week 是 DEFAULT_DUE 推出来的假孕周,
|
||||
拿它配知识就是给人看假数据。所以这里用「=== false」而不是「!pregEmpty」。 */
|
||||
let TIPS = null, tipsLoading = null, tipSel = -1;
|
||||
function loadTips() {
|
||||
if (!tipsLoading) {
|
||||
tipsLoading = fetch('assets/data/preg-tips.json?v=' + VER)
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(arr => {
|
||||
const m = {};
|
||||
(arr || []).forEach(x => { if (x && x.week) m[x.week] = x; });
|
||||
TIPS = m;
|
||||
})
|
||||
.catch(() => { TIPS = {}; });
|
||||
}
|
||||
return tipsLoading;
|
||||
}
|
||||
function renderTips() {
|
||||
const sec = $('#pregTips');
|
||||
if (!sec) return;
|
||||
if (state.pregEmpty !== false || state.postpartum) { sec.classList.add('hide'); return; }
|
||||
if (!TIPS) { loadTips().then(renderTips); return; }
|
||||
const w = Math.max(4, Math.min(40, state.week || 0));
|
||||
const wk = TIPS[w];
|
||||
if (!wk || !(wk.days || []).length) { sec.classList.add('hide'); return; }
|
||||
const today = Math.max(0, Math.min(6, state.day || 0));
|
||||
const size = babySize(w);
|
||||
$('#ptWhen').textContent = '孕' + state.week + '周' + (state.day ? state.day + '天' : '');
|
||||
$('#ptFloat').textContent = size.emoji;
|
||||
$('#ptBabyIco').textContent = size.emoji;
|
||||
$('#ptBaby').textContent = '像' + size.name + '那么大。' + (wk.baby || '');
|
||||
$('#ptMom').textContent = wk.mom || '';
|
||||
$('#ptEat').textContent = wk.eat || '';
|
||||
$('#ptCare').textContent = wk.care || '';
|
||||
/* 一周 7 个圆点:以前可回看,今天实心,以后锁着 */
|
||||
const box = $('#ptDays');
|
||||
box.textContent = '';
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'pt-day ' + (i < today ? 'is-past' : i === today ? 'is-today' : 'is-lock');
|
||||
b.setAttribute('role', 'tab');
|
||||
const n = document.createElement('i'); n.textContent = i + 1;
|
||||
const t = document.createElement('span');
|
||||
t.textContent = i === today ? '今天' : (i === today + 1 ? '明天' : '\u00a0'); // 圈里已是「第几天」,只给今天/明天配字
|
||||
b.appendChild(n); b.appendChild(t);
|
||||
b.addEventListener('click', () => {
|
||||
if (i > today) { toast(i === today + 1 ? '明天再来看新的小知识~' : '到那天再来看哦~'); return; }
|
||||
showTip(wk, i, today);
|
||||
});
|
||||
box.appendChild(b);
|
||||
}
|
||||
tipSel = -1;
|
||||
showTip(wk, today, today);
|
||||
sec.classList.remove('hide');
|
||||
revealTipsGrid();
|
||||
}
|
||||
function showTip(wk, i, today) {
|
||||
if (i === tipSel) return;
|
||||
tipSel = i;
|
||||
const d = wk.days[i] || wk.days[0];
|
||||
$('#ptBadge').textContent = i === today ? '今日小知识' : '第 ' + (i + 1) + ' 天 · 回看';
|
||||
$('#ptT').textContent = d.t || '';
|
||||
$('#ptD').textContent = d.d || '';
|
||||
$$('#ptDays .pt-day').forEach((b, k) => {
|
||||
b.classList.toggle('is-sel', k === i);
|
||||
b.setAttribute('aria-selected', k === i ? 'true' : 'false');
|
||||
});
|
||||
const body = $('#ptBody');
|
||||
body.classList.remove('is-in'); void body.offsetWidth; body.classList.add('is-in'); // 重播进场动画
|
||||
}
|
||||
function revealTipsGrid() {
|
||||
const g = $('#ptGrid');
|
||||
if (!g || g.classList.contains('is-shown')) return;
|
||||
if (!('IntersectionObserver' in window)) { g.classList.add('is-shown'); return; }
|
||||
const io = new IntersectionObserver(es => {
|
||||
if (es.some(e => e.isIntersecting)) { g.classList.add('is-shown'); io.disconnect(); }
|
||||
}, { threshold: 0.2 });
|
||||
io.observe(g);
|
||||
}
|
||||
|
||||
/* ================= 妈妈的树洞(2026-09-30) =================
|
||||
用户选定:心事**只存在本机**(localStorage,不上传),扔进去后只有「已收好」动画、不回应。
|
||||
唯一例外:出现明显轻生字眼时,在「已收好」下面多一行援助热线 —— 兜底,不是回应。 */
|
||||
(function treeHole() {
|
||||
const KEY = 'mmbox.treehole.v1', MAX = 200;
|
||||
const ta = $('#thText'), send = $('#thSend'), list = $('#thList'), done = $('#thDone');
|
||||
if (!ta || !send) return;
|
||||
const SOS = /想死|不想活|自杀|轻生|结束生命|活不下去|去死|割腕|跳楼/;
|
||||
const load = () => { try { const a = JSON.parse(localStorage.getItem(KEY) || '[]'); return Array.isArray(a) ? a : []; } catch (e) { return []; } };
|
||||
const save = a => { try { localStorage.setItem(KEY, JSON.stringify(a.slice(0, MAX))); return true; } catch (e) { return false; } };
|
||||
const pad = n => (n < 10 ? '0' : '') + n;
|
||||
const when = ts => { const d = new Date(ts); return (d.getMonth() + 1) + '月' + d.getDate() + '日 ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); };
|
||||
function count() { $('#thCount').textContent = load().length; }
|
||||
function renderList() {
|
||||
const a = load();
|
||||
list.textContent = '';
|
||||
if (!a.length) {
|
||||
const li = document.createElement('li'); li.className = 'th-empty'; li.textContent = '树洞还是空的';
|
||||
list.appendChild(li); return;
|
||||
}
|
||||
a.forEach(e => {
|
||||
const li = document.createElement('li');
|
||||
const t = document.createElement('time'); t.textContent = when(e.t);
|
||||
const p = document.createElement('p'); p.textContent = e.x;
|
||||
const del = document.createElement('button'); del.className = 'th-del'; del.type = 'button';
|
||||
del.setAttribute('aria-label', '删除这条'); del.textContent = '×';
|
||||
del.addEventListener('click', () => {
|
||||
if (!window.confirm('删掉这条心事?删了就找不回来了')) return;
|
||||
save(load().filter(x => x.t !== e.t)); renderList(); count();
|
||||
});
|
||||
li.appendChild(t); li.appendChild(p); li.appendChild(del);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
function showDone(sos) {
|
||||
done.classList.toggle('is-sos', !!sos);
|
||||
done.textContent = '已收好 🍃';
|
||||
if (sos) {
|
||||
const s = document.createElement('div'); s.className = 'th-sos';
|
||||
s.textContent = '如果你现在很难受,请告诉身边的人,或拨打 12356 心理援助热线,有人愿意听你说。';
|
||||
done.appendChild(s);
|
||||
}
|
||||
done.classList.add('is-on');
|
||||
clearTimeout(showDone._t);
|
||||
if (!sos) showDone._t = setTimeout(() => done.classList.remove('is-on'), 2600);
|
||||
}
|
||||
/* 纸团飞进树洞:从输入框中间出发,弧线落到树洞,边转边缩小 */
|
||||
function fly(text, cb) {
|
||||
const hole = $('#thHole'), tree = $('#thTree');
|
||||
const note = document.createElement('div');
|
||||
if (!hole || !note.animate) { cb(); return; }
|
||||
note.className = 'th-note'; note.textContent = text;
|
||||
document.body.appendChild(note);
|
||||
const a = ta.getBoundingClientRect(), h = hole.getBoundingClientRect();
|
||||
const nw = note.offsetWidth, nh = note.offsetHeight;
|
||||
const x0 = a.left + a.width / 2 - nw / 2, y0 = a.top + a.height / 2 - nh / 2;
|
||||
const x1 = h.left + h.width / 2 - nw / 2, y1 = h.top + h.height / 2 - nh / 2;
|
||||
const xm = (x0 + x1) / 2, ym = Math.min(y0, y1) - 70;
|
||||
const anim = note.animate([
|
||||
{ transform: 'translate(' + x0 + 'px,' + y0 + 'px) scale(1) rotate(0)', opacity: 1, borderRadius: '10px' },
|
||||
{ transform: 'translate(' + xm + 'px,' + ym + 'px) scale(.45) rotate(200deg)', opacity: 1, borderRadius: '50%', offset: .55 },
|
||||
{ transform: 'translate(' + x1 + 'px,' + y1 + 'px) scale(.08) rotate(420deg)', opacity: .2, borderRadius: '50%' }
|
||||
], { duration: 950, easing: 'cubic-bezier(.45,.05,.4,1)', fill: 'forwards' });
|
||||
anim.onfinish = () => {
|
||||
note.remove();
|
||||
if (tree) { tree.classList.remove('is-eat'); void tree.getBoundingClientRect(); tree.classList.add('is-eat'); setTimeout(() => tree.classList.remove('is-eat'), 1000); }
|
||||
cb();
|
||||
};
|
||||
}
|
||||
const sync = () => { send.disabled = !ta.value.trim(); };
|
||||
ta.addEventListener('input', sync);
|
||||
send.addEventListener('click', () => {
|
||||
const x = ta.value.trim();
|
||||
if (!x) return;
|
||||
const a = load(); a.unshift({ t: Date.now(), x: x });
|
||||
if (!save(a)) { toast('手机存储空间不足,没能收好'); return; }
|
||||
ta.value = ''; sync(); ta.blur();
|
||||
fly(x, () => {
|
||||
showDone(SOS.test(x));
|
||||
count();
|
||||
if (!list.classList.contains('hide')) renderList();
|
||||
});
|
||||
});
|
||||
$('#thMine').addEventListener('click', () => {
|
||||
const open = list.classList.toggle('hide') === false;
|
||||
if (open) renderList();
|
||||
});
|
||||
sync(); count();
|
||||
})();
|
||||
|
||||
/* 拉卡片数据(与其它远程数据一起打;失败静默,卡上保留现有值) */
|
||||
function applyRemoteCardsFetch() {
|
||||
const openid = hashParam('openid');
|
||||
|
||||
114
mama-box.css
114
mama-box.css
@@ -429,7 +429,7 @@ a.skip:focus{left:16px;top:16px}
|
||||
对齐的是内容,出血的是底色,这两件事要分开。
|
||||
⚠️ 三块的圆角、底色、左右内边距改成在这里一处定义。以后要调,改这一条,
|
||||
别再回到各自的规则里单独写,那正是上一稿乱掉的原因。 */
|
||||
.daily-check,.tool-dock,.today-line,.shelf{
|
||||
.daily-check,.tool-dock,.preg-tips,.today-line,.tree-hole,.shelf{
|
||||
margin:0 -26px;padding:16px 26px 18px;border-radius:26px;
|
||||
background-color:#fff;
|
||||
/* ⚠️ 这两条是一对,缺一不可,而且必须一起看:
|
||||
@@ -443,7 +443,7 @@ a.skip:focus{left:16px;top:16px}
|
||||
/* 块间距。给后两块,第一块的上间距另有规则(来信卡收放要用)。
|
||||
⚠️ 这是**块与块之间**的缝。某一块自己内部觉得挤,要调的是那块的 padding,
|
||||
不是这里 —— 改这里会把三块的关系一起改掉。 */
|
||||
.tool-dock,.today-line,.shelf{margin-top:16px}
|
||||
.tool-dock,.preg-tips,.today-line,.tree-hole,.shelf{margin-top:16px}
|
||||
|
||||
/* ===== 后两块的暖色渐变底(本批新增)=====
|
||||
用户原话:「在那个宝宝的今日脚印那块,稍微给背景它加一点渐变」
|
||||
@@ -566,6 +566,116 @@ a.skip:focus{left:16px;top:16px}
|
||||
.dc-tag.is-warn{color:var(--alert);background:#FDECE8}
|
||||
.dc-row .chev{flex:0 0 auto;font-size:16px;margin-left:-4px}
|
||||
|
||||
/* ============ 孕期小知识(2026-09-30)============
|
||||
外壳是公共白色通栏。里面三层:
|
||||
① .pt-today 今日小知识大卡:暖粉渐变 + 右上角一颗慢慢浮动的「宝宝大小」emoji(本周水果);
|
||||
底部 7 个圆点是本周第 1–7 天,今天实心、以前可回看、以后锁着。
|
||||
② .pt-grid 四张马卡龙色小卡:宝宝 / 妈妈 / 饮食 / 注意,进入视口时依次浮上来。
|
||||
③ .pt-foot 一行免责小字。
|
||||
动效全部在 prefers-reduced-motion 下关掉。 */
|
||||
.preg-tips.hide{display:none}
|
||||
.pt-head{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
|
||||
.pt-title{font-size:16.5px;font-weight:800;letter-spacing:-.2px}
|
||||
.pt-when{margin-left:auto;font-size:12px;font-weight:700;color:var(--brand);
|
||||
background:#FFF0EA;border-radius:999px;padding:3px 10px}
|
||||
.pt-today{position:relative;overflow:hidden;border-radius:20px;padding:14px 16px 12px;
|
||||
background:linear-gradient(135deg,#FFE9E1 0%,#FFF1E6 55%,#FFF7EE 100%)}
|
||||
.pt-today::after{content:"";position:absolute;right:-30px;top:-40px;width:130px;height:130px;border-radius:50%;
|
||||
background:radial-gradient(circle,rgba(255,255,255,.75),rgba(255,255,255,0) 70%);pointer-events:none}
|
||||
.pt-float{position:absolute;right:14px;top:10px;font-size:34px;line-height:1;z-index:1;
|
||||
animation:ptFloat 3.6s ease-in-out infinite;filter:drop-shadow(0 4px 6px rgba(200,110,80,.18))}
|
||||
@keyframes ptFloat{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-6px) rotate(4deg)}}
|
||||
.pt-badge{display:inline-block;font-size:11px;font-weight:700;color:#fff;border-radius:999px;padding:2px 9px;
|
||||
background:linear-gradient(90deg,#F4593C,#FF8A66)}
|
||||
.pt-body{min-height:88px;padding-right:44px}
|
||||
.pt-body.is-in{animation:ptIn .42s var(--ease)}
|
||||
@keyframes ptIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||
.pt-t{margin-top:8px;font-size:16px;font-weight:800;color:#5A2E22;letter-spacing:-.2px}
|
||||
.pt-d{margin-top:5px;font-size:13px;line-height:1.65;color:#6B4B40}
|
||||
.pt-days{display:flex;justify-content:space-between;margin-top:10px;padding-top:10px;border-top:1px dashed rgba(224,74,46,.18)}
|
||||
.pt-day{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:3px;
|
||||
background:none;border:0;padding:0;font:inherit;color:#B08A7C}
|
||||
.pt-day i{display:block;width:26px;height:26px;border-radius:50%;line-height:26px;text-align:center;
|
||||
font-style:normal;font-size:11.5px;font-weight:700;background:rgba(255,255,255,.75);color:#C07A64;
|
||||
transition:transform .2s var(--ease),background .2s}
|
||||
.pt-day span{font-size:10px}
|
||||
.pt-day.is-past i{background:#fff;color:#E0725A}
|
||||
.pt-day.is-today i{background:linear-gradient(135deg,#F4593C,#FF8A66);color:#fff;
|
||||
box-shadow:0 0 0 3px rgba(244,89,60,.18);animation:ptPulse 2.4s ease-in-out infinite}
|
||||
.pt-day.is-today span{color:var(--brand);font-weight:700}
|
||||
@keyframes ptPulse{0%,100%{box-shadow:0 0 0 3px rgba(244,89,60,.18)}50%{box-shadow:0 0 0 6px rgba(244,89,60,.06)}}
|
||||
.pt-day.is-sel i{transform:scale(1.14)}
|
||||
.pt-day.is-sel:not(.is-today) i{background:#FFD9CC;color:#C24A30}
|
||||
.pt-day.is-lock i{background:rgba(255,255,255,.45);color:#D9BDB2}
|
||||
.pt-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px}
|
||||
.pt-card{position:relative;border-radius:16px;padding:11px 12px 12px;min-width:0}
|
||||
.pt-card b{display:block;font-size:13px;font-weight:800;margin-top:4px}
|
||||
.pt-card p{margin:4px 0 0;font-size:12px;line-height:1.55;color:var(--ink-2)}
|
||||
.pt-ico{display:inline-grid;place-items:center;width:30px;height:30px;border-radius:10px;background:rgba(255,255,255,.8);font-size:17px}
|
||||
.pc-baby{background:linear-gradient(160deg,#FFF1E8,#FFE6DA)} .pc-baby b{color:#C4583C}
|
||||
.pc-mom{background:linear-gradient(160deg,#FCEFF5,#F8E1EC)} .pc-mom b{color:#B3547E}
|
||||
.pc-eat{background:linear-gradient(160deg,#EFF7EA,#E3F0DB)} .pc-eat b{color:#4F7F3E}
|
||||
.pc-care{background:linear-gradient(160deg,#EEF3FB,#E1EAF7)} .pc-care b{color:#4A6FA6}
|
||||
/* 进入视口依次浮上来(JS 给 .pt-grid 加 .is-shown;不支持 IntersectionObserver 时直接加) */
|
||||
.pt-grid .pt-card{opacity:0;transform:translateY(12px);transition:opacity .5s ease,transform .6s var(--ease)}
|
||||
.pt-grid.is-shown .pt-card{opacity:1;transform:none}
|
||||
.pt-grid.is-shown .pt-card:nth-child(2){transition-delay:.08s}
|
||||
.pt-grid.is-shown .pt-card:nth-child(3){transition-delay:.16s}
|
||||
.pt-grid.is-shown .pt-card:nth-child(4){transition-delay:.24s}
|
||||
.pt-foot{margin-top:9px;font-size:11px;color:var(--muted);text-align:center}
|
||||
|
||||
/* ============ 妈妈的树洞(2026-09-30)============
|
||||
浅薄荷→奶白的底,和上下暖色块拉开,读起来「安静」。右侧一棵 SVG 小树,
|
||||
树干上的洞(#thHole)是纸团飞进去的落点。扔进去后:纸团飞入 → 洞口亮一下、树冠晃一下
|
||||
→ 卡内出现「已收好」。不做任何回应文字(用户选定)。 */
|
||||
.tree-hole{background-image:linear-gradient(160deg,#F1F8EE 0%,#FAFCF6 55%,#FFFDF9 100%)}
|
||||
.th-head{display:flex;align-items:center;gap:8px}
|
||||
.th-title{font-size:16.5px;font-weight:800;letter-spacing:-.2px}
|
||||
.th-lock{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:600;
|
||||
color:#5E8A55;background:#E4F1DF;border-radius:999px;padding:3px 9px}
|
||||
.th-stage{display:flex;align-items:flex-end;gap:6px;margin-top:6px}
|
||||
.th-lead{flex:1;min-width:0;font-size:13px;line-height:1.7;color:#5C6B58;padding-bottom:10px}
|
||||
.th-tree{flex:0 0 auto;width:104px;height:108px;overflow:visible}
|
||||
.th-crown{transform-origin:60px 70px;animation:thSway 5s ease-in-out infinite}
|
||||
@keyframes thSway{0%,100%{transform:rotate(-1.2deg)}50%{transform:rotate(1.2deg)}}
|
||||
.th-tree.is-eat .th-crown{animation:thShake .7s ease-in-out}
|
||||
@keyframes thShake{0%,100%{transform:rotate(0)}25%{transform:rotate(-4deg)}60%{transform:rotate(3deg)}}
|
||||
.th-tree.is-eat .th-hole{animation:thGlow .9s ease-out}
|
||||
@keyframes thGlow{0%{fill:#7A5238}35%{fill:#F4C38F}100%{fill:#7A5238}}
|
||||
.th-form{margin-top:4px;background:#fff;border:1px solid #E3EEDD;border-radius:16px;padding:10px 12px}
|
||||
.th-form textarea{display:block;width:100%;border:0;outline:0;resize:none;background:transparent;
|
||||
font:inherit;font-size:14px;line-height:1.6;color:var(--ink)}
|
||||
.th-form textarea::placeholder{color:#A9B5A4}
|
||||
.th-row{display:flex;align-items:center;gap:8px;margin-top:6px}
|
||||
.th-mine{border:0;background:none;padding:0;font:inherit;font-size:12px;font-weight:600;color:#6F8A68}
|
||||
.th-mine b{font-weight:800}
|
||||
.th-send{margin-left:auto;border:0;border-radius:999px;padding:8px 18px;font:inherit;font-size:13.5px;font-weight:700;
|
||||
color:#fff;background:linear-gradient(90deg,#7FB573,#98C98A);box-shadow:0 6px 14px rgba(110,160,95,.25)}
|
||||
.th-send:disabled{opacity:.45;box-shadow:none}
|
||||
.th-send:active{transform:scale(.96)}
|
||||
.th-done{height:0;overflow:hidden;text-align:center;font-size:13px;font-weight:700;color:#5E8A55;
|
||||
transition:height .3s var(--ease),margin .3s var(--ease)}
|
||||
.th-done.is-on{height:22px;margin-top:10px;animation:ptIn .4s var(--ease)}
|
||||
.th-done .th-sos{font-weight:500;color:var(--ink-2)}
|
||||
.th-done.is-sos{height:auto}
|
||||
.th-note{position:fixed;z-index:60;left:0;top:0;pointer-events:none;border-radius:10px;
|
||||
background:#FFFDF6;box-shadow:0 6px 18px rgba(90,110,80,.25);padding:6px 9px;font-size:12px;color:var(--ink-2);
|
||||
max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.th-list{list-style:none;margin:10px 0 0;padding:0}
|
||||
.th-list.hide{display:none}
|
||||
.th-list li{position:relative;background:#fff;border-radius:14px;padding:9px 34px 9px 12px;margin-top:8px;
|
||||
border:1px solid #EAF2E6}
|
||||
.th-list time{display:block;font-size:11px;color:#8FA088}
|
||||
.th-list p{margin:3px 0 0;font-size:13px;line-height:1.6;color:var(--ink-2);white-space:pre-wrap;word-break:break-all}
|
||||
.th-list .th-del{position:absolute;right:8px;top:8px;border:0;background:none;padding:2px 4px;font-size:15px;color:#B9C4B4}
|
||||
.th-list .th-empty{text-align:center;color:var(--muted);font-size:12px;background:none;border:0}
|
||||
.th-help{margin-top:10px;font-size:11px;line-height:1.6;color:var(--muted)}
|
||||
.th-help a{color:#5E8A55;font-weight:700}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.pt-float,.pt-day.is-today i,.th-crown{animation:none}
|
||||
.pt-grid .pt-card{opacity:1;transform:none;transition:none}
|
||||
}
|
||||
|
||||
/* ============ 模块一 · 工具 ============ */
|
||||
.dock-head{display:flex;align-items:center;gap:8px;margin-bottom:7px}
|
||||
/* 说明行仍是 inline-flex:前面那枚小图标要和文字同行居中,
|
||||
|
||||
Reference in New Issue
Block a user