fix: 振动定时器清理、完成保存守卫、暗黑冷启动、头像云存储

- timer: 振动 setTimeout 入栈追踪,暂停/停止/卸载/重练统一清理,
  避免页面销毁后残留震动(_clearVibrateTimers)
- timer: _saving 守卫统一到 _saveAndShowCompletion,自然完成与手动
  结束两路径都防重复保存;修复 onTrainAgain 不重置会话状态导致
  再练一次后第二次无语音/振动且保存被挡
- app.wxss: @media dark 补全 --text/--card-bg/--bg-soft/--border/
  --success/--danger,冷启动即为完整深色,不再背景深卡片白的割裂闪烁
- settings: 头像优先上传云存储存 fileID,三级回退(fileID->base64->
  临时URL),pushAll 不再每次传输图片字节

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 09:17:27 +08:00
parent 850f919269
commit 6fffc5aece
3 changed files with 89 additions and 26 deletions
+37 -22
View File
@@ -140,31 +140,46 @@ Page({
async onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl
if (!avatarUrl) return
// Store the avatar durably. Prefer a cloud-storage fileID: it keeps the
// profile doc small and avoids re-transferring the image bytes on every
// pushAll. Fall back to an inline base64 data URI if cloud storage is
// unavailable, and to the raw temp URL as a last resort.
let permanentUrl = null
try {
// chooseAvatar returns a wxfile:// temporary URL that expires in a few
// days. Read the bytes and store as a data URI so the avatar survives
// long-term. Falls back to the raw URL on size/read error.
const permanentUrl = await util.fileToDataURI(avatarUrl)
this.setData({ avatarUrl: permanentUrl })
const saved = storage.saveProfile({
nickname: this.data.nickName,
avatarUrl: permanentUrl
})
this.setData({
nickName: saved.nickname || this.data.nickName,
avatarUrl: saved.avatarUrl
})
wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 })
permanentUrl = await this._uploadAvatar(avatarUrl)
} catch (err) {
console.warn('avatar convert failed, saving temp URL:', err)
// Fallback: save the raw wxfile URL. Will eventually expire.
this.setData({ avatarUrl })
storage.saveProfile({
nickname: this.data.nickName,
avatarUrl
})
wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 })
console.warn('[avatar] cloud upload failed, trying base64:', err)
try {
permanentUrl = await util.fileToDataURI(avatarUrl)
} catch (err2) {
console.warn('[avatar] base64 convert failed, saving temp URL:', err2)
permanentUrl = avatarUrl
}
}
this.setData({ avatarUrl: permanentUrl })
const saved = storage.saveProfile({
nickname: this.data.nickName,
avatarUrl: permanentUrl
})
this.setData({
nickName: saved.nickname || this.data.nickName,
avatarUrl: saved.avatarUrl
})
wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 })
},
/**
* Upload the chosen avatar to cloud storage and return its fileID.
* Throws if cloud is disabled or the upload fails - caller falls back.
*/
async _uploadAvatar(filePath) {
if (!cloud.enabled) throw new Error('cloud not enabled')
const cloudPath = `avatar/${Date.now()}-${Math.floor(Math.random() * 1e6)}.png`
const res = await wx.cloud.uploadFile({ cloudPath, filePath })
if (!res || !res.fileID) throw new Error('uploadFile returned no fileID')
return res.fileID
},
onNicknameBlur(e) {