主包超出 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>
260 lines
5.9 KiB
Vue
260 lines
5.9 KiB
Vue
<template>
|
||
<view class="exercise-anim" :class="`anim-${animationType}`" :style="cssVars">
|
||
<view class="stage">
|
||
<!-- 哑铃弯举:摆臂 -->
|
||
<view v-if="animationType === 'curl'" class="curl-arm">
|
||
<view class="curl-forearm">
|
||
<view class="curl-dumbbell">{{ icon }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 哑铃推举:上下移动 -->
|
||
<view v-else-if="animationType === 'press'" class="press-wrap">
|
||
<view class="press-dumbbell left">{{ icon }}</view>
|
||
<view class="press-dumbbell right">{{ icon }}</view>
|
||
</view>
|
||
|
||
<!-- 哑铃侧平举:双臂张开 -->
|
||
<view v-else-if="animationType === 'sidefly'" class="sidefly-wrap">
|
||
<view class="sidefly-arm sidefly-left">
|
||
<view class="sidefly-dumbbell">{{ icon }}</view>
|
||
</view>
|
||
<view class="sidefly-arm sidefly-right">
|
||
<view class="sidefly-dumbbell">{{ icon }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 握力环:缩放 -->
|
||
<view v-else-if="animationType === 'grip'" class="grip-ring">
|
||
<view class="grip-ring-inner">{{ icon }}</view>
|
||
</view>
|
||
|
||
<!-- 卷腹:身体折叠 -->
|
||
<view v-else-if="animationType === 'crunch'" class="crunch-wrap">
|
||
<view class="crunch-body">{{ icon }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import type { AnimationType } from '../exercises'
|
||
|
||
interface Props {
|
||
animationType: AnimationType
|
||
bpm: number
|
||
beatsPerRep?: number
|
||
icon?: string
|
||
isPlaying?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
beatsPerRep: 2,
|
||
icon: '🏋',
|
||
isPlaying: false,
|
||
})
|
||
|
||
const cssVars = computed(() => {
|
||
const repDurationMs = (60000 / props.bpm) * props.beatsPerRep
|
||
return {
|
||
'--rep-duration': `${repDurationMs}ms`,
|
||
'--anim-state': props.isPlaying ? 'running' : 'paused',
|
||
} as Record<string, string>
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.exercise-anim {
|
||
width: 100%;
|
||
height: 480rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #f0fdf4, #ecfeff);
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stage {
|
||
width: 320rpx;
|
||
height: 320rpx;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* ===== 弯举 ===== */
|
||
.curl-arm {
|
||
width: 60rpx;
|
||
height: 240rpx;
|
||
background: #fbbf24;
|
||
border-radius: 30rpx;
|
||
position: relative;
|
||
|
||
.curl-forearm {
|
||
width: 60rpx;
|
||
height: 200rpx;
|
||
background: #f59e0b;
|
||
border-radius: 30rpx;
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
transform-origin: bottom center;
|
||
animation: curl-anim var(--rep-duration) ease-in-out infinite;
|
||
animation-play-state: var(--anim-state);
|
||
}
|
||
|
||
.curl-dumbbell {
|
||
position: absolute;
|
||
top: -20rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 56rpx;
|
||
}
|
||
}
|
||
@keyframes curl-anim {
|
||
0%,
|
||
100% {
|
||
transform: rotate(0deg);
|
||
}
|
||
50% {
|
||
transform: rotate(-115deg);
|
||
}
|
||
}
|
||
|
||
/* ===== 推举 ===== */
|
||
.press-wrap {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
padding: 0 20rpx;
|
||
}
|
||
.press-dumbbell {
|
||
font-size: 88rpx;
|
||
animation: press-anim var(--rep-duration) ease-in-out infinite;
|
||
animation-play-state: var(--anim-state);
|
||
}
|
||
@keyframes press-anim {
|
||
0%,
|
||
100% {
|
||
transform: translateY(80rpx);
|
||
}
|
||
50% {
|
||
transform: translateY(-80rpx);
|
||
}
|
||
}
|
||
|
||
/* ===== 侧平举 ===== */
|
||
.sidefly-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.sidefly-arm {
|
||
position: absolute;
|
||
width: 140rpx;
|
||
height: 24rpx;
|
||
background: #f59e0b;
|
||
border-radius: 12rpx;
|
||
top: 50%;
|
||
transform-origin: center right;
|
||
animation: sidefly-left var(--rep-duration) ease-in-out infinite;
|
||
animation-play-state: var(--anim-state);
|
||
|
||
&.sidefly-left {
|
||
right: 50%;
|
||
}
|
||
&.sidefly-right {
|
||
left: 50%;
|
||
transform-origin: center left;
|
||
animation-name: sidefly-right;
|
||
}
|
||
}
|
||
.sidefly-dumbbell {
|
||
position: absolute;
|
||
top: -36rpx;
|
||
font-size: 56rpx;
|
||
}
|
||
.sidefly-left .sidefly-dumbbell {
|
||
left: -20rpx;
|
||
}
|
||
.sidefly-right .sidefly-dumbbell {
|
||
right: -20rpx;
|
||
}
|
||
@keyframes sidefly-left {
|
||
0%,
|
||
100% {
|
||
transform: rotate(80deg);
|
||
}
|
||
50% {
|
||
transform: rotate(0deg);
|
||
}
|
||
}
|
||
@keyframes sidefly-right {
|
||
0%,
|
||
100% {
|
||
transform: rotate(-80deg);
|
||
}
|
||
50% {
|
||
transform: rotate(0deg);
|
||
}
|
||
}
|
||
|
||
/* ===== 握力环 ===== */
|
||
.grip-ring {
|
||
width: 240rpx;
|
||
height: 240rpx;
|
||
animation: grip-anim var(--rep-duration) ease-in-out infinite;
|
||
animation-play-state: var(--anim-state);
|
||
border: 16rpx solid #22c55e;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(34, 197, 94, 0.1);
|
||
}
|
||
.grip-ring-inner {
|
||
font-size: 80rpx;
|
||
}
|
||
@keyframes grip-anim {
|
||
0%,
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
transform: scale(0.65);
|
||
}
|
||
}
|
||
|
||
/* ===== 卷腹 ===== */
|
||
.crunch-wrap {
|
||
width: 280rpx;
|
||
height: 280rpx;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
}
|
||
.crunch-body {
|
||
font-size: 120rpx;
|
||
animation: crunch-anim var(--rep-duration) ease-in-out infinite;
|
||
animation-play-state: var(--anim-state);
|
||
transform-origin: bottom center;
|
||
}
|
||
@keyframes crunch-anim {
|
||
0%,
|
||
100% {
|
||
transform: scaleY(1) translateY(0);
|
||
}
|
||
50% {
|
||
transform: scaleY(0.6) translateY(-10rpx);
|
||
}
|
||
}
|
||
</style>
|