feat(user): 重构用户端竞赛流程并添加新页面
- 将原单页竞赛流程拆分为独立路由页面(首页、封面、规则、抽题、答题、图解、组别、队伍) - 新增 Pinia store 管理竞赛状态、计时器和音频控制 - 添加 SCSS 变量文件定义主题色 - 更新路由配置和类型定义以支持新页面结构 - 重构 QuestionRenderer 组件优化样式 - 添加图标依赖并更新国际化配置 - 移动图片资源到用户目录并清理旧文件
This commit is contained in:
129
apps/admin/src/store/modules/competition.ts
Normal file
129
apps/admin/src/store/modules/competition.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type { CompetitionData } from '@/views/user/types'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
import { mockData } from '@/views/user/data/mock'
|
||||
|
||||
export const useCompetitionStore = defineStore('competition', () => {
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
// --- State ---
|
||||
const data = ref<CompetitionData>(mockData)
|
||||
const currentStep = ref(0) // Node 索引
|
||||
const subStep = ref(0) // Question 索引
|
||||
const timeLeft = ref(0)
|
||||
const timerInterval = ref<number | null>(null)
|
||||
|
||||
// 音效控制器 (支持自定义音频文件,例如:{ start: '/audio/start.mp3' })
|
||||
const audioController = new AudioController()
|
||||
|
||||
// --- Getters ---
|
||||
const currentNode = computed(() => data.value.nodes[currentStep.value])
|
||||
|
||||
const currentQuestion = computed(() => {
|
||||
const questions = currentNode.value?.questions || []
|
||||
return questions[subStep.value] || questions[0]
|
||||
})
|
||||
|
||||
const isLastQuestionInNode = computed(() => {
|
||||
if (!currentNode.value)
|
||||
return true
|
||||
return subStep.value >= currentNode.value.questions.length - 1
|
||||
})
|
||||
|
||||
const isLastNode = computed(() => {
|
||||
return currentStep.value >= data.value.nodes.length - 1
|
||||
})
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
function initData() {
|
||||
// 这里可以重置或者重新拉取数据
|
||||
// data.value = ...
|
||||
resetProgress()
|
||||
}
|
||||
|
||||
function resetProgress() {
|
||||
currentStep.value = 0
|
||||
subStep.value = 0
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
// --- Audio Helpers ---
|
||||
// 音频相关逻辑已抽离到 @/utils/audio.ts
|
||||
|
||||
/**
|
||||
* 启动倒计时定时器
|
||||
*/
|
||||
function startTimer() {
|
||||
stopTimer()
|
||||
if (!currentNode.value)
|
||||
return
|
||||
|
||||
timeLeft.value = currentNode.value.config.timeLimit
|
||||
audioController.play('start') // 播放开始音效
|
||||
|
||||
timerInterval.value = window.setInterval(() => {
|
||||
if (timeLeft.value > 0) {
|
||||
timeLeft.value--
|
||||
// 剩余5秒播放倒计时音效
|
||||
if (timeLeft.value <= 5) {
|
||||
audioController.play('tick')
|
||||
}
|
||||
}
|
||||
else {
|
||||
stopTimer()
|
||||
// 时间到,自动进入解析页
|
||||
routerPushByKey('user_analysis')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value)
|
||||
timerInterval.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入下一题或下一环节
|
||||
* @returns 返回下一步的路由名称,方便组件跳转
|
||||
*/
|
||||
function nextStep() {
|
||||
stopTimer()
|
||||
|
||||
// A. 如果当前环节还有题
|
||||
if (!isLastQuestionInNode.value) {
|
||||
subStep.value++
|
||||
// 回到抽题页
|
||||
return 'user_draw'
|
||||
}
|
||||
// B. 当前环节结束,去下一个环节
|
||||
else if (!isLastNode.value) {
|
||||
currentStep.value++
|
||||
subStep.value = 0
|
||||
// 回到抽题页(新环节)
|
||||
return 'user_draw'
|
||||
}
|
||||
// C. 全部结束
|
||||
else {
|
||||
resetProgress()
|
||||
return 'user_cover'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
currentStep,
|
||||
subStep,
|
||||
timeLeft,
|
||||
currentNode,
|
||||
currentQuestion,
|
||||
initData,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
nextStep,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user