主包超出 2MB 上限无法上传发布版,把训练相关全部移到独立分包。
目录调整:
- pages/training/* → training/pages/*
- hooks/use{Metronome,VoiceCoach,TrainingSession,TrainingBgm}.ts
→ training/hooks/*
- static/training/* → training/static/*
- 主包 hooks/ 目录已清空,所有 use* 都被分包独占
引用路径更新:
- 分包内 hook import 用相对路径 ../hooks/xxx,不依赖 @/ alias
- 资源路径 /static/training/audio/click.mp3
→ /training/static/audio/click.mp3
- useMetronome 默认 clickSrc / useVoiceCoach VOICE_BASE
/ useTrainingBgm BGM_BASE 全部对齐新路径
pages.json:
- 主包 pages 移除 training/index 和 training/metronome
- subPackages 新增 root="training",含 pages/index 和 pages/metronome
- 跟现有 TUIKit / doctor 分包风格一致
调用方:
- components/dev-training-entry navigateTo URL 更新成
/training/pages/metronome (此组件留在主包,主包跳分包是合法操作)
文档/工具同步:
- training/static/README.md 路径示例改为 training/static/...
- scripts/generate-voice.mjs VOICE_DIR 与提示输出对齐新路径
Git mv 检测到的全是 rename(R),历史完整保留。
分包总体 80K(pages 44K + hooks 20K + static 16K),主包瘦身明显。
Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
1.8 KiB
TypeScript
88 lines
1.8 KiB
TypeScript
import { onUnmounted } from 'vue'
|
|
|
|
const VOICE_BASE = '/training/static/voice'
|
|
|
|
export type VoicePromptKey =
|
|
| 'start'
|
|
| 'ready'
|
|
| 'rest'
|
|
| 'next-set'
|
|
| 'last-rep'
|
|
| 'keep-it-up'
|
|
| 'good-job'
|
|
| 'breathe-in'
|
|
| 'breathe-out'
|
|
|
|
export interface UseVoiceCoachOptions {
|
|
enabled?: boolean
|
|
volume?: number
|
|
}
|
|
|
|
export function useVoiceCoach(options: UseVoiceCoachOptions = {}) {
|
|
const { enabled = true, volume = 1 } = options
|
|
|
|
let ctx: UniApp.InnerAudioContext | null = null
|
|
let queue: string[] = []
|
|
let isPlayingQueue = false
|
|
|
|
const ensureCtx = () => {
|
|
if (ctx) return
|
|
ctx = uni.createInnerAudioContext()
|
|
ctx.volume = volume
|
|
ctx.obeyMuteSwitch = false
|
|
ctx.onEnded(() => {
|
|
playNextInQueue()
|
|
})
|
|
ctx.onError(() => {
|
|
playNextInQueue()
|
|
})
|
|
}
|
|
|
|
const playNextInQueue = () => {
|
|
if (!ctx || queue.length === 0) {
|
|
isPlayingQueue = false
|
|
return
|
|
}
|
|
const nextSrc = queue.shift()!
|
|
ctx.src = nextSrc
|
|
ctx.play()
|
|
}
|
|
|
|
const enqueue = (src: string) => {
|
|
if (!enabled) return
|
|
ensureCtx()
|
|
queue.push(src)
|
|
if (!isPlayingQueue) {
|
|
isPlayingQueue = true
|
|
playNextInQueue()
|
|
}
|
|
}
|
|
|
|
const speakNumber = (n: number) => {
|
|
if (n < 1 || n > 30) return
|
|
enqueue(`${VOICE_BASE}/numbers/${n}.mp3`)
|
|
}
|
|
|
|
const speakPrompt = (key: VoicePromptKey) => {
|
|
enqueue(`${VOICE_BASE}/prompts/${key}.mp3`)
|
|
}
|
|
|
|
const stop = () => {
|
|
queue = []
|
|
isPlayingQueue = false
|
|
ctx?.stop()
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
stop()
|
|
ctx?.destroy?.()
|
|
ctx = null
|
|
})
|
|
|
|
return {
|
|
speakNumber,
|
|
speakPrompt,
|
|
stop,
|
|
}
|
|
}
|