2026-02-10 08:45:26 +08:00
|
|
|
|
<!-- eslint-disable no-console -->
|
2026-02-06 17:45:44 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-02-10 08:45:26 +08:00
|
|
|
|
import { storeToRefs } from 'pinia'
|
2026-03-02 09:53:34 +08:00
|
|
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
2026-02-06 17:45:44 +08:00
|
|
|
|
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
|
|
|
|
|
import { useRouterPush } from '@/hooks/common/router'
|
2026-02-10 08:45:26 +08:00
|
|
|
|
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
|
|
|
|
|
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
2026-02-06 17:45:44 +08:00
|
|
|
|
import { useCompetitionStore } from '@/store/modules/competition'
|
|
|
|
|
|
import QuestionRenderer from '../modules/QuestionRenderer.vue'
|
2026-03-02 09:53:34 +08:00
|
|
|
|
import LineCharts from './modules/LineCharts.vue'
|
2026-02-06 17:45:44 +08:00
|
|
|
|
|
|
|
|
|
|
const { routerPushByKey } = useRouterPush()
|
|
|
|
|
|
|
|
|
|
|
|
const store = useCompetitionStore()
|
2026-02-10 08:45:26 +08:00
|
|
|
|
const activityInfoStore = useActivityInfoStore()
|
2026-02-11 17:27:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 直接从 Store 解构所需状态,保持单一数据源
|
|
|
|
|
|
const { activityId, groupId, activityInfo, teamId, teamIds } = storeToRefs(activityInfoStore)
|
2026-02-10 08:45:26 +08:00
|
|
|
|
const currentQuestionInfo = computed(() => store.currentQuestionInfo)
|
|
|
|
|
|
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
2026-02-11 17:27:08 +08:00
|
|
|
|
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
2026-03-17 17:51:12 +08:00
|
|
|
|
const RoundType = computed(() => store.currentQuestionMainInfo?.Activity_Question?.RoundType)
|
2026-02-11 17:27:08 +08:00
|
|
|
|
|
2026-02-10 08:45:26 +08:00
|
|
|
|
const loading = ref(false)
|
2026-02-06 17:45:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
|
|
|
|
|
const isStarted = ref(false)
|
2026-03-02 09:53:34 +08:00
|
|
|
|
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
2026-02-06 17:45:44 +08:00
|
|
|
|
|
2026-02-10 08:45:26 +08:00
|
|
|
|
console.log(activityInfo.value, 'activityInfo.value')
|
|
|
|
|
|
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
|
|
|
|
|
|
console.log(currentQuestionDetail.value, 'currentQuestionDetail.value')
|
2026-02-11 17:27:08 +08:00
|
|
|
|
console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo.value')
|
|
|
|
|
|
|
|
|
|
|
|
const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name || '')
|
|
|
|
|
|
|
|
|
|
|
|
// const subTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent || '')
|
2026-02-10 08:45:26 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await fetchGetQuestionDetailData()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-02 09:53:34 +08:00
|
|
|
|
onUnmounted(() => {
|
2026-03-11 17:58:57 +08:00
|
|
|
|
store.stopTimer()
|
2026-03-02 09:53:34 +08:00
|
|
|
|
stopPolling()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-10 08:45:26 +08:00
|
|
|
|
// 监听倒计时,结束时自动跳转
|
|
|
|
|
|
watch(() => store.timeLeft, (newVal) => {
|
|
|
|
|
|
if (newVal === 0 && isStarted.value) {
|
|
|
|
|
|
store.stopTimer()
|
2026-03-02 09:53:34 +08:00
|
|
|
|
stopPolling()
|
2026-02-11 17:27:08 +08:00
|
|
|
|
routerPushByKey('user_analysis', {
|
|
|
|
|
|
query: {
|
|
|
|
|
|
activityId: String(activityId.value),
|
|
|
|
|
|
groupsId: String(groupId.value),
|
|
|
|
|
|
teamId: String(teamId.value),
|
|
|
|
|
|
teamIds: teamIds.value.join(','),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用 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
|
|
|
|
|
|
})
|
2026-02-06 17:45:44 +08:00
|
|
|
|
|
|
|
|
|
|
const formattedTime = computed(() => {
|
2026-02-10 08:45:26 +08:00
|
|
|
|
const time = store.timeLeft
|
|
|
|
|
|
const m = Math.floor(time / 60)
|
|
|
|
|
|
const s = time % 60
|
2026-02-06 17:45:44 +08:00
|
|
|
|
const mm = m < 10 ? `0${m}` : m
|
|
|
|
|
|
const ss = s < 10 ? `0${s}` : s
|
|
|
|
|
|
return `倒计时 ${mm}:${ss}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-02 09:53:34 +08:00
|
|
|
|
// Chart data
|
|
|
|
|
|
const chartData = ref<any[]>([])
|
2026-02-06 17:45:44 +08:00
|
|
|
|
|
|
|
|
|
|
function handleBack() {
|
|
|
|
|
|
store.stopTimer()
|
2026-03-02 09:53:34 +08:00
|
|
|
|
stopPolling()
|
2026-02-11 17:27:08 +08:00
|
|
|
|
routerPushByKey('user_draw', {
|
|
|
|
|
|
query: {
|
|
|
|
|
|
activityId: String(activityId.value),
|
|
|
|
|
|
groupsId: String(groupId.value),
|
|
|
|
|
|
teamId: String(teamId.value),
|
|
|
|
|
|
teamIds: teamIds.value.join(','),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-02-06 17:45:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 08:45:26 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取题目详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function fetchGetQuestionDetailData() {
|
|
|
|
|
|
if (store.currentQuestionInfo) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const currentActivityId = String(activityId.value || '')
|
2026-02-11 17:27:08 +08:00
|
|
|
|
const questionId = store.currentQuestionInfo?.QuestionID || (store.currentQuestionInfo as any).Id
|
2026-02-10 08:45:26 +08:00
|
|
|
|
const { data: detailData } = await fetchGetQuestionDetail(currentActivityId, questionId)
|
|
|
|
|
|
console.log(detailData, 'detailData')
|
|
|
|
|
|
if (detailData && detailData.data) {
|
|
|
|
|
|
store.setCurrentQuestionDetail(detailData.data)
|
|
|
|
|
|
}
|
2026-02-11 17:27:08 +08:00
|
|
|
|
else {
|
|
|
|
|
|
window?.$message?.error('题目详情不存在,请联系管理员')
|
|
|
|
|
|
}
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch question detail', error)
|
|
|
|
|
|
window.$message?.error('获取题目详情失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleNext() {
|
|
|
|
|
|
// 点击开始答题
|
2026-02-06 17:45:44 +08:00
|
|
|
|
if (!isStarted.value) {
|
|
|
|
|
|
isStarted.value = true
|
2026-03-05 18:08:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取题目时间,优先从 currentQuestionInfo 获取,兼容大小写
|
|
|
|
|
|
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
|
|
|
|
|
|
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
|
|
|
|
|
|
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
|
2026-03-11 17:58:57 +08:00
|
|
|
|
const duration = Number(infoTime || detailTime || 120)
|
2026-03-05 18:08:17 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
|
|
|
|
|
|
store.startTimer(duration)
|
2026-02-10 08:45:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 提交题目使用记录
|
|
|
|
|
|
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
|
2026-03-02 09:53:34 +08:00
|
|
|
|
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
console.log(params, 'params')
|
|
|
|
|
|
await fetchSubmitQuestionResult(params)
|
2026-03-02 09:53:34 +08:00
|
|
|
|
// 立即拉取一次,然后开启轮询
|
2026-02-10 08:45:26 +08:00
|
|
|
|
await fetchGetGameStatisticsData(params)
|
2026-03-02 09:53:34 +08:00
|
|
|
|
startPolling(params)
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('Failed to submit question result', error)
|
|
|
|
|
|
// 不阻塞流程,仅记录错误
|
|
|
|
|
|
}
|
2026-02-06 17:45:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
else {
|
2026-02-10 08:45:26 +08:00
|
|
|
|
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
2026-02-06 17:45:44 +08:00
|
|
|
|
store.stopTimer()
|
2026-03-02 09:53:34 +08:00
|
|
|
|
stopPolling()
|
2026-02-11 17:27:08 +08:00
|
|
|
|
routerPushByKey('user_analysis', {
|
|
|
|
|
|
query: {
|
|
|
|
|
|
activityId: String(activityId.value),
|
|
|
|
|
|
groupsId: String(groupId.value),
|
|
|
|
|
|
teamId: String(teamId.value),
|
|
|
|
|
|
teamIds: teamIds.value.join(','),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-02-06 17:45:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 08:45:26 +08:00
|
|
|
|
|
2026-03-02 09:53:34 +08:00
|
|
|
|
function startPolling(params: Api.Competition.QuestionAddParams) {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
pollingTimer.value = window.setInterval(() => {
|
|
|
|
|
|
fetchGetGameStatisticsData(params)
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopPolling() {
|
|
|
|
|
|
if (pollingTimer.value) {
|
|
|
|
|
|
clearInterval(pollingTimer.value)
|
|
|
|
|
|
pollingTimer.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 08:45:26 +08:00
|
|
|
|
/** 获取游戏现场统计结果 */
|
|
|
|
|
|
async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddParams) {
|
|
|
|
|
|
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await fetchGetGameStatistics(_params)
|
2026-03-09 08:37:32 +08:00
|
|
|
|
// console.log(data, 'data')
|
2026-03-02 09:53:34 +08:00
|
|
|
|
// 更新图表数据
|
|
|
|
|
|
const list = (data?.data || []) as any[]
|
|
|
|
|
|
if (list && Array.isArray(list)) {
|
|
|
|
|
|
// 使用 Map 去重,以 TeamId 为准,保留最新的状态
|
|
|
|
|
|
const teamMap = new Map()
|
|
|
|
|
|
list.forEach((item) => {
|
2026-03-05 18:08:17 +08:00
|
|
|
|
let answerCount = 0
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (item.UserAnswerFont) {
|
|
|
|
|
|
const parsed = JSON.parse(item.UserAnswerFont)
|
|
|
|
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
|
|
|
|
parsed.forEach((p: any) => {
|
|
|
|
|
|
if (p.AnswerText) {
|
|
|
|
|
|
answerCount += String(p.AnswerText).length
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (e) {
|
|
|
|
|
|
console.error('解析 UserAnswerFont 失败', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 09:53:34 +08:00
|
|
|
|
teamMap.set(item.TeamName, {
|
|
|
|
|
|
group: item.TeamName || '未知队伍',
|
2026-03-05 18:08:17 +08:00
|
|
|
|
total: answerCount, // 答题数量
|
|
|
|
|
|
correct: item.Points || 0, // 预估得分 (使用外层 Points)
|
2026-03-02 09:53:34 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-03-13 17:52:13 +08:00
|
|
|
|
|
|
|
|
|
|
const newChartData = Array.from(teamMap.values())
|
|
|
|
|
|
|
|
|
|
|
|
// 只有数据真正变化时才更新
|
|
|
|
|
|
const currentDataStr = JSON.stringify(chartData.value)
|
|
|
|
|
|
const newDataStr = JSON.stringify(newChartData)
|
|
|
|
|
|
|
|
|
|
|
|
if (currentDataStr !== newDataStr) {
|
|
|
|
|
|
chartData.value = newChartData
|
|
|
|
|
|
}
|
2026-03-02 09:53:34 +08:00
|
|
|
|
}
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch game statistics', error)
|
2026-03-02 09:53:34 +08:00
|
|
|
|
// 轮询时出错不频繁弹窗,仅 log
|
2026-02-10 08:45:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 17:51:12 +08:00
|
|
|
|
|
|
|
|
|
|
const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
2026-02-06 17:45:44 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<CompetitionLayout
|
2026-02-11 17:27:08 +08:00
|
|
|
|
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
|
2026-03-17 17:51:12 +08:00
|
|
|
|
: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"
|
2026-02-06 17:45:44 +08:00
|
|
|
|
>
|
2026-02-10 08:45:26 +08:00
|
|
|
|
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
2026-02-06 17:45:44 +08:00
|
|
|
|
<!-- 题目说明 -->
|
2026-02-11 17:27:08 +08:00
|
|
|
|
<!-- <p class="mt-8px text-3xl">
|
|
|
|
|
|
{{ subTitle }}
|
|
|
|
|
|
</p> -->
|
2026-02-06 17:45:44 +08:00
|
|
|
|
<!-- 题目内容区域 -->
|
2026-03-03 08:35:28 +08:00
|
|
|
|
<div v-if="template && content" class="w-full flex flex-1 items-start justify-center overflow-hidden py-4">
|
2026-02-11 17:27:08 +08:00
|
|
|
|
<div class="max-h-full max-w-full">
|
2026-03-03 08:35:28 +08:00
|
|
|
|
<QuestionRenderer class="origin-center transform" />
|
2026-02-10 08:45:26 +08:00
|
|
|
|
</div>
|
2026-02-06 17:45:44 +08:00
|
|
|
|
</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">
|
|
|
|
|
|
<div class="h-6 w-6 rotate-45 bg-blue-500">
|
|
|
|
|
|
<icon-ic:baseline-arrow-right-alt class="text-icon text-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部图表区域 -->
|
|
|
|
|
|
<div
|
2026-02-10 08:45:26 +08:00
|
|
|
|
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"
|
2026-02-06 17:45:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
<!-- 图表标题栏 -->
|
2026-03-03 08:35:28 +08:00
|
|
|
|
<div v-if="isStarted" class="mb-4 flex items-center justify-between px-2">
|
2026-02-06 17:45:44 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<div class="h-3 w-3 rotate-45 bg-orange-400" />
|
|
|
|
|
|
<span class="text-lg text-blue-800 font-bold">答题进度</span>
|
2026-03-02 09:53:34 +08:00
|
|
|
|
<span class="text-md text-gray-400">(仅为ai判定,不代表最终结果)</span>
|
2026-02-06 17:45:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-02 09:53:34 +08:00
|
|
|
|
<!-- 柱状图(ECharts) -->
|
|
|
|
|
|
<LineCharts :data="chartData" :height="208" />
|
2026-02-06 17:45:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CompetitionLayout>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* Question Renderer Override if needed */
|
|
|
|
|
|
:deep(.question-content) {
|
|
|
|
|
|
font-size: 4rem;
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
/* red-500 */
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|