feat(user): 重构用户端竞赛流程,集成后端API并优化交互体验
- 新增 `activityinfo` store 管理活动信息,支持持久化 - 重构 `competition` store,分离数据层与流程控制,新增音效管理 - 新增游戏API模块 (`game.ts`),实现抽题、题目详情、提交结果等接口 - 用户端页面全面对接后端数据:首页轮播展示活动列表,组别/队伍页动态加载,封面页显示题目卡片 - 抽题页改为竹签翻转动画,游戏页集成题目详情与倒计时 - 优化布局组件,支持点击返回首页,统一路由参数传递 - 更新类型定义,支持富文本题目渲染 - 添加 `activityinfo` 拼写检查词条
This commit is contained in:
@ -1,25 +1,59 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
// import { uiTemplatesMapping } from '@/views/user/data/mock'
|
||||
import QuestionRenderer from '../modules/QuestionRenderer.vue'
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const store = useCompetitionStore()
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const { activityId, groupId, activityInfo } = storeToRefs(activityInfoStore)
|
||||
const currentQuestionInfo = computed(() => store.currentQuestionInfo)
|
||||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
const loading = ref(false)
|
||||
|
||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||
const isStarted = ref(false)
|
||||
|
||||
const currentNode = computed(() => store.currentNode)
|
||||
const currentQuestion = computed(() => store.currentQuestion)
|
||||
const timeLeft = computed(() => store.timeLeft)
|
||||
console.log(activityInfo.value, 'activityInfo.value')
|
||||
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
|
||||
console.log(currentQuestionDetail.value, 'currentQuestionDetail.value')
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchGetQuestionDetailData()
|
||||
})
|
||||
|
||||
// 监听倒计时,结束时自动跳转
|
||||
watch(() => store.timeLeft, (newVal) => {
|
||||
if (newVal === 0 && isStarted.value) {
|
||||
store.stopTimer()
|
||||
routerPushByKey('user_analysis')
|
||||
}
|
||||
})
|
||||
|
||||
// 优先使用 store 中的 API 数据,否则回退到 Mock 数据
|
||||
const template = computed(() => store.currentQuestionInfo?.UIType)
|
||||
const content = computed(() => {
|
||||
if (store.currentQuestionDetail) {
|
||||
const { Name, ImageUrl } = store.currentQuestionDetail
|
||||
return {
|
||||
title: Name,
|
||||
titleImage: ImageUrl,
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
const m = Math.floor(timeLeft.value / 60)
|
||||
const s = timeLeft.value % 60
|
||||
const time = store.timeLeft
|
||||
const m = Math.floor(time / 60)
|
||||
const s = time % 60
|
||||
const mm = m < 10 ? `0${m}` : m
|
||||
const ss = s < 10 ? `0${s}` : s
|
||||
return `倒计时 ${mm}:${ss}`
|
||||
@ -38,45 +72,94 @@ function handleBack() {
|
||||
routerPushByKey('user_draw')
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
/**
|
||||
* 获取题目详情
|
||||
*/
|
||||
async function fetchGetQuestionDetailData() {
|
||||
if (store.currentQuestionInfo) {
|
||||
loading.value = true
|
||||
try {
|
||||
const currentActivityId = String(activityId.value || '')
|
||||
const questionId = store.currentQuestionInfo.QuestionID || (store.currentQuestionInfo as any).Id
|
||||
const { data: detailData } = await fetchGetQuestionDetail(currentActivityId, questionId)
|
||||
console.log(detailData, 'detailData')
|
||||
if (detailData && detailData.data) {
|
||||
store.setCurrentQuestionDetail(detailData.data)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch question detail', error)
|
||||
window.$message?.error('获取题目详情失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNext() {
|
||||
// 点击开始答题
|
||||
if (!isStarted.value) {
|
||||
// 点击开始答题
|
||||
isStarted.value = true
|
||||
store.startTimer()
|
||||
store.startTimer(store.currentQuestionInfo?.QuestionTime || 10)
|
||||
|
||||
// 提交题目使用记录
|
||||
try {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
const params: Api.Competition.QuestionAddParams = {
|
||||
RoomID: activityInfo.value?.RoomID || 0,
|
||||
MainID: activityId.value || 0,
|
||||
TeamGroupID: groupId.value || 0,
|
||||
QuestionID: store.currentQuestionInfo.ID, // 使用列表中的 ID
|
||||
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
|
||||
}
|
||||
console.log(params, 'params')
|
||||
await fetchSubmitQuestionResult(params)
|
||||
await fetchGetGameStatisticsData(params)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to submit question result', error)
|
||||
// 不阻塞流程,仅记录错误
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 答题中,点击直接进入下一页(或者也可以设计为暂停等,这里按原逻辑是直接跳过)
|
||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||
store.stopTimer()
|
||||
routerPushByKey('user_analysis')
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddParams) {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
try {
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
console.log(data, 'data')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch game statistics', error)
|
||||
window.$message?.error('获取游戏现场统计结果失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
action-position="top"
|
||||
:show-back="true"
|
||||
:show-next="true"
|
||||
:title="currentNode?.moduleName || ''"
|
||||
back-btn-text="返回"
|
||||
:next-btn-text="isStarted ? formattedTime : '开始答题'"
|
||||
:next-disabled="isStarted"
|
||||
btn-theme="light"
|
||||
@back="handleBack"
|
||||
action-position="top" :show-back="false" :show-next="true"
|
||||
:title="store.currentQuestionInfo?.ActitvityQuestionName || ''" back-btn-text="返回"
|
||||
:next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="false" btn-theme="light" @back="handleBack"
|
||||
@next="handleNext"
|
||||
>
|
||||
<div class="relative h-full w-full flex flex-col items-center px-12 font-sans">
|
||||
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
||||
<!-- 题目说明 -->
|
||||
<!-- <div v-if="currentNode" class="m-2 text-2xl text-gray-800 font-medium">
|
||||
{{ currentNode.description }}
|
||||
</div> -->
|
||||
|
||||
<!-- <p>{{ store.currentQuestionInfo.ActitvityQuestionName }}</p> -->
|
||||
<!-- 题目内容区域 -->
|
||||
<div v-if="currentNode && currentQuestion" class="flex-2 mb-4 mt-12 w-full flex items-center justify-center">
|
||||
<QuestionRenderer
|
||||
:template="currentNode.uiTemplate" :content="currentQuestion.content"
|
||||
class="origin-center scale-150 transform"
|
||||
/>
|
||||
<div v-if="template && content" class="w-full flex flex-1 items-center justify-center overflow-hidden py-4">
|
||||
<div class="max-h-full max-w-full flex items-center justify-center">
|
||||
<QuestionRenderer :template="template" :content="content" class="origin-center scale-150 transform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的 Next 按钮 (方便演示:右上角小区域) -->
|
||||
@ -90,7 +173,7 @@ function handleNext() {
|
||||
|
||||
<!-- 底部图表区域 -->
|
||||
<div
|
||||
class="relative z-20 h-64 max-w-5xl w-full flex flex-col border-2 border-blue-300 rounded-xl bg-white/90 p-4 shadow-lg backdrop-blur"
|
||||
class="relative z-20 h-64 max-w-5xl w-full flex flex-col shrink-0 border-2 border-blue-300 rounded-xl bg-white/90 p-4 shadow-lg backdrop-blur"
|
||||
>
|
||||
<!-- 图表标题栏 -->
|
||||
<div class="mb-4 flex items-center justify-between px-2">
|
||||
|
||||
Reference in New Issue
Block a user