2 Commits

Author SHA1 Message Date
rjl
f4c3734639 fix: 修复翻牌音效播放失败问题
移除 AudioController 依赖,直接使用 Audio 对象播放新增的 checkout.mp3 音频文件,避免因 AudioController 实例化或配置问题导致的音效播放失败。
2026-04-10 15:33:43 +08:00
rjl
38a243af15 feat(admin): 添加答题前3秒倒计时动画和音效
在用户点击"开始答题"后,增加3秒倒计时动画和音效提示,提升答题体验
- 添加倒计时遮罩层和动画效果
- 播放倒计时音效文件
- 修改开始答题按钮状态为"准备中..."
- 调整环境配置中的API地址
2026-04-10 14:41:30 +08:00
5 changed files with 74 additions and 26 deletions

View File

@ -5,8 +5,8 @@
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"

Binary file not shown.

Binary file not shown.

View File

@ -1,20 +1,19 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import checkoutAudio from '@/assets/audio/checkout.mp3'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { PublishStatus } from '@/enum/business'
import { useRouterPush } from '@/hooks/common/router'
import { fetchUpdatePublishStatus } from '@/service/api/competition'
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
import { useActivityInfoStore } from '@/store/modules/activityinfo'
import { AudioController } from '@/utils/audio'
import { filterRepeat } from '@/utils/data'
const activityInfoStore = useActivityInfoStore()
const activityInfo = computed(() => activityInfoStore.activityInfo)
const { routerPushByKey, routerBack } = useRouterPush()
const audioController = new AudioController()
interface QuestionItem {
id: number
@ -128,7 +127,8 @@ function toggleFlip(index: number) {
}
else {
flippedCards.value.add(index)
audioController.play('flip') // 翻牌动效
const audio = new Audio(checkoutAudio)
audio.play().catch(e => console.error('Play audio failed', e))
}
// 每次点击都播放翻牌音效(无论翻开还是翻回去)
// audioController.play('flip')

View File

@ -2,6 +2,7 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import start3Audio from '@/assets/audio/timeout3.mp3'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
@ -26,6 +27,9 @@ const loading = ref(false)
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
const isStarted = ref(false)
const isPreparing = ref(false) // 是否正在准备中321倒计时
const showCountdown = ref(false) // 控制倒计时弹窗显示
const countdownNum = ref(3) // 倒计时数字
const pollingTimer = ref<number | null>(null) // 轮询定时器
console.log(activityInfo.value, 'activityInfo.value')
@ -130,19 +134,10 @@ async function fetchGetQuestionDetailData() {
async function handleNext() {
// 点击开始答题
if (!isStarted.value) {
isStarted.value = true
if (!isStarted.value && !isPreparing.value) {
isPreparing.value = true
// 获取题目时间,优先从 currentQuestionInfo 获取,兼容大小写
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
const duration = Number(infoTime || detailTime || 120)
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
store.startTimer(duration)
// 提交题目使用记录
// 提交题目使用记录 (调用开始接口)
try {
if (store.currentQuestionInfo && store.currentQuestionDetail) {
const params: Api.Competition.QuestionAddParams = {
@ -153,19 +148,45 @@ async function handleNext() {
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
}
console.log(params, 'params')
await fetchSubmitQuestionResult(params)
// 立即拉取一次,然后开启轮询
await fetchGetGameStatisticsData(params)
startPolling(params)
await fetchSubmitQuestionResult(params) // 提交题目使用记录
// 调用接口成功后,显示 321 倒计时弹窗并播放音乐
showCountdown.value = true
countdownNum.value = 3
const audio = new Audio(start3Audio)
audio.play().catch(e => console.error('Play audio failed', e))
const timer = setInterval(() => {
countdownNum.value--
if (countdownNum.value === 0) {
clearInterval(timer)
showCountdown.value = false
isPreparing.value = false
isStarted.value = true
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
const duration = Number(infoTime || detailTime || 120)
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
store.startTimer(duration)
// 立即拉取一次,然后开启轮询
fetchGetGameStatisticsData(params)
startPolling(params)
}
}, 1000)
}
}
catch (error) {
console.error('Failed to submit question result', error)
isPreparing.value = false
// 不阻塞流程,仅记录错误
}
}
else {
else if (isStarted.value) {
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
store.stopTimer()
stopPolling()
@ -255,8 +276,8 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
<CompetitionLayout
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
:title-bg-type="titleBgTypeNum" title-text-color="#ffffff" back-btn-text="返回"
:next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light" @back="handleBack"
@next="handleNext"
:next-btn-text="(isStarted || isPreparing) ? (isPreparing ? '准备中...' : formattedTime) : '开始答题'"
:next-disabled="isStarted || isPreparing" btn-theme="light" @back="handleBack" @next="handleNext"
>
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
<!-- 题目说明 -->
@ -270,6 +291,18 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
</div>
</div>
<!-- 321 倒计时遮罩层 -->
<div
v-if="showCountdown"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm"
>
<Transition name="scale" mode="out-in">
<div :key="countdownNum" class="text-[300px] color-#dd0315 font-bold">
{{ countdownNum }}
</div>
</Transition>
</div>
<!-- 隐藏的 Next 按钮 (方便演示右上角小区域) -->
<div class="absolute right-0 top-0 z-50 h-20 w-20 cursor-pointer" title="Next Step (Debug)" @click="handleNext">
<div class="h-full w-full flex items-center justify-center">
@ -285,7 +318,10 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
>
<!-- 图表标题栏 -->
<div v-if="isStarted" class="z-index-999 relative mb-4">
<img src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度" class="z-index-999 absolute left--30px top--25px h-auto max-h-16 w-auto">
<img
src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度"
class="z-index-999 absolute left--30px top--25px h-auto max-h-16 w-auto"
>
</div>
<!-- 柱状图ECharts -->
@ -303,4 +339,16 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
/* red-500 */
font-weight: bold;
}
/* 321 倒计时动画 */
.scale-enter-active,
.scale-leave-active {
transition: all 0.3s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.5);
}
</style>