feat(admin): 优化用户界面交互与数据展示

- 在队伍选择页面增加卷轴展开动画,优化视觉体验
- 为游戏页面添加实时统计图表,使用 ECharts 展示各队答题进度
- 调整题目渲染器中图片的最大宽度为 200px
- 在路由配置中隐藏 results_results-detail-copy 菜单项
- 修复 API 函数命名和参数类型,更新接口返回类型定义
- 移动拼音听写题目数据文件到 store 目录
- 更新 VSCode 拼写检查配置,添加 ECharts 单词
This commit is contained in:
2026-03-02 09:53:34 +08:00
parent 2b50bd2c71
commit 81643c61ae
20 changed files with 866 additions and 203 deletions

View File

@ -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>