This commit is contained in:
2026-05-29 12:05:22 +08:00
parent 9b5cd5b2b9
commit 7f4fad7b62
7 changed files with 6073 additions and 50 deletions
@@ -14,6 +14,46 @@
<view class="menu-arrow"></view>
</view> -->
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
<view class="menu-icon menu-icon--dumbbell">
<view class="dumbbell-shape">
<view class="dumbbell-plate dumbbell-plate-left" />
<view class="dumbbell-bar" />
<view class="dumbbell-plate dumbbell-plate-right" />
</view>
</view>
<view class="menu-info">
<text class="menu-title">哑铃</text>
<text class="menu-sub">力量塑形</text>
</view>
<view class="menu-arrow"></view>
</view>
<view class="menu-item" @click="goto('/training/pages/foot-pedal')">
<view class="menu-icon menu-icon--pedal">
<view class="foot-pedal-shape">
<view class="pedal-bar pedal-bar-left" />
<view class="pedal-bar pedal-bar-right" />
</view>
</view>
<view class="menu-info">
<text class="menu-title">脚蹬器</text>
<text class="menu-sub">下肢有氧</text>
</view>
<view class="menu-arrow"></view>
</view>
<view class="menu-item" @click="goto('/training/pages/pilates-ring')">
<view class="menu-icon menu-icon--pilates">
<view class="pilates-ring-shape" />
</view>
<view class="menu-info">
<text class="menu-title">瑜伽环</text>
<text class="menu-sub">核心塑形</text>
</view>
<view class="menu-arrow"></view>
</view>
<view class="menu-item" @click="goto('/training/pages/grip-ring')">
<view class="menu-icon menu-icon--grip">
<view class="grip-ring-shape" />
@@ -262,6 +302,18 @@ $brand-light: #34d399;
&--train {
background: linear-gradient(140deg, $brand-light, $brand-deep);
}
&--dumbbell {
background: linear-gradient(140deg, #93c5fd, #3b82f6);
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.36);
}
&--pedal {
background: linear-gradient(140deg, #fdba74, #f97316);
box-shadow: 0 2rpx 8rpx rgba(249, 115, 22, 0.36);
}
&--pilates {
background: linear-gradient(140deg, #c4b5fd, #8b5cf6);
box-shadow: 0 2rpx 8rpx rgba(139, 92, 246, 0.36);
}
&--grip {
background: linear-gradient(140deg, #5eead4, #14b8a6);
box-shadow: 0 2rpx 8rpx rgba(20, 184, 166, 0.36);
@@ -278,6 +330,83 @@ $brand-light: #34d399;
line-height: 1;
}
/* 哑铃小 icon */
.dumbbell-shape {
width: 40rpx;
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dumbbell-plate {
width: 10rpx;
height: 18rpx;
background: #fff;
border-radius: 3rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
.dumbbell-bar {
width: 16rpx;
height: 5rpx;
background: #fff;
border-radius: 2rpx;
margin: 0 2rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
/* 脚蹬器小 icon: 左右踏板 */
.foot-pedal-shape {
width: 40rpx;
height: 28rpx;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 6rpx;
}
.pedal-bar {
width: 14rpx;
background: #fff;
border-radius: 4rpx 4rpx 2rpx 2rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
.pedal-bar-left {
height: 18rpx;
transform: rotate(-12deg);
}
.pedal-bar-right {
height: 22rpx;
transform: rotate(12deg);
}
/* 瑜伽环小 icon: 双弧阻力环 */
.pilates-ring-shape {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 5rpx solid #fff;
position: relative;
box-shadow:
0 1rpx 2rpx rgba(0, 0, 0, 0.18),
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.6);
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18rpx;
height: 18rpx;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 3rpx solid rgba(255, 255, 255, 0.85);
}
}
/* 握力环小 icon: 白色描边的环(甜甜圈) */
.grip-ring-shape {
width: 32rpx;
+29 -2
View File
@@ -124,8 +124,35 @@
"backgroundColor": "#f8fafc"
}
},
{
"path": "pages/metronome",
{
"path": "pages/pilates-ring",
"style": {
"navigationBarTitleText": "瑜伽环训练",
"navigationBarBackgroundColor": "#f8fafc",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8fafc"
}
},
{
"path": "pages/foot-pedal",
"style": {
"navigationBarTitleText": "脚蹬器训练",
"navigationBarBackgroundColor": "#f8fafc",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8fafc"
}
},
{
"path": "pages/dumbbell",
"style": {
"navigationBarTitleText": "哑铃训练",
"navigationBarBackgroundColor": "#f8fafc",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8fafc"
}
},
{
"path": "pages/metronome",
"style": {
"navigationBarTitleText": "耗糖节拍器",
"navigationBarBackgroundColor": "#f8fafc",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+223 -41
View File
@@ -112,6 +112,11 @@
<text class="preset-desc">{{ p.desc }}</text>
</view>
</view>
<!-- 当前强度预估消耗 -->
<view class="intensity-estimate">
<text class="estimate-icon">🔥</text>
<text class="estimate-text">{{ estimateText }}</text>
</view>
</view>
<view class="deck-divider" />
@@ -134,10 +139,38 @@
</view>
</view>
<!-- 仅暂停时显示一行提示 + 结束训练文字链 -->
<view v-if="isPaused" class="action-bar">
<text class="paused-hint-text">已暂停 · 点击握力环继续</text>
<text class="end-link" @click="onEndTraining">结束训练 </text>
<view class="deck-divider" />
<!-- 训练功效介绍(可折叠) -->
<view class="benefit-block">
<view class="benefit-header" @click="benefitExpanded = !benefitExpanded">
<text class="benefit-title">训练功效</text>
<text class="benefit-toggle">{{ benefitExpanded ? '收起 ▲' : '展开 ▼' }}</text>
</view>
<view v-if="benefitExpanded" class="benefit-content">
<view class="benefit-row">
<text class="benefit-label">主要锻炼</text>
<view class="muscle-tags">
<text v-for="m in TRAINING_BENEFIT.muscles" :key="m" class="muscle-tag">{{ m }}</text>
</view>
</view>
<view class="benefit-row">
<text class="benefit-label">训练效果</text>
<text class="benefit-effect">{{ TRAINING_BENEFIT.effect }}</text>
</view>
</view>
</view>
<!-- 暂停时显示醒目大按钮(老年友好) -->
<view v-if="isPaused" class="pause-actions">
<button class="big-btn big-btn-resume" @click="onTogglePlay">
<text class="big-btn-icon"></text>
<text class="big-btn-text">继续训练</text>
</button>
<button class="big-btn big-btn-end" @click="onEndTraining">
<text class="big-btn-icon"></text>
<text class="big-btn-text">结束训练</text>
</button>
</view>
</view>
</view>
@@ -218,7 +251,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { onShow, onHide, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { onShow, onHide, onUnload, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { useMetronome } from '../hooks/useMetronome'
import CrushCanvas from './components/crush-canvas.vue'
@@ -226,16 +259,26 @@ type PresetId = 'light' | 'medium' | 'heavy'
interface GripPreset {
id: PresetId
bpm: number
met: number // 运动强度系数(用于估算糖分消耗)
label: string
desc: string
}
const GRIP_PRESETS: readonly GripPreset[] = [
{ id: 'light', bpm: 60, label: '轻度', desc: '热身 · 恢复' },
{ id: 'medium', bpm: 80, label: '中度', desc: '日常 · 锻炼' },
{ id: 'heavy', bpm: 100, label: '重度', desc: '强化 · 挑战' },
{ id: 'light', bpm: 60, met: 2.5, label: '轻度', desc: '热身 · 恢复' },
{ id: 'medium', bpm: 80, met: 3.5, label: '中度', desc: '日常 · 锻炼' },
{ id: 'heavy', bpm: 100, met: 4.5, label: '重度', desc: '强化 · 挑战' },
]
// 训练功效介绍(专业文案)
const TRAINING_BENEFIT = {
muscles: ['前臂屈肌', '手部小肌群', '握力'],
effect: '增强握力与前臂肌肉耐力,促进手部血液循环,缓解手指与腕部僵硬,适合日常碎片化锻炼及手部功能康复。',
}
// 强度预估参考时长(选择"自由"目标时按此估算)
const ESTIMATE_REF_MINUTES = 10
const PRESET_ICONS: Record<PresetId, string> = {
light: '🌱',
medium: '💪',
@@ -347,6 +390,16 @@ const timerInterval = ref<number | null>(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 metronome = useMetronome({
initialBpm: currentBpm.value,
@@ -440,14 +493,12 @@ function onBeat() {
}
}
// 计算消耗糖分
// 握力环训练 MET ≈ 3.5, 消耗卡路里 = MET × 体重(kg) × 时长(小时)
// 1g 糖分 ≈ 4 kcal, 所以 糖分(g) = 卡路里 / 4
// 计算消耗糖分(随训练强度变化)
// 消耗卡路里 = MET × 体重(kg) × 时长(小时); 1g 糖分 ≈ 4 kcal
function calculateSugar(seconds: number): number {
const hours = seconds / 3600
const weight = 60 // 默认体重60kg
const met = 3.5
const calories = met * weight * hours
const calories = currentMet.value * weight * hours
return calories / 4 // 转换为糖分(克)
}
@@ -676,21 +727,25 @@ onShow(() => {
onShareAppMessage(() => ({
title: shareTitle.value,
path: '/training/pages/grip-ring',
imageUrl: TUTORIAL_POSTER_URL,
}))
// 分享到朋友圈
onShareTimeline(() => ({
title: shareTitle.value,
imageUrl: TUTORIAL_POSTER_URL,
}))
onHide(() => {
// 切出页面 → 自动暂停训练 + 强制停掉所有音频
// 不依赖 isPlaying 判断: 暂停/结算态也可能有未释放的音频实例
// requiredBackgroundModes:['audio'] 下,InnerAudioContext.pause 不一定立即生效,这里用 stop+destroy 兜底
// 切出/卸载页面时强制释放所有媒体(含 BGM + 节拍 + 示范视频)
function cleanupAllMedia() {
try { metronome.stop() } catch (_) {}
stopTimer()
uni.setKeepScreenOn({ keepScreenOn: false })
try {
uni.createVideoContext('gripDemoVideo')?.pause()
} catch (_) {}
if (bgmAudioCtx.value) {
try { bgmAudioCtx.value.stop() } catch (_) {}
try { bgmAudioCtx.value.destroy() } catch (_) {}
@@ -701,7 +756,10 @@ onHide(() => {
try { crushAudioCtx.value.destroy() } catch (_) {}
crushAudioCtx.value = null
}
})
}
onHide(cleanupAllMedia)
onUnload(cleanupAllMedia)
</script>
<style lang="scss" scoped>
@@ -1378,35 +1436,159 @@ $shadow-sm: 0 4rpx 12rpx rgba(15, 23, 42, 0.04);
}
/* ============================================================
* 控制面板底部操作(仅暂停时显示)
* 强度预估消耗
* ============================================================ */
.action-bar {
margin-top: 18rpx;
padding: 14rpx 4rpx 2rpx;
.intensity-estimate {
margin-top: 14rpx;
padding: 16rpx 20rpx;
display: flex;
align-items: center;
gap: 10rpx;
background: $brand-soft;
border-radius: 14rpx;
}
.estimate-icon {
font-size: 30rpx;
line-height: 1;
flex-shrink: 0;
}
.estimate-text {
font-size: 24rpx;
font-weight: 600;
color: $brand-deep;
letter-spacing: 0.5rpx;
}
/* ============================================================
* 训练功效(可折叠)
* ============================================================ */
.benefit-block {
display: flex;
flex-direction: column;
}
.benefit-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
border-top: 2rpx dashed rgba(15, 23, 42, 0.06);
}
.paused-hint-text {
flex: 1;
font-size: 22rpx;
color: $text-3;
letter-spacing: 1rpx;
}
.end-link {
flex-shrink: 0;
padding: 8rpx 4rpx;
font-size: 24rpx;
color: $text-2;
font-weight: 500;
letter-spacing: 1rpx;
padding: 4rpx;
&:active {
color: $text-1;
opacity: 0.7;
}
}
.benefit-title {
font-size: 26rpx;
font-weight: 700;
color: $text-1;
}
.benefit-toggle {
font-size: 22rpx;
color: $brand-deep;
font-weight: 600;
}
.benefit-content {
margin-top: 14rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 20rpx;
background: #f8fafc;
border-radius: 16rpx;
}
.benefit-row {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.benefit-label {
font-size: 22rpx;
font-weight: 700;
color: $text-2;
}
.muscle-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.muscle-tag {
padding: 6rpx 18rpx;
font-size: 22rpx;
font-weight: 600;
color: $brand-deep;
background: $brand-soft;
border-radius: 999rpx;
}
.benefit-effect {
font-size: 24rpx;
line-height: 1.7;
color: $text-2;
}
/* ============================================================
* 暂停态大按钮(老年友好,醒目易点)
* ============================================================ */
.pause-actions {
margin-top: 20rpx;
display: flex;
gap: 16rpx;
}
.big-btn {
flex: 1;
height: 104rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
border-radius: 20rpx;
border: none;
padding: 0;
line-height: 1;
&::after {
border: none;
}
&:active {
transform: scale(0.97);
}
}
.big-btn-icon {
font-size: 30rpx;
line-height: 1;
}
.big-btn-text {
font-size: 34rpx;
font-weight: 800;
letter-spacing: 2rpx;
}
.big-btn-resume {
background: $brand;
color: #fff;
box-shadow: 0 8rpx 20rpx rgba(20, 184, 166, 0.32);
}
.big-btn-end {
background: #fef2f2;
color: #dc2626;
border: 2rpx solid rgba(220, 38, 38, 0.3);
.big-btn-icon {
font-size: 26rpx;
}
}
+7 -7
View File
@@ -86,7 +86,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onShow, onHide } from '@dcloudio/uni-app'
import { onShow, onHide, onUnload } from '@dcloudio/uni-app'
import { useMetronome } from '../hooks/useMetronome'
import WalkerCanvas from './components/walker-canvas.vue'
@@ -206,15 +206,15 @@ onShow(() => {
fg.preload()
})
onHide(() => {
function cleanupMetronome() {
fg.stop()
uni.setKeepScreenOn({ keepScreenOn: false })
})
}
onUnmounted(() => {
fg.stop()
uni.setKeepScreenOn({ keepScreenOn: false })
})
onHide(cleanupMetronome)
onUnload(cleanupMetronome)
onUnmounted(cleanupMetronome)
</script>
<style lang="scss" scoped>
File diff suppressed because it is too large Load Diff