喝水打卡 · 家庭版
设计规范 V1.4
一份「能照着做」的规范:每个元素都给出设计说明、可复制的代码、真实渲染效果。所有效果的尺寸按 375px 逻辑宽还原,小程序里 1rpx = 0.5px,视觉一比一。
设计基调
principle整个产品的视觉气质由三句话定死,所有新增界面都往里靠:
| 基调 | 落地表现 | 反面(禁止) |
|---|---|---|
| 清爽不寡淡 | 白卡 + 极浅蓝底 + 带品牌色的淡阴影,靠留白和层次出质感 | 灰底灰卡、纯黑描边、堆高饱和色块 |
| 克制有反馈 | 动效只服务「进度 / 达标 / 解锁」三件事;点按 0.5s 微反馈,主进度 800ms | 无意义漂浮、循环闪烁、每屏都在动 |
| 语义唯一 | 蓝=过程、绿=完成、金=成就、红=危险。一色一义,全局不混用 | 同一颜色在不同页面表达不同含义 |
色彩令牌
color tokens
颜色只在 page 上定义一次(CSS 变量),页面与组件一律 var(--x) 引用,杜绝散落硬编码。下面是全部令牌——点任意色块可复制色值。
令牌定义(app.wxss 顶部)
/* 品牌色 */
--primary: #2f7cf6; /* 主蓝:进度 0% 起点、主按钮、选中态 */
--primary-dark: #1f63d6;
--primary-soft: #e9f1ff; /* 主色浅底:ghost 按钮 */
/* 完成色(进度 100% 终点) */
--ok: #22b07d;
/* 语义色 */
--danger: #f2555a; --danger-soft: #fff0f0; --warn: #f5a623;
/* 中性 */
--bg: #f4f8ff; --card: #ffffff;
--text: #17233d; /* 主文本:深蓝黑,不用纯黑 #000 */
--text-sub: #7d879c; --text-light: #a6b0c2; --line: #eef2f8;
/* 阴影:带品牌色调,不用中性灰 */
--shadow: 0 8rpx 32rpx rgba(31, 99, 214, 0.08);
语义映射(一色一义)
| 语义 | 颜色 | 使用位置 |
|---|---|---|
| 过程 / 进行中 | --primary | 进度 0–99%、主按钮、Tab 选中、天数徽标 |
| 完成 / 达标 | --ok | 进度 100%、达标文案、「已达目标」标签 |
| 成就 / 稀有 | 金 #ffd67a→#f0a93c | 成就卡描边、解锁标识、终极卡 |
| 危险 / 删除 | --danger | 退出小队、清空记录、解绑 |
| 等待 / 提醒 | --warn | 贴士「注意」类标签、未达标提示 |
字体、圆角与间距
typography & rhythm
字号只允许 6 档(20 / 22 / 24 / 26 / 28 / 30 rpx + 数字特例),圆角只允许 3 档,节奏靠 margin-top: 24rpx 的卡片序列撑起来。
| 圆角 | 卡片 28rpx · 按钮 46rpx(药丸)· 弹层 32rpx(顶角) |
| 页边距 | 左右 28rpx,卡片内 32rpx |
| 卡间距 | 24rpx(统一,不做「同组更紧」的例外) |
| 数字 | font-variant-numeric: tabular-nums,count-up 不抖 |
进度可视化(环 / 条 双模式)
progress核心组件,也是被反复打磨过的地方。三个必须遵守的点:
- 整体变色,不是空间渐变。0% 时整圈/整条全蓝,100% 时整体全绿;中途是整体统一的插值色。左边蓝右边绿、或沿弧渐变,都是错的。
- 两端圆头与弧同帧。圆头坐标由 JS 逐帧算好写入
left/top,绝不给它挂 CSS 过渡,否则会和弧「追不上」。圆头必须是与弧同色的实心圆——白底描边的小球会被看成「珠子」,而不是弧的圆润端头。 - 数字、弧、条、光点四者同源同色。都由
gradColor(p/100)一个函数产出,不允许各自算一遍。
拖动滑块看整体变色,切换「环 / 条」对比两种形态。
| 0% | 整体 #2f7cf6 全蓝 |
| 50% | 整体 #2a967a 蓝绿之间 |
| 100% | 整体 #22b07d 全绿 + 轨道转绿 |
渐进色引擎(唯一色源)
/** 蓝→绿渐进色:f∈[0,1],0=#2f7cf6 蓝,1=#22b07d 绿 */
function gradColor(f) {
const r = Math.round(47 + (34 - 47) * f)
const g = Math.round(124 + (176 - 124) * f)
const b = Math.round(246 + (125 - 246) * f)
const hex = '#' + [r, g, b].map((v) => (v < 16 ? '0' : '') + v.toString(16)).join('')
return { hex, glow: `rgba(${r}, ${g}, ${b}, 0.4)` }
}
/**
* 环形背景样式:整环是一张 SVG(viewBox 380),弧端圆头由 stroke-linecap="round" 负责
* 为什么不用 conic-gradient:它的弧端是径向硬边,想圆润只能补一个同色实心圆盖住 ——
* 那本质就是「一颗珠子」,用户一眼看穿。SVG 圆头是渲染器画进线段的,DOM 里没有端点元素。
*/
const RING_R = 174
const RING_CIRC = 2 * Math.PI * RING_R // ≈ 1093.27
function svgUrl(svg) {
const enc = encodeURIComponent(svg).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29')
return "url('data:image/svg+xml," + enc + "')"
}
function ringBgStyle(p, color, track) {
const dash = Math.max(0.01, RING_CIRC * Math.min(100, p) / 100)
let svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380">'
// 底轨道(满圈)
svg += '<circle cx="190" cy="190" r="' + RING_R + '" fill="none" stroke="' + track + '" stroke-width="32"/>'
// 进度弧:round 线帽 → 起点终点都是天然半圆,不需要任何补圆元素
if (p > 0) {
svg += '<circle cx="190" cy="190" r="' + RING_R + '" fill="none" stroke="' + color + '"' +
' stroke-width="32" stroke-linecap="round"' +
' stroke-dasharray="' + dash.toFixed(2) + ' ' + RING_CIRC.toFixed(2) + '"' +
' transform="rotate(-90 190 190)"/>'
}
return 'background-image:' + svgUrl(svg + '</svg>')
}
单帧渲染(一次 setData 打满所有元素)
applyPctStyle(ml, pct) {
const p = Math.max(0, Math.min(100, pct))
const done = p >= 100
const track = done ? 'rgba(34, 176, 125, 0.15)' : '#eef0f4'
// 整体渐变:中间进度「整体统一」为插值色,不是沿弧的空间渐变
const color = gradColor(p / 100).hex
this.setData({ // ← 一次 setData,绝不分多次
displayMl: ml, percent: Math.round(p), percentText: `${Math.round(p)}%`,
// 环:整个环就一个 view,背景是 SVG data URI,圆头在 SVG 内部
ringStyle: ringBgStyle(p, color, track),
pctColor: color,
barPct: Math.round(p * 10) / 10,
done
})
// ⚠️ 没有 dotX / dotY / dotGlow / barDotX —— 端点不再由任何元素承担
}
WXML 结构
<!-- 环:整个环只有一个 .ring,背景是 SVG;两端圆头由 stroke-linecap="round" 画进线段 -->
<view wx:if="{{progressStyle === 'ring'}}" class="ring-wrap">
<view class="ring" style="{{ringStyle}}"></view>
<view class="ring-inner">
<view class="ring-pct" style="color:{{pctColor}}">{{percentText}}</view>
<view class="ring-goal">{{displayMl}}/{{targetMl}}ml</view>
</view>
</view>
<!-- 条:填充自带 border-radius:999rpx,两端天然圆润,同样不需要端点元素 -->
<view wx:else class="bar-wrap">
<view class="bar-num" style="color:{{pctColor}}">{{displayMl}}<text class="bar-unit">ml</text></view>
<view class="bar-track">
<view class="bar-fill" style="width:{{barPct}}%;background:{{pctColor}}"></view>
</view>
<view class="bar-goal">目标 {{targetMl}} ml · {{percentText}}</view>
</view>
关键样式(节选)
.ring-wrap { position: relative; width: 380rpx; height: 380rpx; margin: 36rpx auto 0; }
.ring { position: absolute; inset: 0; border-radius: 50%; } /* 背景由 ringStyle 注入 conic-gradient */
/* 环:背景由 ringStyle 注入 SVG data URI;必须关掉 repeat 并拉伸到 100%,
否则 SVG 会按固有尺寸平铺,看起来像一圈碎弧 */
.ring {
position: absolute; left: 0; top: 0; width: 380rpx; height: 380rpx;
border-radius: 50%; background-repeat: no-repeat; background-size: 100% 100%;
}
/* ❌ 不要再出现 .ring-cap / .ring-head / .ring-dot:
同色圆盖硬边 = 换皮的珠子,用户照样看得出来。判定标准:停在 12% 放大看,
能看到一颗独立珠子轮廓就是错的。 */
/* 数字条:轨道 24rpx,填充整体色。填充自带 999rpx 圆角 → 两端天然圆润,不需要任何端点元素 */
.bar-track { position: relative; height: 24rpx; border-radius: 999rpx; background: #edf1f6; }
.bar-fill { position: relative; height: 100%; border-radius: 999rpx; overflow: hidden; }
conic-gradient 配一颗白底彩边的圆,谁都看得出是球;第二轮改成与弧同色的实心圆盖住硬边——看着像解决了,其实是「换皮的珠子」,被当场指出。最终方案是彻底弃用 conic-gradient,改 SVG stroke-linecap="round":圆头是渲染器画进线段两端的,DOM 里根本没有端点元素,从结构上杜绝了小球。
动效与达标庆祝
motion动效分三层:微反馈(点击涟漪)→ 主进度(800ms count-up + 弧增长)→ 庆祝(达标粒子,2.6–4.6s 自动收)。三层不叠加触发,避免「一次打卡满屏乱飞」。
| 层 | 时长 | 缓动 | 规则 |
|---|---|---|---|
| 点击涟漪 | 0.5s | cubic-bezier(.22,.61,.36,1) | 细圈 scale 0.4→2.6,透明度 0.85→0,不抢主体 |
| 主进度 | 800ms | ease-out | requestAnimationFrame 逐帧算百分比,数字用 tabular-nums |
| 达标回弹 | 0.7s | cubic-bezier(.34,1.56,.64,1) | 卡片轻微过冲回弹,仅达标瞬间一次 |
| 庆祝粒子 | 2.6–4.6s | linear / ease-out | 六模式随机(彩带/水滴/气泡/星芒/流光/光环),冷色水蓝家族,全程无红橙 |
| 弹层 | 0.28s | cubic-bezier(.22,.61,.36,1) | 自底部升起,遮罩 0.2s 淡入 |
点一下,触发达标庆祝层(真实粒子参数,随机位置/延迟/旋转)。
/* 点击涟漪:细圈 scale 2–3,0.5s,不抢主体 */
.tap-ripple {
position: absolute; width: 56rpx; height: 56rpx; margin: -28rpx 0 0 -28rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(72,202,228,.5), rgba(72,202,228,0) 70%);
animation: tap-ripple 0.5s cubic-bezier(.22,.61,.36,1) forwards;
}
@keyframes tap-ripple {
0% { transform: scale(.4); opacity: .85; }
100% { transform: scale(2.6); opacity: 0; }
}
/* 庆祝层容器:整屏、不接收事件 */
.celebrate { position: fixed; inset: 0; pointer-events: none; z-index: 80; overflow: hidden; }
--sx / --sy / --rot / --rz / --delay / --color 与 animation-duration,样式全部内联注入,避免为每个粒子写一套 keyframes。
成就卡弹层
achievement成就卡的视觉逻辑是「暗底 + 金边 + 流光」:深蓝黑渐变底让金色发光有色可依,conic 旋转描边制造「开箱感」,图标缓慢呼吸。它是全产品唯一允许用金色和深色底的地方。
| 底色 | 深蓝黑渐变 #232a38→#141922 |
| 金边 | 0 0 0 1px rgba(255,214,120,.34) |
| 流光 | conic-gradient 旋转 5.2s linear |
| 图标 | 46px + 金色 drop-shadow,呼吸 2.6s |
| 主按钮 | 金色实心 38px 高,深褐字 #3a2508 |
.achv-card {
position: relative; border-radius: 34rpx; padding: 44rpx 36rpx; text-align: center; overflow: hidden;
background: linear-gradient(160deg, #232a38, #141922 55%, #1d232f);
box-shadow: 0 0 0 1rpx rgba(255,214,120,.34),
0 36rpx 80rpx rgba(0,0,0,.42),
0 0 92rpx rgba(255,196,84,.16);
}
/* 旋转流光 */
.achv-card::before {
content: ''; position: absolute; inset: -60%; pointer-events: none;
background: conic-gradient(from 0deg, transparent 0 32%,
rgba(255,214,120,.42) 42%, transparent 52% 100%);
animation: achvSpin 5.2s linear infinite;
}
@keyframes achvSpin { to { transform: rotate(360deg); } }
cf- / drop- / bubble-,成就层 achv-。
导航与 tabBar
navigation
首页用自定义导航(navigationStyle: custom):左侧是小程序自身 app icon + 标题「喝水打卡」,右侧「成就墙」胶囊贴着系统胶囊左侧,保证不打架。tabBar 是自定义组件,只有三个文字项,选中项放大加粗。
下面是导航与 tabBar 的真实尺寸还原(状态栏 34 + 标题区 34 = 68px;tabBar 55px + 安全区)。
导航高度必须用 API 算,不能写死
const sys = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
const menu = wx.getMenuButtonBoundingClientRect() // 系统胶囊位置
const statusBarH = sys.statusBarHeight
const navH = (menu.top - statusBarH) * 2 + menu.height
// 成就墙胶囊贴到系统胶囊左侧,留 8px 间距
const achvRight = sys.windowWidth - menu.left + 8
this.setData({ statusBarH, navH, navTotal: statusBarH + navH, achvRight })
tabBar 隐藏机制(本版修复的关键问题)
自定义 tabBar 渲染在页面之上,页面内弹层把 z-index 调到 9999 也压不住它——个人资料弹窗的「保存」按钮就曾被它挡住。正确做法是打开弹层时主动收起 tabBar。
/* 1. 组件加 hidden 字段与过渡 */
Component({ data: { selected: 0, hidden: false, list: [...] } })
/* wxml */
<view class="tab-bar {{hidden ? 'is-hidden' : ''}}"> … </view>
/* wxss */
.tab-bar { transition: transform 0.24s cubic-bezier(.22,.61,.36,1), opacity 0.24s ease; }
.tab-bar.is-hidden { transform: translateY(100%); opacity: 0; pointer-events: none; }
/* 2. 页面封装 */
_setTabBarHidden(hidden) {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ hidden })
}
}
// 打开弹层 → true;关闭/保存成功 → false;onShow 里再兜底置 false
同一机制也适用于首页的成就卡弹层(全屏遮罩):celebrateAchv() 打开时收起、closeAchvPop() 关闭时恢复。三个 tabBar 页面的 onShow 都统一重置 hidden: false,防止状态残留。
底部弹窗 · 分段控件 · 双列选择
sheet & picker
底部弹窗统一规格:顶角 32rpx 圆角、遮罩 rgba(20,28,46,.5)、底部留 env(safe-area-inset-bottom)。表单类弹窗的操作按钮固定在弹层底部(不跟随滚动)。
生日/纪念日用 月 + 日两列 picker-view:公历按当月实际天数生成日列(闰年自动 29 天),农历固定 30 天;换月或换历法时对已选日做钳位,不会出现「2月30日」。
点按钮切换 tabBar 状态,直观看到「遮挡 → 让位」的差别。
/* 弹层:底部安全区留白,操作按钮固定在最底部 */
.sheet {
width: 100%; background: #fff; border-radius: 32rpx 32rpx 0 0;
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
animation: sheetUp 0.28s cubic-bezier(.22,.61,.36,1);
}
/* 两列联动:换月/换历法后重建日列并钳位 */
onProfilePick(e) {
const m = Number(e.detail.value[0]) + 1
const d = Number(e.detail.value[1]) + 1
const max = this._dayCount(this.data.pfBirthCal, m) // 公历实天数 / 农历 30
const day = Math.min(d, max) // 钳位,绝不出现 2月30日
this.setData({ pfBirthMonth: m, pfBirthDay: day, pfBirthDays: this._dayNames(cal, m) })
}
列表、卡片与小贴士
lists & cards列表块统一「白卡 + 32rpx 内边距 + 24rpx 卡间距」。时间线用 1rpx 竖线 + 7rpx 圆点,只标今日记录;贴士卡用竖向 swiper(5s 自动、circular 循环),每条一个彩色分类 chip。
| 时间线 | 竖线 1rpx + 圆点 7rpx,时间列固定 44px |
| 提醒卡 | 浅渐变底 #eef6ff→#e6fbf3,区别于普通白卡 |
| 贴士 chip | 时机蓝 / 科学绿 / 注意黄 / 过量红 |
| 免责 | 单行省略,24rpx 灰字,不换行不占两行 |
成员行「提醒」按钮:可用 / 已提醒 两态
每个成员对其他每个成员,每自然日各 1 次提醒额度(服务端按北京时间自然日频控,到次日 00:00 自动恢复)。A 提醒过 B 后,只有 B 那一行按钮灰度;A 对 C、D… 仍各有 1 次。点击灰度按钮不再发请求,直接给一句文案——比让用户点了再弹失败提示体验好得多。
点蓝色发提醒 → 该成员行置灰(对 TA 今日额度用尽),点灰色看文案提示。
| 可用态 | 底 rgba(47,124,246,.08) 字 #2f7cf6 |
| 灰度态 | 底 #f2f4f8 字 #b3bdcc |
| 尺寸 | padding 10/22rpx,圆角 999rpx,字 22rpx |
| 状态来源 | 云函数 getFamilyDailySummary 按成员富集 remindedToday(当前用户当日是否已提醒过该成员) |
<!-- wxml:按该成员是否被提醒过挂 dim 类,并把状态带进 dataset -->
<view wx:if="{{!item.isMe && item.status !== 'left'}}"
class="member-remind {{item.remindedToday ? 'dim' : ''}}"
data-id="{{item.userId}}" data-name="{{item.displayName}}"
data-reminded="{{item.remindedToday ? 1 : 0}}"
catchtap="onRemindMember">提醒</view>
/* wxss:灰度态 —— 只降饱和,不降透明度(避免看起来像禁用按钮点不动) */
.member-remind.dim { background: #f2f4f8; color: #b3bdcc; }
// js:已提醒该成员则不发请求,直接给文案;发送成功后本地即时置灰该行
onRemindMember(e) {
const reminded = Number(e.currentTarget.dataset.reminded) === 1
if (reminded) {
wx.showToast({ title: '今天已提醒过该成员了', icon: 'none' })
return
}
// …确认弹窗 → api.call('sendFamilyReminder') → 本地置灰该行
const idx = this.data.members.findIndex((m) => m.userId === toOpenid)
if (idx >= 0) this.setData({ [`members[${idx}].remindedToday`]: true })
}
// 云函数 getFamilyDailySummary:按「我当日发出的、按目标分组」富集状态
const today = beijingDate()
const logRes = await db.collection('reminder_log')
.where({ fromOpenid: OPENID, date: today })
.limit(200).get()
const remindedMap = {}
;(logRes.data || []).forEach((l) => { remindedMap[l.toOpenid] = true })
members.forEach((m) => { m.remindedToday = !!remindedMap[m.userId] })
// 云函数 sendFamilyReminder:发送前按「fromOpenid + toOpenid + 自然日」频控,逐对用尽即拒
const c = await db.collection('reminder_log')
.where({ fromOpenid: OPENID, toOpenid, date: today }).count()
if (c.total >= 1) return fail('ERR_RATE_LIMITED', '今天已提醒过该成员了')
remindedToday=false)。另外服务端拒绝时前端也会同步置灰,双保险。
头像体系
avatars两套池子:用户头像 40 个动物 emoji;小队头像 14 个友谊符号(现成 emoji,全中性、不指代人数)。头像一律无彩色底——emoji 本身即视觉主体,加蓝渐变底会显脏;只有「字母回退」态才给淡蓝底。
小队头像 · 14 个友谊符号(V1.4 定稿)
用户头像 · 40 个动物(节选 12)
// 小队头像池:友谊符号主题(14 个,全中性无人数指代)
const FAMILY_AVATARS = [
{ value: '🤝', type: 'emoji', label: '握手同盟' },
{ value: '🫂', type: 'emoji', label: '好友拥抱' },
{ value: '🪢', type: 'emoji', label: '同心结' },
{ value: '🫶', type: 'emoji', label: '比心' },
{ value: '💛', type: 'emoji', label: '友谊黄心' },
{ value: '💞', type: 'emoji', label: '心心相印' },
{ value: '✨', type: 'emoji', label: '闪闪友谊' },
{ value: '📸', type: 'emoji', label: '合影留念' },
{ value: '🍀', type: 'emoji', label: '幸运同队' },
{ value: '🎉', type: 'emoji', label: '派对时刻' },
{ value: '🍻', type: 'emoji', label: '举杯碰杯' },
{ value: '☕', type: 'emoji', label: '咖啡之约' },
{ value: '🧩', type: 'emoji', label: '缺一不可' },
{ value: '♾️', type: 'emoji', label: '友谊无限' }
]
utils/avatars.js 与云函数 createFamily / updateFamilyName 的 ALLOWED_AVATARS 必须一字不差,否则用户选了新头像保存会「莫名失败」。
音效
audio feedback不用音频文件,用 WebAudio 振荡器合成:零资源、零包体、点击瞬间即响。三个音色对应三个语义,音量 0.18、时长 120ms、正弦波,短促不吵。
| 波形 | sine(正弦) |
| 包络 | 0.01s 起音 → 0.12s 指数衰减到 0.0001 |
| 降级 | 基础库 < 2.19.0 无 API 时静默跳过,不报错 |
function beep(type) {
var freq = type === 'unlock' ? 1046 : type === 'done' ? 1318 : 880;
try {
if (typeof wx === 'undefined' || !wx.createWebAudioContext) return;
var ctx = wx.createWebAudioContext();
var osc = ctx.createOscillator(), gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.type = 'sine'; osc.frequency.value = freq;
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.18, ctx.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.12);
osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.13);
} catch (e) { /* 静默降级 */ }
}
bindtap 处理函数的第一行。第一版写在 await api.call() 之后,听起来比点击晚半秒,被判定为「顿挫感」。现在的写法是:点击 → 立即响 → 并行发请求。
工程规范
engineering代码层面的硬约束,改代码前先过一遍,能避免 90% 的性能与体验坑。
| 类别 | 规则 |
|---|---|
| setData | 一次渲染只调一次,合并所有字段;不 setData 整个 data 对象;列表项字段用精确路径 'list[3].ml' |
| 动画 | 只动 transform / opacity;绝不动 width / top / left(环光点例外:由 JS 逐帧写入,不加过渡) |
| 请求 | 统一走 utils/api.js 的 api.call(name, data),禁止散调用 wx.cloud.callFunction |
| 云函数 | 统一返回 { ok: true, data } 或 { ok: false, msg };错误由 api.showError 统一提示 |
| 时间 | 统一用北京时间:new Date(Date.now() + 8*3600*1000) 取 UTC 字段,不用本地时区 |
| 隐私 | 首屏不强制登录,浏览可用;收集项必须在隐私弹窗内勾选同意后继续 |
| 包体 | 主包 < 2MB;设计稿/文档一律不放进工程根,`packOptions.ignore` 排除 _assets、设计规范 |
| 命名 | wxss 用 block-element 短横线;关键帧带模块前缀(achv- / cf-),禁止跨模块重名 |
| 注释 | 「为什么」写注释,「做什么」靠命名;重要踩坑记录写在文件头 |
统一请求层
/** 云函数调用统一包装:{ ok, data } / { ok:false, msg } */
function call(name, data = {}) {
return wx.cloud.callFunction({ name, data }).then((res) => {
const r = res.result || {}
if (r.ok === false) throw new Error(r.msg || '操作失败')
return r.data
})
}
function showError(e) {
wx.showToast({ title: (e && e.message) || '网络开小差了', icon: 'none' })
}
module.exports = { call, showError }
资产索引
assets
全部可复用资产在 v1.4-附件/01-资产库/,wxss / wxml 均为工程源文件原样切片,可直接粘贴运行。详细依赖与复用步骤见 01-资产库/README.md。
| # | 资产 | 路径 | 说明 |
|---|---|---|---|
| 00 | 设计令牌 | 01-资产库/tokens.wxss | 全局变量 + 原子类,可作新工程 app.wxss 骨架 |
| 01 | 进度环 / 条 | styles/01-progress.wxssjs/grad-color.jssnippets/progress-ring.wxml | 双模式 + 整体变色引擎 + 两端同色圆头(无端点小球) |
| 02 | 涟漪 / 庆祝层 | styles/02-celebrate.wxss | 六模式粒子,CSS 变量驱动 |
| 03 | 成就卡 | styles/03-achievement.wxsssnippets/achievement-card.wxml | 金色流光卡 + 独立关键帧命名空间 |
| 04 | 自定义导航 | styles/04-custom-nav.wxsssnippets/custom-nav.wxml | 品牌居左 + 成就墙胶囊贴系统胶囊 |
| 05 | 列表块 | styles/05-blocks.wxss | 时间线 / 提醒卡 / 贴士轮播 |
| 06 | 底部弹窗 | styles/06-sheet-picker.wxsssnippets/sheet-picker.wxml | sheet + seg + 双列 picker(需 tabBar hidden) |
| 07 | 音效 | js/sound.js | WebAudio 合成,零资源 |
| 08 | 头像体系 | js/avatars.js | 40 动物 + 14 友谊符号,双端白名单 |
| 09–10 | 贴士 / 趋势 | js/healthTips.jsjs/trend.js | 贴士数据 + 趋势计算 |
| 11 | 组件 | components/avatar-picker/components/privacy-modal/ | 四件套完整目录 |
| 12–14 | 云函数 | cloudfunctions/getWeather/getMyAchievements/getFamilyDailySummary/ | 天气代理 / 成就结算 / 小队日汇总 |
| 07–08 | 订阅提醒 UI + 自检面板 | styles/07-subscribe.wxssstyles/08-diag.wxsssnippets/slot-grid.wxmlsnippets/quota-card.wxmlsnippets/diag-panel.wxml | 卡头开关 / 整点格子 / 额度卡 / 授权按钮 / 诊断弹层 详见 喝水打卡_推送提醒设计规范_V1.4.html |
| 19–22 | 推送云函数 | pushScheduledReminders/checkPushStatus/sendFamilyReminder/setReminderPlan/ | 定时推送 / 链路自检 / 队友互提醒 / 计划读写(含额度自愈) |
07-设计规范/喝水打卡_推送提醒设计规范_V1.4.html(10 章)。本页只管「长什么样」,那份管「为什么这么做、怎么做才不会静默失效」。
01-资产库/ 里的资产 + 本规范文档,页面里保持引用一致;两者一旦分叉,新页面就会「长歪」。改资产时同步更新本页对应的代码块。