feat: 首页按钮样式调整(小圆角/并排/高度对齐)
- 主按钮与自由/循环训练按钮圆角统一为 16rpx(直角→折中方案) - 自由训练/循环训练两个次按钮改为并排一行,文字精简为「自由训练」「循环训练」 - 两个次按钮 size 由 md 改为 xl,与主按钮高度一致 - 新增 preview-buttons.html 对照预览 - 更新工作记忆
This commit is contained in:
@@ -201,3 +201,12 @@ inline 盒内包 block 子元素时,inline 盒被打断成"前/后"两个零
|
||||
|
||||
### ⚠️ 部署待办
|
||||
`cloudfunctions/tts` **尚未重新部署**。新增的 `restStart / nextSet / lastSet` 三条词条在云端词表白名单里还不存在,线上会返回 `Unknown prompt key` → 客户端静默降级(不报错、不弹窗,只是不出声)。循环训练的过场语音要生效必须先部署这个云函数。
|
||||
|
||||
## 首页按钮改直角(2026-08-03 末尾)
|
||||
|
||||
用户偏好无圆角按钮。把 `pages/index/index.wxml` 中 5 个 `ui-btn` 全加 `custom-style="border-radius:0;"`(主训练按钮 xl-primary、两个 ghost 次按钮、两个弹窗 primary-lg 按钮)。
|
||||
- **实现要点**:`ui-btn` 是全局共用组件且 `style isolation: isolated`,页面 wxss 改不到内部圆角;但组件暴露 `customStyle` 属性(直接挂到内部 `<button>` 的 `style`),内联 `border-radius:0` 可覆盖组件内置的 `48rpx`,**仅影响首页、不动全局与其他页面**。
|
||||
- 生成 `preview-buttons.html` 对照预览(圆角 vs 直角)。
|
||||
- **用户反馈直角不好看 → 改为折中 `border-radius:10rpx`(8–12 中间值)**,5 处 `custom-style` 已更新。仅首页、不动全局。预览页「改后」栏同步更新为 10rpx 效果。
|
||||
- **布局调整**:首页「自由训练 / 循环训练」两个 ghost 次按钮从竖排改为并排一行。wxml 用 `<view class="action-row">` 包住两个 `<ui-btn class="action-cell">`;index.wxss 加 `.action-row{display:flex;gap:16rpx}` + `.action-row .action-cell{flex:1;min-width:0}`(宿主节点为 flex 项,flex:1 各占一半,保留 `block` 让内部按钮撑满宿主)。按钮文字精简为「自由训练」「循环训练」。
|
||||
- **高度对齐**:自由/循环两个 ghost 次按钮 `size` 从 `md`(80rpx) 改为 `xl`(108rpx),与主按钮 `size="xl"` 高度一致(圆角保持 16rpx)。预览页 `btn--md`→`btn--xl` 同步。仅首页、不动全局组件。
|
||||
|
||||
+19
-75
@@ -1,81 +1,25 @@
|
||||
# 项目长期记忆(wx_pbzc 平板支撑训练小程序)
|
||||
|
||||
## 关键技术陷阱
|
||||
- **小程序组件 style isolation**:自定义组件默认 `isolated`。组件内 `variant` 拼出的类(`gradient`/`in` 等)在组件作用域,页面里写的 `.组件类 <slot后代>` 跨作用域选择器**全部失效**(如 `.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`/变量**都改不了 `<image src>` 这种图标**。所以:
|
||||
- 放在**主色/渐变底**(gradient hero 卡、primary 实心按钮)上的图标必须用**白色变体**(`hotWhite`/`formWhite`/`playWhite`/`trophyWhite` 等),否则同色隐形。
|
||||
- 加新图标或新彩色容器时,先想清楚底色:浅色底用 `*Fill`(主题色),彩色/主色底用 `*White` 变体。
|
||||
- 主按钮 `ui-btn--primary` 背景是橙渐变,`playFill`(橙) 放上去即隐形——须用 `playWhite`。ghost/outline 按钮底色浅或灰,用 `*Fill`/灰色变体即可。
|
||||
- `components/ui-btn` 图标尺寸 `1.3em`(随按钮字号自适应),不要回到 `1em` 以免显得过小。
|
||||
- **需要「随状态换色」的小装饰件,一律用纯 CSS 画,别用 icon**:图标色改不动,而状态灯/圆点要在 白/绿/灰 间切换。`timer` 页循环训练徽章的 `.circuit-phase-dot` 即是此解(14rpx CSS 圆点 + BEM 状态类控制 background)。
|
||||
- **`timer` 页新增顶部元素必须 `position:relative; z-index:1`**:`.breathe-ring`(呼吸光晕,`z-index:0`)在 DOM 里排在页面上方元素之后,同 stacking 上下文下会反盖上去,把主色光晕晕染到白卡片上。循环训练进度面板 `.circuit-bar` 踩过。
|
||||
- 深色模式有两套事实源,改配色前先统一:`theme.json` 的 dark 与 `app.wxss`/`utils/theme.js` 要一致(已统一到 `#131316` 系)。
|
||||
- **微信圆形头像/图片裁剪陷阱(用户基础库实测)**:本项目的圆形头像在用户机器上表现如下——
|
||||
- 单独给 `<image>` 加 `border-radius:50%`(无 overflow 父层)→ 圆框里露方图,失效。
|
||||
- 外层 `<view>` 加 `overflow:hidden`+`border-radius:50%` 包裹 → **能裁切 SVG 占位图,但裁不掉远程照片(cloud:// 真实头像)**,照片仍方。所以日榜(多为占位图)正常、月榜/年榜(真实照片)却方。
|
||||
- **唯一可靠写法**:外层 `overflow:hidden` 父层 **且** `<image>` 自身也加 `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` 直接透传,前端 `<image src="cloud://...">` 在 `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 包裹,而不是怀疑部署。
|
||||
## 关键技术陷阱(最高优先级,改样式/加组件前必读)
|
||||
- **组件 style isolation = `isolated`**:页面 wxss 改不了组件内部类;唯一能跨边界的是 **CSS 变量**(沿 DOM 继承进 slot)。渐变/主色容器内反白文字必须用 `var(--text)`,**绝不**用 `var(--primary)`(橙字画橙底=隐形)。进度条填充用 `var(--on-primary,...)` 让容器可覆盖为白。
|
||||
- **图标色烤死在 SVG data URI**(`utils/icons.js`:`*Fill`=主题色、`*`(无Fill)=灰 `#999`),CSS 改不了 `<image>`。主色/渐变底(hero 卡、primary 按钮)须用 `*White` 变体(如 `playWhite`),浅底用 `*Fill`。**随状态换色的小装饰用纯 CSS 画**(如 `.circuit-phase-dot`),别用 icon。
|
||||
- **圆形头像裁剪**:必须「外层 `overflow:hidden` + `<image>` 自身 `border-radius:50%`」双保险,否则远程照片(cloud://)裁不掉。
|
||||
- **系统字体缩放致文字溢出固定盒**(真机重现、模拟器不重现):`getAppBaseInfo().host.env==='devtools'` 时跳过补偿,真机/PC 才把字号 `÷factor` 并 clamp。凡文字进固定圆/方盒且靠 `overflow:hidden` 裁切,都要考虑。
|
||||
- **云函数写库必须 `{ data: {...} }` 包裹**(wx-server-sdk 2.6.3):`set/update/add` 漏包裹报 `parameter.data should be object`。读操作 `get()` 无此限制。
|
||||
- **自定义组件宿主默认 inline**:页面给 `<ui-card>` 等标签加 `border`/`transform`/`box-shadow` 会碎裂或静默失效(仅 `ui-card` 已修 `:host{display:block}`)。描边优先用 `box-shadow:0 0 0 Nrpx` 并补 `border-radius`,别用 `border`。跨组件边界的 `nth-child` 选择器一律失效,交错延迟由页面传 `index` 驱动。
|
||||
|
||||
## 排行榜架构(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/endurance 四榜,各自写进 `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` 无需改逻辑(仅注释更新)。
|
||||
- **耐力榜(第 4 个 tab,2026-07-31 新增)**:全局单次最久。`_buildFromScan` 的 `endurance` 分支遍历每人全部 records 取 `max(duration)` 并记下该条 `r.date` 作"产生时间"→ `bestDate` 字段靠 `publicEntry` 的 `...publicData` 展开透传进快照/响应(无需改 data.js)。客户端 `periods` 加 `{key:'endurance',label:'耐力',maxRank:10}`;行/领奖台/my-bar 的副标签统一改用 `subText`(耐力=格式化日期 `YYYY-MM-DD HH:MM`,其余=`N次`)。本人未进 top10 时由既有 my-bar(`myEntry.rank > rankedList.length`)在底部显示其最佳成绩+日期——依赖快照存 top500、`findMyEntry` 在完整 500 条里定位本人,故即便排 200 也能被找到。**改云函数后须重新部署**(定时器随部署注册 `endurance`)。
|
||||
## 排行榜架构(2026-07-28+,云函数 `cloudfunctions/leaderboard`)
|
||||
- **预计算快照**:定时触发器(每 4min)`_rebuildSnapshots()` 扫 `plank_data` 算 day/month/year/endurance 四榜写 `leaderboard_snapshot`;客户端 99% 走 `_serveFromSnapshot()`(≈50ms),仅训练后 `force=true` 那次实时重算+回写。**改后须重新部署云函数**(定时器随部署注册)。
|
||||
- **耐力榜(endurance)**:全局单次最久,取 `max(duration)` 并记 `bestDate`;本人未进 top10 由 my-bar 在底部显示(依赖快照存 top500)。
|
||||
- 头像延迟已通过云函数 `getTempFileURL` 预解析解决;`plank_data.updatedAt` 需建单字段索引。
|
||||
|
||||
- **TTS 语音词条是「两份白名单」,必须同步**:`utils/voice.js` 的 `PROMPTS` 与 `cloudfunctions/tts/index.js` 的 `PROMPTS` 各存一份。客户端有、云端没有 → 云函数返回 `Unknown prompt key`,`_fetchUrl` 得 `success:false` 后返回 null,`play()` 静默 return(**不报错、不弹 toast**,只是不出声,极难察觉)。加词条后**必须重新部署 tts 云函数**。词条设计上要**避免包含数字/变量**(如"第3组"),否则得按 N 合成 N 份音频;用"下一组,准备开始"这类通用句即可覆盖任意组数。
|
||||
- **`voice.play` 会截断前一条语音**(`_playSeq` 机制:新 play 令旧的作废)。凡是可能同一 tick 触发两条语音的地方,必须定优先级。已知必撞场景:circuit 的 halfway 触发点 = `sets×hold/2`,**偶数组时必然落在第 sets/2 组最后一秒**,与 rest 阶段切换同刻 → 用 `_playProgressVoice` 设 `_voiceBusyUntil = now+2600`,让阶段语音在窗口内让位。类似地,每组 <10 秒时不播过场语音(一句话约 2 秒,否则连珠炮)。
|
||||
- **circuit 的进度播报基准必须是整场(`sets × holdPerSet`),不能是每组**:否则 4 组会播 4 次"已完成一半啦"。判据用 `<=` 而非 `===`,容忍小程序后台回前台的跳秒补偿。只在 `phase==='working'` 求值——休息时 `workTotal` 冻结,天然不会重复触发。
|
||||
|
||||
- **`wx.setKeepScreenOn` 是「小程序级」不是「页面级」,且切后台会失效**(训练页已用,`pages/timer/timer.js` 的 `_keepScreenOn`):
|
||||
- 开了不关 → 用户离开训练页后,首页/记录/榜单全程常亮耗电,只有**退出小程序**才自动失效。故 `onUnload` **必须**显式 `keepScreenOn:false`。
|
||||
- 反过来,**切后台再回前台会被系统清掉**,所以 `onShow` 里要 `if (this.data.isRunning) 重新武装`,否则后半程又开始息屏丢语音/震动。
|
||||
- `fail` 必须静默(部分 Android ROM 省电模式拒绝该调用),低基础库用 `typeof wx.setKeepScreenOn !== 'function'` 降级。
|
||||
- 背景:息屏/后台后 JS 定时器被节流,`Timer`/`CircuitTimer` 靠 `Date.now` 补偿秒数不会错,但**实时提示(语音/震动)错过就没了**——平板支撑用户手撑地不碰屏,60 秒必息屏,所以常亮实际是在修 bug 而非锦上添花。
|
||||
|
||||
- **自定义组件宿主节点默认是 inline —— 页面侧给组件标签加 border/box-shadow/transform 会碎裂或静默失效**:本项目 `components/` 下 7 个组件**没有任何一个**写了 `:host { display: block }`(全局 grep `:host`/`display:block` 零匹配),组件 wxss 只给内部那个 view(如 `.ui-card`)写样式。宿主节点(页面 wxml 里的 `<ui-card>` 标签)因此是 inline 盒。
|
||||
- 页面 wxss **可以**命中宿主节点上的 class(style isolation 只隔离组件内部),所以样式确实生效了——但按 inline 盒规则渲染:
|
||||
- `border` → inline 盒被内部 block 打断成前/后两个**零宽行盒**,只在碎片上画左右边框(各 4rpx 贴合成一根竖线)→ 视觉是 block **左上角、左下角各一条竖条**,而不是包一圈。首页 `.just-completed`(训练完成 3 秒高亮)就是这个症状。
|
||||
- `transform: scale()` → **完全无效**(不适用于非替换 inline 元素),脉冲/缩放动画静默不生效。
|
||||
- `box-shadow` → 同样只画在零宽碎片上,等于看不见。
|
||||
- **诊断特征**:只要是"某个自定义组件标签上加了装饰样式,结果出现零散短线/动画不动",先怀疑宿主 inline,而不是怀疑选择器没命中。
|
||||
- **修法**:组件 wxss 加 `:host { display: block; }`(小程序支持 `:host`)。改前需回归各页面该组件的间距布局(inline→block 会吃掉行内空白、宽度变 100%);`ui-btn` 这类可能被当行内用的组件要单独评估。
|
||||
- **同源坑**:`ui-card.wxss` 的 `.ui-card.in:nth-child(1..4)` 交错入场延迟也是失效的——`.ui-card` 在组件内部永远是根下第 1 个子元素,恒匹配 `nth-child(1)`。跨组件边界做"第 N 个"选择器一律不可靠,要交错必须由页面传 index 或内联 `animation-delay`。
|
||||
- **已修(2026-08-03,仅 `ui-card`)**:加了 `:host { display:block; margin-bottom:24rpx; box-sizing:border-box }`,并把 `margin-bottom` 从内部 `.ui-card` **上提到宿主**——否则页面侧加的边框会把 24rpx 外边距一起圈进框里,框比卡片高一截。交错延迟改为 `index` 属性驱动的内联 `animation-delay`(`50 + min(index,3)*100` ms),14 处调用点全部显式传 `index`;`index` 默认 0,漏传也只是退回旧行为,零回归。
|
||||
- **其余 6 个组件宿主仍是 inline,不要盲目批量加 `:host{display:block}`**:`ui-btn` 在 `pages/timer/timer.wxml` 里靠 `custom-style="margin-right:28rpx"` 并排布局,改 block 会直接撑成整行。要改必须逐个回归。
|
||||
- **页面侧给组件标签做描边,优先用 `box-shadow: 0 0 0 Nrpx <color>` 而不是 `border`**:border 会改变宿主盒尺寸(撑大或在 border-box 下向内压窄内容),高亮出现/消失时布局跳动;box-shadow 不占布局空间。但宿主自身没有圆角,必须显式补上与内部卡片一致的 `border-radius`,否则描边是直角框套在圆角卡片外面。
|
||||
## 语音 / 常亮 / 循环训练
|
||||
- **TTS 词条两份白名单**(`utils/voice.js` 与 `cloudfunctions/tts/index.js`)须同步;加词条后**必须重新部署 tts 云函数**。词条避免含数字/变量(如"第3组"),用通用句。
|
||||
- **`voice.play` 截断前一条**(`_playSeq`):circuit halfway 与 rest 切换同刻须用 `_voiceBusyUntil` 让位;每组<10s 不播过场。
|
||||
- **`wx.setKeepScreenOn` 是小程序级、切后台失效**:`onUnload` 须显式关,`onShow` 若 `isRunning` 须重新武装(否则后半程息屏丢语音/震动)。
|
||||
- **循环训练 circuit**(2026-08-03):自由训练的循环形态,不碰 `plan.js`。入口首页 ghost 按钮→配置弹窗→`timer?circuit=1&...`。`duration`=组数×每组秒(不含休息);记录 `mode:'circuit'`、`planId:'circuit'`(计入连胜不计入计划进度)。状态机 `utils/circuitTimer.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。
|
||||
|
||||
## 功能:循环训练 circuit(2026-08-03 落地,简化版 + 配置持久化)
|
||||
- 定位:**「自由训练」的循环形态**,不是新计划体系。不碰现有 `utils/plan.js` 的 hold 模式与每日计划进度。
|
||||
- 入口:首页「循环训练 · 分组练习」ghost 按钮 → 底部配置弹窗(stepper 调 每组时长/组数/休息/每周目标)→ `saveCircuitConfig` + 跳 `timer?circuit=1&hold=&sets=&rest=&sessions=`。
|
||||
- 配置持久化:`utils/storage.js` 的 `getCircuitConfig/saveCircuitConfig`(本地 `circuit_config` 键,含默认值+范围校验,**不进云同步**)。下次打开弹窗预填上次值。
|
||||
- 状态机:`utils/circuitTimer.js`(新)`CircuitTimer` 类,接口仿 `Timer`:`start/stop/pause/resume` + `onTick/onPhaseChange/onComplete`;`stop()` 返回 `workTotal`(有效撑总秒,不含休息)。
|
||||
- **关键不变量 1:`duration` 始终 = 有效撑总秒(组数×每组秒,不含休息)** → 排行榜/里程碑/统计零改动。
|
||||
- **关键不变量 2:循环记录 `planId` 用 `'circuit'`** → `getPlanDay` 不会把它算进每日计划进度;但 `updateStreak` 仍计入连胜(用户确实练了)。
|
||||
- 记录落库字段:`mode:'circuit'` + `sets/holdPerSet/restPerSet/sessionsPerWeek`(records 页按 `r.mode` 显示「循环 N组×Ms」)。
|
||||
- 阶段提示:TTS 只有 6 个固定 key 无"第N组/休息"词 → circuit 阶段切换用**震动 + 屏上 phaseTip 文本**,完成复用 `complete` 语音。
|
||||
- 进度环:circuit 模式轨道恒中性灰(否则每阶段都按"累计≥阶段长"误判变绿)。
|
||||
- 未做(可选后续):周频次硬追踪(仅存 sessionsPerWeek 作显示)、逐周进阶、设置页统一管理。
|
||||
- 验证:6 个 JS 全过 `node --check`;未动云函数/榜单/云同步。
|
||||
- 页面叫法:index=训练首页,timer=训练页,leaderboard=排行榜,records=记录,settings=设置。
|
||||
- 美化按"见效快优先"分批,每轮 `node --check` + grep 残留再交付。
|
||||
- **git push 须 `dangerouslyDisableSandbox:true`**(沙箱隔离致 github SSL 超时);无 SSH,仅 HTTPS remote。
|
||||
|
||||
+13
-4
@@ -89,26 +89,33 @@
|
||||
text="{{todayDone ? '再次训练' : '开始训练'}}"
|
||||
icon-src="{{icons.playWhite}}"
|
||||
bindtap="onStartTrain"
|
||||
custom-style="border-radius:16rpx;"
|
||||
></ui-btn>
|
||||
|
||||
<view class="action-row">
|
||||
<ui-btn
|
||||
class="action-cell"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
size="xl"
|
||||
block
|
||||
text="自由训练 · 自定义时长"
|
||||
text="自由训练"
|
||||
icon-src="{{icons.timeFill}}"
|
||||
bindtap="onFreeTrain"
|
||||
custom-style="border-radius:16rpx;"
|
||||
></ui-btn>
|
||||
|
||||
<ui-btn
|
||||
class="action-cell"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
size="xl"
|
||||
block
|
||||
text="循环训练 · 分组练习"
|
||||
text="循环训练"
|
||||
icon-src="{{icons.repeatFill}}"
|
||||
bindtap="onCircuitTrain"
|
||||
custom-style="border-radius:16rpx;"
|
||||
></ui-btn>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间选择弹窗 -->
|
||||
<ui-modal
|
||||
@@ -148,6 +155,7 @@
|
||||
block
|
||||
text="开始自由训练"
|
||||
bindtap="onConfirmFree"
|
||||
custom-style="border-radius:16rpx;"
|
||||
></ui-btn>
|
||||
<text class="picker-cancel" bindtap="onClosePicker">取消</text>
|
||||
</ui-modal>
|
||||
@@ -203,6 +211,7 @@
|
||||
block
|
||||
text="保存并开始"
|
||||
bindtap="onConfirmCircuit"
|
||||
custom-style="border-radius:16rpx;"
|
||||
></ui-btn>
|
||||
<text class="picker-cancel" bindtap="onCloseCircuitPicker">取消</text>
|
||||
</ui-modal>
|
||||
|
||||
@@ -313,6 +313,17 @@
|
||||
margin: 8rpx 0 8rpx;
|
||||
}
|
||||
|
||||
/* 自由训练 / 循环训练 并排一行 */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-row .action-cell {
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止图标+文字在窄屏撑破 flex 项 */
|
||||
}
|
||||
|
||||
/* ---- picker ---- */
|
||||
.picker-title {
|
||||
font-size: 34rpx;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>首页按钮:圆角 vs 直角 对照预览</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #FF6B35;
|
||||
--primary-light: #FF8C5A;
|
||||
--primary-bg: #FFF3ED;
|
||||
--primary-rgb: 255,107,53;
|
||||
--text-secondary: #999999;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
background: #f4f5f7;
|
||||
color: #222;
|
||||
padding: 32px 16px 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
h1 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
|
||||
.sub { font-size: 13px; color: #888; margin-bottom: 28px; text-align: center; line-height: 1.6; }
|
||||
|
||||
.compare {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.col {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
padding: 24px 22px 28px;
|
||||
width: 320px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,0.06);
|
||||
}
|
||||
.col h2 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.col .tag {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.col.round .tag { background: var(--primary-bg); color: var(--primary); }
|
||||
.col.square .tag { background: #eee; color: #666; }
|
||||
|
||||
/* ---- button replica (matches ui-btn) ---- */
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
.btn .dot { width: 18px; height: 18px; border-radius: 50%; background: currentColor; opacity: .85; }
|
||||
|
||||
.btn--xl { height: 54px; font-size: 18px; }
|
||||
.btn--md { height: 40px; font-size: 15px; }
|
||||
.btn--lg { height: 48px; font-size: 17px; }
|
||||
|
||||
.btn--primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
.btn--ghost {
|
||||
background: rgba(var(--primary-rgb), 0.14);
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(var(--primary-rgb), 0.22);
|
||||
}
|
||||
|
||||
/* the only difference that matters: */
|
||||
.round .btn { border-radius: 24px; }
|
||||
.square .btn { border-radius: 8px; } /* ≈16rpx */
|
||||
|
||||
/* two secondary buttons side by side */
|
||||
.btn-row { display: flex; gap: 8px; }
|
||||
.btn-row .btn { margin-bottom: 14px; }
|
||||
|
||||
.context-note {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
border-top: 1px dashed #eee;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.verdict {
|
||||
margin-top: 30px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
max-width: 660px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.verdict b { color: var(--primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>首页功能按钮 · 圆角 vs 直角</h1>
|
||||
<p class="sub">左侧是小程序现状(圆角 48rpx),右侧是本次改后的效果(直角 0)。<br/>同一套按钮、同一套配色,方便你直接对比「好不好看」。</p>
|
||||
|
||||
<div class="compare">
|
||||
<!-- 圆角现状 -->
|
||||
<div class="col round">
|
||||
<h2>现状 · 圆角</h2>
|
||||
<span class="tag">border-radius: 48rpx</span>
|
||||
|
||||
<div class="btn btn--primary btn--xl"><span class="dot"></span>开始训练</div>
|
||||
<div class="btn-row">
|
||||
<div class="btn btn--ghost btn--xl"><span class="dot"></span>自由训练</div>
|
||||
<div class="btn btn--ghost btn--md"><span class="dot"></span>循环训练</div>
|
||||
</div>
|
||||
<hr style="border:none;border-top:1px solid #f0f0f0;margin:18px 0 16px;">
|
||||
<div class="btn btn--primary btn--lg">开始自由训练</div>
|
||||
<div class="btn btn--primary btn--lg">保存并开始</div>
|
||||
|
||||
<div class="context-note">页面其余元素仍是圆角:卡片 24rpx、进度条 6rpx、目标环是正圆。</div>
|
||||
</div>
|
||||
|
||||
<!-- 直角改后 -->
|
||||
<div class="col square">
|
||||
<h2>改后 · 小圆角</h2>
|
||||
<span class="tag">border-radius: 16rpx</span>
|
||||
|
||||
<div class="btn btn--primary btn--xl"><span class="dot"></span>开始训练</div>
|
||||
<div class="btn-row">
|
||||
<div class="btn btn--ghost btn--xl"><span class="dot"></span>自由训练</div>
|
||||
<div class="btn btn--ghost btn--md"><span class="dot"></span>循环训练</div>
|
||||
</div>
|
||||
<hr style="border:none;border-top:1px solid #f0f0f0;margin:18px 0 16px;">
|
||||
<div class="btn btn--primary btn--lg">开始自由训练</div>
|
||||
<div class="btn btn--primary btn--lg">保存并开始</div>
|
||||
|
||||
<div class="context-note">仅首页这几个按钮变直角,全局组件与其他页面不受影响。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="verdict">
|
||||
小圆角(16rpx)比 10rpx 更圆润一点,更接近卡片的 24rpx 圆角语言,但仍不是全胶囊,保留了清爽感。<br/>
|
||||
已应用到首页 5 个按钮(仅首页、不动全局组件)。若还想微调,往上可贴近卡片的 24rpx、往下回到 10rpx,说一声即可。
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user