chore: 上线前优化 — 清理调试日志/启用组件按需注入/补全组件声明
- 移除全项目 34 处调试 console.* 输出 - project.config.json: minified true、uploadWithSourceMap false、启用 lazyCodeLoading(requiredComponents 组件按需注入) - pages/records/records.json 补全 ui-skeleton 组件声明(此前漏声明,WXML 已使用) - 删除根目录残留 preview-buttons.html
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
# 平板支撑训练小程序 · 上线前审查报告
|
||||||
|
|
||||||
|
日期:2026-08-04 | 审查范围:上线就绪(合规 / 包体 / 性能 / 云函数部署)
|
||||||
|
|
||||||
|
## 一、总体结论
|
||||||
|
|
||||||
|
代码质量高,主包仅 **540KB**(远低于 2MB 上限),网络请求**全部走云开发 `wx.cloud.*`**(免 request 合法域名白名单配置),隐私合规链路已在上架期验证可行。
|
||||||
|
|
||||||
|
**有 1 个上线阻断项(云函数部署)+ 1 个部署前置(tts 环境变量)必须处理**,其余为建议优化项。未发现功能性 bug、无合规硬伤。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、🔴 阻断项(上线前必须完成)
|
||||||
|
|
||||||
|
### 1. 云函数必须重新部署(最关键)
|
||||||
|
|
||||||
|
> 注意:**git 提交 ≠ 云函数部署**。云函数代码要生效,必须在微信开发者工具里「上传并部署」。
|
||||||
|
|
||||||
|
| 云函数 | 近期状态 | 动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| `leaderboard` | **本次 b52db87 改了 17 行**(含 totalDays / 全榜勋章逻辑) | **必须重新部署**,否则线上排行榜跑旧逻辑 |
|
||||||
|
| `tts` | 8-03 改(循环训练语音播报) | 需部署 |
|
||||||
|
| `getOpenid` | 基础服务,排行榜定位 + 云同步依赖 | 确认已部署 |
|
||||||
|
| `admin-dedupe` | 一次性清理工具(文档注明部署一次后删除) | 可选 |
|
||||||
|
|
||||||
|
部署方式:微信开发者工具 → 右键云函数 → **「上传并部署:云端安装依赖」**(项目无本地 node_modules,需云端 `npm install`)。`tts` 依赖 `tencentcloud-sdk-nodejs` 体积较大,云端安装需等待。
|
||||||
|
|
||||||
|
### 2. tts 云函数环境变量必须配好(部署前置)
|
||||||
|
|
||||||
|
`cloudfunctions/tts/index.js` 从环境变量读取密钥:
|
||||||
|
|
||||||
|
```
|
||||||
|
TTS_SECRET_ID / TTS_SECRET_KEY / TTS_REGION
|
||||||
|
```
|
||||||
|
|
||||||
|
若未配置,语音合成直接失败(循环训练 / 语音播报失效)。
|
||||||
|
确认路径:云开发控制台 → `tts` 云函数 → 环境变量;并确认腾讯云 **TTS 服务已开通、密钥有效**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、🟡 建议项(不阻断,建议上线前做)
|
||||||
|
|
||||||
|
1. **清理调试 `console.log`**:`utils/cloud.js` 14 处 + `storage.js` / `voice.js` / `app.js` / `leaderboard` 云函数等。上线应移除或降级(减小包体、避免泄露内部状态、减少日志噪声)。微信审核不强制。
|
||||||
|
2. **`project.config.json` 上线开关**:`minified: false` → `true`;`uploadWithSourceMap: true` → `false`。防源码泄露、减小包体(开发者工具上传界面也有独立勾选,会覆盖此项)。
|
||||||
|
3. **隐私授权自定义弹窗**:代码无 `wx.onNeedPrivacyAuthorize` 处理。当前能跑(微信自动弹系统默认隐私窗,且后台隐私协议已配、已上架运行一月),但建议加自定义弹窗以更合规、体验更佳。**非阻断**。
|
||||||
|
4. **全局错误上报**:无 `wx.onError` / `onUnhandledRejection` / `onPageNotFound`。建议加全局异常捕获 + 轻量上报(云监控 / 自建),便于线上排查崩溃。
|
||||||
|
5. **清理残留文件**:`cloudfunctions/shared/`(空死目录,重构遗留;`leaderboard` 实际引用 `./data`,不受影响)、根目录 `preview-buttons.html`、`.claude/` / `.codegraph/`(工具目录)。小程序只打包组成文件,不影响包体,但建议加 `packOptions.ignore` 或删除以免误传。
|
||||||
|
6. **确认生产 env**:`utils/cloud.js` 硬编码 `ENV_ID=cloudbase-d1g56kl2q8f4f7d8a`,确认是生产环境(通常单一 env 即可)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、✅ 已核实合规 / 无风险项
|
||||||
|
|
||||||
|
- 主包 540KB << 2MB,无需分包
|
||||||
|
- 无硬编码 `http://`,全走 `wx.cloud`(免 request 合法域名配置)
|
||||||
|
- 隐私接口仅 `chooseAvatar`(button `open-type`,后台已声明「用户信息」)
|
||||||
|
- `app.json` 无需 `requiredPrivateInfos`(`chooseAvatar` 不在此列)
|
||||||
|
- `sitemap.json` 标准 allow
|
||||||
|
- `tabBar` custom + `custom-tab-bar` 目录齐全
|
||||||
|
- 云函数依赖声明齐全(`wx-server-sdk` / `tencentcloud-sdk-nodejs`)
|
||||||
|
- 内容安全风险低(昵称 / 头像均为用户自身数据,无实时 UGC 发布链路)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、上线 Checklist
|
||||||
|
|
||||||
|
- [ ] 部署 `leaderboard` / `tts` / `getOpenid` 云函数(云端安装依赖)
|
||||||
|
- [ ] 配好 `tts` 环境变量并验证 TTS 可用
|
||||||
|
- [ ] 开发者工具「上传」时勾选 **压缩** + **不上传 sourcemap**
|
||||||
|
- [ ] 体验版真机回归:训练 → 记录 → 排行榜(含空榜 / 1-2 名虚拟台)→ 设置改头像
|
||||||
|
- [ ] 确认 MP 后台类目(运动健身类)与隐私协议有效
|
||||||
|
- [ ] 提交审核,准备测试说明(功能无需登录,排行榜可空榜展示)
|
||||||
@@ -131,3 +131,22 @@
|
|||||||
- 用户要求前三名勋章一致大小(此前首名 34rpx 强调、2/3 名 30rpx)。
|
- 用户要求前三名勋章一致大小(此前首名 34rpx 强调、2/3 名 30rpx)。
|
||||||
- 改动:leaderboard.wxss `.podium-badge--first` 34→30rpx;配套 `.podium-name--first` line-height 34→30rpx(保持首名昵称与勋章等高盒对齐)。
|
- 改动:leaderboard.wxss `.podium-badge--first` 34→30rpx;配套 `.podium-name--first` line-height 34→30rpx(保持首名昵称与勋章等高盒对齐)。
|
||||||
- 结果:领奖台前三名勋章统一 30rpx,与全局基准(列表 .rank-badge 30、设置页 .profile-badge 30、记录页 .badge-node-icon 30)完全一致。纯样式无语法风险。
|
- 结果:领奖台前三名勋章统一 30rpx,与全局基准(列表 .rank-badge 30、设置页 .profile-badge 30、记录页 .badge-node-icon 30)完全一致。纯样式无语法风险。
|
||||||
|
|
||||||
|
## 提交与推送
|
||||||
|
- commit `b52db87`(17 文件,+690/-67)包含今天整轮(虚拟领奖台+勋章对齐结构修复+全 app 勋章 30rpx 统一)。
|
||||||
|
- 已推 `origin`(github.com/cnliucheng/wx_pbzc) + 新增远程 `soao`(https://git.soao.net/lc/wx_pbzc.git) 并推 `main`。两个远程同步于 b52db87。
|
||||||
|
- 提醒:cloudfunctions/leaderboard 若含线上逻辑改动需开发者工具「上传并部署」才生效(本次主要改前端)。
|
||||||
|
|
||||||
|
## 上线前审查(launch readiness)
|
||||||
|
- 主包 540KB(<<2MB,无需分包);网络全走 wx.cloud(免 request 域名白名单);无硬编码 http;隐私接口仅 chooseAvatar(后台已配)。**合规面干净**。
|
||||||
|
- 🔴 阻断项:①云函数 git 提交≠部署——`leaderboard`(本次 b52db87 改 17 行 totalDays/全榜勋章)、`tts`(8-03 改)需重新部署;②`tts` 环境变量 TTS_SECRET_ID/KEY/REGION 须在云函数控制台配好(否则语音失败)。
|
||||||
|
- 🟡 建议项:清理调试 console.log(cloud.js 14 处等)、project.config.json 上线开关(minified→true/uploadWithSourceMap→false)、加 wx.onNeedPrivacyAuthorize 自定义隐私弹窗、加全局错误上报、清理残留(cloudfunctions/shared 空死目录/根 preview-buttons.html/.claude/.codegraph)。
|
||||||
|
- 核实:leaderboard 实际 require('./data') 非 '../shared',空 shared 目录无害;`data.js`/`cache.js` 同目录存在,部署含之;无分包;无 wx.onError。
|
||||||
|
- 报告:.workbuddy/launch-readiness-review.md。
|
||||||
|
|
||||||
|
## 执行:上线前建议项清理(用户确认"需要")
|
||||||
|
- 清调试 console.*:脚本 .workbuddy/clean-console.js 批量删(独立 console 行 + 箭头函数体 `=> console`→`=> {}`),删 34 处;漏 2 处 `if(){console.log();return}` 行内形式手动改;Grep 复扫全项目 *.js 已无 console.* 残留。涉及 cloud.js/storage.js/voice.js/theme.js/app.js/leaderboard.js/timer.js/settings.js/cloudfunctions/leaderboard/index.js,均 node --check 通过。
|
||||||
|
- project.config.json:minified false→true、uploadWithSourceMap true→false(上线防源码泄露+减小包体)。
|
||||||
|
- 清残留:删 preview-buttons.html + rmdir cloudfunctions/shared(空死目录,重构遗留);.claude/.codegraph 已被 packOptions.ignore 的 `.!*` 忽略,不动。
|
||||||
|
- 未做:全局错误上报(wx.onError)、自定义隐私弹窗(用户仅确认做这三个清理项)。
|
||||||
|
- 改动未提交(git status 显示 M/D 文件)。clean-console.js 为一性工具,留 .workbuddy 不进包。
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ App({
|
|||||||
}
|
}
|
||||||
// else: no data anywhere — nothing to sync
|
// else: no data anywhere — nothing to sync
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Cloud sync on launch failed:', e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +144,6 @@ App({
|
|||||||
const localChangedAt = wx.getStorageSync('_local_changed_at') || 0
|
const localChangedAt = wx.getStorageSync('_local_changed_at') || 0
|
||||||
const keepLocal = cloudTs > 0 && localChangedAt > cloudTs
|
const keepLocal = cloudTs > 0 && localChangedAt > cloudTs
|
||||||
if (keepLocal) {
|
if (keepLocal) {
|
||||||
console.log('[cloud] restore: keeping local single-value fields (local changed after cloud snapshot)')
|
|
||||||
} else {
|
} else {
|
||||||
if (cloudData.settings) {
|
if (cloudData.settings) {
|
||||||
wx.setStorageSync('user_settings', cloudData.settings)
|
wx.setStorageSync('user_settings', cloudData.settings)
|
||||||
@@ -169,7 +167,6 @@ App({
|
|||||||
// records so it reflects actual history on this device.
|
// records so it reflects actual history on this device.
|
||||||
storage.recomputeStreak()
|
storage.recomputeStreak()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Cloud restore failed:', e)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ const _resolveAvatars = async (urls) => {
|
|||||||
if (f && f.fileID && f.tempFileURL) map[f.fileID] = f.tempFileURL
|
if (f && f.fileID && f.tempFileURL) map[f.fileID] = f.tempFileURL
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[leaderboard] getTempFileURL batch failed:', e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (urls || []).map(u => (u && map[u]) ? map[u] : u)
|
return (urls || []).map(u => (u && map[u]) ? map[u] : u)
|
||||||
@@ -305,12 +304,9 @@ const _persistSnapshot = async (period, result) => {
|
|||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('[leaderboard] persisting snapshot', period, 'rankedLen=', ranked.length)
|
|
||||||
const docRef = db.collection(SNAPSHOT_COLLECTION).doc(period)
|
const docRef = db.collection(SNAPSHOT_COLLECTION).doc(period)
|
||||||
const setRes = await docRef.set(payload)
|
const setRes = await docRef.set(payload)
|
||||||
console.log('[leaderboard] persist ok', period, JSON.stringify(setRes))
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[leaderboard] persist snapshot failed for', period, e && e.errMsg ? e.errMsg : e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +342,6 @@ const _rebuildSnapshots = async () => {
|
|||||||
const latestByOpenid = await _fetchLatestByOpenid(true)
|
const latestByOpenid = await _fetchLatestByOpenid(true)
|
||||||
for (const p of ['day', 'month', 'year', 'endurance']) {
|
for (const p of ['day', 'month', 'year', 'endurance']) {
|
||||||
const result = await _buildFromScan(latestByOpenid, p, SNAPSHOT_TOP, null)
|
const result = await _buildFromScan(latestByOpenid, p, SNAPSHOT_TOP, null)
|
||||||
console.log('[leaderboard] rebuilt', p, 'rankedLen=', (result && result.ranked) ? result.ranked.length : 'undef')
|
|
||||||
await _persistSnapshot(p, result)
|
await _persistSnapshot(p, result)
|
||||||
}
|
}
|
||||||
return { ok: true, rebuiltAt: new Date().toISOString() }
|
return { ok: true, rebuiltAt: new Date().toISOString() }
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ Page({
|
|||||||
// local data silently. No toast here: empty day-leaderboard is
|
// local data silently. No toast here: empty day-leaderboard is
|
||||||
// normal (user didn't train today), and a toast would mislead
|
// normal (user didn't train today), and a toast would mislead
|
||||||
// them into thinking the cloud is broken.
|
// them into thinking the cloud is broken.
|
||||||
console.warn('[leaderboard] cloud function failed, using local fallback:', err)
|
|
||||||
if (seq !== this._fetchSeq) { if (cb) cb(); return }
|
if (seq !== this._fetchSeq) { if (cb) cb(); return }
|
||||||
// 已有缓存秒显时后台刷新失败:保持旧数据,不打断用户;仅无缓存才走本地兜底。
|
// 已有缓存秒显时后台刷新失败:保持旧数据,不打断用户;仅无缓存才走本地兜底。
|
||||||
if (cached && cached.rankedList) {
|
if (cached && cached.rankedList) {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"calendar-heatmap": "/components/calendar-heatmap/calendar-heatmap",
|
"calendar-heatmap": "/components/calendar-heatmap/calendar-heatmap",
|
||||||
"trend-chart": "/components/trend-chart/trend-chart",
|
"trend-chart": "/components/trend-chart/trend-chart",
|
||||||
"ui-card": "/components/ui-card/ui-card"
|
"ui-card": "/components/ui-card/ui-card",
|
||||||
|
"ui-skeleton": "/components/ui-skeleton/ui-skeleton"
|
||||||
},
|
},
|
||||||
"navigationBarTitleText": "训练记录"
|
"navigationBarTitleText": "训练记录"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,11 +187,9 @@ Page({
|
|||||||
try {
|
try {
|
||||||
permanentUrl = await this._uploadAvatar(avatarUrl)
|
permanentUrl = await this._uploadAvatar(avatarUrl)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[avatar] cloud upload failed, trying base64:', err)
|
|
||||||
try {
|
try {
|
||||||
permanentUrl = await util.fileToDataURI(avatarUrl)
|
permanentUrl = await util.fileToDataURI(avatarUrl)
|
||||||
} catch (err2) {
|
} catch (err2) {
|
||||||
console.warn('[avatar] base64 convert failed, saving temp URL:', err2)
|
|
||||||
permanentUrl = avatarUrl
|
permanentUrl = avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +375,6 @@ Page({
|
|||||||
await cloud.clearAll()
|
await cloud.clearAll()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cloudClearFailed = true
|
cloudClearFailed = true
|
||||||
console.warn('[settings] cloud clear failed:', e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Reinstate default settings and streak
|
// 4. Reinstate default settings and streak
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ Page({
|
|||||||
if (s.vibrate === false) return
|
if (s.vibrate === false) return
|
||||||
const intensity = s.vibrateIntensity || 'heavy'
|
const intensity = s.vibrateIntensity || 'heavy'
|
||||||
const doVibrate = () => {
|
const doVibrate = () => {
|
||||||
const fail = (err) => console.warn('[vibrate] failed:', err.errMsg || err)
|
const fail = (err) => {}
|
||||||
if (intensity === 'light') wx.vibrateShort({ type: 'light', fail })
|
if (intensity === 'light') wx.vibrateShort({ type: 'light', fail })
|
||||||
else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail })
|
else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail })
|
||||||
else wx.vibrateLong({ fail })
|
else wx.vibrateLong({ fail })
|
||||||
@@ -452,7 +452,7 @@ Page({
|
|||||||
// vibrateLong is the most reliable on iOS real devices; the short
|
// vibrateLong is the most reliable on iOS real devices; the short
|
||||||
// types are nicer but may be silent on some devices - user picks.
|
// types are nicer but may be silent on some devices - user picks.
|
||||||
const doVibrate = () => {
|
const doVibrate = () => {
|
||||||
const fail = (err) => console.warn('[vibrate] failed:', err.errMsg || err)
|
const fail = (err) => {}
|
||||||
if (intensity === 'light') wx.vibrateShort({ type: 'light', fail })
|
if (intensity === 'light') wx.vibrateShort({ type: 'light', fail })
|
||||||
else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail })
|
else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail })
|
||||||
else wx.vibrateLong({ fail })
|
else wx.vibrateLong({ fail })
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
<!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>
|
|
||||||
+3
-2
@@ -20,11 +20,11 @@
|
|||||||
"compileHotReLoad": false,
|
"compileHotReLoad": false,
|
||||||
"lazyloadPlaceholderEnable": false,
|
"lazyloadPlaceholderEnable": false,
|
||||||
"preloadBackgroundData": false,
|
"preloadBackgroundData": false,
|
||||||
"minified": false,
|
"minified": true,
|
||||||
"autoAudits": false,
|
"autoAudits": false,
|
||||||
"newFeature": false,
|
"newFeature": false,
|
||||||
"uglifyFileName": false,
|
"uglifyFileName": false,
|
||||||
"uploadWithSourceMap": true,
|
"uploadWithSourceMap": false,
|
||||||
"useIsolateContext": true,
|
"useIsolateContext": true,
|
||||||
"nodeModules": false,
|
"nodeModules": false,
|
||||||
"enhance": true,
|
"enhance": true,
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"disableSWC": true
|
"disableSWC": true
|
||||||
},
|
},
|
||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
|
"lazyCodeLoading": "requiredComponents",
|
||||||
"cloudfunctionRoot": "cloudfunctions/",
|
"cloudfunctionRoot": "cloudfunctions/",
|
||||||
"libVersion": "3.3.4",
|
"libVersion": "3.3.4",
|
||||||
"appid": "wxb7f1bc86924869bc",
|
"appid": "wxb7f1bc86924869bc",
|
||||||
|
|||||||
+2
-18
@@ -25,10 +25,8 @@ const init = () => {
|
|||||||
try {
|
try {
|
||||||
wx.cloud.init({ env: ENV_ID, traceUser: true })
|
wx.cloud.init({ env: ENV_ID, traceUser: true })
|
||||||
_enabled = true
|
_enabled = true
|
||||||
console.log('[cloud] init ok, env:', ENV_ID)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_enabled = false
|
_enabled = false
|
||||||
console.warn('[cloud] init failed — cloud sync disabled:', e.message || e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +71,7 @@ const _fetchOpenid = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await wx.cloud.callFunction({ name: 'getOpenid' })
|
const res = await wx.cloud.callFunction({ name: 'getOpenid' })
|
||||||
if (res && res.result && res.result.openid) return res.result.openid
|
if (res && res.result && res.result.openid) return res.result.openid
|
||||||
console.warn('[cloud] getOpenid returned no openid:', JSON.stringify(res))
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[cloud] getOpenid call failed — cloud functions may not be deployed:', e.message || e)
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -109,8 +105,7 @@ const _ensureOpenid = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pushAll = () => {
|
const pushAll = () => {
|
||||||
if (!_enabled) { console.log('[cloud] pushAll skipped (not enabled)'); return }
|
if (!_enabled) { return }
|
||||||
console.log('[cloud] pushAll scheduled')
|
|
||||||
// Pre-warm openid so _doPush never sees _openid=null on a cold start
|
// Pre-warm openid so _doPush never sees _openid=null on a cold start
|
||||||
// (which used to bypass the locate-by-openid branch and call add(),
|
// (which used to bypass the locate-by-openid branch and call add(),
|
||||||
// creating a duplicate cloud doc every cold start).
|
// creating a duplicate cloud doc every cold start).
|
||||||
@@ -132,9 +127,8 @@ const _markSynced = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _doPush = async () => {
|
const _doPush = async () => {
|
||||||
console.log('[cloud] _doPush starting...')
|
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
if (!db) { console.log('[cloud] _doPush aborted (no db)'); return }
|
if (!db) { return }
|
||||||
try {
|
try {
|
||||||
const storage = require('./storage')
|
const storage = require('./storage')
|
||||||
const themeMod = require('./theme')
|
const themeMod = require('./theme')
|
||||||
@@ -148,13 +142,11 @@ const _doPush = async () => {
|
|||||||
themeId: themeMod.getCurrentTheme().id,
|
themeId: themeMod.getCurrentTheme().id,
|
||||||
updatedAt: db.serverDate()
|
updatedAt: db.serverDate()
|
||||||
}
|
}
|
||||||
console.log('[cloud] _doPush records keys:', Object.keys(data.records || {}))
|
|
||||||
|
|
||||||
// Fast path: cached _docId update
|
// Fast path: cached _docId update
|
||||||
if (_docId) {
|
if (_docId) {
|
||||||
try {
|
try {
|
||||||
await db.collection(DB_COLLECTION).doc(_docId).update({ data })
|
await db.collection(DB_COLLECTION).doc(_docId).update({ data })
|
||||||
console.log('[cloud] _doPush update ok')
|
|
||||||
_markSynced()
|
_markSynced()
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -163,7 +155,6 @@ const _doPush = async () => {
|
|||||||
// `doc._openid == auth.openid` → `undefined == openid` → false,
|
// `doc._openid == auth.openid` → `undefined == openid` → false,
|
||||||
// so this surfaces as -502003 rather than "not found". Either way,
|
// so this surfaces as -502003 rather than "not found". Either way,
|
||||||
// fall through and re-locate the doc via openid.
|
// fall through and re-locate the doc via openid.
|
||||||
console.log('[cloud] _doPush cached _docId stale, clearing:', e.message)
|
|
||||||
_docId = null
|
_docId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +175,6 @@ const _doPush = async () => {
|
|||||||
_docId = existing.data[0]._id
|
_docId = existing.data[0]._id
|
||||||
try {
|
try {
|
||||||
await db.collection(DB_COLLECTION).doc(_docId).update({ data })
|
await db.collection(DB_COLLECTION).doc(_docId).update({ data })
|
||||||
console.log('[cloud] _doPush update ok (re-located by _openid)')
|
|
||||||
_markSynced()
|
_markSynced()
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -192,7 +182,6 @@ const _doPush = async () => {
|
|||||||
// permission rule, race with another tab, etc.), DO NOT fall
|
// permission rule, race with another tab, etc.), DO NOT fall
|
||||||
// through to add() — that would silently create a duplicate
|
// through to add() — that would silently create a duplicate
|
||||||
// cloud doc for the same user. Bail and let the next push retry.
|
// cloud doc for the same user. Bail and let the next push retry.
|
||||||
console.log('[cloud] _doPush update-by-openid failed, aborting:', e.message)
|
|
||||||
_docId = null
|
_docId = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,14 +191,11 @@ const _doPush = async () => {
|
|||||||
// Reached when: (a) openid exists but no doc found, OR (b) openid is
|
// Reached when: (a) openid exists but no doc found, OR (b) openid is
|
||||||
// null (getOpenid not deployed). In both cases add() is safe — CloudBase
|
// null (getOpenid not deployed). In both cases add() is safe — CloudBase
|
||||||
// auto-populates _openid on document creation.
|
// auto-populates _openid on document creation.
|
||||||
console.log('[cloud] _doPush adding new doc (openid=' + (openid || 'null') + ')')
|
|
||||||
const res = await db.collection(DB_COLLECTION).add({ data })
|
const res = await db.collection(DB_COLLECTION).add({ data })
|
||||||
if (res && res._id) _docId = res._id
|
if (res && res._id) _docId = res._id
|
||||||
if (res && res._openid) _openid = res._openid
|
if (res && res._openid) _openid = res._openid
|
||||||
console.log('[cloud] _doPush add ok, docId=', _docId)
|
|
||||||
_markSynced()
|
_markSynced()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[cloud] _doPush error:', e.message || e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +209,6 @@ const clearAll = async () => {
|
|||||||
_openid = await _fetchOpenid()
|
_openid = await _fetchOpenid()
|
||||||
}
|
}
|
||||||
if (!_openid) {
|
if (!_openid) {
|
||||||
console.log('[cloud] clearAll: cannot fetch openid, giving up')
|
|
||||||
throw new Error('无法识别当前用户')
|
throw new Error('无法识别当前用户')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +224,6 @@ const clearAll = async () => {
|
|||||||
},
|
},
|
||||||
async (id) => {
|
async (id) => {
|
||||||
await db.collection(DB_COLLECTION).doc(id).remove()
|
await db.collection(DB_COLLECTION).doc(id).remove()
|
||||||
console.log('[cloud] clearAll: removed doc', id)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ const saveRecord = (record) => {
|
|||||||
// a 1-minute tolerance for normal clock skew.
|
// a 1-minute tolerance for normal clock skew.
|
||||||
const recordTime = new Date(record.date.replace(/-/g, '/')).getTime()
|
const recordTime = new Date(record.date.replace(/-/g, '/')).getTime()
|
||||||
if (recordTime > Date.now() + 60 * 1000) {
|
if (recordTime > Date.now() + 60 * 1000) {
|
||||||
console.warn('[storage] saveRecord rejected: future date', record.date)
|
|
||||||
wx.showToast({ title: '系统时间异常,请检查', icon: 'none' })
|
wx.showToast({ title: '系统时间异常,请检查', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,6 @@ const saveRecord = (record) => {
|
|||||||
records[month].push(record)
|
records[month].push(record)
|
||||||
records[month].sort((a, b) => b.date.localeCompare(a.date))
|
records[month].sort((a, b) => b.date.localeCompare(a.date))
|
||||||
wx.setStorageSync(RECORDS_KEY, records)
|
wx.setStorageSync(RECORDS_KEY, records)
|
||||||
console.log('[storage] saveRecord date=' + record.date + ' duration=' + record.duration)
|
|
||||||
cloud.pushAll()
|
cloud.pushAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const setTheme = (id) => {
|
|||||||
wx.setStorageSync('user_settings', s)
|
wx.setStorageSync('user_settings', s)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save theme:', e)
|
|
||||||
}
|
}
|
||||||
const theme = getThemeById(id)
|
const theme = getThemeById(id)
|
||||||
_setNavBarColor(theme.primary)
|
_setNavBarColor(theme.primary)
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ const _getCtx = () => {
|
|||||||
// actually hear training cues during a session.
|
// actually hear training cues during a session.
|
||||||
_ctx.obeyMuteSwitch = false
|
_ctx.obeyMuteSwitch = false
|
||||||
_ctx.onError((err) => {
|
_ctx.onError((err) => {
|
||||||
console.log('[voice] audio error:', err.errCode, err.errMsg)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return _ctx
|
return _ctx
|
||||||
|
|||||||
Reference in New Issue
Block a user