feat(training): 握力环页面简化重构
## 主要改动 ### 1. 连续训练模式 - 移除组数/休息逻辑,只统计总次数和时长 - 训练持续进行直到用户手动暂停 - 实时显示次数、时长、消耗糖分 ### 2. 递进式奖励系统 - 鸡蛋(8-12次) → 核桃(15-20次) → 易拉罐(25-30次) → 气球(40-50次) - 每个奖励在区间内随机触发 - 奖励按顺序递进,不会跳级 - 捏碎粒子特效 + 奖励弹窗 ### 3. 糖分计算与食物对比 - 公式: (MET × 体重 × 时长) / 4 - MET = 3.5, 体重 = 60kg - 食物对比: 苹果/香蕉/米饭/巧克力/可乐 ### 4. 视觉设计优化 - 真实握力环造型(完整圆环 + 中空 + 握点) - 立体渐变和纹理效果 - 挤压动画(训练时整体缩放) - 简洁交互(整个环可点击,无中心按钮) ### 5. 问题修复 - 修复进入页面时音效预热响声(静音预热) - 暂时禁用音效URL(避免404错误) - 修复捏碎特效层级问题 ## 技术实现 - 新增: grip-ring.vue (主页面) - 新增: grip-ring-canvas.vue (画布组件) - 修改: useMetronome.ts (静音预热) - 修改: exercises.ts (移除握力环) - 修改: index.vue (添加头部链接) - 修改: dev-training-entry/index.vue (添加菜单入口) - 修改: pages.json (添加路由) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<view class="grip-ring-canvas">
|
||||
<!-- 捏碎特效画布 (最底层) -->
|
||||
<CrushCanvas ref="crushCanvasRef" />
|
||||
|
||||
<!-- 握力环容器 -->
|
||||
<view class="ring-container" :class="{ playing: isPlaying }" @click="onButtonClick">
|
||||
<!-- 握力环主体 -->
|
||||
<view class="grip-ring">
|
||||
<!-- 外圈 -->
|
||||
<view class="ring-outer-circle" />
|
||||
|
||||
<!-- 内圈 -->
|
||||
<view class="ring-inner-circle" />
|
||||
|
||||
<!-- 纹理层 -->
|
||||
<view class="ring-texture" />
|
||||
|
||||
<!-- 顶部握点 -->
|
||||
<view class="grip-dot grip-dot-top" />
|
||||
|
||||
<!-- 底部握点 -->
|
||||
<view class="grip-dot grip-dot-bottom" />
|
||||
</view>
|
||||
|
||||
<!-- 中心提示 (仅未开始时显示) -->
|
||||
<view v-if="!isPlaying" class="center-hint">
|
||||
<view class="hint-icon">▶</view>
|
||||
<text class="hint-text">点击开始</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import CrushCanvas from './crush-canvas.vue'
|
||||
|
||||
interface Props {
|
||||
bpm: number
|
||||
isPlaying: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
togglePlay: []
|
||||
}>()
|
||||
|
||||
const crushCanvasRef = ref()
|
||||
|
||||
const onButtonClick = () => {
|
||||
emit('togglePlay')
|
||||
}
|
||||
|
||||
// 暴露捏碎特效触发方法
|
||||
const triggerCrushEffect = (itemType: 'egg' | 'walnut' | 'can' | 'balloon') => {
|
||||
crushCanvasRef.value?.triggerCrush(itemType)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
triggerCrushEffect
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.grip-ring-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f0fdf4, #ecfeff);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 握力环容器
|
||||
* ============================================================ */
|
||||
.ring-container {
|
||||
position: relative;
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 握力环主体 - 模拟真实握力环
|
||||
* ============================================================ */
|
||||
.grip-ring {
|
||||
position: relative;
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.ring-container:active .grip-ring {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 外圈 - 主体圆环 */
|
||||
.ring-outer-circle {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #34d399 0%, #10b981 50%, #059669 100%);
|
||||
box-shadow:
|
||||
0 12rpx 40rpx rgba(16, 185, 129, 0.4),
|
||||
inset 0 -8rpx 16rpx rgba(0, 0, 0, 0.2),
|
||||
inset 0 8rpx 16rpx rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 内圈 - 中空部分 */
|
||||
.ring-inner-circle {
|
||||
position: absolute;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f0fdf4, #ecfeff);
|
||||
box-shadow:
|
||||
inset 0 4rpx 12rpx rgba(16, 185, 129, 0.3),
|
||||
inset 0 -4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 纹理层 - 模拟握力环表面纹理 */
|
||||
.ring-texture {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-image:
|
||||
repeating-conic-gradient(
|
||||
from 0deg,
|
||||
transparent 0deg,
|
||||
transparent 2deg,
|
||||
rgba(255, 255, 255, 0.08) 2deg,
|
||||
rgba(255, 255, 255, 0.08) 4deg
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 握点 - 顶部和底部的凸起 */
|
||||
.grip-dot {
|
||||
position: absolute;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #10b981, #047857);
|
||||
box-shadow:
|
||||
0 6rpx 20rpx rgba(16, 185, 129, 0.5),
|
||||
inset 0 2rpx 6rpx rgba(255, 255, 255, 0.4),
|
||||
inset 0 -2rpx 6rpx rgba(0, 0, 0, 0.3);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.grip-dot-top {
|
||||
top: -12rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.grip-dot-bottom {
|
||||
bottom: -12rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* 播放时的挤压动画 */
|
||||
.ring-container.playing .grip-ring {
|
||||
animation: squeeze-ring 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ring-container.playing .grip-dot {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes squeeze-ring {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部握点特殊处理 */
|
||||
.grip-dot-bottom {
|
||||
animation: dot-pulse-bottom 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse-bottom {
|
||||
0%, 100% {
|
||||
transform: translateX(-50%) translateY(0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) translateY(0) scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 中心提示 (仅未开始时显示)
|
||||
* ============================================================ */
|
||||
.center-hint {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
pointer-events: none;
|
||||
animation: hint-fade-in 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes hint-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 44rpx;
|
||||
color: #10b981;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 8rpx 24rpx rgba(16, 185, 129, 0.3),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8);
|
||||
animation: hint-bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes hint-bounce {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 26rpx;
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user