From 0225e9f971bb8d4e3371f392354ed40648d68a98 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 27 May 2026 18:26:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(training):=20=E6=8F=A1=E5=8A=9B=E7=8E=AF?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=AE=80=E5=8C=96=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要改动 ### 1. 连续训练模式 - 移除组数/休息逻辑,只统计总次数和时长 - 训练持续进行直到用户手动暂停 - 实时显示次数、时长、消耗糖分 ### 2. 递进式奖励系统 - 鸡蛋(8-12次) → 核桃(15-20次) → 易拉罐(25-30次) → 气球(40-50次) - 每个奖励在区间内随机触发 - 奖励按顺序递进,不会跳级 - 捏碎粒子特效 + 奖励弹窗 ### 3. 糖分计算与食物对比 - 公式: (MET × 体重 × 时长) / 4 - MET = 3.5, 体重 = 60kg - 食物对比: 苹果/香蕉/米饭/巧克力/可乐 ### 4. 视觉设计优化 - 真实握力环造型(完整圆环 + 中空 + 握点) - 立体渐变和纹理效果 - 挤压动画(训练时整体缩放) - 简洁交互(整个环可点击,无中心按钮) ### 5. 问题修复 - 修复进入页面时音效预热响声(静音预热) - 暂时禁用音效URL(避免404错误) - 修复捏碎特效层级问题 ## 技术实现 - 新增: grip-ring.vue (主页面) - 新增: grip-ring-canvas.vue (画布组件) - 修改: useMetronome.ts (静音预热) - 修改: exercises.ts (移除握力环) - 修改: index.vue (添加头部链接) - 修改: dev-training-entry/index.vue (添加菜单入口) - 修改: pages.json (添加路由) Co-Authored-By: Claude Opus 4.7 --- .../components/dev-training-entry/index.vue | 21 +- TUICallKit-Vue3/pages.json | 12 +- .../training/hooks/useMetronome.ts | 50 +- .../pages/components/grip-ring-canvas.vue | 274 ++++++ TUICallKit-Vue3/training/pages/exercises.ts | 19 +- TUICallKit-Vue3/training/pages/grip-ring.vue | 809 ++++++++++++++++++ TUICallKit-Vue3/training/pages/index.vue | 20 +- 7 files changed, 1175 insertions(+), 30 deletions(-) create mode 100644 TUICallKit-Vue3/training/pages/components/grip-ring-canvas.vue create mode 100644 TUICallKit-Vue3/training/pages/grip-ring.vue diff --git a/TUICallKit-Vue3/components/dev-training-entry/index.vue b/TUICallKit-Vue3/components/dev-training-entry/index.vue index cb3217b7..cc851e33 100644 --- a/TUICallKit-Vue3/components/dev-training-entry/index.vue +++ b/TUICallKit-Vue3/components/dev-training-entry/index.vue @@ -2,7 +2,7 @@ - 💪 @@ -13,7 +13,18 @@ - --> + + + + 🟢 + + + 握力环 + 握力训练 + + + + @@ -21,7 +32,7 @@ - 节拍器 + 耗糖节拍器 健走配速 @@ -251,6 +262,10 @@ $brand-light: #34d399; &--train { background: linear-gradient(140deg, $brand-light, $brand-deep); } + &--grip { + background: linear-gradient(140deg, #86efac, #22c55e); + box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.36); + } &--metro { background: linear-gradient(140deg, #fbbf24, #d97706); box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.36); diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index 11f9cdae..c4cfb124 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -115,10 +115,20 @@ "navigationBarTitleText": "练一练" } }, + { + "path": "pages/grip-ring", + "style": { + "navigationBarTitleText": "握力环训练", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc", + "requiredBackgroundModes": ["audio"] + } + }, { "path": "pages/metronome", "style": { - "navigationBarTitleText": "节拍器", + "navigationBarTitleText": "耗糖节拍器", "navigationBarBackgroundColor": "#f8fafc", "navigationBarTextStyle": "black", "backgroundColor": "#f8fafc", diff --git a/TUICallKit-Vue3/training/hooks/useMetronome.ts b/TUICallKit-Vue3/training/hooks/useMetronome.ts index adb26e52..e86df632 100644 --- a/TUICallKit-Vue3/training/hooks/useMetronome.ts +++ b/TUICallKit-Vue3/training/hooks/useMetronome.ts @@ -38,9 +38,8 @@ class AudioPool { } /** - * 预热:极小音量短暂播放,让音频文件下载/解码到内存 + * 预热:短暂播放,让音频文件下载/解码到内存 * 真正首次 play 不会再卡冷启动 - * 注意:iOS 上 volume = 0 不一定真正解码,所以用 0.01 极小音量 */ warmUp() { if (this.warmedUp) return @@ -48,12 +47,12 @@ class AudioPool { this.list.forEach((ctx) => { try { - ctx.volume = 0.01 + ctx.volume = 0 // 静音预热,避免进入页面时响声 ctx.play() setTimeout(() => { try { ctx.stop() - ctx.volume = 1 + ctx.volume = 1 // 恢复音量 } catch (_) {} }, 60) } catch (_) {} @@ -69,8 +68,11 @@ class AudioPool { const ctx = this.list[this.cursor] this.cursor = (this.cursor + 1) % this.list.length try { + // 强制停止所有正在播放的音频,避免重叠 + this.list.forEach(c => { + try { c.stop() } catch (_) {} + }) // iOS 上 seek(0) + play() 不一定能从头播放;stop() + play() 更稳 - ctx.stop() ctx.play() } catch (_) { try { ctx.play() } catch (__) {} @@ -179,6 +181,43 @@ export function useMetronome(options: MetronomeOptions = {}) { beatIndex.value = 0 } + /** + * 动态更新音频源(用于切换音色) + */ + const updateAudioSrc = (newClickSrc: string, newAccentSrc?: string) => { + const wasPlaying = isPlaying.value + if (wasPlaying) stop() + + // 销毁旧的音频池 + clickPool?.destroy() + accentPool?.destroy() + clickPool = null + accentPool = null + + // 创建新的音频池(静默预热,不播放) + clickPool = new AudioPool(newClickSrc, poolSize) + if (newAccentSrc) { + accentPool = new AudioPool(newAccentSrc, Math.max(2, Math.ceil(poolSize / 2))) + } + + // 播放一次预览音效(适中音量) + const previewCtx = uni.createInnerAudioContext() + previewCtx.src = newClickSrc + previewCtx.obeyMuteSwitch = false + previewCtx.volume = 0.4 + try { + previewCtx.play() + setTimeout(() => { + try { + previewCtx.destroy?.() + } catch (_) {} + }, 500) + } catch (_) {} + + // 恢复播放状态 + if (wasPlaying) start() + } + /** * 主动预加载(推荐在页面 onShow 时调用,让用户进页面就完成预热) * 同时再次设置 setInnerAudioOption,防 App.vue 全局设置失效(iOS 静音键) @@ -214,6 +253,7 @@ export function useMetronome(options: MetronomeOptions = {}) { stop, setBpm, setAccentEvery, + updateAudioSrc, preload, } } diff --git a/TUICallKit-Vue3/training/pages/components/grip-ring-canvas.vue b/TUICallKit-Vue3/training/pages/components/grip-ring-canvas.vue new file mode 100644 index 00000000..443bc934 --- /dev/null +++ b/TUICallKit-Vue3/training/pages/components/grip-ring-canvas.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/TUICallKit-Vue3/training/pages/exercises.ts b/TUICallKit-Vue3/training/pages/exercises.ts index b72f9c0f..bb91a4c3 100644 --- a/TUICallKit-Vue3/training/pages/exercises.ts +++ b/TUICallKit-Vue3/training/pages/exercises.ts @@ -2,13 +2,12 @@ export type AnimationType = | 'curl' | 'press' | 'sidefly' - | 'grip' | 'crunch' export interface ExercisePreset { id: string name: string - equipment: '哑铃' | '握力环' | '健身环' | '徒手' + equipment: '哑铃' | '健身环' | '徒手' icon: string animationType: AnimationType description: string @@ -76,22 +75,6 @@ export const EXERCISE_PRESETS: ExercisePreset[] = [ defaultRestSec: 60, beatsPerRep: 2, }, - { - id: 'grip-ring', - name: '握力环训练', - equipment: '握力环', - icon: '🟢', - animationType: 'grip', - description: '锻炼前臂屈肌群,改善握力', - tips: ['完全握紧停顿 1 秒', '完全张开放松', '左右手交替'], - defaultBpm: 80, - bpmMin: 60, - bpmMax: 120, - defaultSets: 4, - defaultReps: 20, - defaultRestSec: 45, - beatsPerRep: 2, - }, { id: 'crunch', name: '集中机(仰卧卷腹)', diff --git a/TUICallKit-Vue3/training/pages/grip-ring.vue b/TUICallKit-Vue3/training/pages/grip-ring.vue new file mode 100644 index 00000000..dd5387ef --- /dev/null +++ b/TUICallKit-Vue3/training/pages/grip-ring.vue @@ -0,0 +1,809 @@ + + + + + diff --git a/TUICallKit-Vue3/training/pages/index.vue b/TUICallKit-Vue3/training/pages/index.vue index 8424dced..17dbbba8 100644 --- a/TUICallKit-Vue3/training/pages/index.vue +++ b/TUICallKit-Vue3/training/pages/index.vue @@ -5,8 +5,13 @@ 练一练 器械动作跟练 - - 🎵 节拍器 + + + 🟢 握力环 + + + 🎵 节拍器 + @@ -185,7 +190,7 @@ import { useTrainingBgm } from '../hooks/useTrainingBgm' import { useTTS } from '../hooks/useTTS' import { formatDuration } from '../utils/calorie' -const equipmentList = ['哑铃', '握力环', '健身环', '徒手'] as const +const equipmentList = ['哑铃', '健身环', '徒手'] as const type Equipment = (typeof equipmentList)[number] const currentEquipment = ref('哑铃') @@ -338,6 +343,10 @@ const goMetronome = () => { uni.navigateTo({ url: '/training/pages/metronome' }) } +const goGripRing = () => { + uni.navigateTo({ url: '/training/pages/grip-ring' }) +} + onHide(() => { if (isTraining.value || isResting.value) { onStop() @@ -379,6 +388,11 @@ onUnmounted(() => { margin-top: 6rpx; } + .header-links { + display: flex; + gap: 12rpx; + } + .header-link { padding: 12rpx 24rpx; background: #f3f4f6;