feat(admin): 优化用户界面交互与数据展示
- 在队伍选择页面增加卷轴展开动画,优化视觉体验 - 为游戏页面添加实时统计图表,使用 ECharts 展示各队答题进度 - 调整题目渲染器中图片的最大宽度为 200px - 在路由配置中隐藏 results_results-detail-copy 菜单项 - 修复 API 函数命名和参数类型,更新接口返回类型定义 - 移动拼音听写题目数据文件到 store 目录 - 更新 VSCode 拼写检查配置,添加 ECharts 单词
This commit is contained in:
@ -1,13 +1,14 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, 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 QuestionRenderer from '../modules/QuestionRenderer.vue'
|
||||
import LineCharts from './modules/LineCharts.vue'
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
@ -24,6 +25,7 @@ const loading = ref(false)
|
||||
|
||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||
const isStarted = ref(false)
|
||||
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
||||
|
||||
console.log(activityInfo.value, 'activityInfo.value')
|
||||
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
|
||||
@ -38,10 +40,15 @@ onMounted(async () => {
|
||||
await fetchGetQuestionDetailData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
// 监听倒计时,结束时自动跳转
|
||||
watch(() => store.timeLeft, (newVal) => {
|
||||
if (newVal === 0 && isStarted.value) {
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -75,16 +82,12 @@ const formattedTime = computed(() => {
|
||||
return `倒计时 ${mm}:${ss}`
|
||||
})
|
||||
|
||||
// Mock chart data
|
||||
const chartData = [
|
||||
{ group: '第一组', total: 42, correct: 36 },
|
||||
{ group: '第二组', total: 20, correct: 16 },
|
||||
{ group: '第三组', total: 19, correct: 19 },
|
||||
{ group: '第四组', total: 17, correct: 14 },
|
||||
]
|
||||
// Chart data
|
||||
const chartData = ref<any[]>([])
|
||||
|
||||
function handleBack() {
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_draw', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -138,10 +141,13 @@ async function handleNext() {
|
||||
TeamGroupID: groupId.value || 0,
|
||||
QuestionID: store.currentQuestionInfo.ID, // 使用列表中的 ID
|
||||
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)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -152,6 +158,7 @@ async function handleNext() {
|
||||
else {
|
||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -163,16 +170,55 @@ async function handleNext() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddParams) {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
try {
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
console.log(data, 'data')
|
||||
// 更新图表数据
|
||||
// 假设接口返回格式为: [{ TeamName: '组1', Points: 10, ResultPotins: 5 }, ...]
|
||||
// 需要根据实际接口返回结构调整映射逻辑
|
||||
const list = (data?.data || []) as any[]
|
||||
if (list && Array.isArray(list)) {
|
||||
// 根据 TeamName 分组并去重(如果是同一个小组多条记录,可能需要合并,这里假设直接展示返回列表)
|
||||
// 如果后端返回的就是每个小组一条汇总数据,直接映射即可
|
||||
// 根据需求描述,返回的数据可能有多条同名小组记录,或者就是每个小组一条
|
||||
// 鉴于示例数据中有重复的 TeamName (第一小队, 第二小队),这里做一个简单的去重或聚合
|
||||
// 实际上 fetchGetGameStatistics 应该是返回统计数据,如果返回了明细,前端需要聚合
|
||||
// 但看字段 Points 和 ResultPotins 像是统计值,这里直接映射展示,如果有重复名,ECharts category 会自动处理或需要前端聚合
|
||||
// 为了稳妥,这里按 TeamId 或 TeamName 聚合一下,取最新或求和?
|
||||
// 假设返回的是当前所有小组的状态,直接映射即可
|
||||
|
||||
// 使用 Map 去重,以 TeamId 为准,保留最新的状态
|
||||
const teamMap = new Map()
|
||||
list.forEach((item) => {
|
||||
teamMap.set(item.TeamName, {
|
||||
group: item.TeamName || '未知队伍',
|
||||
total: item.Points || 0,
|
||||
correct: item.ResultPotins || 0,
|
||||
})
|
||||
})
|
||||
chartData.value = Array.from(teamMap.values())
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch game statistics', error)
|
||||
window.$message?.error('获取游戏现场统计结果失败')
|
||||
// 轮询时出错不频繁弹窗,仅 log
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,6 +253,7 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
|
||||
<!-- 底部图表区域 -->
|
||||
<div
|
||||
v-if="isStarted"
|
||||
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"
|
||||
>
|
||||
<!-- 图表标题栏 -->
|
||||
@ -214,45 +261,12 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex gap-6 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rounded-full bg-blue-400" />
|
||||
<span class="text-gray-600">答题数量</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rounded-full bg-orange-300" />
|
||||
<span class="text-gray-600">正确数量</span>
|
||||
</div>
|
||||
<span class="text-md text-gray-400">(仅为ai判定,不代表最终结果)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 柱状图 -->
|
||||
<div class="flex flex-1 items-end justify-around px-8 pb-2">
|
||||
<div v-for="item in chartData" :key="item.group" class="w-16 flex flex-col items-center gap-2">
|
||||
<div class="h-32 flex items-end gap-2">
|
||||
<!-- Blue Bar -->
|
||||
<div
|
||||
class="relative w-6 rounded-t-md from-blue-500 to-blue-300 bg-gradient-to-t transition-all duration-1000"
|
||||
:style="{ height: `${item.total * 2}px` }"
|
||||
>
|
||||
<span class="absolute left-1/2 text-blue-800 font-bold -top-6 -translate-x-1/2">{{ item.total
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Orange Bar -->
|
||||
<div
|
||||
class="relative w-6 rounded-t-md from-orange-400 to-orange-200 bg-gradient-to-t transition-all duration-1000"
|
||||
:style="{ height: `${item.correct * 2}px` }"
|
||||
>
|
||||
<span class="absolute left-1/2 text-orange-600 font-bold -top-6 -translate-x-1/2">{{ item.correct
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-blue-800 font-bold">
|
||||
{{ item.group }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 柱状图(ECharts) -->
|
||||
<LineCharts :data="chartData" :height="208" />
|
||||
</div>
|
||||
</div>
|
||||
</CompetitionLayout>
|
||||
|
||||
Reference in New Issue
Block a user