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:
@@ -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
@@ -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 --------
|
||||
|
||||
@@ -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": "设置"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user