From 132a17b9462e95e436a80cd0d5605601873c78ec Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Fri, 29 May 2026 18:17:44 +0800 Subject: [PATCH] update --- .../components/dev-training-entry/index.vue | 12 - TUICallKit-Vue3/pages.json | 6 - TUICallKit-Vue3/scripts/generate-voice.mjs | 213 ----- .../training/hooks/useMetronome.ts | 3 + TUICallKit-Vue3/training/hooks/useTTS.ts | 32 - .../training/hooks/useTrainingBgm.ts | 115 --- .../training/hooks/useTrainingSession.ts | 210 ----- .../training/hooks/useVoiceCoach.ts | 87 --- .../pages/components/exercise-anim.vue | 280 ------- TUICallKit-Vue3/training/pages/dumbbell.vue | 165 ++-- TUICallKit-Vue3/training/pages/exercises.ts | 99 --- TUICallKit-Vue3/training/pages/foot-pedal.vue | 165 ++-- TUICallKit-Vue3/training/pages/grip-ring.vue | 169 ++-- TUICallKit-Vue3/training/pages/index.vue | 739 ------------------ .../training/pages/pilates-ring.vue | 165 ++-- 15 files changed, 285 insertions(+), 2175 deletions(-) delete mode 100644 TUICallKit-Vue3/scripts/generate-voice.mjs delete mode 100644 TUICallKit-Vue3/training/hooks/useTTS.ts delete mode 100644 TUICallKit-Vue3/training/hooks/useTrainingBgm.ts delete mode 100644 TUICallKit-Vue3/training/hooks/useTrainingSession.ts delete mode 100644 TUICallKit-Vue3/training/hooks/useVoiceCoach.ts delete mode 100644 TUICallKit-Vue3/training/pages/components/exercise-anim.vue delete mode 100644 TUICallKit-Vue3/training/pages/exercises.ts delete mode 100644 TUICallKit-Vue3/training/pages/index.vue diff --git a/TUICallKit-Vue3/components/dev-training-entry/index.vue b/TUICallKit-Vue3/components/dev-training-entry/index.vue index b0e23666..374f0bc3 100644 --- a/TUICallKit-Vue3/components/dev-training-entry/index.vue +++ b/TUICallKit-Vue3/components/dev-training-entry/index.vue @@ -2,18 +2,6 @@ - - - diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index a605b43a..cc635b8e 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -109,12 +109,6 @@ { "root": "training", "pages": [ - { - "path": "pages/index", - "style": { - "navigationBarTitleText": "练一练" - } - }, { "path": "pages/grip-ring", "style": { diff --git a/TUICallKit-Vue3/scripts/generate-voice.mjs b/TUICallKit-Vue3/scripts/generate-voice.mjs deleted file mode 100644 index ece61c83..00000000 --- a/TUICallKit-Vue3/scripts/generate-voice.mjs +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-console */ -/** - * 用小米 MiMo TTS 生成训练语音素材 - * - * 文档: https://platform.xiaomimimo.com/docs/en-US/usage-guide/speech-synthesis-v2.5 - * 接口: POST https://api.xiaomimimo.com/v1/chat/completions - * - * 准备: - * export MIMO_API_KEY=sk-your-key-here - * (或 export XIAOMI_API_KEY=...) - * - * 运行: - * cd uniapp - * node scripts/generate-voice.mjs # 默认音色 冰糖 - * node scripts/generate-voice.mjs --voice 茉莉 - * node scripts/generate-voice.mjs --force # 已存在的也重新生成 - * - * 输出(分包): - * training/static/voice/numbers/{1..30}.mp3 - * training/static/voice/prompts/{key}.mp3 - * - * 需要 Node 18+(用内置 fetch) - */ - -import { mkdir, writeFile, access, stat } from 'node:fs/promises' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { Buffer } from 'node:buffer' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const ROOT = join(__dirname, '..') -const VOICE_DIR = join(ROOT, 'training/static/voice') -const NUMBERS_DIR = join(VOICE_DIR, 'numbers') -const PROMPTS_DIR = join(VOICE_DIR, 'prompts') - -// ============ 参数 ============ -const args = process.argv.slice(2) -const FORCE = args.includes('--force') -const VOICE = pickArg('--voice') || '冰糖' -const MODEL = pickArg('--model') || 'mimo-v2.5-tts' -const FORMAT = pickArg('--format') || 'mp3' - -const API_KEY = - process.env.MIMO_API_KEY || - process.env.XIAOMI_API_KEY || - process.env.XIAOMI_MIMO_API_KEY -const ENDPOINT = - process.env.MIMO_ENDPOINT || 'https://api.xiaomimimo.com/v1/chat/completions' - -if (!API_KEY) { - console.error('❌ 未找到 API Key') - console.error(' 请先 export MIMO_API_KEY=your-key') - process.exit(1) -} - -function pickArg(name) { - const idx = args.indexOf(name) - if (idx === -1) return null - return args[idx + 1] -} - -// ============ 语音内容 ============ - -// 数字报数:简短的"教练点数"风格 -const NUMBER_STYLE = - '用简短有力的健身教练口吻报数,干净利落,节奏明快,每个数字独立清晰。' - -// 口令:温柔有鼓励性的女教练 -const PROMPT_STYLE = - '用温柔但有力量的女教练口吻说话,语调亲切自然,节奏适中,像在带学员训练。' - -const PROMPTS = { - start: '开始', - ready: '准备', - rest: '休息一下', - 'next-set': '下一组开始', - 'last-rep': '最后一次', - 'keep-it-up': '继续坚持', - 'good-job': '很棒,训练完成', - 'breathe-in': '吸气', - 'breathe-out': '呼气', -} - -// ============ 工具 ============ - -async function fileExists(p) { - try { - await access(p) - return true - } catch { - return false - } -} - -async function ensureDir(d) { - await mkdir(d, { recursive: true }) -} - -async function tts(text, outPath, styleInstruction) { - if (!FORCE && (await fileExists(outPath))) { - const s = await stat(outPath) - if (s.size > 0) { - console.log(`[skip] ${outPath} (已存在)`) - return - } - } - - const body = { - model: MODEL, - messages: [ - { role: 'user', content: styleInstruction }, - { role: 'assistant', content: text }, - ], - audio: { format: FORMAT, voice: VOICE }, - } - - const res = await fetch(ENDPOINT, { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - - if (!res.ok) { - const errText = await res.text().catch(() => '') - throw new Error(`HTTP ${res.status} ${res.statusText}: ${errText.slice(0, 300)}`) - } - - const json = await res.json() - const audioBase64 = json?.choices?.[0]?.message?.audio?.data - if (!audioBase64) { - throw new Error( - `响应里找不到 audio.data 字段。完整响应: ${JSON.stringify(json).slice(0, 500)}`, - ) - } - - const buf = Buffer.from(audioBase64, 'base64') - await writeFile(outPath, buf) - console.log(`[ok] ${outPath} (${buf.length} bytes) → "${text}"`) -} - -async function sleep(ms) { - return new Promise((r) => setTimeout(r, ms)) -} - -// ============ 主流程 ============ - -async function main() { - console.log('====== 小米 MiMo TTS 语音生成 ======') - console.log(`Endpoint : ${ENDPOINT}`) - console.log(`Model : ${MODEL}`) - console.log(`Voice : ${VOICE}`) - console.log(`Format : ${FORMAT}`) - console.log(`Force : ${FORCE}`) - console.log(`Output : ${VOICE_DIR}`) - console.log('====================================\n') - - await ensureDir(NUMBERS_DIR) - await ensureDir(PROMPTS_DIR) - - let successCount = 0 - let failCount = 0 - - console.log('--- 数字 1-30 ---') - for (let n = 1; n <= 30; n++) { - try { - await tts(String(n), join(NUMBERS_DIR, `${n}.${FORMAT}`), NUMBER_STYLE) - successCount++ - await sleep(120) - } catch (e) { - failCount++ - console.error(`[fail] 数字 ${n}: ${e.message}`) - } - } - - console.log('\n--- 口令 ---') - for (const [key, text] of Object.entries(PROMPTS)) { - try { - await tts(text, join(PROMPTS_DIR, `${key}.${FORMAT}`), PROMPT_STYLE) - successCount++ - await sleep(120) - } catch (e) { - failCount++ - console.error(`[fail] ${key}: ${e.message}`) - } - } - - console.log('\n====================================') - console.log(`成功 ${successCount} | 失败 ${failCount}`) - console.log('====================================') - - if (FORMAT !== 'mp3') { - console.log( - `\n⚠️ 当前生成格式是 ${FORMAT},但 hooks/useVoiceCoach.ts 默认引用 .mp3。`, - ) - console.log(' 要么换 --format mp3 重跑,要么修改 hooks 里的 src 后缀。') - } - - console.log('\n下一步:') - console.log(' 1. 准备 click 音 → training/static/audio/click.mp3') - console.log(' 2. 准备 BGM → training/static/bgm/{train-light,rest-meditation}.mp3') - console.log(' 3. 编译运行 → 进入 /training/pages/index') -} - -main().catch((e) => { - console.error('\n❌ 致命错误:', e) - process.exit(1) -}) diff --git a/TUICallKit-Vue3/training/hooks/useMetronome.ts b/TUICallKit-Vue3/training/hooks/useMetronome.ts index a4a8c629..8e71205a 100644 --- a/TUICallKit-Vue3/training/hooks/useMetronome.ts +++ b/TUICallKit-Vue3/training/hooks/useMetronome.ts @@ -9,6 +9,7 @@ export interface MetronomeOptions { accentEvery?: number poolSize?: number volume?: number // 节拍音音量 0-1 + silent?: boolean // 静音模式:只驱动节拍回调,不发声(如握力环只需视觉节拍) onBeat?: (beatIndex: number, isAccent: boolean) => void } @@ -119,6 +120,7 @@ export function useMetronome(options: MetronomeOptions = {}) { accentSrc, poolSize = 4, volume = 1, + silent = false, onBeat, } = options @@ -136,6 +138,7 @@ export function useMetronome(options: MetronomeOptions = {}) { let nextTickAt = 0 const ensureAudio = () => { + if (silent) return if (!clickPool) { clickPool = new AudioPool(clickSrc, poolSize, volume) clickPool.warmUp() diff --git a/TUICallKit-Vue3/training/hooks/useTTS.ts b/TUICallKit-Vue3/training/hooks/useTTS.ts deleted file mode 100644 index 8ebbb07e..00000000 --- a/TUICallKit-Vue3/training/hooks/useTTS.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * TTS 语音鼓励 Hook - * 用于在捏碎特效触发时播放语音鼓励 - * - * 当前状态:预留接口,待集成 MiMo TTS - * TODO: 集成 MiMo TTS SDK - */ - -export function useTTS() { - /** - * 播放语音鼓励 - * 随机播放:"加油"、"太棒了"、"继续"、"很好"等 - * - * 实现计划: - * 1. 引入 MiMo TTS SDK - * 2. 配置语音库(鼓励短语列表) - * 3. 随机选择短语并播放 - */ - const playEncouragement = () => { - // TODO: 集成 MiMo TTS - // 示例实现: - // const phrases = ['加油', '太棒了', '继续', '很好', '坚持'] - // const phrase = phrases[Math.floor(Math.random() * phrases.length)] - // mimoTTS.speak(phrase) - - console.log('[TTS] 播放语音鼓励(待实现 MiMo TTS 集成)') - } - - return { - playEncouragement - } -} diff --git a/TUICallKit-Vue3/training/hooks/useTrainingBgm.ts b/TUICallKit-Vue3/training/hooks/useTrainingBgm.ts deleted file mode 100644 index 0bbdc48c..00000000 --- a/TUICallKit-Vue3/training/hooks/useTrainingBgm.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ref, onUnmounted } from 'vue' - -const BGM_BASE = '/training/static/bgm' - -export type BgmMode = 'none' | 'training' | 'resting' - -export interface UseTrainingBgmOptions { - trainingSrc?: string - restingSrc?: string - volume?: number - fadeMs?: number -} - -export function useTrainingBgm(options: UseTrainingBgmOptions = {}) { - const { - trainingSrc = `${BGM_BASE}/train-light.mp3`, - restingSrc = `${BGM_BASE}/rest-meditation.mp3`, - volume = 0.6, - fadeMs = 600, - } = options - - const mode = ref('none') - const enabled = ref(true) - - let trainCtx: UniApp.InnerAudioContext | null = null - let restCtx: UniApp.InnerAudioContext | null = null - let fadeTimers: ReturnType[] = [] - - const ensureCtx = () => { - if (!trainCtx) { - trainCtx = uni.createInnerAudioContext() - trainCtx.src = trainingSrc - trainCtx.loop = true - trainCtx.obeyMuteSwitch = false - trainCtx.volume = 0 - } - if (!restCtx) { - restCtx = uni.createInnerAudioContext() - restCtx.src = restingSrc - restCtx.loop = true - restCtx.obeyMuteSwitch = false - restCtx.volume = 0 - } - } - - const fade = ( - ctx: UniApp.InnerAudioContext, - from: number, - to: number, - duration = fadeMs, - ) => { - const steps = 16 - const stepMs = Math.max(16, duration / steps) - let i = 0 - const t = setInterval(() => { - i++ - const v = from + (to - from) * (i / steps) - ctx.volume = Math.max(0, Math.min(1, v)) - if (i >= steps) { - clearInterval(t) - fadeTimers = fadeTimers.filter((x) => x !== t) - } - }, stepMs) - fadeTimers.push(t) - } - - const switchTo = (target: BgmMode) => { - if (!enabled.value) return - if (mode.value === target) return - ensureCtx() - - if (target === 'training' && trainCtx && restCtx) { - fade(restCtx, restCtx.volume, 0) - setTimeout(() => restCtx?.pause(), fadeMs) - trainCtx.play() - fade(trainCtx, 0, volume) - } else if (target === 'resting' && trainCtx && restCtx) { - fade(trainCtx, trainCtx.volume, 0) - setTimeout(() => trainCtx?.pause(), fadeMs) - restCtx.play() - fade(restCtx, 0, volume) - } else if (target === 'none') { - if (trainCtx) { - fade(trainCtx, trainCtx.volume, 0) - setTimeout(() => trainCtx?.pause(), fadeMs) - } - if (restCtx) { - fade(restCtx, restCtx.volume, 0) - setTimeout(() => restCtx?.pause(), fadeMs) - } - } - mode.value = target - } - - const setEnabled = (v: boolean) => { - enabled.value = v - if (!v) switchTo('none') - } - - onUnmounted(() => { - fadeTimers.forEach((t) => clearInterval(t)) - fadeTimers = [] - trainCtx?.destroy?.() - restCtx?.destroy?.() - trainCtx = null - restCtx = null - }) - - return { - mode, - enabled, - switchTo, - setEnabled, - } -} diff --git a/TUICallKit-Vue3/training/hooks/useTrainingSession.ts b/TUICallKit-Vue3/training/hooks/useTrainingSession.ts deleted file mode 100644 index acdc5df0..00000000 --- a/TUICallKit-Vue3/training/hooks/useTrainingSession.ts +++ /dev/null @@ -1,210 +0,0 @@ -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, - } -} diff --git a/TUICallKit-Vue3/training/hooks/useVoiceCoach.ts b/TUICallKit-Vue3/training/hooks/useVoiceCoach.ts deleted file mode 100644 index bc27bbae..00000000 --- a/TUICallKit-Vue3/training/hooks/useVoiceCoach.ts +++ /dev/null @@ -1,87 +0,0 @@ -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, - } -} diff --git a/TUICallKit-Vue3/training/pages/components/exercise-anim.vue b/TUICallKit-Vue3/training/pages/components/exercise-anim.vue deleted file mode 100644 index 00487b4a..00000000 --- a/TUICallKit-Vue3/training/pages/components/exercise-anim.vue +++ /dev/null @@ -1,280 +0,0 @@ - - - - - diff --git a/TUICallKit-Vue3/training/pages/dumbbell.vue b/TUICallKit-Vue3/training/pages/dumbbell.vue index e3bec311..76a2f018 100644 --- a/TUICallKit-Vue3/training/pages/dumbbell.vue +++ b/TUICallKit-Vue3/training/pages/dumbbell.vue @@ -72,26 +72,9 @@ - - - 次数 - {{ hasStats ? totalReps : '—' }} - - - 时长 - {{ hasStats ? formatTime(elapsedSeconds) : '—' }} - - - 糖分 - {{ hasStats ? `≈${totalSugar.toFixed(1)}g` : '—' }} - - - - - - 训练强度 + 选择训练强度 训练中不可切换 @@ -102,26 +85,24 @@ :class="{ active: currentPreset === p.id, disabled: isPlaying }" @click="onPresetTap(p.id)" > - {{ PRESET_ICONS[p.id] }} {{ p.label }} {{ p.bpm }} BPM - {{ p.desc }} - - 🔥 - {{ estimateText }} - + - - 训练目标 - 到时间自动结束 + + 训练时长 + + {{ currentTargetLabel }} + {{ targetExpanded ? '▲' : '▼' }} + - + - - + 训练功效 @@ -264,21 +244,6 @@ const DUMBBELL_PRESETS: readonly DumbbellPreset[] = [ { id: 'heavy', bpm: 80, met: 6.5, label: '重度', desc: '强化 · 挑战' }, ] -// 训练功效介绍(专业文案) -const TRAINING_BENEFIT = { - muscles: ['肱二头肌', '三角肌', '胸大肌', '背阔肌'], - effect: '多角度抗阻训练上肢与肩背肌群,提升肌肉力量与耐力,雕塑手臂与肩部线条,增强骨密度,改善上肢日常功能。', -} - -// 强度预估参考时长(选择"自由"目标时按此估算) -const ESTIMATE_REF_MINUTES = 10 - -const PRESET_ICONS: Record = { - light: '🌱', - medium: '💪', - heavy: '🔥', -} - // 画布指导视频 const TUTORIAL_VIDEO_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260528/202605281739117bf3c2914.mp4' const TUTORIAL_POSTER_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/images/20260529/202605291142269212d9402.jpg' @@ -349,16 +314,21 @@ const timerInterval = ref(null) const preset = computed(() => DUMBBELL_PRESETS.find(p => p.id === currentPreset.value) || DUMBBELL_PRESETS[1]) const currentBpm = computed(() => preset.value.bpm) const currentMet = computed(() => preset.value.met) -const benefitExpanded = ref(false) -// 当前强度预估消耗文案: 按选中目标时长(自由→10分钟参考)估算 -const estimateText = computed(() => { - const mins = targetMinutes.value > 0 ? targetMinutes.value : ESTIMATE_REF_MINUTES - const sugar = (currentMet.value * 60 * (mins / 60)) / 4 - const suffix = targetMinutes.value > 0 ? '' : '(按 10 分钟估算)' - return `${preset.value.label}训练 ${mins} 分钟,约消耗 ${sugar.toFixed(1)}g 糖分${suffix}` +// 训练时长折叠状态 + 当前时长文案 +const targetExpanded = ref(false) +const currentTargetLabel = computed(() => { + const opt = TARGET_OPTIONS.find(t => t.minutes === targetMinutes.value) + return opt ? opt.label : '自由' }) +// 训练功效介绍(专业文案) + 折叠状态 +const benefitExpanded = ref(false) +const TRAINING_BENEFIT = { + muscles: ['肱二头肌', '三角肌', '胸大肌', '背阔肌'], + effect: '多角度抗阻训练上肢与肩背肌群,提升肌肉力量与耐力,雕塑手臂与肩部线条,增强骨密度,改善上肢日常功能。', +} + // 示范视频跟随训练状态 watch(isPlaying, (playing) => { const videoCtx = uni.createVideoContext('dumbbellDemoVideo') @@ -826,7 +796,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); margin-left: -28rpx; margin-right: -28rpx; flex-shrink: 0; - height: 520rpx; + height: 740rpx; display: flex; justify-content: center; align-items: center; @@ -859,8 +829,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .ring-container { position: relative; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; display: flex; align-items: center; justify-content: center; @@ -869,8 +839,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .beat-halo { position: absolute; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; border-radius: 50%; background: radial-gradient(circle, rgba(59, 130, 246, 0.45) 0%, @@ -899,8 +869,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .video-ring { position: relative; - width: 460rpx; - height: 460rpx; + width: 640rpx; + height: 640rpx; border-radius: 50%; overflow: hidden; box-shadow: @@ -964,8 +934,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-icon { - width: 116rpx; - height: 116rpx; + width: 140rpx; + height: 140rpx; display: flex; align-items: center; justify-content: center; @@ -981,10 +951,10 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .play-triangle { width: 0; height: 0; - border-left: 34rpx solid #ffffff; - border-top: 23rpx solid transparent; - border-bottom: 23rpx solid transparent; - margin-left: 8rpx; + border-left: 44rpx solid #ffffff; + border-top: 30rpx solid transparent; + border-bottom: 30rpx solid transparent; + margin-left: 10rpx; } @keyframes hint-bounce { @@ -997,7 +967,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-text { - font-size: 26rpx; + font-size: 34rpx; color: #ffffff; font-weight: 700; letter-spacing: 3rpx; @@ -1182,8 +1152,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .presets-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } @@ -1200,38 +1170,26 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .preset { flex: 1; background: #f8fafc; - border: 2rpx solid transparent; - border-radius: 20rpx; - padding: 20rpx 6rpx 18rpx; + border: 3rpx solid transparent; + border-radius: 24rpx; + padding: 32rpx 8rpx 28rpx; display: flex; flex-direction: column; align-items: center; - gap: 4rpx; + gap: 10rpx; transition: all 0.18s; - .preset-icon { - font-size: 32rpx; - line-height: 1; - margin-bottom: 4rpx; - } - .preset-name { - font-size: 26rpx; - font-weight: 700; + font-size: 40rpx; + font-weight: 800; color: $text-1; } .preset-bpm { - font-size: 22rpx; + font-size: 26rpx; color: $text-2; font-weight: 700; font-variant-numeric: tabular-nums; } - .preset-desc { - font-size: 18rpx; - color: $text-3; - text-align: center; - line-height: 1.3; - } &:active { transform: scale(0.97); @@ -1270,17 +1228,34 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); display: flex; align-items: center; justify-content: space-between; - padding: 0 4rpx; + padding: 12rpx 6rpx; + border-radius: 16rpx; + + &:active { + background: #f8fafc; + } } .target-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } -.target-hint { - font-size: 20rpx; +.target-current { + display: flex; + align-items: center; + gap: 12rpx; +} + +.target-current-text { + font-size: 30rpx; + font-weight: 700; + color: $brand-deep; +} + +.target-toggle { + font-size: 22rpx; color: $text-3; } @@ -1291,7 +1266,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .target-chip { flex: 1; - height: 72rpx; + height: 88rpx; display: flex; align-items: center; justify-content: center; @@ -1301,8 +1276,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); transition: all 0.18s; .target-chip-text { - font-size: 24rpx; - font-weight: 600; + font-size: 30rpx; + font-weight: 700; color: $text-2; } diff --git a/TUICallKit-Vue3/training/pages/exercises.ts b/TUICallKit-Vue3/training/pages/exercises.ts deleted file mode 100644 index bb91a4c3..00000000 --- a/TUICallKit-Vue3/training/pages/exercises.ts +++ /dev/null @@ -1,99 +0,0 @@ -export type AnimationType = - | 'curl' - | 'press' - | 'sidefly' - | '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: '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/foot-pedal.vue b/TUICallKit-Vue3/training/pages/foot-pedal.vue index 8671561a..34e8ae14 100644 --- a/TUICallKit-Vue3/training/pages/foot-pedal.vue +++ b/TUICallKit-Vue3/training/pages/foot-pedal.vue @@ -72,26 +72,9 @@ - - - 次数 - {{ hasStats ? totalReps : '—' }} - - - 时长 - {{ hasStats ? formatTime(elapsedSeconds) : '—' }} - - - 糖分 - {{ hasStats ? `≈${totalSugar.toFixed(1)}g` : '—' }} - - - - - - 训练强度 + 选择训练强度 训练中不可切换 @@ -102,26 +85,24 @@ :class="{ active: currentPreset === p.id, disabled: isPlaying }" @click="onPresetTap(p.id)" > - {{ PRESET_ICONS[p.id] }} {{ p.label }} {{ p.bpm }} BPM - {{ p.desc }} - - 🔥 - {{ estimateText }} - + - - 训练目标 - 到时间自动结束 + + 训练时长 + + {{ currentTargetLabel }} + {{ targetExpanded ? '▲' : '▼' }} + - + - - + 训练功效 @@ -264,21 +244,6 @@ const FOOT_PEDAL_PRESETS: readonly FootPedalPreset[] = [ { id: 'heavy', bpm: 85, met: 7.0, label: '重度', desc: '强化 · 挑战' }, ] -// 训练功效介绍(专业文案) -const TRAINING_BENEFIT = { - muscles: ['股四头肌', '腘绳肌', '小腿肌群', '臀大肌'], - effect: '低冲击有氧蹬踏,强化下肢肌力与心肺耐力,促进腿部血液循环、帮助燃脂塑形,特别适合久坐人群激活双腿。', -} - -// 强度预估参考时长(选择"自由"目标时按此估算) -const ESTIMATE_REF_MINUTES = 10 - -const PRESET_ICONS: Record = { - light: '🌱', - medium: '💪', - heavy: '🔥', -} - // 画布指导视频 const TUTORIAL_VIDEO_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260528/20260528173911fd8002487.mp4' const TUTORIAL_POSTER_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/images/20260529/2026052911251788eb49233.jpg' @@ -349,16 +314,21 @@ const timerInterval = ref(null) const preset = computed(() => FOOT_PEDAL_PRESETS.find(p => p.id === currentPreset.value) || FOOT_PEDAL_PRESETS[1]) const currentBpm = computed(() => preset.value.bpm) const currentMet = computed(() => preset.value.met) -const benefitExpanded = ref(false) -// 当前强度预估消耗文案: 按选中目标时长(自由→10分钟参考)估算 -const estimateText = computed(() => { - const mins = targetMinutes.value > 0 ? targetMinutes.value : ESTIMATE_REF_MINUTES - const sugar = (currentMet.value * 60 * (mins / 60)) / 4 - const suffix = targetMinutes.value > 0 ? '' : '(按 10 分钟估算)' - return `${preset.value.label}训练 ${mins} 分钟,约消耗 ${sugar.toFixed(1)}g 糖分${suffix}` +// 训练时长折叠状态 + 当前时长文案 +const targetExpanded = ref(false) +const currentTargetLabel = computed(() => { + const opt = TARGET_OPTIONS.find(t => t.minutes === targetMinutes.value) + return opt ? opt.label : '自由' }) +// 训练功效介绍(专业文案) + 折叠状态 +const benefitExpanded = ref(false) +const TRAINING_BENEFIT = { + muscles: ['股四头肌', '腘绳肌', '小腿肌群', '臀大肌'], + effect: '低冲击有氧蹬踏,强化下肢肌力与心肺耐力,促进腿部血液循环、帮助燃脂塑形,特别适合久坐人群激活双腿。', +} + // 示范视频跟随训练状态 watch(isPlaying, (playing) => { const videoCtx = uni.createVideoContext('footPedalDemoVideo') @@ -826,7 +796,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); margin-left: -28rpx; margin-right: -28rpx; flex-shrink: 0; - height: 520rpx; + height: 740rpx; display: flex; justify-content: center; align-items: center; @@ -859,8 +829,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .ring-container { position: relative; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; display: flex; align-items: center; justify-content: center; @@ -869,8 +839,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .beat-halo { position: absolute; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; border-radius: 50%; background: radial-gradient(circle, rgba(249, 115, 22, 0.45) 0%, @@ -899,8 +869,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .video-ring { position: relative; - width: 460rpx; - height: 460rpx; + width: 640rpx; + height: 640rpx; border-radius: 50%; overflow: hidden; box-shadow: @@ -964,8 +934,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-icon { - width: 116rpx; - height: 116rpx; + width: 140rpx; + height: 140rpx; display: flex; align-items: center; justify-content: center; @@ -981,10 +951,10 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .play-triangle { width: 0; height: 0; - border-left: 34rpx solid #ffffff; - border-top: 23rpx solid transparent; - border-bottom: 23rpx solid transparent; - margin-left: 8rpx; + border-left: 44rpx solid #ffffff; + border-top: 30rpx solid transparent; + border-bottom: 30rpx solid transparent; + margin-left: 10rpx; } @keyframes hint-bounce { @@ -997,7 +967,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-text { - font-size: 26rpx; + font-size: 34rpx; color: #ffffff; font-weight: 700; letter-spacing: 3rpx; @@ -1182,8 +1152,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .presets-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } @@ -1200,38 +1170,26 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .preset { flex: 1; background: #f8fafc; - border: 2rpx solid transparent; - border-radius: 20rpx; - padding: 20rpx 6rpx 18rpx; + border: 3rpx solid transparent; + border-radius: 24rpx; + padding: 32rpx 8rpx 28rpx; display: flex; flex-direction: column; align-items: center; - gap: 4rpx; + gap: 10rpx; transition: all 0.18s; - .preset-icon { - font-size: 32rpx; - line-height: 1; - margin-bottom: 4rpx; - } - .preset-name { - font-size: 26rpx; - font-weight: 700; + font-size: 40rpx; + font-weight: 800; color: $text-1; } .preset-bpm { - font-size: 22rpx; + font-size: 26rpx; color: $text-2; font-weight: 700; font-variant-numeric: tabular-nums; } - .preset-desc { - font-size: 18rpx; - color: $text-3; - text-align: center; - line-height: 1.3; - } &:active { transform: scale(0.97); @@ -1270,17 +1228,34 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); display: flex; align-items: center; justify-content: space-between; - padding: 0 4rpx; + padding: 12rpx 6rpx; + border-radius: 16rpx; + + &:active { + background: #f8fafc; + } } .target-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } -.target-hint { - font-size: 20rpx; +.target-current { + display: flex; + align-items: center; + gap: 12rpx; +} + +.target-current-text { + font-size: 30rpx; + font-weight: 700; + color: $brand-deep; +} + +.target-toggle { + font-size: 22rpx; color: $text-3; } @@ -1291,7 +1266,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .target-chip { flex: 1; - height: 72rpx; + height: 88rpx; display: flex; align-items: center; justify-content: center; @@ -1301,8 +1276,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); transition: all 0.18s; .target-chip-text { - font-size: 24rpx; - font-weight: 600; + font-size: 30rpx; + font-weight: 700; color: $text-2; } diff --git a/TUICallKit-Vue3/training/pages/grip-ring.vue b/TUICallKit-Vue3/training/pages/grip-ring.vue index ddb4ed87..27be64d7 100644 --- a/TUICallKit-Vue3/training/pages/grip-ring.vue +++ b/TUICallKit-Vue3/training/pages/grip-ring.vue @@ -74,28 +74,11 @@ - + - - - 次数 - {{ hasStats ? totalReps : '—' }} - - - 时长 - {{ hasStats ? formatTime(elapsedSeconds) : '—' }} - - - 糖分 - {{ hasStats ? `≈${totalSugar.toFixed(1)}g` : '—' }} - - - - - - 训练强度 + 选择训练强度 训练中不可切换 @@ -106,27 +89,24 @@ :class="{ active: currentPreset === p.id, disabled: isPlaying }" @click="onPresetTap(p.id)" > - {{ PRESET_ICONS[p.id] }} {{ p.label }} {{ p.bpm }} BPM - {{ p.desc }} - - - 🔥 - {{ estimateText }} - + - - 训练目标 - 到时间自动结束 + + 训练时长 + + {{ currentTargetLabel }} + {{ targetExpanded ? '▲' : '▼' }} + - + - + 训练功效 @@ -270,21 +250,6 @@ const GRIP_PRESETS: readonly GripPreset[] = [ { id: 'heavy', bpm: 100, met: 4.5, label: '重度', desc: '强化 · 挑战' }, ] -// 训练功效介绍(专业文案) -const TRAINING_BENEFIT = { - muscles: ['前臂屈肌', '手部小肌群', '握力'], - effect: '增强握力与前臂肌肉耐力,促进手部血液循环,缓解手指与腕部僵硬,适合日常碎片化锻炼及手部功能康复。', -} - -// 强度预估参考时长(选择"自由"目标时按此估算) -const ESTIMATE_REF_MINUTES = 10 - -const PRESET_ICONS: Record = { - light: '🌱', - medium: '💪', - heavy: '🔥', -} - // 画布指导视频(跟随训练状态播放) const TUTORIAL_VIDEO_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260528/20260528173911bf6f93386.mp4' // 视频封面(未开始/暂停时显示的静态首帧) @@ -391,20 +356,25 @@ const timerInterval = ref(null) const preset = computed(() => GRIP_PRESETS.find(p => p.id === currentPreset.value) || GRIP_PRESETS[1]) const currentBpm = computed(() => preset.value.bpm) const currentMet = computed(() => preset.value.met) -const benefitExpanded = ref(false) -// 当前强度预估消耗文案: 按选中目标时长(自由→10分钟参考)估算 -const estimateText = computed(() => { - const mins = targetMinutes.value > 0 ? targetMinutes.value : ESTIMATE_REF_MINUTES - const sugar = (currentMet.value * 60 * (mins / 60)) / 4 - const suffix = targetMinutes.value > 0 ? '' : '(按 10 分钟估算)' - return `${preset.value.label}训练 ${mins} 分钟,约消耗 ${sugar.toFixed(1)}g 糖分${suffix}` +// 训练时长折叠状态 + 当前时长文案 +const targetExpanded = ref(false) +const currentTargetLabel = computed(() => { + const opt = TARGET_OPTIONS.find(t => t.minutes === targetMinutes.value) + return opt ? opt.label : '自由' }) +// 训练功效介绍(专业文案) + 折叠状态 +const benefitExpanded = ref(false) +const TRAINING_BENEFIT = { + muscles: ['前臂屈肌', '手部小肌群', '握力'], + effect: '增强握力与前臂肌肉耐力,促进手部血液循环,缓解手指与腕部僵硬,适合日常碎片化锻炼及手部功能康复。', +} + const metronome = useMetronome({ initialBpm: currentBpm.value, accentEvery: 2, - volume: 0.35, // 节拍音音量调小,避免木鱼声太大 + silent: true, // 移除木鱼节奏音,仅保留节拍驱动(视觉缩放/次数统计) onBeat: onBeat, }) const { isPlaying } = metronome @@ -905,7 +875,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); margin-left: -28rpx; margin-right: -28rpx; flex-shrink: 0; - height: 520rpx; + height: 740rpx; display: flex; justify-content: center; align-items: center; @@ -938,8 +908,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .ring-container { position: relative; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; display: flex; align-items: center; justify-content: center; @@ -948,8 +918,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .beat-halo { position: absolute; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; border-radius: 50%; background: radial-gradient(circle, rgba(20, 184, 166, 0.45) 0%, @@ -978,8 +948,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .video-ring { position: relative; - width: 460rpx; - height: 460rpx; + width: 640rpx; + height: 640rpx; border-radius: 50%; overflow: hidden; box-shadow: @@ -1043,8 +1013,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-icon { - width: 116rpx; - height: 116rpx; + width: 140rpx; + height: 140rpx; display: flex; align-items: center; justify-content: center; @@ -1060,10 +1030,10 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .play-triangle { width: 0; height: 0; - border-left: 34rpx solid #ffffff; - border-top: 23rpx solid transparent; - border-bottom: 23rpx solid transparent; - margin-left: 8rpx; + border-left: 44rpx solid #ffffff; + border-top: 30rpx solid transparent; + border-bottom: 30rpx solid transparent; + margin-left: 10rpx; } @keyframes hint-bounce { @@ -1076,7 +1046,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-text { - font-size: 26rpx; + font-size: 34rpx; color: #ffffff; font-weight: 700; letter-spacing: 3rpx; @@ -1261,8 +1231,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .presets-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } @@ -1279,38 +1249,26 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .preset { flex: 1; background: #f8fafc; - border: 2rpx solid transparent; - border-radius: 20rpx; - padding: 20rpx 6rpx 18rpx; + border: 3rpx solid transparent; + border-radius: 24rpx; + padding: 32rpx 8rpx 28rpx; display: flex; flex-direction: column; align-items: center; - gap: 4rpx; + gap: 10rpx; transition: all 0.18s; - .preset-icon { - font-size: 32rpx; - line-height: 1; - margin-bottom: 4rpx; - } - .preset-name { - font-size: 26rpx; - font-weight: 700; + font-size: 40rpx; + font-weight: 800; color: $text-1; } .preset-bpm { - font-size: 22rpx; + font-size: 26rpx; color: $text-2; font-weight: 700; font-variant-numeric: tabular-nums; } - .preset-desc { - font-size: 18rpx; - color: $text-3; - text-align: center; - line-height: 1.3; - } &:active { transform: scale(0.97); @@ -1349,17 +1307,34 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); display: flex; align-items: center; justify-content: space-between; - padding: 0 4rpx; + padding: 12rpx 6rpx; + border-radius: 16rpx; + + &:active { + background: #f8fafc; + } } .target-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } -.target-hint { - font-size: 20rpx; +.target-current { + display: flex; + align-items: center; + gap: 12rpx; +} + +.target-current-text { + font-size: 30rpx; + font-weight: 700; + color: $brand-deep; +} + +.target-toggle { + font-size: 22rpx; color: $text-3; } @@ -1370,7 +1345,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .target-chip { flex: 1; - height: 72rpx; + height: 88rpx; display: flex; align-items: center; justify-content: center; @@ -1380,8 +1355,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); transition: all 0.18s; .target-chip-text { - font-size: 24rpx; - font-weight: 600; + font-size: 30rpx; + font-weight: 700; color: $text-2; } diff --git a/TUICallKit-Vue3/training/pages/index.vue b/TUICallKit-Vue3/training/pages/index.vue deleted file mode 100644 index 17dbbba8..00000000 --- a/TUICallKit-Vue3/training/pages/index.vue +++ /dev/null @@ -1,739 +0,0 @@ - - - - - diff --git a/TUICallKit-Vue3/training/pages/pilates-ring.vue b/TUICallKit-Vue3/training/pages/pilates-ring.vue index 66f0fd37..d5c1ebb6 100644 --- a/TUICallKit-Vue3/training/pages/pilates-ring.vue +++ b/TUICallKit-Vue3/training/pages/pilates-ring.vue @@ -72,26 +72,9 @@ - - - 次数 - {{ hasStats ? totalReps : '—' }} - - - 时长 - {{ hasStats ? formatTime(elapsedSeconds) : '—' }} - - - 糖分 - {{ hasStats ? `≈${totalSugar.toFixed(1)}g` : '—' }} - - - - - - 训练强度 + 选择训练强度 训练中不可切换 @@ -102,26 +85,24 @@ :class="{ active: currentPreset === p.id, disabled: isPlaying }" @click="onPresetTap(p.id)" > - {{ PRESET_ICONS[p.id] }} {{ p.label }} {{ p.bpm }} BPM - {{ p.desc }} - - 🔥 - {{ estimateText }} - + - - 训练目标 - 到时间自动结束 + + 训练时长 + + {{ currentTargetLabel }} + {{ targetExpanded ? '▲' : '▼' }} + - + - - + 训练功效 @@ -264,21 +244,6 @@ const PILATES_PRESETS: readonly PilatesPreset[] = [ { id: 'heavy', bpm: 80, met: 5.0, label: '重度', desc: '强化 · 挑战' }, ] -// 训练功效介绍(专业文案) -const TRAINING_BENEFIT = { - muscles: ['核心肌群', '盆底肌', '大腿内侧', '臀部'], - effect: '通过持续抗阻收紧核心与盆底肌,强化深层稳定肌群,改善体态、收紧腹部与腿部线条,提升身体协调性与柔韧度。', -} - -// 强度预估参考时长(选择"自由"目标时按此估算) -const ESTIMATE_REF_MINUTES = 10 - -const PRESET_ICONS: Record = { - light: '🌱', - medium: '💪', - heavy: '🔥', -} - // 画布指导视频 const TUTORIAL_VIDEO_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260528/202605281739110fa2d3784.mp4' const TUTORIAL_POSTER_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/images/20260529/2026052911251785db25125.jpg' @@ -349,16 +314,21 @@ const timerInterval = ref(null) const preset = computed(() => PILATES_PRESETS.find(p => p.id === currentPreset.value) || PILATES_PRESETS[1]) const currentBpm = computed(() => preset.value.bpm) const currentMet = computed(() => preset.value.met) -const benefitExpanded = ref(false) -// 当前强度预估消耗文案: 按选中目标时长(自由→10分钟参考)估算 -const estimateText = computed(() => { - const mins = targetMinutes.value > 0 ? targetMinutes.value : ESTIMATE_REF_MINUTES - const sugar = (currentMet.value * 60 * (mins / 60)) / 4 - const suffix = targetMinutes.value > 0 ? '' : '(按 10 分钟估算)' - return `${preset.value.label}训练 ${mins} 分钟,约消耗 ${sugar.toFixed(1)}g 糖分${suffix}` +// 训练时长折叠状态 + 当前时长文案 +const targetExpanded = ref(false) +const currentTargetLabel = computed(() => { + const opt = TARGET_OPTIONS.find(t => t.minutes === targetMinutes.value) + return opt ? opt.label : '自由' }) +// 训练功效介绍(专业文案) + 折叠状态 +const benefitExpanded = ref(false) +const TRAINING_BENEFIT = { + muscles: ['核心肌群', '盆底肌', '大腿内侧', '臀部'], + effect: '通过持续抗阻收紧核心与盆底肌,强化深层稳定肌群,改善体态、收紧腹部与腿部线条,提升身体协调性与柔韧度。', +} + // 示范视频跟随训练状态 watch(isPlaying, (playing) => { const videoCtx = uni.createVideoContext('pilatesDemoVideo') @@ -826,7 +796,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); margin-left: -28rpx; margin-right: -28rpx; flex-shrink: 0; - height: 520rpx; + height: 740rpx; display: flex; justify-content: center; align-items: center; @@ -859,8 +829,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .ring-container { position: relative; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; display: flex; align-items: center; justify-content: center; @@ -869,8 +839,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .beat-halo { position: absolute; - width: 560rpx; - height: 560rpx; + width: 720rpx; + height: 720rpx; border-radius: 50%; background: radial-gradient(circle, rgba(139, 92, 246, 0.45) 0%, @@ -899,8 +869,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .video-ring { position: relative; - width: 460rpx; - height: 460rpx; + width: 640rpx; + height: 640rpx; border-radius: 50%; overflow: hidden; box-shadow: @@ -964,8 +934,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-icon { - width: 116rpx; - height: 116rpx; + width: 140rpx; + height: 140rpx; display: flex; align-items: center; justify-content: center; @@ -981,10 +951,10 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .play-triangle { width: 0; height: 0; - border-left: 34rpx solid #ffffff; - border-top: 23rpx solid transparent; - border-bottom: 23rpx solid transparent; - margin-left: 8rpx; + border-left: 44rpx solid #ffffff; + border-top: 30rpx solid transparent; + border-bottom: 30rpx solid transparent; + margin-left: 10rpx; } @keyframes hint-bounce { @@ -997,7 +967,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .hint-text { - font-size: 26rpx; + font-size: 34rpx; color: #ffffff; font-weight: 700; letter-spacing: 3rpx; @@ -1182,8 +1152,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); } .presets-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } @@ -1200,38 +1170,26 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .preset { flex: 1; background: #f8fafc; - border: 2rpx solid transparent; - border-radius: 20rpx; - padding: 20rpx 6rpx 18rpx; + border: 3rpx solid transparent; + border-radius: 24rpx; + padding: 32rpx 8rpx 28rpx; display: flex; flex-direction: column; align-items: center; - gap: 4rpx; + gap: 10rpx; transition: all 0.18s; - .preset-icon { - font-size: 32rpx; - line-height: 1; - margin-bottom: 4rpx; - } - .preset-name { - font-size: 26rpx; - font-weight: 700; + font-size: 40rpx; + font-weight: 800; color: $text-1; } .preset-bpm { - font-size: 22rpx; + font-size: 26rpx; color: $text-2; font-weight: 700; font-variant-numeric: tabular-nums; } - .preset-desc { - font-size: 18rpx; - color: $text-3; - text-align: center; - line-height: 1.3; - } &:active { transform: scale(0.97); @@ -1270,17 +1228,34 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); display: flex; align-items: center; justify-content: space-between; - padding: 0 4rpx; + padding: 12rpx 6rpx; + border-radius: 16rpx; + + &:active { + background: #f8fafc; + } } .target-title { - font-size: 26rpx; - font-weight: 700; + font-size: 32rpx; + font-weight: 800; color: $text-1; } -.target-hint { - font-size: 20rpx; +.target-current { + display: flex; + align-items: center; + gap: 12rpx; +} + +.target-current-text { + font-size: 30rpx; + font-weight: 700; + color: $brand-deep; +} + +.target-toggle { + font-size: 22rpx; color: $text-3; } @@ -1291,7 +1266,7 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); .target-chip { flex: 1; - height: 72rpx; + height: 88rpx; display: flex; align-items: center; justify-content: center; @@ -1301,8 +1276,8 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04); transition: all 0.18s; .target-chip-text { - font-size: 24rpx; - font-weight: 600; + font-size: 30rpx; + font-weight: 700; color: $text-2; }