fix: 优化图片更新逻辑防止闪烁并调整样式

- 在结果详情页和用户分析页引入图片缓存和静默更新机制,通过比较图片大小避免不必要的更新,防止界面闪烁
- 调整用户分析页的布局样式,优化团队名称和评分信息的显示,避免内容溢出
- 禁用折线图动画以提升性能,并更新环境配置中的服务地址
This commit is contained in:
2026-04-02 17:42:19 +08:00
parent a76263375a
commit 0d03fdd782
4 changed files with 126 additions and 20 deletions

View File

@ -5,7 +5,8 @@
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"

View File

@ -270,6 +270,42 @@ async function fetchCurrentQuestion(shouldSync = true) {
}
}
const blobSizeCache = new Map<string, number>()
const objectUrlCache = new Map<string, string>()
async function checkAndUpdateImage(uniqueId: string, originalUrl: string, item: 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由于数据已在内存中更新时不会闪烁
item.AnswerValuePicture = objectUrl
}
catch (e) {
console.error('Silent image update failed:', e)
}
}
/**
* 获取答题卡数据
*/
@ -371,13 +407,22 @@ async function fetchAnswerData() {
// 强制更新图片 URL防止缓存
// 优先使用 ans 中的图片,如果没有则回退到 answerData 中的图片
const imageUrl = ans.AnswerValuePicture || (parsedAnswers.length === 1 && parsedAnswers[0] === ans ? answerData.UserAnswerPicture : '')
const uniqueId = `${answerData.TeamId}-${ans.Guid}`
let finalImageUrl = imageUrl
if (imageUrl) {
if (oldAns && oldAns._originalUrl === imageUrl) {
finalImageUrl = oldAns.AnswerValuePicture
}
else {
finalImageUrl = imageUrl
}
}
const cleanBaseUrl = ''
const timestamp = new Date().getTime()
finalImageUrl = `${finalImageUrl}?_t=${timestamp}`
console.log(finalImageUrl, 'finalImageUrl')
// 判断是否手动干预:
// 1. 本地存储有状态 (hasStoredStatus) 或有分数 (hasStoredScore)
// 2. 内存中有手动标记 (oldAns._isManual)
@ -407,6 +452,7 @@ async function fetchAnswerData() {
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
AnswerText: ans.AnswerText,
AnswerValuePicture: finalImageUrl,
_originalUrl: imageUrl,
_baseImgUrl: cleanBaseUrl,
_t: timestamp, // 保存时间戳
Score: finalScore,
@ -415,17 +461,21 @@ async function fetchAnswerData() {
_aiStatus: aiStatus,
_aiScore: aiScore,
}
if (imageUrl) {
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
}
return resultItem
}
// 如果没有手动编辑
return {
const resultItem = {
...ans,
TeamId: answerData.TeamId, // [新增] 注入 TeamId
QuestionId: answerData.QuestionId, // [新增]
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
AnswerText: ans.AnswerText,
AnswerValuePicture: finalImageUrl,
_originalUrl: imageUrl,
_baseImgUrl: cleanBaseUrl,
_t: timestamp, // 保存时间戳
Status: ans.Status,
@ -434,6 +484,10 @@ async function fetchAnswerData() {
Score: null, // 默认 null
_isManual: false,
}
if (imageUrl) {
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
}
return resultItem
})
return {

View File

@ -117,6 +117,42 @@ async function checkNextStep() {
}
}
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) {
@ -129,12 +165,25 @@ async function fetchGetGameStatisticsData() {
const { data } = await fetchGetGameStatistics(_params)
const rawTeams = (data?.data as any[]) || []
// 为每个队伍的图片 URL 添加时间戳,防止缓存
teams.value = rawTeams.map((team) => {
const uniqueId = team.TeamID || team.TeamName || Math.random().toString()
team._uniqueId = uniqueId
if (team.UserAnswerPicture) {
const timestamp = Date.now()
const separator = team.UserAnswerPicture.includes('?') ? '&' : '?'
team.UserAnswerPicture = `${team.UserAnswerPicture}${separator}_t=${timestamp}`
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
})
@ -164,7 +213,7 @@ onBeforeUnmount(() => {
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-12 pt-10 font-sans">
<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">
@ -177,15 +226,17 @@ onBeforeUnmount(() => {
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 gap-2 rounded-full from-sky-300 to-cyan-200 bg-gradient-to-r px-4 py-2 shadow-sm"
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="text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
<span class="rounded-full bg-green-100/90 px-5 py-2 text-sm">
评委评分 <span class="text-lg text-orange-600 font-bold">{{ team.ResultPotins }}</span>
</span>
<span class="rounded-full bg-orange-100/60 px-5 py-2 text-sm">
AI评分 <span class="text-md text-orange-600 font-bold">{{ team.Points }}</span>
</span>
<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 -->

View File

@ -41,9 +41,9 @@ function buildOption(list: BarDatum[]): ECOption {
return {
// 启用动画,让柱子变化更平滑
animation: true,
animation: false,
animationDuration: 800,
animationEasing: 'cubicOut',
animationEasing: 'linear', // 动画缓动函数平滑过渡cubicOutlinearquadOutquadIn
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },