Browse Source

feat:新增页面

st 2 months ago
parent
commit
b2810167f4

+ 24 - 0
src/pages.json

@@ -56,6 +56,30 @@
         "navigationBarTitleText": ""
       }
     },
+    {
+      "path": "pages/play/detail",
+      "type": "page",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": ""
+      }
+    },
+    {
+      "path": "pages/play/game",
+      "type": "page",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": ""
+      }
+    },
+    {
+      "path": "pages/play/index",
+      "type": "page",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": ""
+      }
+    },
     {
       "path": "pages/purse/index",
       "type": "page",

+ 1 - 0
src/pages/index/index.vue

@@ -61,6 +61,7 @@
     </view>
     <view class="w-650rpx flex items-center mt-20rpx justify-between">
       <view
+        @click="goPage('/pages/play/index')"
         class="w-310rpx h-80rpx flex items-center justify-center relative bg-#111 rounded-20rpx"
       >
         <image src="@/static/images/production/btn.png" class="w-310rpx h-80rpx absolute"></image>

+ 200 - 0
src/pages/play/detail.vue

@@ -0,0 +1,200 @@
+<route lang="json5">
+{
+  style: {
+    navigationStyle: 'custom',
+    navigationBarTitleText: '',
+  },
+}
+</route>
+<template>
+  <image src="@/static/images/play/detailbg.png" class="w-full h-411rpx absolute top-70rpx"></image>
+  <view class="w-full min-h-screen bg-bgc flex flex-col items-center">
+    <view class="w-full text-48rpx font-700 text-white text-center mt-30rpx z-1">
+      Increase in profit per hour
+    </view>
+    <view
+      class="w-456rpx h-182rpx rounded-20rpx bg-cardlight flex flex-col items-center mt-40rpx z-1"
+    >
+      <view class="mt-50rpx">
+        <view
+          v-if="scoreData < correctData / 3"
+          class="h-80rpx rounded-10rpx mb-10rpx flex items-center justify-between box-border p-x-30rpx"
+        >
+          <image src="@/static/images/play/shalou.png" class="w-35rpx h-35rpx"></image>
+          <text class="text-white text-26rpx">{{ $t('play.again') }}</text>
+        </view>
+        <view
+          v-else
+          class="h-80rpx rounded-10rpx mb-10rpx flex flex-col items-center justify-between box-border p-x-30rpx"
+        >
+          <view
+            class="flex items-center"
+            v-if="scoreData >= correctData / 3 && scoreData < correctData / 3 + correctData / 3"
+          >
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+            <image
+              src="/static/images/play/no.png"
+              mode="scaleToFill"
+              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-if="
+              scoreData >= correctData / 3 + correctData / 3 &&
+              scoreData < correctData / 3 + correctData / 3 + correctData / 3
+            "
+          >
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+            <image src="/static/images/play/no.png" mode="scaleToFill" class="w-38rpx h-38rpx" />
+          </view>
+
+          <view
+            class="flex items-center"
+            v-if="scoreData >= correctData / 3 + correctData / 3 + correctData / 3"
+          >
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+            <image src="/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+          </view>
+          <view class="w-130rpx flex items-center text-white justify-between mt-10rpx">
+            <image src="@/static/images/play/shalou.png" class="w-35rpx h-35rpx"></image>
+            <text class="text-26rpx">X{{ proportion }}%</text>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view class="w-690rpx h-266rpx rounded-20rpx bg-cardlight mt-190rpx flex flex-col items-center">
+      <text class="w-616rpx mt-32rpx text-white flex items-center justify-center h-180rpx">
+        <view class="w-616rpx flex justify-between items-center mb-20rpx">
+          <text class="text-26rpx">{{ $t('play.Duration') }}:</text>
+          <text class="text-24rpx">00:00:45</text>
+        </view>
+        <view class="w-616rpx flex justify-between items-center mb-20rpx">
+          <text class="text-26rpx">Quantity of food:</text>
+          <text class="text-24rpx">{{ correctData }}</text>
+        </view>
+        <view class="w-616rpx flex justify-between items-center mb-20rpx">
+          <text class="text-26rpx">Received food:</text>
+          <text class="text-24rpx">{{ scoreData }}</text>
+        </view>
+      </text>
+    </view>
+    <view
+      @click="onConfirm"
+      class="w-630rpx h-100rpx rounded-20rpx bg-primary flex items-center justify-center text-textc items-center mt-40rpx"
+    >
+      {{ $t('play.OK') }}
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { onLoad } from '@dcloudio/uni-app'
+import { useTelegramBackButton } from '@/hooks/useTelegramBackButton'
+
+const { isVisible } = useTelegramBackButton(() => {
+  uni.navigateTo({
+    url: '/pages/play/index',
+  })
+})
+const correctData = ref(0)
+const scoreData = ref(0) // 点击数量
+// 当前比例
+const proportion = ref(0)
+
+const onConfirm = () => {
+  uni.redirectTo({
+    url: '/pages/play/index',
+  })
+}
+
+// 判断比例方法
+const da = (bl: number) => {
+  if (scoreData.value >= correctData.value / 3) {
+    proportion.value = 5 + bl
+  }
+  if (scoreData.value >= correctData.value / 3 + correctData.value / 3) {
+    proportion.value = 10 + bl
+  }
+  if (scoreData.value >= correctData.value / 3 + correctData.value / 3 + correctData.value / 3) {
+    proportion.value = 15 + bl
+  }
+}
+
+// 提交数据方法
+const submit = async () => {
+  console.log(correctData.value)
+  // 默认难度
+  if (correctData.value === 30) {
+    da(0)
+  } else if (correctData.value === 60) {
+    console.log('中级难度')
+    da(1)
+  } else {
+    da(2)
+    console.log('告级难度')
+  }
+
+  // 提交数据
+  // const { data } = await postMusicRewardApi(proportion.value)
+}
+
+onLoad((res) => {
+  if (res.correct !== '') {
+    correctData.value = parseInt(res.correct)
+  }
+  if (res.score !== '') {
+    scoreData.value = parseInt(res.score)
+    if (scoreData.value >= correctData.value / 3) {
+      submit()
+    }
+  }
+})
+</script>
+
+<style scoped>
+.header-bg {
+  background: #636363;
+  opacity: 0.4;
+}
+
+.tab-bg {
+  background: #636363;
+}
+
+.button-bg {
+  background: #8ae54a;
+}
+
+.page-container {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100vh;
+  overflow: hidden;
+  background: #10274e;
+}
+
+.header-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1;
+  width: 100%;
+  height: 550rpx;
+  background: #09090b;
+  opacity: 0.4;
+}
+
+.content-container {
+  position: relative;
+  z-index: 2;
+  flex: 1;
+  height: 100%;
+}
+</style>

