Files
wx_pbzc/.workbuddy/memory/2026-08-03.md
T
lc bce796d893 feat: 首页按钮样式调整(小圆角/并排/高度对齐)
- 主按钮与自由/循环训练按钮圆角统一为 16rpx(直角→折中方案)
- 自由训练/循环训练两个次按钮改为并排一行,文字精简为「自由训练」「循环训练」
- 两个次按钮 size 由 md 改为 xl,与主按钮高度一致
- 新增 preview-buttons.html 对照预览
- 更新工作记忆
2026-08-03 17:04:05 +08:00

213 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-08-03 工作日志
## 规划:分阶段训练(循环训练 circuit)功能
用户想在平板支撑小程序加入"分阶段训练":自定义 每组时长 / 每次组数 / 每周训练次数。
分析现有代码后结论:
- 现有 `utils/plan.js` 是"单组持续撑"模型(hold 模式),计划按天线性加秒;设置页已能自定义公式参数(totalDays/startTarget/increment/cycleDays),但无"组数/休息/周频次"概念。
- 用户要的是"循环训练(circuit)"范式,当前数据模型与计时器都表达不了,但云同步/计划选择/进度条/记录页骨架可复用。
### 已确认的设计决策(与用户拍板)
1. **双模式并存**:保留现有3个预设计划和老用户进度(mode:'hold'),新增 circuit 类型(mode:'circuit')。不破坏榜单/已有数据。
2. **弹性训练**:任意一天都能练;sessionsPerWeek 只用于显示「本周 X/N 次」(自然周周一重置),不卡进度(`getPlanDay` 已按训练日推进)。
3. **扁平 routine 先上**:整个计划每次 routine 相同(如 4组×30秒),靠"完成次数"和"每周次数"做进度;逐周进阶作为后续增强,MVP 不做。
### 约定细节
- 默认组间休息 15 秒(编辑器可改)。
- 循环预设示例:"新手 4组×20秒·休息15秒·3次/周·4周"。
- `duration` 始终存"有效撑总秒数 = sets×holdPerSet"(不含休息),保证排行榜/里程碑零改动。
### 计划改动清单(原方案,较完整,本次未走此路线)
- utils/plan.js:加 mode + circuit 目标解析 + 1 个循环预设
- utils/storage.jssaveRecord 支持 sets/holdPerSet/restPerSet/mode
- pages/timer + 新 utils/circuitTimer.jscircuit 状态机与 UI
- pages/index:目标显示"4组×30秒·休息15秒" + 本周进度
- pages/settings:编辑器加"循环模式"输入项
- pages/records:展示"4组×30秒"
- 榜单/云函数:不动
## 方案简化(用户两次追问"还有没有再简单一点的方案")
原方案把 circuit 做成完整"计划管理系统"(mode/预设/进度进阶/周频次追踪/设置页编辑器) 过度设计。
最终定位为「自由训练」的循环形态,而非新计划体系:
- 首页加「循环训练」入口:输入 每组时长/组数/休息秒数 → 跳 timer?circuit=1&hold=&sets=&rest=
- timer 内用新 utils/circuitTimer.js(类 Timer 接口: start/stop/pause/resume + onTick/onPhaseChange) 跑 工作/休息 状态机;UI 显示「第X/Y组」+ 阶段 + 组内剩余
- 首页主「开始训练」仍走原 hold 计划,circuit 不替换每日目标 → 完全不动 index 的计划逻辑
- storage.saveRecord 加可选 sets/holdPerSet/restPerSet/modeduration 仍=有效撑总秒(sets×hold),榜单零改
- records 页:circuit 记录显示「4组×30秒」
- **砍掉**plan.js mode、circuit 预设、设置页编辑器、plan-day 进阶、周频次追踪(周频次可仅作为 picker 里可选提示,先不做追踪)
- 唯一"真新逻辑"= circuitTimer 状态机;其余皆小 UI。风险与原方案同级(无新权限/域名)。
用户最终确认:**先做简化版,但循环训练设置要持久化**(下次打开不重填)。
## 已实现:简化版循环训练 + 配置持久化(2026-08-03 落地)
### 改动文件
- **utils/storage.js**:新增 `CIRCUIT_CONFIG_KEY` / `getCircuitConfig` / `saveCircuitConfig`(本地持久化,含默认值 `{holdPerSet:30, sets:4, restPerSet:15, sessionsPerWeek:3}` + 范围校验,**不进云同步**——纯设备偏好)。
- **utils/circuitTimer.js**(新):`CircuitTimer` 状态机 `idle→working→resting→…→done`,类 `Timer` 接口(`start/stop/pause/resume` + `onTick/onPhaseChange/onComplete`),`stop()` 返回 `workTotal`(有效撑总秒,不含休息),复用 `Date.now`+`onAppShow` 前台补偿。
- **pages/timer/timer.{js,wxml,wxss}**`_init` 检测 `options.circuit``_initCircuit` 建 FSM + circuit 字段;新增 `_circuitCue/_circuitPhase/_onCircuitComplete/_vibrateOnce/_int`;阶段切换走**震动 + phaseTip 文本**TTS 无"第N组/休息"词,刻意不堆语音),完成复用 `complete` 语音;`_saveAndShowCompletion``record.mode='circuit'` + `sets/holdPerSet/restPerSet/sessionsPerWeek`**`planId``'circuit'`**(避免 `getPlanDay` 把循环记录算进每日计划进度);`onTrainAgain` 在 circuit 模式重建 FSMcompletion overtime 在 circuit 隐藏(每阶段是固定目标,无"超长发挥"概念)。
- **pages/index/index.{js,wxml,wxss}**:加「循环训练 · 分组练习」ghost 按钮(`repeatFill` 图标);底部配置弹窗(stepper 调 每组时长/组数/休息/每周目标),确认后 `saveCircuitConfig` + 跳 `timer?circuit=1&hold=&sets=&rest=&sessions=`
- **utils/icons.js**:加 `repeat`/`repeatFill`Material repeat 路径,build 自动给 gray + primary 两变体)。
- **pages/records/records.{js,wxml}**circuit 记录显示「循环 N组×Ms(·休Rs)」,基于 `r.mode`
### 关键不变量(续做时牢记)
- `duration` 始终 = 有效撑总秒(组数×每组秒,不含休息)→ 排行榜/里程碑/统计零改动。
- 循环记录 `planId:'circuit'`,不污染每日计划 `getPlanDay`;连胜 `updateStreak` 仍计入(用户确实练了)。
- 进度环在 circuit 模式轨道恒中性灰(否则每阶段都按"累计≥阶段长"误判变绿)。
### 验证
6 个 JS 全过 `node --check`。未动 plan.js / 云函数 / 榜单 / 云同步。
### 未做(可选后续)
- 周频次硬追踪(仅存 sessionsPerWeek 作显示,未做"本周 X/N 次"计数)
- 逐周自动进阶
- 设置页统一管理入口
## 修复+重做:循环训练页顶部进度面板(2026-08-03 二次迭代)
用户反馈"顶部只有『1/3组,撑住』太简单难看"。**根因:上一轮只写了 `.circuit-bar` 的 WXML 结构,`timer.wxss` 里一行样式都没写**(grep 确认零匹配),所以渲染出来就是两段裸文字。
### 新设计(信息层次分工)
- **顶部面板管宏观「第几组」**,中间进度环管微观「本组还剩几秒」——不再重复展示同一信息。
- 结构:组进度胶囊点阵 → 大数字 `2/4 组` + 阶段徽章 → 副行「已撑 42 秒 · 目标 120 秒」。
- 点阵三态:`is-done`(实心主色) / `is-active`(加宽+主色渐变+发光呼吸) / `is-next`(休息时高亮下一组,success 绿呼吸)。
- 徽章四态 BEM`--work`(主色渐变实心+白灯白字) / `--rest`(success 绿淡底) / `--idle` / `--pause`(中性灰)。休息态整张卡边框也转绿,视觉降温。
- 待机态显示总组数 `4 组` + "准备开始" + "每组30秒·休息15秒";运行态显示 `2/4 组` + 累计秒数。
### 实现要点(踩坑记录)
- **状态指示灯用纯 CSS 圆点,不用 icon**:项目里 SVG 图标颜色烤死在 data URI,CSS 改不动;而这个灯需要随状态换色(白/绿/灰),纯 CSS 是唯一干净解。
- **点阵用 `flex:1` + `max-width`**,任意组数都均分不溢出;`is-active``flex:2` 加宽突出。组数 >15 (`DOTS_MAX`) 退化为线性进度条 `.circuit-track`
- **面板不设固定 height**,靠 padding 撑开——真机大字体档会放大文字但不放大 rpx 盒子,固定高度必裁字(项目老坑)。
- **`.circuit-bar` 必须 `position:relative; z-index:1`**`.breathe-ring`(z-index:0) 在 DOM 里排在面板之后,不抬层橙色光晕会晕染到白卡片上。
- 暂停时 `.is-paused` 冻结所有 dot 呼吸动画——静止本身就是"已暂停"信号。
- dots 三态在 `onTick` 里算(与 `isResting` 同批 setData,不会状态不同步):resting 时 CircuitTimer 的 `setIndex` 仍指向"刚做完那组",故 `dotsDone=setIndex``dotsActive=0``dotsNext=setIndex+1`
- `_initCircuit` 新增 `circuitDots`(仅 [1..sets] 供 wx:for) / `circuitShowDots` / `circuitTotalWork`(sets×hold)。
- hint-section 的 circuit idle 文案改为「循环训练 · 共需撑 N 秒」,避免与面板副文案重复。
改动文件:`pages/timer/timer.{js,wxml,wxss}``node --check` 通过,无旧类名残留。
## 微调:组进度点阵改为按组数严格等分铺满(2026-08-03 三次迭代)
用户反馈"点阵只占顶部一部分,应该按组数平均分配到整行"。
- **根因**`.circuit-dot``max-width: 64rpx` 给每段宽度封顶,`.circuit-dots` 默认 `justify-content:flex-start` → 组数少时(3-4 组)点阵只占左边一截。此外 `is-active``flex:2` 加宽,本身就破坏了"按组数等分"。
- **修法**`.circuit-dot``flex:1 1 0; min-width:0`**删掉 `max-width`**`.circuit-dots``width:100%`。当前组/下一组不再靠"加宽"强调,改为 **增高**12rpx → 18rpx,圆角同步 6→9rpx)+ 渐变/发光/呼吸,宽度恒等分。
- `.circuit-dots``min-height:18rpx` 锁行高,避免 idle(无 active,行高 12rpx)→ running(有 active18rpx)时整块跳 6rpx。
- transition 从 `flex` 改为 `height`(强调维度变了)。
- 仅改 `pages/timer/timer.wxss`,无 JS/WXML 改动。
## 修复:循环训练缺少过程语音(2026-08-03 四次迭代)
用户反馈"循环训练缺少正常训练时候的那些语音提示"。**根因有两处,都是设计遗漏**:
1. **`_circuitCue` 从未调用 `_remind`**:单组模式 `Timer.onTick` 里有 `this._remind(tick)`(负责 halfway/last30/last10 三条语音),circuit 的 `onTick` 只调 `_circuitCue`,里面仅有"最后一组末 5 秒震动"。
2. **`_circuitPhase` 当初刻意不放语音**(注释写明"TTS 无第N组/休息词条")。
结果整场只剩 2 声:`onStart``start` + `_onCircuitComplete``complete`
### 修法
- **新增 3 个 TTS 词条**`utils/voice.js``cloudfunctions/tts/index.js` 的 PROMPTS **必须同步**):`restStart`(休息一下,调整呼吸) / `nextSet`(下一组,准备开始) / `lastSet`(最后一组,全力冲刺!)。**刻意不含组号数字** → 固定 3 条覆盖任意组数,无需按 N 合成 N 份音频。
- **`_circuitCue` 加整场进度播报**:基准是 `total = sets × holdPerSet`(**整场有效秒,不是每组**)——否则 4 组会播 4 次"已完成一半啦",语义全错。判据用 `done >= total/2``left <= 30 / <= 10``<=` 而非 `===`,容忍后台回前台跳秒)。只在 `phase==='working'` 求值(休息时 workTotal 冻结,天然不会重复触发)。门槛沿用 `_remind`halfway 需 total>=10、last30 需 total>60、last10 需 total>20。
- **`_circuitPhase` 加阶段语音 + `chatty` 闸门**`holdPerSet >= 10` 才播 `nextSet`/`restStart`(一句话约 2 秒,短组会变语音连珠炮);`restStart` 另需 `restPerSet >= 5``lastSet` 无条件播(唯一值得打断的节点)。第 1 组不播(`onStart``start` 同刻已响)。
- **`_playProgressVoice` 优先窗口(关键时序坑)**:halfway 触发点 = `sets×hold/2`,**组数为偶数时必然落在第 sets/2 组的最后一秒**,与 rest 阶段切换同一 tick;`voice.play``_playSeq` 会让后播的截断先播的 → 默认 4 组配置下"已完成一半啦"必被 `restStart` 掐断。故进度语音播放时设 `this._voiceBusyUntil = Date.now()+2600``_circuitPhase` 在窗口内让位不播。`_initCircuit` 需重置 `_voiceBusyUntil = 0`(否则"再来一次"会误静音首条过场语音)。
- **preload 分模式**`voice.HOLD_KEYS` / `voice.CIRCUIT_KEYS` 导出,`onLoad``isCircuitMode` 取,避免单组训练白白预载 3 条循环语音。
### 部署要求
**必须重新部署 `tts` 云函数**,否则 3 个新 key 云端返回 `Unknown prompt key`。降级是安全的(`_fetchUrl` 拿到 `success:false` → 返回 null → `play` 静默 return,不报错、不弹 toast),只是这 3 条不出声。首次播放会各触发一次 TTS 合成(一次性费用,之后走 fileID + 本地文件双层缓存)。
改动文件:`utils/voice.js``cloudfunctions/tts/index.js``pages/timer/timer.js`。三个 `node --check` 全过,两边词条 grep 核对一致。
## 分析(未改代码):训练时禁止息屏
用户问"训练过程中能否禁止屏幕息屏"。结论:能,`wx.setKeepScreenOn({keepScreenOn})`,基础库 1.4.0+,无授权/无域名/无审核风险。项目当前 **零处调用**grep `setKeepScreenOn` 无匹配)。
### 为什么这不只是体验优化,而是修 bug
息屏/后台后小程序 JS 定时器被系统节流挂起 → 语音播报(halfway/last30/last10、circuit 的 restStart/nextSet/lastSet)与震动**全部丢失**。`Timer`/`CircuitTimer``Date.now` 前台补偿,秒数不会错,但中段提示是废的。平板支撑用户手撑地上不碰屏幕,60 秒必息屏 → 现状几乎每次训练的过程提示都收不到。
### 挂钩位置(关键)
- `wx.setKeepScreenOn` 作用域是**整个小程序**,跨页面持续生效,只有**退出小程序**才自动失效 → 在 `pages/timer` 打开后,若不手动关,用户返回首页/记录页/排行榜也会一直常亮耗电。
- 建议:`onStart` 开启 → `onUnload` 必须关闭(timer.js 现有 `onLoad`/`onShow`/`onUnload`**无 `onHide`**)。暂停时保持常亮(用户可能只是短暂调整姿势)。完成弹窗期间也保持,离开页面才关。
- `wx.setKeepScreenOn` 是异步 API`fail` 要静默吞掉(部分 Android ROM 省电模式会拒绝),不弹 toast。
### 已知限制(需向用户说明)
- 只阻止**自动**息屏;用户手动按电源键仍会锁屏。
- 部分 Android 厂商 ROM 在省电模式/低电量下会忽略该设置。
- 不影响亮度,屏幕可能仍会自动调暗(如需要可配 `wx.setScreenBrightness`,但必须在 `onUnload` 恢复,否则污染用户系统亮度——不建议做)。
### 建议的落地范围
约 5 行,只动 `pages/timer/timer.js`;可选在设置页加「训练时屏幕常亮」开关(默认开),与 `voiceGuide` 同样存在 settings 里。
## 已实现:训练时屏幕常亮(2026-08-03 五次迭代)
用户确认落地,且明确「先不要加开关」→ 默认恒开,不入 settings。
### 实现
仅改 `pages/timer/timer.js`,新增 `_keepScreenOn(on)` 方法 + 4 个挂钩点:
- `_keepScreenOn(on)``typeof wx.setKeepScreenOn !== 'function'` 时直接 return(低基础库降级),`fail(){}` 静默吞掉(Android 省电模式会拒绝,弹 toast 只会让用户困惑)。
- `onStart()` 正常启动分支 → 开启;**暂停时不关**(用户多半在调整姿势,闪屏更烦);**完成弹窗期间不关**(让用户看成绩)。
- `onStart()``isPaused` resume 分支 → 也调一次(暂停期间若切过后台,标志位已被系统清掉)。
- `onShow()``if (this.data.isRunning) this._keepScreenOn(true)` **重新武装**。这是最容易漏的一点:退出/切后台会清掉 app 级标志,回来不重设则后半程又静音。完成后 `isRunning` 已是 false(第 831 行 setData),不会误开。
- `onUnload()`**必须关**。标志是 app 级不是页面级,不关则用户返回首页/记录/榜单全程常亮耗电。
hold 与 circuit 两种模式共用这一套(都走 onStart/onUnload),无需分别处理。`node --check` 通过。
## 分析(未改代码):首页打卡卡片左上/左下出现两条竖条
用户反馈"训练结束返回首页,顶部连续打卡卡片左上角和左下角各显示一个竖条,2-3 秒后消失"。
### 根因
`pages/index/index.wxss``.just-completed`(训练完成高亮)把 `border: 4rpx solid var(--primary) !important` + `box-shadow` 加在了 **`<ui-card>` 宿主节点**上(`index.wxml:13``class="{{justCompleted ? 'just-completed' : ''}}"`)。而 `components/ui-card/ui-card.wxss` **只给内部 `.ui-card` view 写样式,宿主节点没有任何 `display` 声明** → 宿主是 **inline 盒**
inline 盒内包 block 子元素时,inline 盒被打断成"前/后"两个零宽行盒,border 只画在这两个碎片上;左右各 4rpx 贴合 → 视觉上就是 block 上方一条、下方一条竖线,且都贴左侧 = 用户看到的"左上角+左下角两个竖条"。
时序对上:`index.js:125-129``onShow``justCompleted=true``setTimeout` 3000ms 后置回 false —— 正是那 2-3 秒。
### 连带失效(同一根因,用户还没察觉)
- `.just-completed``animation: justCompletedPulse``transform: scale`**从未生效** —— `transform` 对非替换 inline 元素无效。
- `box-shadow` 同样只画在两个零宽碎片上,橙色光晕看不见。
- 全局 grep `:host` / `display:block``components/` **零匹配** → 项目里**所有**自定义组件宿主都是 inline,任何在页面侧给组件标签加 border/shadow/transform 的写法都会踩同一个坑。
- 附带发现:`ui-card.wxss``.ui-card.in:nth-child(1..4)` 交错入场延迟**也是失效的** —— `.ui-card` 是组件内部根节点下唯一的第 1 个子元素,永远匹配 `nth-child(1)`,所有卡片延迟恒为 0.05s。要做交错必须由页面侧传 index 或用 `animation-delay` 内联 style。
### 候选修法(待用户拍板,尚未实施)
1. **推荐**`components/ui-card/ui-card.wxss``:host { display: block; }`(微信小程序支持 `:host` 选择器)。一处修复所有 ui-card 用法,border/shadow/transform 全部恢复正常。风险:宿主从 inline 变 block 可能影响其他页面既有布局,需回归各页面卡片间距。
2. 保守版:只把 `.just-completed` 的高亮样式改成作用于**内部**元素(如页面侧改成给 `.header-card` 加高亮),不动组件。影响面最小但治标不治本。
3. 顺带可给其余 6 个自定义组件统一补 `:host { display: block; }`(需逐个回归,尤其 `ui-btn` 若被当行内元素用则不能改)。
## 已实现:修复竖条 + 恢复交错入场(2026-08-03 六次迭代,用户选方案 A)
### 改动
- **`components/ui-card/ui-card.wxss`**:新增 `:host { display:block; margin-bottom:24rpx; box-sizing:border-box }`,并把 `margin-bottom:24rpx` 从内部 `.ui-card` 移除(**上提到宿主**)——否则页面侧加的描边会把 24rpx 外边距一起圈进去,框比卡片高一截。删掉失效的 `.ui-card.in:nth-child(1..4)` 规则。
- **`components/ui-card/ui-card.js`**:加 `index` 属性(Number,默认 0)。
- **`components/ui-card/ui-card.wxml`**`style="animation-delay:{{50 + (index>3?3:(index>0?index:0))*100}}ms;{{customStyle}}"`。**customStyle 必须写在最后**才能覆盖内联 delay。>3 钳到 3,避免设置页第 7 张卡等 0.65s。
- **`pages/index/index.wxss`** 的 `.just-completed``border: 4rpx solid``box-shadow: 0 0 0 4rpx var(--primary), 0 4rpx 24rpx rgba(--primary-rgb,.3)`,加 `border-radius: 24rpx`,去掉两个 `!important`(宿主上已无竞争样式)。
- **14 处调用点补 `index`**index 页 0-1records 页 0,1,2(trend 分支),2(records 分支,与 trend 互斥故同号),3settings 页 0-6。
### 校验与风险
- `ui-card.json``styleIsolation` 覆盖(默认 isolated),`:host` 有效。`customStyle` 从未用在 ui-card 上(只有 ui-btn 在 timer 页用),改动无冲突。
- `hidden="{{...}}"`(records 5 张卡)安全:微信内置 `[hidden]{display:none!important}``!important`,优先级高于 `:host``display:block``:host` 未加 !important)。
- 待真机确认:inline→block 会吃掉标签间行内空白,各页卡片间距可能略收紧几 px(属修正,非回归);`transform:scale(1.02)` 脉冲首次真正生效。
---
## 提交与推送
今日全部改动已合为一个提交推送到 `origin/main`
- commit `3532d01` `feat: 循环训练模式 + 语音补全 / 屏幕常亮 / 卡片渲染修复`23 files, +1433 / -44
- 前一个提交是 `76cd2b6`。推送后 `git rev-list --left-right --count origin/main...main``0 0`,工作区干净。
- **未拆分成多个 commit 的原因**:`pages/timer/timer.js` 被循环训练 / 顶部面板 / 语音 / 常亮四轮改动同时触及,按文件粒度无法干净切分,hunk 级拆分成本高且易错。改为一次提交、commit message 分四段写清各自根因。
- `.workbuddy/memory/*.md` 一并提交(该目录本就在版本控制内,已有 2026-07-21/22/28 三份历史日志被跟踪)。
- 推送按项目惯例走沙箱外(`dangerouslyDisableSandbox`),沙箱内会 SSL 超时。
### ⚠️ 部署待办
`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`812 中间值)**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` 同步。仅首页、不动全局组件。