feat: 修复清除数据、语音缓存、排行榜限制,新增项目配置文件

- 修复 cloud.clearAll 静默失败导致重启后数据恢复
- 清除数据弹窗改用自定义 ui-modal 替代 wx.showModal
- TTS 语音合成增加 fileID 持久化缓存,重启不再重新合成
- 排行榜限制前15名,maxRank 参数化到 config.js
- 新增 config.js 统一管理版本号/更新日期/开发者/排行榜限制
- progress-ring 消除 getSystemInfoSync 弃用警告
- voice.js 增加 InnerAudioContext 错误监听
- storage/util 完善用户资料、打卡日期精度、记录日志
This commit is contained in:
2026-06-11 11:30:47 +08:00
parent 1d55df72b3
commit 2e2594d154
16 changed files with 604 additions and 139 deletions
+9 -2
View File
@@ -2,6 +2,7 @@ const themeMod = require('../../utils/theme')
const iconsMod = require('../../utils/icons')
const util = require('../../utils/util')
const storage = require('../../utils/storage')
const config = require('../../config')
Page({
data: {
@@ -56,7 +57,10 @@ Page({
this.setData({ loading: true, empty: false })
wx.cloud.callFunction({
name: 'leaderboard',
data: { period: this.data.activePeriod }
data: {
period: this.data.activePeriod,
maxRank: config.leaderboardMaxRank
}
}).then(res => {
this._applyResult(res.result || {})
if (cb) cb()
@@ -92,6 +96,8 @@ Page({
const today = storage.getToday()
const thisMonth = today.substring(0, 7)
const thisYear = String(new Date().getFullYear())
const profile = storage.getProfile()
const localName = profile.nickname || '我'
let duration = 0
let sessions = 0
@@ -103,7 +109,8 @@ Page({
if (duration > 0) {
const entry = {
rank: 1, openid: 'local', name: '我', duration, durationText: util.formatDuration(duration), sessions
rank: 1, openid: 'local', name: localName, nickname: profile.nickname || '',
duration, durationText: util.formatDuration(duration), sessions
}
this.setData({ rankedList: [entry], myEntry: { ...entry, isMe: true }, loading: false, empty: false })
} else {
+85 -36
View File
@@ -3,6 +3,7 @@ const themeMod = require('../../utils/theme')
const planMod = require('../../utils/plan')
const iconsMod = require('../../utils/icons')
const cloud = require('../../utils/cloud')
const config = require('../../config')
/**
* Build the simplified plans list shown in the settings UI by overlaying
@@ -51,18 +52,27 @@ Page({
voiceGuide: true,
vibrate: true,
icons: iconsMod.build(),
nickName: '',
avatarUrl: '',
// Editor state
showPlanEditor: false,
editingPlanId: '',
editingIsCustom: false,
editingDraft: null, // { name, totalDays, startTarget, increment, cycleDays }
editorPreview: [] // [{ day, target }, ...]
editorPreview: [], // [{ day, target }, ...]
// Clear-data confirmation
showClearConfirm: false,
// App info from config
appVersion: config.version,
appUpdatedAt: config.updatedAt,
appDeveloper: config.developer
},
onLoad() {
const s = storage.getSettings()
const theme = themeMod.getCurrentTheme()
themeMod.applyThemeToPage(this)
const profile = storage.getProfile()
this.setData({
currentPlanId: s.planId,
voiceGuide: s.voiceGuide,
@@ -70,7 +80,9 @@ Page({
currentThemeId: theme.id,
theme,
icons: iconsMod.build(theme),
plans: buildPlansList(storage.getCustomPlans())
plans: buildPlansList(storage.getCustomPlans()),
nickName: profile.nickname || '',
avatarUrl: profile.avatarUrl || ''
})
},
@@ -81,14 +93,45 @@ Page({
} catch (e) {}
themeMod.applyThemeToPage(this)
const theme = themeMod.getCurrentTheme()
const profile = storage.getProfile()
this.setData({
currentThemeId: theme.id,
theme,
icons: iconsMod.build(theme),
plans: buildPlansList(storage.getCustomPlans())
plans: buildPlansList(storage.getCustomPlans()),
nickName: profile.nickname || '',
avatarUrl: profile.avatarUrl || ''
})
},
// --- Profile handlers ---
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl
if (!avatarUrl) return
this.setData({ avatarUrl })
const saved = storage.saveProfile({
nickname: this.data.nickName,
avatarUrl
})
this.setData({
nickName: saved.nickname || this.data.nickName,
avatarUrl: saved.avatarUrl
})
wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 })
},
onNicknameBlur(e) {
const value = (e.detail.value || '').trim()
if (!value) return
this.setData({ nickName: value })
storage.saveProfile({
nickname: value,
avatarUrl: this.data.avatarUrl
})
wx.showToast({ title: '昵称已保存', icon: 'success', duration: 1200 })
},
onSelectTheme(e) {
const id = e.currentTarget.dataset.id
const theme = themeMod.setTheme(id)
@@ -136,47 +179,53 @@ Page({
onCopyWechat() {
wx.setClipboardData({
data: '刘承',
data: config.developer,
success: () => {
wx.showToast({ title: '已复制', icon: 'success', duration: 1500 })
}
})
},
// Show custom confirmation instead of wx.showModal to avoid the dev-tools
// bug where the native dialog refuses to dismiss on first tap.
onClearData() {
wx.showModal({
title: '清除数据',
content: '将删除所有训练记录和连续打卡数据,此操作不可恢复。确定继续吗?',
confirmText: '确定清除',
confirmColor: '#E53935',
success: async (res) => {
if (!res.confirm) return
wx.removeStorageSync('training_records')
wx.removeStorageSync('current_streak')
wx.removeStorageSync('user_settings')
wx.removeStorageSync('app_theme')
wx.removeStorageSync('custom_plans')
try { await cloud.clearAll() } catch (e) {}
wx.setStorageSync('user_settings', {
planId: 'beginner',
voiceGuide: true,
vibrate: true,
planStartDate: storage.getToday()
})
wx.setStorageSync('current_streak', { count: 0, lastDate: '' })
themeMod.setTheme('orange')
this.setData({
currentPlanId: 'beginner',
voiceGuide: true,
vibrate: true,
currentThemeId: 'orange'
})
themeMod.applyThemeToPage(this)
// Reset plans list — custom_plans are gone
this.setData({ plans: buildPlansList(storage.getCustomPlans()) })
wx.showToast({ title: '已清除', icon: 'success', duration: 1500 })
}
this.setData({ showClearConfirm: true })
},
onCancelClear() {
this.setData({ showClearConfirm: false })
},
onConfirmClear() {
this.setData({ showClearConfirm: false })
this._doClearData()
},
async _doClearData() {
wx.removeStorageSync('training_records')
wx.removeStorageSync('current_streak')
wx.removeStorageSync('user_settings')
wx.removeStorageSync('app_theme')
wx.removeStorageSync('custom_plans')
try { await cloud.clearAll() } catch (e) {}
wx.setStorageSync('user_settings', {
planId: 'beginner',
voiceGuide: true,
vibrate: true,
planStartDate: storage.getToday()
})
wx.setStorageSync('current_streak', { count: 0, lastDate: '' })
themeMod.setTheme('orange')
this.setData({
currentPlanId: 'beginner',
voiceGuide: true,
vibrate: true,
currentThemeId: 'orange'
})
themeMod.applyThemeToPage(this)
// Reset plans list — custom_plans are gone
this.setData({ plans: buildPlansList(storage.getCustomPlans()) })
wx.showToast({ title: '已清除', icon: 'success', duration: 1500 })
},
// -------- Plan editor --------
+2 -1
View File
@@ -1,7 +1,8 @@
{
"usingComponents": {
"ui-card": "/components/ui-card/ui-card",
"ui-btn": "/components/ui-btn/ui-btn"
"ui-btn": "/components/ui-btn/ui-btn",
"ui-modal": "/components/ui-modal/ui-modal"
},
"navigationBarTitleText": "设置"
}
+59 -3
View File
@@ -1,4 +1,38 @@
<view class="container" style="{{themeStyle}}">
<!-- 个人资料 -->
<ui-card variant="in">
<view class="section-header">
<image class="section-icon" src="{{icons.peopleFill}}" mode="aspectFit"></image>
<text class="section-title">个人资料</text>
</view>
<view class="profile-row">
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar-img" src="{{avatarUrl || icons.peopleFill}}" mode="aspectFit"></image>
<view class="avatar-overlay">
<text>点击设置</text>
</view>
</button>
<view class="profile-info">
<text class="profile-name">{{nickName || '未设置'}}</text>
<text class="profile-sub">排行榜将显示此昵称</text>
</view>
</view>
<view class="setting-row">
<view class="setting-left">
<image class="setting-icon" src="{{icons.formFill}}" mode="aspectFit"></image>
<text class="setting-label">昵称</text>
</view>
<input
type="nickname"
class="nickname-input"
placeholder="点击设置昵称"
value="{{nickName}}"
bindblur="onNicknameBlur"
maxlength="16"
/>
</view>
</ui-card>
<!-- 配色方案 -->
<ui-card variant="in">
<view class="section-header">
@@ -98,15 +132,15 @@
<view class="about-list">
<view class="about-row">
<text class="about-label">版本</text>
<text class="about-value">v1.5</text>
<text class="about-value">{{appVersion}}</text>
</view>
<view class="about-row">
<text class="about-label">更新日期</text>
<text class="about-value">2026-06-10</text>
<text class="about-value">{{appUpdatedAt}}</text>
</view>
<view class="about-row" bindtap="onCopyWechat">
<text class="about-label">开发者</text>
<text class="about-value about-link">刘承</text>
<text class="about-value about-link">{{appDeveloper}}</text>
</view>
</view>
<view class="about-footer">
@@ -207,4 +241,26 @@
<text class="editor-cancel" bindtap="onCloseEditor">取消</text>
</view>
</view>
<!-- 清除数据确认弹窗:用自定义 ui-modal 替代 wx.showModal
避免开发者工具中 wx.showModal 首次点击不关闭的问题 -->
<ui-modal
visible="{{showClearConfirm}}"
position="center"
showHandle="{{false}}"
bind:close="onCancelClear"
>
<view class="confirm-dialog" catchtap="onNoop">
<text class="confirm-title">清除数据</text>
<text class="confirm-desc">将删除所有训练记录和连续打卡数据,此操作不可恢复。确定继续吗?</text>
<view class="confirm-actions">
<view class="confirm-btn confirm-btn--cancel" bindtap="onCancelClear">
<text>取消</text>
</view>
<view class="confirm-btn confirm-btn--danger" bindtap="onConfirmClear">
<text>确定清除</text>
</view>
</view>
</view>
</ui-modal>
</view>
+137
View File
@@ -226,6 +226,89 @@
font-weight: 500;
}
/* ---- profile ---- */
.profile-row {
display: flex;
align-items: center;
padding: 16rpx 0 24rpx;
}
.avatar-btn {
position: relative;
width: 120rpx;
height: 120rpx;
padding: 0;
margin: 0;
background: #F5F5F5;
border-radius: 50%;
border: none;
line-height: 1;
overflow: hidden;
flex-shrink: 0;
}
.avatar-btn::after {
border: none;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
/* peopleFill svg is mostly transparent; give it a soft tint */
background: #EEE;
padding: 24rpx;
box-sizing: border-box;
}
.avatar-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 36rpx;
background: rgba(0, 0, 0, 0.55);
color: #FFFFFF;
font-size: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.profile-info {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
min-width: 0;
}
.profile-name {
font-size: 32rpx;
font-weight: 600;
color: var(--text);
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-sub {
font-size: 22rpx;
color: var(--text-secondary);
margin-top: 8rpx;
line-height: 1;
}
.nickname-input {
flex: 1;
text-align: right;
font-size: 28rpx;
color: var(--text);
padding: 0;
background: transparent;
}
.about-footer {
margin-top: 24rpx;
padding-top: 20rpx;
@@ -440,3 +523,57 @@
color: var(--text-secondary);
padding: 12rpx 0 0;
}
/* ---- 清除数据确认弹窗 ---- */
.confirm-dialog {
padding: 48rpx 40rpx 32rpx;
text-align: center;
}
.confirm-title {
display: block;
font-size: 34rpx;
font-weight: 600;
color: var(--text);
margin-bottom: 16rpx;
}
.confirm-desc {
display: block;
font-size: 26rpx;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 40rpx;
}
.confirm-actions {
display: flex;
gap: 20rpx;
}
.confirm-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
transition: opacity 0.15s;
}
.confirm-btn:active {
opacity: 0.8;
}
.confirm-btn--cancel {
background: var(--bg);
color: var(--text-secondary);
}
.confirm-btn--danger {
background: #E53935;
color: #FFFFFF;
}