## 主要改动 ### 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>
275 lines
6.2 KiB
Vue
275 lines
6.2 KiB
Vue
<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>
|