import { ref, computed, onUnmounted } from 'vue' import { calculateCalories, getFoodComparison } from '../utils/calorie' import type { FoodComparison } from '../utils/calorie' export type SessionPhase = 'idle' | 'training' | 'resting' | 'done' export type CrushItemType = 'egg' | 'walnut' | 'can' | 'balloon' export interface TrainingSessionConfig { sets: number reps: number restSec: number beatsPerRep?: number onRepComplete?: (currentRep: number, totalReps: number) => void onSetComplete?: (currentSet: number, totalSets: number) => void onEnterRest?: (restSec: number) => void onExitRest?: () => void onDone?: () => void onCountdownTick?: (remainingSec: number) => void onCrushTrigger?: (itemType: CrushItemType) => void } export interface TrainingStats { totalSets: number totalReps: number durationSeconds: number calories: number foodComparison: FoodComparison } // 捏碎特效配置 const CRUSH_TRIGGER_INTERVAL_MIN = 8 // 最小触发间隔 const CRUSH_TRIGGER_INTERVAL_MAX = 12 // 最大触发间隔 const CRUSH_ITEMS: CrushItemType[] = ['egg', 'walnut', 'can', 'balloon'] // 随机触发判断(平均每 8-12 次触发一次) function shouldTriggerCrush(): boolean { // 使用随机间隔的平均值:(8 + 12) / 2 = 10 const avgInterval = (CRUSH_TRIGGER_INTERVAL_MIN + CRUSH_TRIGGER_INTERVAL_MAX) / 2 return Math.random() < (1 / avgInterval) } // 随机选择物品 function randomCrushItem(): CrushItemType { return CRUSH_ITEMS[Math.floor(Math.random() * CRUSH_ITEMS.length)] } export function useTrainingSession(bpm?: number) { const phase = ref('idle') const currentSet = ref(0) const currentRep = ref(0) const restRemainingSec = ref(0) const beatCounter = ref(0) let config: TrainingSessionConfig | null = null let restTimer: ReturnType | null = null let crushTimers: ReturnType[] = [] // 存储所有捏碎特效的 setTimeout let currentBpm = bpm ?? 80 let startTime = 0 // 训练开始时间戳 const totalReps = computed(() => config?.reps ?? 0) const totalSets = computed(() => config?.sets ?? 0) const beatsPerRep = computed(() => config?.beatsPerRep ?? 2) const isTraining = computed(() => phase.value === 'training') const isResting = computed(() => phase.value === 'resting') const isDone = computed(() => phase.value === 'done') const setBpm = (newBpm: number) => { currentBpm = newBpm } const start = (cfg: TrainingSessionConfig) => { config = cfg phase.value = 'training' currentSet.value = 1 currentRep.value = 0 beatCounter.value = 0 restRemainingSec.value = 0 startTime = Date.now() // 记录开始时间 } // 获取训练统计数据 const getStats = (): TrainingStats => { const durationSeconds = Math.floor((Date.now() - startTime) / 1000) const actualTotalReps = (config?.sets ?? 0) * (config?.reps ?? 0) const calories = calculateCalories({ totalReps: actualTotalReps, bpm: currentBpm, userWeight: 60, // 默认 60kg }) return { totalSets: config?.sets ?? 0, totalReps: actualTotalReps, durationSeconds, calories, foodComparison: getFoodComparison(calories), } } const onBeat = () => { if (phase.value !== 'training' || !config) return beatCounter.value++ if (beatCounter.value % beatsPerRep.value !== 0) return currentRep.value++ config.onRepComplete?.(currentRep.value, totalReps.value) // 随机触发捏碎特效 if (shouldTriggerCrush() && config.onCrushTrigger) { const item = randomCrushItem() // 延迟到动画峰值(50%)触发 const repDurationMs = (60000 / currentBpm) * beatsPerRep.value const timer = setTimeout(() => { config?.onCrushTrigger?.(item) }, repDurationMs / 2) crushTimers.push(timer) } if (currentRep.value >= totalReps.value) { config.onSetComplete?.(currentSet.value, totalSets.value) if (currentSet.value >= totalSets.value) { phase.value = 'done' config.onDone?.() return } enterRest() } } const enterRest = () => { if (!config) return phase.value = 'resting' restRemainingSec.value = config.restSec config.onEnterRest?.(config.restSec) restTimer = setInterval(() => { restRemainingSec.value-- config?.onCountdownTick?.(restRemainingSec.value) if (restRemainingSec.value <= 0) { exitRest() } }, 1000) } const exitRest = () => { if (restTimer) { clearInterval(restTimer) restTimer = null } if (!config) return currentSet.value++ currentRep.value = 0 beatCounter.value = 0 phase.value = 'training' config.onExitRest?.() } const skipRest = () => { if (phase.value !== 'resting') return exitRest() } const stop = () => { if (restTimer) { clearInterval(restTimer) restTimer = null } // 清理所有待触发的捏碎特效 crushTimers.forEach(timer => clearTimeout(timer)) crushTimers = [] phase.value = 'idle' currentSet.value = 0 currentRep.value = 0 beatCounter.value = 0 restRemainingSec.value = 0 } onUnmounted(() => { if (restTimer) clearInterval(restTimer) crushTimers.forEach(timer => clearTimeout(timer)) crushTimers = [] }) return { phase, currentSet, currentRep, restRemainingSec, totalReps, totalSets, isTraining, isResting, isDone, start, stop, skipRest, onBeat, setBpm, getStats, } }