From c5ae332cbef9fadda73b93ee6ab985b42f6eb8f4 Mon Sep 17 00:00:00 2001 From: cnliucheng Date: Tue, 28 Jul 2026 16:25:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(records):=20=E8=AE=AD=E7=BB=83=E5=8B=8B?= =?UTF-8?q?=E7=AB=A0=20UI=20=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 勋章图标从 emoji 改为矢量 SVG 线性图标(utils/icons.js 新增 bolt/moon/diamond 路径及 white 变体) - 命名改为累计语义(一周累计/半月累计/月度累计/双月累计/百日累计),并标注"非连续打卡" - 进度条改回等距节点,解锁时填充正好抵达节点、满级满格 - 新增单枚解锁庆祝:持久化已见解锁集合,进入页面弹 toast + 节点 pop 动画 - 已解锁节点常驻显示勋章名 + 右上角 ✓ 角标,直观体现"已拥有" - utils/storage.js 新增 totalDays(累计去重训练日),为勋章统计提供数据源 --- config.js | 24 +++++- pages/records/records.js | 84 +++++++++++++++++++ pages/records/records.wxml | 33 ++++++++ pages/records/records.wxss | 163 +++++++++++++++++++++++++++++++++++++ utils/icons.js | 11 +++ utils/storage.js | 6 +- 6 files changed, 316 insertions(+), 5 deletions(-) diff --git a/config.js b/config.js index 94b681f..f70fb33 100644 --- a/config.js +++ b/config.js @@ -4,13 +4,13 @@ */ module.exports = { /** 应用版本号,外显在设置页「关于」 */ - version: 'v3.01', + version: 'v3.02', /** 最后更新日期,外显在设置页「关于」 */ updatedAt: '2026-07-28', /** 开发者名称 / 微信号,设置页点击可复制 */ - developer: '刘承', + developer: '三口一瓶', /** * 公众号名称。设置页「关于 → 公众号」直接展示,用户可在微信搜索关注, @@ -30,8 +30,8 @@ module.exports = { * speed: 语速 -2~2(正数加快,0 为默认)。 */ tts: { - female: { voiceType: 501004, volume: 9, speed: 1 }, - male: { voiceType: 502005, volume: 9, speed: 1 } + female: { voiceType: 501004, volume: 9, speed: 0 }, + male: { voiceType: 502005, volume: 9, speed: 0 } }, /** @@ -54,6 +54,22 @@ module.exports = { /** 姿势要领每条显示的秒数 */ tipInterval: 10, + /** + * 累计训练天数里程碑勋章,按累计去重训练日(可中断,非连续打卡)解锁。 + * 记录页用里程碑进度条展示,节点按 sqrt 感知映射分布(早期里程碑更显眼), + * 填充与节点同映射,解锁时填充正好抵达该节点、满级时满格。增删改这里即可, + * 但 name 一律用"累计"措辞,避免暗示连续打卡误导用户。 + */ + trainingDayBadges: [ + { days: 3, name: '抖登', icon: 'hot', iconWhite: 'hotWhite' }, + { days: 7, name: '中登', icon: 'bolt', iconWhite: 'boltWhite' }, + { days: 14, name: '老登', icon: 'moon', iconWhite: 'moonWhite' }, + { days: 30, name: '神登', icon: 'trophy', iconWhite: 'trophyWhite' }, + { days: 60, name: '上古神登', icon: 'diamond', iconWhite: 'diamondWhite' }, + { days: 80, name: '洪荒神登', icon: 'diamond', iconWhite: 'diamondWhite' }, + { days: 100, name: '究极登祖', icon: 'crown', iconWhite: 'crownWhite' } + ], + /** * 小程序分享配置。微信分享能力依赖每个 Page 实现 onShareAppMessage / * onShareTimeline,这里集中维护所有场景的分享文案,改这里即可。 diff --git a/pages/records/records.js b/pages/records/records.js index 3b1b6aa..6ba2ee2 100644 --- a/pages/records/records.js +++ b/pages/records/records.js @@ -14,6 +14,10 @@ Page({ totalSessions: 0, maxDuration: 0, maxDurationText: '', + totalDays: 0, + trainingBadges: [], + badgeProgress: 0, + nextBadgeHint: '', currentYear: new Date().getFullYear(), currentMonth: new Date().getMonth() + 1, monthRecords: [], @@ -69,6 +73,43 @@ Page({ if (historyList.length >= 30) break } + // 累计训练天数勋章(里程碑进度条):统计口径=累计去重训练日(可中断)。 + // 节点等距排列(间距一致、视觉整齐),填充在区间内线性插值, + // 解锁时填充正好抵达该节点,满级时填充满格。 + 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 + const trainingBadges = badgeDefs.map((b, i) => ({ + ...b, + 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 unlockedDays = trainingBadges.filter(b => b.unlocked).map(b => b.days) + const newlySet = this._checkBadgeCelebration(unlockedDays) + if (newlySet) trainingBadges.forEach(b => { b.justUnlocked = newlySet.has(b.days) }) + const nextIdx = trainingBadges.findIndex(b => !b.unlocked) + const nextBadgeHint = nextIdx === -1 + ? '全部勋章已解锁!' + : `还差 ${badgeDefs[nextIdx].days - totalDays} 天解锁「${badgeDefs[nextIdx].name}」` + const theme = themeMod.getCurrentTheme() this.setData({ theme, @@ -78,6 +119,10 @@ Page({ totalSessions: stats.totalSessions, maxDuration: stats.maxDuration, maxDurationText: util.formatDuration(stats.maxDuration), + totalDays, + trainingBadges, + badgeProgress, + nextBadgeHint, monthRecords: records, historyList, listMode: this.data.listMode || 'month', @@ -245,5 +290,44 @@ Page({ return { title: config.share.timelineTitle } + }, + + /** + * 点击勋章节点:显示勋章名称与解锁状态。 + */ + onBadgeTap(e) { + const days = Number(e.currentTarget.dataset.days) + const b = config.trainingDayBadges.find(x => x.days === days) + if (!b) return + const unlocked = this.data.totalDays >= days + wx.showToast({ + title: unlocked ? `已解锁:${b.name}` : `再练 ${days - this.data.totalDays} 天解锁「${b.name}」`, + icon: 'none', + duration: 1800 + }) + }, + + /** + * 单枚勋章解锁庆祝:对比持久化的"已见解锁集合",只对新增的解锁弹 toast + * 并标记 justUnlocked(节点高亮动画)。首次运行(无存储)仅初始化集合、不庆祝, + * 避免老用户一打开就刷一堆历史勋章。返回新增天数集合(无新增返回 null)。 + */ + _checkBadgeCelebration(unlockedDays) { + const KEY = 'badge_celebrated' + const prev = wx.getStorageSync(KEY) + if (!Array.isArray(prev)) { + wx.setStorageSync(KEY, unlockedDays) + return null + } + const prevSet = new Set(prev) + const newly = unlockedDays.filter(d => !prevSet.has(d)) + wx.setStorageSync(KEY, unlockedDays) + if (newly.length === 0) return null + const names = newly.map(d => { + const b = config.trainingDayBadges.find(x => x.days === d) + return b ? b.name : (d + '天') + }) + wx.showToast({ title: '解锁勋章:' + names.join('、'), icon: 'none', duration: 2000 }) + return new Set(newly) } }) diff --git a/pages/records/records.wxml b/pages/records/records.wxml index a3252fc..ea87771 100644 --- a/pages/records/records.wxml +++ b/pages/records/records.wxml @@ -44,6 +44,39 @@ + + +