+ 784 - 0
src/pages/play/game.vue

@@ -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>

+ 224 - 0
src/pages/play/index.vue

@@ -0,0 +1,224 @@
+<route lang="json5">
+{
+  style: {
+    navigationStyle: 'custom',
+    navigationBarTitleText: '',
+  },
+}
+</route>
+
+<template>
+  <view class="w-full min-h-screen bg-bgc flex flex-col items-center">
+    <view class="w-full flex flex-col items-center">
+      <view class="w-full min-h-screen flex flex-col items-center">
+        <view class="w-full flex flex-col items-center z-1">
+          <view
+            class="pa-[10rpx] flex items-center justify-center rounded-30rpx bg-cardlight fw-540 mt-30rpx"
+          >
+            <view
+              v-for="(vale, index) in classList"
+              :key="index"
+              :style="{ color: activeTab === vale.type ? '#0E0E0E' : '#fff' }"
+              class="w-220rpx h-80rpx rounded-20rpx flex items-center justify-center text-26rpx"
+              :class="activeTab === vale.type ? 'bg-primary' : ''"
+              @click="changeActiveTab(vale.type, vale.time, vale.correct, index)"
+            >
+              {{ vale.title }}
+            </view>
+          </view>
+          <image
+            src="@/static/images/play/fengmian.png"
+            class="w-690rpx h-371rpx rounded-20rpx mt-30rpx"
+          ></image>
+          <view class="w-690rpx rounded-20rpx bg-cardlight mt-30rpx flex flex-col items-center">
+            <text
+              class="w-616rpx mt-32rpx text-white flex items-center justify-center title-custom h-140rpx"
+            >
+              <view class="w-616rpx flex justify-between items-center mb-20rpx">
+                <text class="text-26rpx">{{ $t('play.Duration') }}:</text>
+                <text class="text-24rpx">{{ secondsToTime(timeData) }}</text>
+              </view>
+              <view class="w-616rpx flex justify-between items-center mb-20rpx">
+                <text class="text-26rpx">{{ $t('play.Click') }}:</text>
+                <text class="text-24rpx">{{ musicBlockData }}</text>
+              </view>
+            </text>
+            <text class="text-white text-26rpx mt-35rpx">{{ $t('play.Increase') }}</text>
+            <view class="mt-50rpx border-box rounded-20rpx mb-50rpx flex flex-col justify-center">
+              <view
+                class="w-630rpx h-70rpx rounded-10rpx mb-10rpx flex items-center justify-between box-border p-x-30rpx"
+                v-for="(vale, index) in awardList"
+                :key="index"
+              >
+                <view v-if="index == 0">
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx"></image>
+                </view>
+                <view v-else-if="index == 1">
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+                </view>
+
+                <view v-else-if="index == 2">
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+                  <image src="@/static/images/play/star.png" class="w-38rpx h-38rpx mr-1"></image>
+                </view>
+
+                <view class="w-130rpx flex items-center text-white justify-between">
+                  <image src="@/static/images/play/shalou.png" class="w-40rpx h-40rpx"></image>
+                  <text class="text-26rpx">X{{ vale.percentage }}%</text>
+                </view>
+              </view>
+            </view>
+          </view>
+
+          <!-- 进入游戏按钮 -->
+          <view
+            @click="onConfirm"
+            class="w-630rpx h-100rpx rounded-20rpx bg-primary flex items-center justify-center text-textc items-center mt-40rpx fw-700 text-28rpx mb-20rpx"
+          >
+            {{ $t('play.Go') }}
+          </view>
+          <!--          <view-->
+          <!--            @click="onConfirm"-->
+          <!--            class="w-630rpx h-100rpx rounded-20rpx bg-primary flex items-center justify-center text-textc items-center mt-40rpx fw-700 mb-20rpx text-28rpx"-->
+          <!--          >-->
+          <!--            <view class="flex items-center">-->
+          <!--              <image src="@/static/images/purse/u.png" class="w-40rpx h-40rpx mr-10rpx"></image>-->
+          <!--              <text>80</text>-->
+          <!--            </view>-->
+          <!--          </view>-->
+          <!--          <view>-->
+          <!--            <image src="@/static/images/play/desbg.png" class="w-40rpx h-40rpx mr-10rpx"></image>-->
+          <!--          </view>-->
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import i18n from '@/locale/index'
+import { useTelegramBackButton } from '@/hooks/useTelegramBackButton'
+
+const { isVisible } = useTelegramBackButton(() => {
+  uni.switchTab({
+    url: '/pages/index/index',
+  })
+})
+
+type Tab = string
+const activeTab = ref<Tab>('one')
+// 游戏时长(默认90秒第一难度)
+const timeData = ref<number>(45)
+// 点块数量
+const musicBlockData = ref<number>(30)
+const changeActiveTab = (tab: Tab, time: number, musicBlock: number, index: number) => {
+  timeData.value = time
+  musicBlockData.value = musicBlock
+  activeTab.value = tab
+  awardList.value = [
+    {
+      percentage: classList.value[index].grade1,
+    },
+    {
+      percentage: classList.value[index].grade2,
+    },
+    { percentage: classList.value[index].grade3 },
+  ]
+}
+
+// 当前等级
+const classList = ref<
+  Array<{
+    title: string
+    type: string
+    correct: number
+    time: number
+    grade1: number
+    grade2: number
+    grade3: number
+  }>
+>([])
+const updateLists = () => {
+  classList.value = [
+    {
+      title: i18n.global.t('play.Relaxed'),
+      type: 'one',
+      correct: 30,
+      time: 90,
+      grade1: 5,
+      grade2: 10,
+      grade3: 15,
+    },
+    {
+      title: i18n.global.t('play.Ordinary'),
+      type: 'two',
+      correct: 60,
+      time: 90,
+      grade1: 6,
+      grade2: 11,
+      grade3: 16,
+    },
+    {
+      title: i18n.global.t('play.Nervousness'),
+      type: 'three',
+      correct: 90,
+      time: 90,
+      grade1: 7,
+      grade2: 12,
+      grade3: 17,
+    },
+  ]
+}
+// 时间换算器
+const secondsToTime = (seconds: number) => {
+  // 计算小时、分钟和剩余的秒数
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  const secs = seconds % 60
+
+  // 格式化为 HH:MM:SS
+  const formattedTime = [
+    hours.toString().padStart(2, '0'),
+    minutes.toString().padStart(2, '0'),
+    secs.toString().padStart(2, '0'),
+  ].join(':')
+
+  return formattedTime
+}
+
+// 等级
+const awardList = ref<any[]>([
+  {
+    percentage: 5,
+  },
+  {
+    percentage: 10,
+  },
+  { percentage: 15 },
+])
+
+// 跳转开始游戏页面
+const onConfirm = () => {
+  uni.redirectTo({
+    url: `/pages/play/game?correct=${musicBlockData.value}&activeTab=${activeTab.value}`,
+  })
+}
+watch(() => i18n.global.locale, updateLists)
+
+onMounted(() => {
+  updateLists()
+})
+</script>
+
+<style scoped>
+.title-custom {
+  background: transparent;
+  border-bottom: 1px solid #636363;
+}
+
+.border-box {
+  border: 2rpx #636363 solid;
+}
+</style>

