diff --git a/.gitignore b/.gitignore
index f7dd36f3..87f8ee1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,4 @@ bin-release/
/server/.spool
/server/.claude
/.spool
+TUICallKit-Vue3/.env
diff --git a/TUICallKit-Vue3/App.vue b/TUICallKit-Vue3/App.vue
index c58b9fcc..b88a68b7 100644
--- a/TUICallKit-Vue3/App.vue
+++ b/TUICallKit-Vue3/App.vue
@@ -15,6 +15,19 @@ const globalData = {
let avatarUrl = ref(""); // 声明为响应式变量
uni.CallManager = new CallManager();
onLaunch(() => {
+ // iOS 静音键模式下 InnerAudioContext 默认不发声,
+ // 必须在 onLaunch 全局调用此 API,单实例 obeyMuteSwitch 在 iOS 不可靠
+ // #ifdef MP-WEIXIN
+ try {
+ uni.setInnerAudioOption({
+ obeyMuteSwitch: false,
+ mixWithOther: true,
+ })
+ } catch (e) {
+ console.warn('setInnerAudioOption failed:', e)
+ }
+ // #endif
+
uni.login({
provider: 'weixin',
success: function (loginRes) {
diff --git a/TUICallKit-Vue3/components/dev-training-entry/index.vue b/TUICallKit-Vue3/components/dev-training-entry/index.vue
new file mode 100644
index 00000000..cb3217b7
--- /dev/null
+++ b/TUICallKit-Vue3/components/dev-training-entry/index.vue
@@ -0,0 +1,309 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json
index e19aca2a..8fe89074 100644
--- a/TUICallKit-Vue3/pages.json
+++ b/TUICallKit-Vue3/pages.json
@@ -105,6 +105,27 @@
}
}
]
+ },
+ {
+ "root": "training",
+ "pages": [
+ {
+ "path": "pages/index",
+ "style": {
+ "navigationBarTitleText": "练一练"
+ }
+ },
+ {
+ "path": "pages/metronome",
+ "style": {
+ "navigationBarTitleText": "节拍器",
+ "navigationBarBackgroundColor": "#f8fafc",
+ "navigationBarTextStyle": "black",
+ "backgroundColor": "#f8fafc",
+ "disableScroll": true
+ }
+ }
+ ]
}
],
"tabBar": {
diff --git a/TUICallKit-Vue3/pages/user/user.vue b/TUICallKit-Vue3/pages/user/user.vue
index 4cc3c0dc..24c8a1a7 100644
--- a/TUICallKit-Vue3/pages/user/user.vue
+++ b/TUICallKit-Vue3/pages/user/user.vue
@@ -88,13 +88,18 @@
+
+
+
+
+
diff --git a/TUICallKit-Vue3/training/pages/exercises.ts b/TUICallKit-Vue3/training/pages/exercises.ts
new file mode 100644
index 00000000..b72f9c0f
--- /dev/null
+++ b/TUICallKit-Vue3/training/pages/exercises.ts
@@ -0,0 +1,116 @@
+export type AnimationType =
+ | 'curl'
+ | 'press'
+ | 'sidefly'
+ | 'grip'
+ | 'crunch'
+
+export interface ExercisePreset {
+ id: string
+ name: string
+ equipment: '哑铃' | '握力环' | '健身环' | '徒手'
+ icon: string
+ animationType: AnimationType
+ description: string
+ tips: string[]
+ contraindication?: string
+
+ defaultBpm: number
+ bpmMin: number
+ bpmMax: number
+ defaultSets: number
+ defaultReps: number
+ defaultRestSec: number
+
+ beatsPerRep: number
+}
+
+export const EXERCISE_PRESETS: ExercisePreset[] = [
+ {
+ id: 'dumbbell-curl',
+ name: '哑铃弯举',
+ equipment: '哑铃',
+ icon: '🏋',
+ animationType: 'curl',
+ description: '锻炼肱二头肌,注意肘部固定不动',
+ tips: ['肘部贴紧身体', '上举吸气,下落呼气', '动作放慢,控制重量'],
+ contraindication: '肘关节炎症急性期不宜',
+ defaultBpm: 60,
+ bpmMin: 40,
+ bpmMax: 100,
+ defaultSets: 3,
+ defaultReps: 12,
+ defaultRestSec: 60,
+ beatsPerRep: 2,
+ },
+ {
+ id: 'dumbbell-press',
+ name: '哑铃推举',
+ equipment: '哑铃',
+ icon: '🏋',
+ animationType: 'press',
+ description: '锻炼肩部三角肌,核心保持收紧',
+ tips: ['不要含胸驼背', '推举时呼气', '不要锁死肘关节'],
+ contraindication: '肩袖损伤者请咨询医生',
+ defaultBpm: 60,
+ bpmMin: 40,
+ bpmMax: 90,
+ defaultSets: 3,
+ defaultReps: 10,
+ defaultRestSec: 90,
+ beatsPerRep: 2,
+ },
+ {
+ id: 'dumbbell-sidefly',
+ name: '哑铃侧平举',
+ equipment: '哑铃',
+ icon: '🏋',
+ animationType: 'sidefly',
+ description: '锻炼三角肌中束,重量宜轻不宜重',
+ tips: ['手肘微屈', '抬至与肩平齐即可', '想象用肩膀发力,不是手臂'],
+ defaultBpm: 60,
+ bpmMin: 40,
+ bpmMax: 80,
+ defaultSets: 3,
+ defaultReps: 12,
+ 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: '集中机(仰卧卷腹)',
+ equipment: '健身环',
+ icon: '💪',
+ animationType: 'crunch',
+ description: '锻炼腹直肌,借助健身环增加阻力',
+ tips: ['下背贴地', '用腹部发力,不是脖子', '上抬呼气,下落吸气'],
+ contraindication: '腰椎间盘突出急性期不宜',
+ defaultBpm: 50,
+ bpmMin: 40,
+ bpmMax: 80,
+ defaultSets: 3,
+ defaultReps: 15,
+ defaultRestSec: 60,
+ beatsPerRep: 2,
+ },
+]
+
+export function getPresetById(id: string): ExercisePreset | undefined {
+ return EXERCISE_PRESETS.find((e) => e.id === id)
+}
diff --git a/TUICallKit-Vue3/training/pages/index.vue b/TUICallKit-Vue3/training/pages/index.vue
new file mode 100644
index 00000000..6552e37b
--- /dev/null
+++ b/TUICallKit-Vue3/training/pages/index.vue
@@ -0,0 +1,610 @@
+
+
+
+
+
+
+ {{ eq }}
+
+
+
+
+
+
+ {{ ex.icon }}
+ {{ ex.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selected.description }}
+
+
+
+
+ 节奏
+
+ {{ bpm }} BPM
+
+
+
+
+ 组数
+
+ -
+ {{ sets }}
+ +
+
+
+
+ 次数
+
+ -
+ {{ reps }}
+ +
+
+
+
+ 休息(秒)
+
+ -
+ {{ restSec }}
+ +
+
+
+
+
+
+
+
+
+
+
+ 训练中
+ 第 {{ currentSet }} / {{ totalSets }} 组
+ {{ currentRep }} / {{ totalReps }}
+
+
+
+
+
+
+ 休息中
+ {{ restRemainingSec }}s
+
+
+
+
+
+
+
+ 训练完成 🎉
+ 共 {{ totalSets }} 组 × {{ totalReps }} 次
+
+
+
+
+
+
+ 动作要领
+
+ • {{ tip }}
+
+
+ ⚠ {{ selected.contraindication }}
+
+
+
+
+
+
+
+
+
diff --git a/TUICallKit-Vue3/training/pages/metronome.vue b/TUICallKit-Vue3/training/pages/metronome.vue
new file mode 100644
index 00000000..023797ee
--- /dev/null
+++ b/TUICallKit-Vue3/training/pages/metronome.vue
@@ -0,0 +1,625 @@
+
+
+
+
+
+
+
+ {{ bpm }}
+ BPM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ p.icon }}
+ {{ p.name }}
+ {{ p.bpm }} BPM
+ {{ p.desc }}
+
+
+
+
+
+
+ −
+ 微调
+ +
+
+
+
+
+ {{ m.label }}
+
+ 拍
+
+
+
+
+
+
+
+
diff --git a/TUICallKit-Vue3/training/static/README.md b/TUICallKit-Vue3/training/static/README.md
new file mode 100644
index 00000000..1c5442f4
--- /dev/null
+++ b/TUICallKit-Vue3/training/static/README.md
@@ -0,0 +1,105 @@
+# 训练模块静态资源
+
+本目录用于存放"练一练"功能的所有音频素材。
+
+## 目录结构
+
+```
+training/static/
+├── audio/
+│ └── click.mp3 # 节拍器主音(短促金属敲击声,约 50ms)
+├── voice/
+│ ├── numbers/
+│ │ └── 1.mp3 ~ 30.mp3 # 数字报数
+│ └── prompts/
+│ ├── start.mp3
+│ ├── ready.mp3
+│ ├── rest.mp3
+│ ├── next-set.mp3
+│ ├── last-rep.mp3
+│ ├── keep-it-up.mp3
+│ ├── good-job.mp3
+│ ├── breathe-in.mp3
+│ └── breathe-out.mp3
+└── bgm/
+ ├── train-light.mp3 # 训练时背景乐(轻快有节奏感)
+ └── rest-meditation.mp3 # 休息时背景乐(舒缓冥想)
+```
+
+## 准备步骤
+
+### 1. 语音素材(小米 MiMo TTS 自动生成)
+
+需要 Node 18+(用内置 `fetch`)。先准备小米 MiMo API Key:
+
+- 文档: https://platform.xiaomimimo.com/
+- API Key 形如 `sk-xxxxxxxx`
+
+```bash
+export MIMO_API_KEY=sk-your-key-here
+
+cd uniapp
+node scripts/generate-voice.mjs
+```
+
+会自动调用 `mimo-v2.5-tts` 模型生成 39 个 mp3(30 个数字 + 9 个口令)到 `voice/` 目录。
+
+**可选参数**:
+
+```bash
+# 换音色(默认 冰糖;可选:冰糖/茉莉/苏打/白桦/Mia/Chloe/Milo/Dean)
+node scripts/generate-voice.mjs --voice 茉莉
+
+# 换格式(默认 mp3,需要跟 hooks/useVoiceCoach.ts 里的 .mp3 后缀对应)
+node scripts/generate-voice.mjs --format wav
+
+# 强制重新生成(默认存在则跳过)
+node scripts/generate-voice.mjs --force
+```
+
+**音色推荐**(中文女声更适合健身教练):
+- `冰糖`:温柔甜美,亲和力强(默认)
+- `茉莉`:清爽利落,有"运动博主"感
+- `苏打`:年轻男声,有力量感
+- `白桦`:成熟男声,沉稳
+
+### 2. 节拍器 click 音
+
+**已内置 3 种音色**(来自 [chrono-bump](https://github.com/johnnysn/chrono-bump) MIT 协议项目):
+
+| 文件 | 用途 | 大小 |
+|------|------|------|
+| `audio/click.mp3` | 默认 click(标准节拍器音) | 3.4 KB |
+| `audio/click-soft.mp3` | 柔和音色 | 4.2 KB |
+| `audio/click-wood.mp3` | 木鱼/原木质感(推荐用于重音 accent) | 4.6 KB |
+
+如果想换更好听的,可以去:
+- [freesound.org](https://freesound.org) 搜 "click" / "tick"(注意 CC 协议)
+- [pixabay.com/sound-effects](https://pixabay.com/sound-effects/) 搜 "metronome"(免费可商用)
+
+替换时保持文件名不变即可,无需改代码。
+
+### 3. 背景音乐
+
+每首 30 秒~2 分钟即可(loop 后听不出接缝)。
+
+推荐来源(免费可商用):
+- [pixabay.com/music](https://pixabay.com/music) 搜 "calm" / "meditation" / "lofi"
+- [freemusicarchive.org](https://freemusicarchive.org)
+- [bensound.com](https://bensound.com)(含署名)
+
+文件大小建议 < 1MB(mp3 128kbps 单声道即可)。
+
+## 在代码里被引用的位置
+
+- `training/hooks/useMetronome.ts` → `audio/click.mp3`
+- `training/hooks/useVoiceCoach.ts` → `voice/numbers/*.mp3`、`voice/prompts/*.mp3`
+- `training/hooks/useTrainingBgm.ts` → `bgm/*.mp3`
+
+如需修改路径,修改对应 hook 文件里的常量即可。
+
+## 注意事项
+
+- 微信小程序对单个文件大小有 10MB 上限,本目录所有文件加起来建议控制在 5MB 以内。
+- 小程序整包大小限制(主包 2MB / 总包 20MB),如果资源较大建议放 CDN 而非本地 static。
+ - 把 `useVoiceCoach.ts` 里的 `VOICE_BASE` 改成 CDN URL 即可。
diff --git a/TUICallKit-Vue3/training/static/audio/click-wood.mp3 b/TUICallKit-Vue3/training/static/audio/click-wood.mp3
new file mode 100644
index 00000000..fb3fff34
Binary files /dev/null and b/TUICallKit-Vue3/training/static/audio/click-wood.mp3 differ
diff --git a/TUICallKit-Vue3/training/static/audio/click.mp3 b/TUICallKit-Vue3/training/static/audio/click.mp3
new file mode 100644
index 00000000..faa1693a
Binary files /dev/null and b/TUICallKit-Vue3/training/static/audio/click.mp3 differ
diff --git a/TUICallKit-Vue3/training/static/footprint.svg b/TUICallKit-Vue3/training/static/footprint.svg
new file mode 100644
index 00000000..83290b90
--- /dev/null
+++ b/TUICallKit-Vue3/training/static/footprint.svg
@@ -0,0 +1,11 @@
+
+