|
@@ -0,0 +1,784 @@
|
|
|
+<route lang="json5">
|
|
|
+{
|
|
|
+ style: {
|
|
|
+ navigationStyle: 'custom',
|
|
|
+ navigationBarTitleText: '',
|
|
|
+ },
|
|
|
+}
|
|
|
+</route>
|
|
|
+<script setup lang="ts">
|
|
|
+import WebApp from '@twa-dev/sdk'
|
|
|
+
|
|
|
+// === 类型定义 ===
|
|
|
+interface Note {
|
|
|
+ id: number
|
|
|
+ lane: number
|
|
|
+ position: number
|
|
|
+ type: string
|
|
|
+}
|
|
|
+
|
|
|
+// === 状态管理 ===
|
|
|
+const classData = ref<string>('')
|
|
|
+const speed = ref(0)
|
|
|
+const isContinuous = ref(false)
|
|
|
+const isMiss = ref(false)
|
|
|
+const score = ref(0)
|
|
|
+const isPlaying = ref(false)
|
|
|
+const activeTab = ref('one')
|
|
|
+const continuousData = ref(0)
|
|
|
+const dz = ref(-1)
|
|
|
+const remainingTime = ref(45)
|
|
|
+const screenHeight = ref(0)
|
|
|
+const statusBarHeight = ref(0)
|
|
|
+const isLoading = ref(true)
|
|
|
+const loadingProgress = ref(0)
|
|
|
+const isAudioReady = ref(false)
|
|
|
+const showStartButton = ref(false) // 新增:控制开始按钮显示
|
|
|
+const gameStarted = ref(false) // 新增:控制游戏是否已经开始过
|
|
|
+
|
|
|
+// === 音频管理 ===
|
|
|
+const musicType = ref<number>(Math.floor(Math.random() * 3) + 1)
|
|
|
+const bgm = ref<UniApp.InnerAudioContext>()
|
|
|
+const missSound = ref<UniApp.InnerAudioContext>()
|
|
|
+
|
|
|
+const audioSources = {
|
|
|
+ bgm: {
|
|
|
+ 1: '/static/audio/mzdhl.mp3',
|
|
|
+ 2: '/static/audio/kn.mp3',
|
|
|
+ 3: '/static/audio/M5000016XYcc2hEve0.mp3',
|
|
|
+ },
|
|
|
+ effects: {
|
|
|
+ miss: '/static/audio/missTS.mp3',
|
|
|
+ },
|
|
|
+}
|
|
|
+
|
|
|
+// === 游戏状态 ===
|
|
|
+const activeNotes = ref<Note[]>([])
|
|
|
+const pressedKeys = ref(Array(6).fill(false))
|
|
|
+const buttons = ['A', 'B', 'C', 'D']
|
|
|
+const buttonTypes = buttons.map((b) => b.toLowerCase())
|
|
|
+
|
|
|
+let timerInterval: number | null = null
|
|
|
+let animationFrame: number | null = null
|
|
|
+let lastTime = 0
|
|
|
+let noteId = 0
|
|
|
+
|
|
|
+// === 计算属性 ===
|
|
|
+const containerStyle = computed(() => ({
|
|
|
+ height: `${screenHeight.value - statusBarHeight.value}px`,
|
|
|
+ backgroundSize: '100% 100%',
|
|
|
+}))
|
|
|
+
|
|
|
+const playButtonSrc = computed(() =>
|
|
|
+ isPlaying.value ? '/static/images/play/start.png' : '/static/images/play/stop.png',
|
|
|
+)
|
|
|
+// 添加倒计时相关的计算属性
|
|
|
+const showCountdown = computed(() => {
|
|
|
+ return remainingTime.value <= 10 && remainingTime.value > 0
|
|
|
+})
|
|
|
+
|
|
|
+const countdownClass = computed(() => {
|
|
|
+ return {
|
|
|
+ 'text-red-500': remainingTime.value <= 3,
|
|
|
+ 'text-yellow-500': remainingTime.value > 3 && remainingTime.value <= 5,
|
|
|
+ 'text-white': remainingTime.value > 5,
|
|
|
+ }
|
|
|
+})
|
|
|
+// === 音频管理方法 ===
|
|
|
+const initBGM = () => {
|
|
|
+ const audio = uni.createInnerAudioContext()
|
|
|
+ audio.src = audioSources.bgm[musicType.value]
|
|
|
+ audio.loop = true
|
|
|
+ audio.autoplay = false
|
|
|
+ audio.obeyMuteSwitch = false
|
|
|
+
|
|
|
+ // 音频事件监听
|
|
|
+ audio.onError((res) => {
|
|
|
+ console.error('BGM error:', res)
|
|
|
+ })
|
|
|
+
|
|
|
+ return audio
|
|
|
+}
|
|
|
+
|
|
|
+const initSoundEffect = () => {
|
|
|
+ const audio = uni.createInnerAudioContext()
|
|
|
+ audio.src = audioSources.effects.miss
|
|
|
+ audio.autoplay = false
|
|
|
+ audio.obeyMuteSwitch = false
|
|
|
+
|
|
|
+ audio.onError((res) => {
|
|
|
+ console.error('Sound effect error:', res)
|
|
|
+ })
|
|
|
+
|
|
|
+ return audio
|
|
|
+}
|
|
|
+
|
|
|
+const initializeAudio = async () => {
|
|
|
+ isLoading.value = true
|
|
|
+ loadingProgress.value = 0
|
|
|
+
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
+ try {
|
|
|
+ bgm.value = initBGM()
|
|
|
+ missSound.value = initSoundEffect()
|
|
|
+
|
|
|
+ let loadedCount = 0
|
|
|
+ const checkLoaded = () => {
|
|
|
+ loadedCount++
|
|
|
+ loadingProgress.value = loadedCount * 50
|
|
|
+ if (loadedCount >= 2) {
|
|
|
+ isAudioReady.value = true
|
|
|
+ showStartButton.value = true
|
|
|
+ isLoading.value = false
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bgm.value.onCanplay(() => {
|
|
|
+ checkLoaded()
|
|
|
+ })
|
|
|
+
|
|
|
+ missSound.value.onCanplay(() => {
|
|
|
+ checkLoaded()
|
|
|
+ })
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (!isAudioReady.value) {
|
|
|
+ isAudioReady.value = true
|
|
|
+ showStartButton.value = true
|
|
|
+ isLoading.value = false
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ }, 2000)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Audio init error:', e)
|
|
|
+ isLoading.value = false
|
|
|
+ showStartButton.value = true
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const playBGM = async () => {
|
|
|
+ if (!bgm.value || !isAudioReady.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ bgm.value.seek(0)
|
|
|
+ await new Promise<void>((resolve) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (bgm.value) {
|
|
|
+ bgm.value.play()
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ }, 100)
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Play BGM error:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const playMissSound = () => {
|
|
|
+ if (!missSound.value || !isAudioReady.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ missSound.value.stop()
|
|
|
+ missSound.value.seek(0)
|
|
|
+ setTimeout(() => {
|
|
|
+ missSound.value?.play()
|
|
|
+ }, 50)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Play sound effect error:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// === 游戏核心逻辑 ===
|
|
|
+const getLanePosition = (lane: number) => {
|
|
|
+ const buttonWidth = 90
|
|
|
+ const spacing = 80
|
|
|
+ const startX = 100
|
|
|
+ return startX + lane * (buttonWidth + spacing)
|
|
|
+}
|
|
|
+
|
|
|
+const gameLoop = (timestamp: number) => {
|
|
|
+ if (!isPlaying.value) return
|
|
|
+
|
|
|
+ const deltaTime = timestamp - lastTime
|
|
|
+ if (deltaTime >= 16) {
|
|
|
+ updateNotes()
|
|
|
+ lastTime = timestamp
|
|
|
+ }
|
|
|
+
|
|
|
+ animationFrame = requestAnimationFrame(gameLoop)
|
|
|
+}
|
|
|
+
|
|
|
+const updateNotes = () => {
|
|
|
+ if (!isPlaying.value) return
|
|
|
+
|
|
|
+ activeNotes.value = activeNotes.value.filter((note) => {
|
|
|
+ const moveSpeed = activeTab.value === 'one' ? 5 : activeTab.value === 'two' ? 6 : 7
|
|
|
+
|
|
|
+ note.position += moveSpeed
|
|
|
+
|
|
|
+ const endPosition = screenHeight.value + 290
|
|
|
+ if (note.position > endPosition) {
|
|
|
+ playMissSound()
|
|
|
+ handleMiss()
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const generateNotes = () => {
|
|
|
+ if (!isPlaying.value) return
|
|
|
+
|
|
|
+ spawnNote()
|
|
|
+ speed.value = musicType.value === 1 ? 600 : musicType.value === 2 ? 1000 : 800
|
|
|
+
|
|
|
+ setTimeout(generateNotes, speed.value)
|
|
|
+}
|
|
|
+
|
|
|
+const spawnNote = () => {
|
|
|
+ if (!isPlaying.value) return
|
|
|
+
|
|
|
+ const lane = Math.floor(Math.random() * buttons.length)
|
|
|
+ activeNotes.value.push({
|
|
|
+ id: noteId++,
|
|
|
+ lane,
|
|
|
+ position: -134,
|
|
|
+ type: buttonTypes[lane],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// === 游戏控制 ===
|
|
|
+const startGame = async () => {
|
|
|
+ if (!isAudioReady.value || gameStarted.value) return
|
|
|
+
|
|
|
+ gameStarted.value = true
|
|
|
+ showStartButton.value = false
|
|
|
+ isPlaying.value = true
|
|
|
+ remainingTime.value = 45 // 确保每次开始时重置为45秒
|
|
|
+
|
|
|
+ try {
|
|
|
+ await playBGM()
|
|
|
+
|
|
|
+ if (!animationFrame) {
|
|
|
+ lastTime = performance.now()
|
|
|
+ animationFrame = requestAnimationFrame(gameLoop)
|
|
|
+ startTimer()
|
|
|
+ generateNotes()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Start game error:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const pauseGame = () => {
|
|
|
+ isPlaying.value = false
|
|
|
+
|
|
|
+ if (bgm.value) {
|
|
|
+ bgm.value.pause()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (animationFrame) {
|
|
|
+ cancelAnimationFrame(animationFrame)
|
|
|
+ animationFrame = null
|
|
|
+ }
|
|
|
+ stopTimer()
|
|
|
+}
|
|
|
+
|
|
|
+const handleKeyPress = (index: number) => {
|
|
|
+ pressedKeys.value[index] = true
|
|
|
+
|
|
|
+ const endPosition = screenHeight.value + 230
|
|
|
+ const hitbox = 150
|
|
|
+
|
|
|
+ activeNotes.value = activeNotes.value.filter((note) => {
|
|
|
+ if (note.lane === index && Math.abs(note.position - endPosition) < hitbox) {
|
|
|
+ dz.value = note.lane
|
|
|
+ score.value += 1
|
|
|
+ shake()
|
|
|
+ continuous()
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ pressedKeys.value[index] = false
|
|
|
+ }, 100)
|
|
|
+}
|
|
|
+
|
|
|
+const handleMiss = () => {
|
|
|
+ isMiss.value = true
|
|
|
+ isContinuous.value = false
|
|
|
+ continuousData.value = 0
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ isMiss.value = false
|
|
|
+ }, 500)
|
|
|
+}
|
|
|
+
|
|
|
+const continuous = () => {
|
|
|
+ if (continuousData.value < 8) {
|
|
|
+ isContinuous.value = true
|
|
|
+ setTimeout(() => {
|
|
|
+ isContinuous.value = false
|
|
|
+ }, 500)
|
|
|
+ continuousData.value += 1
|
|
|
+ } else {
|
|
|
+ setTimeout(() => {
|
|
|
+ continuousData.value = 0
|
|
|
+ }, 500)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const shake = () => {
|
|
|
+ if (WebApp.HapticFeedback) {
|
|
|
+ WebApp.HapticFeedback.impactOccurred('medium')
|
|
|
+ } else {
|
|
|
+ console.info('🚀 ~ file:game method:shake line:340 -----', 11)
|
|
|
+ uni.vibrateShort({
|
|
|
+ success: function () {},
|
|
|
+ fail: function () {},
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const togglePause = () => {
|
|
|
+ if (!gameStarted.value) return
|
|
|
+
|
|
|
+ isPlaying.value = !isPlaying.value
|
|
|
+ if (isPlaying.value) {
|
|
|
+ // 继续游戏
|
|
|
+ if (bgm.value) {
|
|
|
+ bgm.value.play()
|
|
|
+ }
|
|
|
+ lastTime = performance.now()
|
|
|
+ animationFrame = requestAnimationFrame(gameLoop)
|
|
|
+ startTimer()
|
|
|
+ generateNotes()
|
|
|
+ } else {
|
|
|
+ pauseGame()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// === 计时器控制 ===
|
|
|
+const startTimer = () => {
|
|
|
+ if (!timerInterval) {
|
|
|
+ timerInterval = setInterval(() => {
|
|
|
+ if (remainingTime.value <= 0) {
|
|
|
+ gameOver()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ remainingTime.value--
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const stopTimer = () => {
|
|
|
+ if (timerInterval) {
|
|
|
+ clearInterval(timerInterval)
|
|
|
+ timerInterval = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const gameOver = () => {
|
|
|
+ gameStarted.value = false
|
|
|
+ isPlaying.value = false
|
|
|
+ pauseGame()
|
|
|
+ stopTimer()
|
|
|
+
|
|
|
+ uni.redirectTo({
|
|
|
+ url: `/pages/play/detail?correct=${classData.value}&score=${score.value}`,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const cleanupAudio = () => {
|
|
|
+ try {
|
|
|
+ if (bgm.value) {
|
|
|
+ bgm.value.stop()
|
|
|
+ bgm.value.destroy()
|
|
|
+ }
|
|
|
+ if (missSound.value) {
|
|
|
+ missSound.value.stop()
|
|
|
+ missSound.value.destroy()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Cleanup audio error:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const back = () => {
|
|
|
+ gameStarted.value = false
|
|
|
+ pauseGame()
|
|
|
+ cleanupAudio()
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/play/index',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// === 生命周期钩子 ===
|
|
|
+onMounted(async () => {
|
|
|
+ const systemInfo = uni.getSystemInfoSync()
|
|
|
+ screenHeight.value = systemInfo.windowHeight
|
|
|
+ statusBarHeight.value = systemInfo.statusBarHeight || 0
|
|
|
+
|
|
|
+ uni.onWindowResize(() => {
|
|
|
+ screenHeight.value = uni.getSystemInfoSync().windowHeight
|
|
|
+ })
|
|
|
+
|
|
|
+ await initializeAudio()
|
|
|
+})
|
|
|
+
|
|
|
+onLoad((res) => {
|
|
|
+ if (res.correct) {
|
|
|
+ classData.value = res.correct
|
|
|
+ }
|
|
|
+ if (res.activeTab) {
|
|
|
+ activeTab.value = res.activeTab
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ pauseGame()
|
|
|
+ stopTimer()
|
|
|
+ cleanupAudio()
|
|
|
+})
|
|
|
+</script>
|
|
|
+<template>
|
|
|
+ <image src="/static/images/play/bg.jpg" class="w-100% h-100% absolute" />
|
|
|
+ <view class="flex flex-col items-center relative" :style="containerStyle">
|
|
|
+ <!-- 加载遮罩和开始按钮 -->
|
|
|
+ <view
|
|
|
+ v-if="(isLoading || showStartButton) && !gameStarted"
|
|
|
+ class="fixed inset-0 bg-black/50 flex flex-col items-center justify-center z-50"
|
|
|
+ >
|
|
|
+ <template v-if="isLoading">
|
|
|
+ <view class="text-white text-xl mb-4">loading... {{ loadingProgress }}%</view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="showStartButton && !gameStarted">
|
|
|
+ <view
|
|
|
+ class="w-200rpx h-200rpx rounded-full bg-gradient-to-r from-green-400 to-blue-500 flex items-center justify-center text-white text-2xl font-bold shadow-lg cursor-pointer hover:scale-105 active:scale-95 transition-all duration-300 transform"
|
|
|
+ @click="startGame"
|
|
|
+ >
|
|
|
+ <text class="tracking-wider">START</text>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 顶部分数和控制栏 -->
|
|
|
+ <view
|
|
|
+ class="top-bg w-680rpx h-160rpx rounded-30rpx bg-cardlight m-b mt-20rpx flex items-center justify-around"
|
|
|
+ >
|
|
|
+ <view @click="back" class="w-80rpx h-80rpx rounded-10rpx flex items-center justify-center">
|
|
|
+ <image src="/static/images/play/exit.png" class="w-45rpx h-35rpx" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="flex flex-col w-150rpx items-center text-white">
|
|
|
+ <view class="text-60rpx mb-10rpx">{{ score }}</view>
|
|
|
+ <!-- 星星评分 -->
|
|
|
+ <view class="flex items-center" v-if="score < parseInt(classData) / 3">
|
|
|
+ <image
|
|
|
+ v-for="i in 3"
|
|
|
+ :key="i"
|
|
|
+ src="/static/images/play/no.png"
|
|
|
+ mode="scaleToFill"
|
|
|
+ class="w-38rpx h-38rpx"
|
|
|
+ :class="{ 'mr-1': i !== 3 }"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ class="flex items-center"
|
|
|
+ v-else-if="score >= parseInt(classData) / 3 && score < (parseInt(classData) * 2) / 3"
|
|
|
+ >
|
|
|
+ <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1" />
|
|
|
+ <image
|
|
|
+ v-for="i in 2"
|
|
|
+ :key="i"
|
|
|
+ src="/static/images/play/no.png"
|
|
|
+ mode="scaleToFill"
|
|
|
+ class="w-38rpx h-38rpx"
|
|
|
+ :class="{ 'mr-1': i !== 2 }"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ class="flex items-center"
|
|
|
+ v-else-if="score >= (parseInt(classData) * 2) / 3 && score < parseInt(classData)"
|
|
|
+ >
|
|
|
+ <image
|
|
|
+ v-for="i in 2"
|
|
|
+ :key="i"
|
|
|
+ src="/static/images/play/star.png"
|
|
|
+ class="w-38rpx h-38rpx mr-1"
|
|
|
+ />
|
|
|
+ <image src="/static/images/play/no.png" mode="scaleToFill" class="w-38rpx h-38rpx" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="flex items-center" v-else>
|
|
|
+ <image
|
|
|
+ v-for="i in 3"
|
|
|
+ :key="i"
|
|
|
+ src="/static/images/play/star.png"
|
|
|
+ class="w-38rpx h-38rpx"
|
|
|
+ :class="{ 'mr-1': i !== 3 }"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ class="w-80rpx h-80rpx rounded-10rpx opacity-60 flex items-center justify-center"
|
|
|
+ @click="togglePause"
|
|
|
+ >
|
|
|
+ <image :src="playButtonSrc" class="w-22rpx h-27rpx" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 游戏区域 -->
|
|
|
+ <view class="flex-1 w-full relative">
|
|
|
+ <!-- 倒计时显示 -->
|
|
|
+ <view
|
|
|
+ v-if="showCountdown"
|
|
|
+ class="absolute top-1/3 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50"
|
|
|
+ >
|
|
|
+ <text :class="['text-8xl font-bold countdown-animation', countdownClass]">
|
|
|
+ {{ remainingTime }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <!-- 连击效果 -->
|
|
|
+ <view class="w-100% z-100 absolute text-center">
|
|
|
+ <view class="flex flex-col" v-show="isContinuous && continuousData > 1">
|
|
|
+ <text
|
|
|
+ class="text-white text-[55rpx] fw-800 oblique"
|
|
|
+ :class="{ 'continuous-number': isContinuous }"
|
|
|
+ >
|
|
|
+ {{ continuousData }}
|
|
|
+ </text>
|
|
|
+ <text class="text-white text-[50rpx] fw-800 oblique">continuous</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-show="isMiss" class="text-white text-[50rpx] fw-1000 oblique miss-text">MISS!</view>
|
|
|
+
|
|
|
+ <text
|
|
|
+ v-show="continuousData >= 8 && !isContinuous"
|
|
|
+ class="text-[50rpx] text fw-800 text-[italic]"
|
|
|
+ >
|
|
|
+ Good!
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 音符区域 -->
|
|
|
+ <view class="w-full h-full absolute top-0 left-0 overflow-hidden">
|
|
|
+ <view
|
|
|
+ v-for="note in activeNotes"
|
|
|
+ :key="note.id"
|
|
|
+ class="absolute note"
|
|
|
+ :style="{
|
|
|
+ left: `${getLanePosition(note.lane) - 25}rpx`,
|
|
|
+ top: `${note.position}rpx`,
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <image :src="`/static/images/play/${note.type}.png`" class="w-92rpx h-150rpx" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部按键区域 -->
|
|
|
+ <view
|
|
|
+ class="absolute overflow-hidden bottom-20rpx w-680rpx h-134rpx rounded-30rpx mt-20rpx flex items-center justify-around"
|
|
|
+ >
|
|
|
+ <view v-for="(key, index) in buttons" :key="index">
|
|
|
+ <image
|
|
|
+ src="`/static/images/play/tong.png`"
|
|
|
+ :style="{ color: pressedKeys[index] ? '#0E0E0E' : '#fff' }"
|
|
|
+ :class="[
|
|
|
+ 'w-115rpx h-155rpx rounded-20rpx flex items-center justify-center text-32rpx fw-550 relative z-[10]',
|
|
|
+ pressedKeys[index] ? 'anniu-custom-ac' : 'anniu-custom',
|
|
|
+ ]"
|
|
|
+ @tap="handleKeyPress(index)"
|
|
|
+ />
|
|
|
+ <image
|
|
|
+ v-show="isContinuous && index == dz"
|
|
|
+ src="/static/images/play/bz.png"
|
|
|
+ mode="scaleToFill"
|
|
|
+ class="w-160rpx h-160rpx absolute mt-[-125rpx] ml-[-15rpx] z-[11]"
|
|
|
+ :class="{ 'img-bz': isContinuous }"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.m-b {
|
|
|
+ border: 1px solid #686868;
|
|
|
+}
|
|
|
+
|
|
|
+.m-b-b {
|
|
|
+ background: transparent;
|
|
|
+ background-color: rgba(132, 132, 133, 0.8);
|
|
|
+ backdrop-filter: blur(30px);
|
|
|
+ -webkit-backdrop-filter: blur(30px);
|
|
|
+}
|
|
|
+
|
|
|
+.m-b-b-no {
|
|
|
+ background: transparent;
|
|
|
+ background-color: rgba(255, 0, 0, 0.15);
|
|
|
+ border: 1px solid #ff0000;
|
|
|
+ backdrop-filter: blur(30px);
|
|
|
+ -webkit-backdrop-filter: blur(30px);
|
|
|
+}
|
|
|
+
|
|
|
+.anniu-custom {
|
|
|
+ background: #555;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ transform: scale(0.95);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.anniu-custom-ac {
|
|
|
+ background: #8ae54a;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ transform: scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
+.note {
|
|
|
+ transition: top 16ms linear;
|
|
|
+ transform: translateZ(0);
|
|
|
+ will-change: top;
|
|
|
+
|
|
|
+ &.fade-enter-active,
|
|
|
+ &.fade-leave-active {
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.fade-enter-from,
|
|
|
+ &.fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.text {
|
|
|
+ background-image: linear-gradient(#ffffff, #2fbec7);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ animation: resizeText 0.7s 1;
|
|
|
+}
|
|
|
+
|
|
|
+.miss-text {
|
|
|
+ background-image: linear-gradient(#ffffff, #ff0000);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ animation: resizeText 0.7s 1;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes resizeText {
|
|
|
+ 0% {
|
|
|
+ font-size: 0rpx;
|
|
|
+ opacity: 0;
|
|
|
+ transform: scale(0.5);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1.2);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ font-size: 50rpx;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.continuous-number {
|
|
|
+ animation: number 0.5s 1;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes number {
|
|
|
+ 0% {
|
|
|
+ font-size: 0rpx;
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20rpx);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(-10rpx);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ font-size: 50rpx;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes fireworks {
|
|
|
+ 0% {
|
|
|
+ transform: scale(0);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1.2);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.img-bz {
|
|
|
+ animation: fireworks 0.2s ease-out forwards;
|
|
|
+}
|
|
|
+
|
|
|
+// 添加全局过渡效果
|
|
|
+.fade-enter-active,
|
|
|
+.fade-leave-active {
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.fade-enter-from,
|
|
|
+.fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+// 添加加载动画
|
|
|
+@keyframes spin {
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+// 添加倒计时动画样式
|
|
|
+.countdown-animation {
|
|
|
+ animation: countdown-pulse 1s ease-in-out infinite;
|
|
|
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes countdown-pulse {
|
|
|
+ 0% {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: scale(1.2);
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.top-bg {
|
|
|
+ box-shadow: 0px 4px 4px 0px rgba(255, 255, 255, 0.25);
|
|
|
+ backdrop-filter: blur(6.099999904632568px);
|
|
|
+}
|
|
|
+</style>
|