之前误将训练模块加在 uniapp 项目,实际目标项目是 TUICallKit-Vue3 (甄养堂主小程序壳),整体迁入: - hooks/useMetronome.ts 节拍器(音频实例池+预热,无冷启动延迟) - hooks/useTrainingSession.ts 训练 Session 状态机 - hooks/useVoiceCoach.ts 语音教练队列 - hooks/useTrainingBgm.ts 训练/休息 BGM 切换+渐变 - pages/training/index.vue 练一练主页(器械跟练) - pages/training/metronome.vue 独立节拍器(老人友好版, 中央圆即开始/暂停按钮,纯 CSS 几何 icon,大字大按钮) - pages/training/components/exercise-anim.vue CSS 动作动画 - pages/training/exercises.ts 5 个器械动作预设 - static/training/audio/*.mp3 3 种 click 音色(MIT 协议) - scripts/generate-voice.mjs MiMo TTS 批量生成语音脚本 - pages.json 注册 pages/training/index 与 pages/training/metronome - 「我的」页加 DevTrainingEntry 悬浮入口,生产环境自动隐藏 Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
4.1 KiB
Vue
181 lines
4.1 KiB
Vue
<template>
|
|
<view v-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
|
|
<view v-if="expanded" class="menu-list" @click.stop>
|
|
<view class="menu-item" @click="goto('/pages/training/index')">
|
|
<view class="menu-icon">💪</view>
|
|
<view class="menu-info">
|
|
<text class="menu-title">练一练</text>
|
|
<text class="menu-sub">器械跟练</text>
|
|
</view>
|
|
</view>
|
|
<view class="menu-item" @click="goto('/pages/training/metronome')">
|
|
<view class="menu-icon metronome-icon">🎵</view>
|
|
<view class="menu-info">
|
|
<text class="menu-title">节拍器</text>
|
|
<text class="menu-sub">独立工具</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="entry-fab" :class="{ expanded }" @click="toggle">
|
|
<text class="fab-icon">{{ expanded ? '✕' : '💪' }}</text>
|
|
<text v-if="!expanded" class="fab-text">练</text>
|
|
<view v-if="!expanded" class="fab-tag">DEV</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
bottom?: number
|
|
}>(),
|
|
{ bottom: 200 },
|
|
)
|
|
|
|
const isDevMode = (): boolean => {
|
|
// HBuilderX 工程:发行(release)模式下 NODE_ENV === 'production'
|
|
try {
|
|
return process.env.NODE_ENV !== 'production'
|
|
} catch (_) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const visible = ref(isDevMode())
|
|
const expanded = ref(false)
|
|
|
|
const toggle = () => {
|
|
expanded.value = !expanded.value
|
|
}
|
|
|
|
const goto = (url: string) => {
|
|
expanded.value = false
|
|
uni.navigateTo({ url })
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.dev-entry-wrap {
|
|
position: fixed;
|
|
right: 24rpx;
|
|
z-index: 999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.entry-fab {
|
|
width: 110rpx;
|
|
height: 110rpx;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
|
box-shadow: 0 6rpx 20rpx rgba(34, 197, 94, 0.4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
transition: transform 0.2s, background 0.2s;
|
|
|
|
&:active {
|
|
transform: scale(0.92);
|
|
}
|
|
|
|
&.expanded {
|
|
background: linear-gradient(135deg, #64748b, #334155);
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.fab-icon {
|
|
font-size: 36rpx;
|
|
line-height: 1;
|
|
}
|
|
|
|
.fab-text {
|
|
font-size: 18rpx;
|
|
color: #fff;
|
|
margin-top: 4rpx;
|
|
line-height: 1;
|
|
}
|
|
|
|
.fab-tag {
|
|
position: absolute;
|
|
top: -8rpx;
|
|
right: -8rpx;
|
|
background: #ef4444;
|
|
color: #fff;
|
|
font-size: 16rpx;
|
|
padding: 2rpx 8rpx;
|
|
border-radius: 8rpx;
|
|
line-height: 1.4;
|
|
}
|
|
}
|
|
|
|
.menu-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12rpx;
|
|
animation: slide-up 0.2s ease-out;
|
|
}
|
|
@keyframes slide-up {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20rpx);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
background: #fff;
|
|
padding: 16rpx 24rpx;
|
|
border-radius: 32rpx;
|
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
|
min-width: 240rpx;
|
|
|
|
&:active {
|
|
background: #f0fdf4;
|
|
}
|
|
|
|
.menu-icon {
|
|
width: 64rpx;
|
|
height: 64rpx;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 32rpx;
|
|
|
|
&.metronome-icon {
|
|
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
}
|
|
}
|
|
|
|
.menu-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4rpx;
|
|
}
|
|
|
|
.menu-title {
|
|
font-size: 28rpx;
|
|
color: #111827;
|
|
font-weight: 600;
|
|
}
|
|
.menu-sub {
|
|
font-size: 20rpx;
|
|
color: #9ca3af;
|
|
}
|
|
}
|
|
</style>
|