BIN
src/static/images/play/a.png


BIN
src/static/images/play/b.png


BIN
src/static/images/play/bg.jpg


BIN
src/static/images/play/bz.png


BIN
src/static/images/play/c.png


BIN
src/static/images/play/d.png


BIN
src/static/images/play/desbg.png


BIN
src/static/images/play/detailbg.png


BIN
src/static/images/play/exit.png


BIN
src/static/images/play/fengmian.png


BIN
src/static/images/play/no.png


BIN
src/static/images/play/shalou.png


BIN
src/static/images/play/star.png


BIN
src/static/images/play/start.png


BIN
src/static/images/play/stop.png


BIN
src/static/images/play/tong.png


+ 88 - 5
src/types/auto-import.d.ts

@@ -97,7 +97,7 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }
 // for vue template auto import
@@ -161,7 +161,6 @@ declare module 'vue' {
     readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
     readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
     readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
-    readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
     readonly provide: UnwrapRef<typeof import('vue')['provide']>
     readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
     readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
@@ -179,13 +178,97 @@ declare module 'vue' {
     readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
     readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
     readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
-    readonly useId: UnwrapRef<typeof import('vue')['useId']>
     readonly useImgPath: UnwrapRef<typeof import('../hooks/useImgPath')['useImgPath']>
-    readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
     readonly useRequest: UnwrapRef<typeof import('../hooks/useRequest')['default']>
     readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
     readonly useTelegramBackButton: UnwrapRef<typeof import('../hooks/useTelegramBackButton')['useTelegramBackButton']>
-    readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
+    readonly useUpload: UnwrapRef<typeof import('../hooks/useUpload')['default']>
+    readonly watch: UnwrapRef<typeof import('vue')['watch']>
+    readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
+    readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
+    readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
+  }
+}
+declare module '@vue/runtime-core' {
+  interface GlobalComponents {}
+  interface ComponentCustomProperties {
+    readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
+    readonly computed: UnwrapRef<typeof import('vue')['computed']>
+    readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
+    readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
+    readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
+    readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
+    readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
+    readonly formatAmount: UnwrapRef<typeof import('../hooks/moneyProcessing')['formatAmount']>
+    readonly formatAmountNoFloat: UnwrapRef<typeof import('../hooks/moneyProcessing')['formatAmountNoFloat']>
+    readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
+    readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
+    readonly h: UnwrapRef<typeof import('vue')['h']>
+    readonly inject: UnwrapRef<typeof import('vue')['inject']>
+    readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
+    readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
+    readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
+    readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
+    readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
+    readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
+    readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
+    readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
+    readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
+    readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
+    readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
+    readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
+    readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
+    readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
+    readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
+    readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
+    readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
+    readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
+    readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
+    readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
+    readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
+    readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
+    readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
+    readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
+    readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
+    readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
+    readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
+    readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
+    readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
+    readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
+    readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
+    readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
+    readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
+    readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
+    readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
+    readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
+    readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
+    readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
+    readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
+    readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
+    readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
+    readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
+    readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
+    readonly provide: UnwrapRef<typeof import('vue')['provide']>
+    readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
+    readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
+    readonly ref: UnwrapRef<typeof import('vue')['ref']>
+    readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
+    readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
+    readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
+    readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
+    readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
+    readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
+    readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
+    readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
+    readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
+    readonly unref: UnwrapRef<typeof import('vue')['unref']>
+    readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
+    readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
+    readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
+    readonly useImgPath: UnwrapRef<typeof import('../hooks/useImgPath')['useImgPath']>
+    readonly useRequest: UnwrapRef<typeof import('../hooks/useRequest')['default']>
+    readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
+    readonly useTelegramBackButton: UnwrapRef<typeof import('../hooks/useTelegramBackButton')['useTelegramBackButton']>
     readonly useUpload: UnwrapRef<typeof import('../hooks/useUpload')['default']>
     readonly watch: UnwrapRef<typeof import('vue')['watch']>
     readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>

+ 3 - 0
src/types/uni-pages.d.ts

@@ -5,6 +5,9 @@
 
 interface NavigateToOptions {
   url: "/pages/index/index" |
+       "/pages/play/detail" |
+       "/pages/play/game" |
+       "/pages/play/index" |
        "/pages/purse/index" |
        "/pages/task/index" |
        "/pages/team/index" |