- 在结果详情页和用户分析页引入图片缓存和静默更新机制,通过比较图片大小避免不必要的更新,防止界面闪烁 - 调整用户分析页的布局样式,优化团队名称和评分信息的显示,避免内容溢出 - 禁用折线图动画以提升性能,并更新环境配置中的服务地址
292 lines
9.9 KiB
Vue
292 lines
9.9 KiB
Vue
<script setup lang="ts">
|
||
import { NImage } from 'naive-ui'
|
||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||
import { useRouterPush } from '@/hooks/common/router'
|
||
import { fetchCheckNextStep, fetchGetCurrentQuestion, fetchGetGameStatistics } from '@/service/api/game'
|
||
import { useCompetitionStore } from '@/store/modules/competition'
|
||
|
||
const store = useCompetitionStore()
|
||
|
||
const { routerPushByKey } = useRouterPush()
|
||
const route = useRoute()
|
||
const activityId = computed(() => route.query?.activityId as string)
|
||
const groupsId = computed(() => route.query?.groupsId as string)
|
||
const teamId = computed(() => route.query?.teamId as string)
|
||
const teamIds = computed(() => route.query?.teamIds as string)
|
||
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||
|
||
const TeamGroup_QuestionID = computed(() => currentQuestionMainInfo.value?.TeamGroup_QuestionID || 0)
|
||
const QuestionID = computed(() => currentQuestionMainInfo.value?.Activity_Question.ID || 0)
|
||
const QuestionDetailID = computed(() => currentQuestionDetail.value?.Id || 0)
|
||
|
||
const zoomedImage = ref<string | null>(null)
|
||
const isCompleted = ref(false)
|
||
const showNextBtn = ref(false)
|
||
|
||
const teams = ref<any[]>([])
|
||
|
||
function handleBack() {
|
||
routerPushByKey('user_game', {
|
||
query: {
|
||
activityId: activityId.value,
|
||
groupsId: groupsId.value,
|
||
teamId: teamId.value,
|
||
teamIds: teamIds.value,
|
||
},
|
||
})
|
||
}
|
||
|
||
function handleNext() {
|
||
if (isCompleted.value) {
|
||
routerPushByKey('user_groups', {
|
||
query: {
|
||
activityId: activityId.value,
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
routerPushByKey('user_draw', {
|
||
query: {
|
||
activityId: activityId.value,
|
||
groupsId: groupsId.value,
|
||
teamId: teamId.value,
|
||
teamIds: teamIds.value,
|
||
RoundType: Number(currentQuestionMainInfo.value?.Activity_Question.RoundType) || 0,
|
||
},
|
||
})
|
||
// 清空当前题目信息
|
||
store.initData()
|
||
}
|
||
|
||
/**
|
||
* 获取题目详情
|
||
* 检测是否还有题可以抽取,如果data为0,则提示没有题可抽取
|
||
*/
|
||
async function initQuestions() {
|
||
if (!activityId.value)
|
||
return
|
||
|
||
try {
|
||
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||
ActivityID: Number(activityId.value),
|
||
GroupID: Number(groupsId.value),
|
||
RoundType: (currentQuestionMainInfo.value?.Activity_Question.RoundType) || 0,
|
||
})
|
||
// eslint-disable-next-line no-console
|
||
console.log(outlineData, 'outlineData')
|
||
if (error) {
|
||
window?.$message?.error(error.message)
|
||
}
|
||
isCompleted.value = outlineData?.data?.IsCompleted || false
|
||
}
|
||
catch (error) {
|
||
window.$message?.error('获取题目失败,请重试')
|
||
console.error(error)
|
||
}
|
||
}
|
||
|
||
const timer = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||
const statisticsTimer = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||
|
||
/** 定时刷新 检查评委是否已经完成打分,是否可以下一步 fetchCheckNextStep */
|
||
async function checkNextStep() {
|
||
if (!activityId.value || !groupsId.value)
|
||
return
|
||
|
||
try {
|
||
const { data: nextStepData, error } = await fetchCheckNextStep(TeamGroup_QuestionID.value || 0)
|
||
// eslint-disable-next-line no-console
|
||
console.log(nextStepData, 'nextStepData')
|
||
if (error) {
|
||
window?.$message?.error(error.message)
|
||
}
|
||
const isUse = nextStepData?.data?.IsUse || false
|
||
showNextBtn.value = isUse
|
||
if (isUse) {
|
||
await initQuestions()
|
||
clearInterval(timer.value)
|
||
}
|
||
}
|
||
catch (error) {
|
||
window.$message?.error('检查是否可以下一步失败,请重试')
|
||
console.error(error)
|
||
}
|
||
}
|
||
|
||
const blobSizeCache = new Map<string, number>()
|
||
const objectUrlCache = new Map<string, string>()
|
||
|
||
async function checkAndUpdateImage(uniqueId: string, originalUrl: string, team: any) {
|
||
try {
|
||
const timestamp = Date.now()
|
||
const separator = originalUrl.includes('?') ? '&' : '?'
|
||
const fetchUrl = `${originalUrl}${separator}_t=${timestamp}`
|
||
|
||
const response = await fetch(fetchUrl)
|
||
if (!response.ok)
|
||
return
|
||
const blob = await response.blob()
|
||
|
||
const prevSize = blobSizeCache.get(uniqueId)
|
||
if (prevSize === blob.size) {
|
||
return // 内容大小未变,认为图片没变,不更新 src,避免闪烁
|
||
}
|
||
|
||
blobSizeCache.set(uniqueId, blob.size)
|
||
const objectUrl = URL.createObjectURL(blob)
|
||
|
||
const oldUrl = objectUrlCache.get(uniqueId)
|
||
if (oldUrl) {
|
||
URL.revokeObjectURL(oldUrl)
|
||
}
|
||
objectUrlCache.set(uniqueId, objectUrl)
|
||
|
||
// 替换为本地 Blob URL,由于数据已在内存中,更新时不会闪烁
|
||
team.UserAnswerPicture = objectUrl
|
||
}
|
||
catch (e) {
|
||
console.error('Silent image update failed:', e)
|
||
}
|
||
}
|
||
|
||
/** 获取游戏现场统计结果 */
|
||
async function fetchGetGameStatisticsData() {
|
||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||
try {
|
||
const _params = {
|
||
TeamGroupID: Number(groupsId.value),
|
||
QuestionID: Number(QuestionID.value),
|
||
QuestionDetaiID: Number(QuestionDetailID.value),
|
||
}
|
||
const { data } = await fetchGetGameStatistics(_params)
|
||
const rawTeams = (data?.data as any[]) || []
|
||
|
||
teams.value = rawTeams.map((team) => {
|
||
const uniqueId = team.TeamID || team.TeamName || Math.random().toString()
|
||
team._uniqueId = uniqueId
|
||
|
||
if (team.UserAnswerPicture) {
|
||
const originalUrl = team.UserAnswerPicture
|
||
const oldTeam = teams.value.find(t => t._uniqueId === uniqueId)
|
||
|
||
if (oldTeam && oldTeam.UserAnswerPicture && oldTeam._originalUrl === originalUrl) {
|
||
// 保持原有的 URL,避免每次轮询都刷新
|
||
team.UserAnswerPicture = oldTeam.UserAnswerPicture
|
||
}
|
||
else {
|
||
team.UserAnswerPicture = originalUrl
|
||
}
|
||
team._originalUrl = originalUrl
|
||
|
||
// 后台静默获取最新图片并对比
|
||
checkAndUpdateImage(uniqueId, originalUrl, team)
|
||
}
|
||
return team
|
||
})
|
||
}
|
||
catch (error: any) {
|
||
console.error('Failed to fetch game statistics', error)
|
||
window.$message?.error(error.message || '获取游戏现场统计结果失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 定时刷新 检查是否可以下一步
|
||
timer.value = setInterval(checkNextStep, 3000)
|
||
// 定时刷新 游戏统计数据
|
||
statisticsTimer.value = setInterval(fetchGetGameStatisticsData, 3000)
|
||
|
||
/** 销毁 定时刷新 检查是否可以下一步 */
|
||
onBeforeUnmount(() => {
|
||
clearInterval(timer.value)
|
||
clearInterval(statisticsTimer.value)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<CompetitionLayout
|
||
:show-title="true" :show-back="false" :show-next="showNextBtn" :title-bg-type="3" title-text-color="#ffffff" title="答题图解" action-position="top"
|
||
back-btn-text="返回" :next-btn-text="isCompleted ? '已完成,返选组' : '下一题'" btn-theme="light" @back="handleBack"
|
||
@next="handleNext"
|
||
>
|
||
<div class="relative h-full w-full flex flex-col items-center px-10 pt-10px font-sans">
|
||
<!-- Content -->
|
||
<div class="w-full flex flex-col flex-1 overflow-hidden">
|
||
<div class="mb-2 text-xl text-red-600 font-bold">
|
||
备注:评委正在评分中,请等待...
|
||
</div>
|
||
|
||
<div v-if="teams && teams.length > 0" class="grid grid-cols-4 h-full gap-6 pb-8">
|
||
<div
|
||
v-for="team in teams" :key="team.id"
|
||
class="flex flex-col border-2 border-gray-200 rounded-xl bg-white/90 p-4 shadow-sm"
|
||
>
|
||
<div
|
||
class="mb-3 flex items-center justify-between gap-2 rounded-full from-sky-300 to-cyan-200 bg-gradient-to-r px-4 py-2 shadow-sm"
|
||
>
|
||
<span class="overflow-hidden text-ellipsis whitespace-nowrap text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
|
||
<div class="flex shrink-0 gap-2">
|
||
<span class="whitespace-nowrap rounded-full bg-green-100/90 px-3 py-1.5 text-sm">
|
||
评委评分 <span class="text-lg text-orange-600 font-bold">{{ team.ResultPotins }}</span>
|
||
</span>
|
||
<span class="whitespace-nowrap rounded-full bg-orange-100/60 px-3 py-1.5 text-sm">
|
||
AI评分 <span class="text-md text-orange-600 font-bold">{{ team.Points }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Answer Image Area -->
|
||
<div class="relative mb-4 flex-1 overflow-hidden border border-gray-300 rounded bg-gray-50">
|
||
<!-- Grid Background Simulation -->
|
||
<div class="pointer-events-none absolute inset-0 grid grid-cols-8 gap-px bg-gray-200 opacity-20">
|
||
<!-- <div v-for="n in 64" :key="n" class="bg-white" /> -->
|
||
</div>
|
||
<NImage :src="team.UserAnswerPicture" class="h-full w-full" object-fit="contain" />
|
||
</div>
|
||
|
||
<!-- Stats & Score -->
|
||
<!-- <div class="flex flex-col gap-2">
|
||
<div class="text-sm text-gray-700 font-medium">
|
||
写了数量{{ team.writtenCount }},正确{{ team.correctCount }}个,积分{{ team.score }}
|
||
</div>
|
||
</div> -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zoom Modal -->
|
||
<div
|
||
v-if="zoomedImage" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||
@click="zoomedImage = null"
|
||
>
|
||
<div class="relative max-h-[90vh] max-w-[90vw] p-4">
|
||
<img :src="zoomedImage" class="max-h-full max-w-full rounded bg-white shadow-2xl" alt="Zoomed Answer">
|
||
<div class="absolute bottom-4 left-1/2 text-sm text-white/80 -translate-x-1/2">
|
||
点击任意处关闭
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CompetitionLayout>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* Custom Scrollbar for content if needed */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|