From 11d49a26dfbcdae4bc7b5b46b8538ebe57a69fed Mon Sep 17 00:00:00 2001 From: cnliucheng Date: Wed, 29 Jul 2026 11:31:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=BB=E9=A2=98=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=B8=8E=E5=8B=8B=E7=AB=A0=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 36 ++++++++++++++++++++---------------- pages/records/records.js | 19 ++++--------------- pages/settings/settings.js | 1 + tests/badge-state.test.js | 27 +++++++++++++++++++++++++++ utils/badge-state.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 tests/badge-state.test.js create mode 100644 utils/badge-state.js diff --git a/app.js b/app.js index 4b9fae2..52732af 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,21 @@ const darkMod = require('./utils/darkMode') const voice = require('./utils/voice') const config = require('./config') +const refreshOpenThemeViews = () => { + const pages = getCurrentPages() + pages.forEach(p => { + if (p && typeof p.applyThemeToPage === 'function') { + p.applyThemeToPage() + } else if (p && p.route) { + try { themeMod.applyThemeToPage(p) } catch (e) {} + } + try { + const tb = p && p.getTabBar && p.getTabBar() + if (tb && typeof tb.updateTheme === 'function') tb.updateTheme() + } catch (e) {} + }) +} + App({ async onLaunch() { // 尽早设导航栏色 + 窗口背景:原生导航栏在 JS 执行前用 app.json 写死的 @@ -13,22 +28,7 @@ App({ // Init dark mode listener (refreshes all open pages when system theme changes) darkMod.watchDarkMode((isDark) => { - const pages = getCurrentPages() - pages.forEach(p => { - if (p && typeof p.applyThemeToPage === 'function') { - p.applyThemeToPage() - } else if (p && p.route) { - // index/records/leaderboard 等页面没定义 applyThemeToPage 方法, - // 但都在 onLoad/onShow 里套用过主题,这里直接用 themeMod 兜底刷新, - // 否则系统深/浅色切换时这些页不会实时变色。 - try { themeMod.applyThemeToPage(p) } catch (e) {} - } - // 自定义 tabBar 不在 pages 列表里,单独通知它切换暗黑/明亮 - try { - const tb = p.getTabBar && p.getTabBar() - if (tb && typeof tb.updateTheme === 'function') tb.updateTheme() - } catch (e) {} - }) + refreshOpenThemeViews() }) // Init cloud storage (no-op if not configured) @@ -158,6 +158,10 @@ App({ } if (cloudData.themeId) { themeMod.setTheme(cloudData.themeId) + // Cloud restore is asynchronous: pages may have already rendered + // with their initial orange data. Reapply immediately so they don't + // wait for a later tab switch to reflect the restored theme. + refreshOpenThemeViews() } } diff --git a/pages/records/records.js b/pages/records/records.js index 6ba2ee2..7a69622 100644 --- a/pages/records/records.js +++ b/pages/records/records.js @@ -3,6 +3,7 @@ const util = require('../../utils/util') const themeMod = require('../../utils/theme') const iconsMod = require('../../utils/icons') const config = require('../../config') +const { getBadgeProgress, getBadgeCelebration } = require('../../utils/badge-state') Page({ data: { @@ -79,7 +80,6 @@ Page({ const totalDays = stats.totalDays const badgeDefs = config.trainingDayBadges const n = badgeDefs.length - const maxDays = badgeDefs[n - 1].days const TRACK_PAD = 4 // 首尾留白,保证首尾节点/标签不溢出 const segWidth = n > 1 ? (100 - TRACK_PAD * 2) / (n - 1) : 0 const posOf = (i) => TRACK_PAD + i * segWidth @@ -88,17 +88,7 @@ Page({ unlocked: totalDays >= b.days, pos: Math.round(posOf(i) * 10) / 10 })) - let badgeProgress - if (totalDays <= 0) { - badgeProgress = 0 - } else if (totalDays >= maxDays) { - badgeProgress = 100 - } else { - let i = 0 - while (i < n - 1 && totalDays >= badgeDefs[i + 1].days) i++ - const ratio = (totalDays - badgeDefs[i].days) / (badgeDefs[i + 1].days - badgeDefs[i].days) - badgeProgress = Math.round((posOf(i) + ratio * segWidth) * 10) / 10 - } + const badgeProgress = getBadgeProgress(badgeDefs, totalDays) // 单枚解锁庆祝反馈:对比持久化"已见解锁集合",只庆祝新增勋章。 // 首次运行(无存储)仅初始化集合,避免老用户一打开就被历史勋章刷屏。 @@ -319,9 +309,8 @@ Page({ wx.setStorageSync(KEY, unlockedDays) return null } - const prevSet = new Set(prev) - const newly = unlockedDays.filter(d => !prevSet.has(d)) - wx.setStorageSync(KEY, unlockedDays) + const { newly, seen } = getBadgeCelebration(prev, unlockedDays) + wx.setStorageSync(KEY, seen) if (newly.length === 0) return null const names = newly.map(d => { const b = config.trainingDayBadges.find(x => x.days === d) diff --git a/pages/settings/settings.js b/pages/settings/settings.js index 1c6e74e..3354bd5 100644 --- a/pages/settings/settings.js +++ b/pages/settings/settings.js @@ -350,6 +350,7 @@ Page({ wx.removeStorageSync('app_theme') wx.removeStorageSync('custom_plans') wx.removeStorageSync('user_profile') + wx.removeStorageSync('badge_celebrated') // 3. Clear every cloud doc for this openid. Local data is already gone // even if this fails, so surface that partial failure honestly. diff --git a/tests/badge-state.test.js b/tests/badge-state.test.js new file mode 100644 index 0000000..5f517fc --- /dev/null +++ b/tests/badge-state.test.js @@ -0,0 +1,27 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { getBadgeProgress, getBadgeCelebration } = require('../utils/badge-state') + +const badges = [{ days: 3 }, { days: 7 }, { days: 14 }] + +test('badge progress remains zero before the first milestone', () => { + assert.equal(getBadgeProgress(badges, 1), 0) + assert.equal(getBadgeProgress(badges, 2), 0) + assert.equal(getBadgeProgress(badges, 3), 4) +}) + +test('badge celebration keeps prior milestones seen after records are deleted', () => { + const afterDeletion = getBadgeCelebration([3], []) + assert.deepEqual(afterDeletion, { newly: [], seen: [3] }) + + const afterRetraining = getBadgeCelebration(afterDeletion.seen, [3]) + assert.deepEqual(afterRetraining, { newly: [], seen: [3] }) +}) + +test('badge celebration only reports milestones never seen before', () => { + assert.deepEqual(getBadgeCelebration([3], [3, 7]), { + newly: [7], + seen: [3, 7] + }) +}) diff --git a/utils/badge-state.js b/utils/badge-state.js new file mode 100644 index 0000000..3c88a46 --- /dev/null +++ b/utils/badge-state.js @@ -0,0 +1,28 @@ +const TRACK_PAD = 4 + +const getBadgeProgress = (badges, totalDays) => { + const defs = Array.isArray(badges) ? badges : [] + if (defs.length === 0) return 0 + + const days = Math.max(0, Number(totalDays) || 0) + if (days < defs[0].days) return 0 + if (defs.length === 1 || days >= defs[defs.length - 1].days) return 100 + + const segmentWidth = (100 - TRACK_PAD * 2) / (defs.length - 1) + let index = 0 + while (index < defs.length - 1 && days >= defs[index + 1].days) index++ + + const ratio = (days - defs[index].days) / (defs[index + 1].days - defs[index].days) + return Math.round((TRACK_PAD + index * segmentWidth + ratio * segmentWidth) * 10) / 10 +} + +const getBadgeCelebration = (previous, unlockedDays) => { + const unlocked = Array.isArray(unlockedDays) ? unlockedDays : [] + if (!Array.isArray(previous)) return { newly: [], seen: Array.from(new Set(unlocked)) } + + const previousSet = new Set(previous) + const newly = unlocked.filter(days => !previousSet.has(days)) + return { newly, seen: Array.from(new Set(previous.concat(unlocked))) } +} + +module.exports = { getBadgeProgress, getBadgeCelebration }