- 将原单页竞赛流程拆分为独立路由页面(首页、封面、规则、抽题、答题、图解、组别、队伍) - 新增 Pinia store 管理竞赛状态、计时器和音频控制 - 添加 SCSS 变量文件定义主题色 - 更新路由配置和类型定义以支持新页面结构 - 重构 QuestionRenderer 组件优化样式 - 添加图标依赖并更新国际化配置 - 移动图片资源到用户目录并清理旧文件
130 lines
3.1 KiB
TypeScript
130 lines
3.1 KiB
TypeScript
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,
|
|
}
|
|
})
|