# 项目长期记忆(wx_pbzc 平板支撑训练小程序) ## 关键技术陷阱 - **小程序组件 style isolation**:自定义组件默认 `isolated`。组件内 `variant` 拼出的类(`gradient`/`in` 等)在组件作用域,页面里写的 `.组件类 ` 跨作用域选择器**全部失效**(如 `.ui-card.gradient .streak-num`)。唯一能跨边界传递的是 **CSS 自定义属性**(会沿 DOM 继承进 slot)。 - 推论:渐变/主色容器内要反白的文字,必须用 `var(--text)/var(--text-secondary)`(容器在 `.gradient` 里重定义过即会变白),**绝不能用 `var(--primary)`**——否则橙色字画在橙色底上=看不见。 - 进度条填充同理:用 `var(--on-primary, <原渐变>)` 让容器可覆盖为白,而非依赖失效的后代选择器。 - **图标 SVG 颜色是烤死在 data URI 里的**(`utils/icons.js` 的 `build()`:`*Fill` 用 `primary` 主题色、`*`(无 Fill)用灰 `#999`)。CSS 的 `color`/`currentColor`/变量**都改不了 `` 这种图标**。所以: - 放在**主色/渐变底**(gradient hero 卡、primary 实心按钮)上的图标必须用**白色变体**(`hotWhite`/`formWhite`/`playWhite`/`trophyWhite` 等),否则同色隐形。 - 加新图标或新彩色容器时,先想清楚底色:浅色底用 `*Fill`(主题色),彩色/主色底用 `*White` 变体。 - 主按钮 `ui-btn--primary` 背景是橙渐变,`playFill`(橙) 放上去即隐形——须用 `playWhite`。ghost/outline 按钮底色浅或灰,用 `*Fill`/灰色变体即可。 - `components/ui-btn` 图标尺寸 `1.3em`(随按钮字号自适应),不要回到 `1em` 以免显得过小。 - 深色模式有两套事实源,改配色前先统一:`theme.json` 的 dark 与 `app.wxss`/`utils/theme.js` 要一致(已统一到 `#131316` 系)。 - **微信圆形头像/图片裁剪陷阱(用户基础库实测)**:本项目的圆形头像在用户机器上表现如下—— - 单独给 `` 加 `border-radius:50%`(无 overflow 父层)→ 圆框里露方图,失效。 - 外层 `` 加 `overflow:hidden`+`border-radius:50%` 包裹 → **能裁切 SVG 占位图,但裁不掉远程照片(cloud:// 真实头像)**,照片仍方。所以日榜(多为占位图)正常、月榜/年榜(真实照片)却方。 - **唯一可靠写法**:外层 `overflow:hidden` 父层 **且** `` 自身也加 `border-radius:50%`(双保险)。设置页 `.avatar-btn`(button,overflow:hidden)+`.avatar-img`(image,border-radius:50%) 即是此写法且实测正常。排行榜 `.avatar`/`.avatar__img` 已对齐这一写法。 - 推论:以后做圆形头像,**必须** image 自身 `border-radius:50%`,不能只靠父层 overflow。 - **微信系统字体缩放导致文字溢出固定盒子(模拟器不重现、真机重现)**:微信会按用户「设置→通用→字体大小」自动放大**所有文字(含 rpx 字号)**,但**不放大 rpx 盒子尺寸**(width/height/padding)。症状:固定尺寸的圆形/方形容器里的文字在真机被放大、超出后被 `overflow:hidden` 裁掉,模拟器(标准档倍率 1.0)正常。 - 获取缩放:`wx.getAppBaseInfo().fontSizeScaleFactor`(当前字号÷标准 17px);旧接口 `getSystemInfoSync().fontSizeSetting`(px,标准 17)。 - **真机/模拟器不对称(关键坑)**:`getAppBaseInfo().host.env` 在模拟器是 `'devtools'`、真机是 `'wechat'`。模拟器会**原样返回你手机的真实缩放倍率**,但**渲染时并不会把文字放大**;真机才会真的放大。所以若不做区分,补偿把字号缩成 `design/factor` 后:真机放大回 design(正常),模拟器按缩小值渲染(数字变小=「不正常」)。→ **必须在 `host.env === 'devtools'` 时跳过补偿、直接用设计字号**;仅在真机/PC 微信才补偿。首页 `_readFontScale()` 已加此判断。 - 补偿法:把容器内的关键文字字号反向除以该倍率(`designRpx / factor`),并在 JS 里 clamp 到安全区间(如 48–96),使真机渲染成和模拟器一致的视觉大小、永不溢出。首页 `.target-time` 已用此法(`index.js` `_readFontScale()`)。 - 配套 CSS:`white-space:nowrap` 防换行顶出圆圈。 - 推论:以后凡把文字放进**固定尺寸的圆形/方形容器**且依赖 `overflow:hidden` 裁切,都必须考虑字体缩放补偿,否则真机大字体档必溢出。 - **排行榜头像 1–2s 延迟根因(设计使然,非 bug)**:`profile.avatarUrl` 存的是 `cloud://` 云存储 fileID(`settings._uploadAvatar` 用 `wx.cloud.uploadFile` 返回 fileID);云函数 `leaderboard` 直接透传,前端 `` 在 `setData` 渲染时才把 fileID 解析成临时 https URL(等效一次 `getTempFileURL` 网络往返)再下载。文字(昵称/时长/排名)是本地字符串瞬时渲染 → 故"文字先出、头像后出",延迟正是头像专属的「云存储解析 + 原图下载」段,叠加云函数冷启动。 - 占位图 `peopleFill` 是本地 SVG data URI 瞬时,所以日榜(多为占位)无感、月/年榜(真实照片)延迟明显。 - 优化方向:①云函数返回前对 `cloud://` 的 `avatarUrl` 批量 `cloud.getTempFileURL` 预解析成 https;②`_uploadAvatar` 上传前把 `chooseAvatar` 原图压缩/裁剪到最长边 ~200px、转 jpg/webp,体积降一个数量级;③云函数保活降冷启动。 - **云函数数据库写操作必须 `{ data: {...} }` 包裹(wx-server-sdk 关键坑)**:`wx-server-sdk`(本项目 2.6.3) 的 `db.collection().doc(id).set(x)` / `.update(x)` / `.add(x)` **写操作**,参数必须是 `{ data: realObj }` 包裹形式——SDK 内部读 `parameter.data` 当作要写入的文档。直接传裸对象 `set(realObj)` 会让 SDK 读 `realObj.data` === `undefined`,报 `parameter.data should be object instead of undefined`(读操作 `get()` 不受影响,裸对象/无参都行)。`leaderboard` 云函数 `_persistSnapshot` 踩过此坑:写快照全失败、集合一直空。 - 修复:`docRef.set({ data: { period, ranked, myOpenid, updatedAt } })`。写入后文档内容即 `realObj`(data 外套被 SDK 解开),读取 `doc.data` 仍是 `realObj`,读逻辑无需改。 - 推论:**以后任何云函数写库代码,先写 `{ data: ... }`**,别沿用客户端 SDK 的裸对象习惯;出错时先怀疑是不是漏了 data 包裹,而不是怀疑部署。 ## 排行榜架构(2026-07-28 落地 P0/P1/P2 后) - **预计算快照**:`cloudfunctions/leaderboard` 的 `exports.main` 现在区分两种调用—— - **定时触发器**(`config.json` 的 `triggers`:`snapshotTimer` / `0 */4 * * * * *`,每 4 分钟)进入 `_rebuildSnapshots()`:扫描一次 `plank_data`(`_fetchLatestByOpenid`,走 updatedAt 索引+投影),复用 `_buildFromScan()` 算 day/month/year 三榜,各自写进 `leaderboard_snapshot` 集合(doc._id=周期,字段 `period/ranked/myOpenid/updatedAt`;首次写自动建集合)。 - **客户端请求**:默认走 `_serveFromSnapshot()`(1 次 `get` ≈ 50ms),快照缺失/过期(>6min)才 `_computeBoard()` 实时重算并回写。**首开/冷启动已从 2-3s 降到 ≈50ms。** - **force 语义**:仅训练后那一次(客户端 `onShow` 读 `_lb_force_refresh` 置 `force=true`)走 `_computeBoard(force:true,persist:true)` 实时重算+回写快照;其他 99% 请求读快照,已满足"force 不再绕过云缓存"诉求。 - **P1 启动预热**:`app.js` `onLaunch` 在 `cloud.init()` 后 fire-and-forget 调一次 `leaderboard`(day),提前暖云实例/快照。 - **头像延迟**:云函数内 `getTempFileURL` 预解析已落地(在 `_buildFromScan` 里),前端不再懒加载,故"文字先出头像后出"的 1-2s 延迟已大幅消除;快照每 4min 重写,临时 URL 始终新鲜。 - **改动注意**:要生效必须**重新部署 `leaderboard` 云函数**(定时器随部署注册)。`plank_data.updatedAt` 单字段索引需在控制台已建(用户已建)。响应结构 `{period,ranked,myOpenid,myEntry,updatedAt}` 不变,客户端 `leaderboard.js` 无需改逻辑(仅注释更新)。 ## 约定 - UI 动词说法:index=首页/训练首页,timer=训练页,leaderboard=排行榜,records=记录,settings=设置。 - 美化按"见效快优先"分批做,每轮 node --check + grep 残留再交付。 - **git 推送需走 sandbox 外**:本机 Bash 工具默认 sandbox 隔离网络,`git push` 到 github.com 会 `SSL connection timeout`(约 5 分钟才报错)。必须用 `dangerouslyDisableSandbox: true` 在沙箱外执行推送(或 `run_in_background` + 沙箱外)才能连上。无 SSH key,remote 仅 HTTPS。