Compare commits
8 Commits
3d3f7afcd2
...
feat/v1.0.
| Author | SHA1 | Date | |
|---|---|---|---|
| f4c3734639 | |||
| 38a243af15 | |||
| 0d03fdd782 | |||
| a76263375a | |||
| 9bc58c6270 | |||
| 7b505ec4aa | |||
| a7cf35e6bf | |||
| c617307c79 |
@ -10,7 +10,7 @@ VITE_APP_TITLE="阅读比赛配置后台"
|
|||||||
VITE_BASE_OSS_URL = https://oss.qyzhjy.com/
|
VITE_BASE_OSS_URL = https://oss.qyzhjy.com/
|
||||||
|
|
||||||
# 上传图片的URL
|
# 上传图片的URL
|
||||||
VITE_BASE_UPLOAD_URL="http://192.168.20.150:8080"
|
VITE_BASE_UPLOAD_URL="https://api.qyzhjy.com"
|
||||||
|
|
||||||
VITE_APP_DESC="阅读比赛配置"
|
VITE_APP_DESC="阅读比赛配置"
|
||||||
|
|
||||||
|
|||||||
@ -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://172.16.10.130:5000
|
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
|
||||||
|
# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
|
||||||
|
|
||||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
"demo": "http://localhost:9528"
|
"demo": "http://localhost:9528"
|
||||||
|
|||||||
BIN
apps/admin/src/assets/audio/checkout.mp3
Normal file
BIN
apps/admin/src/assets/audio/checkout.mp3
Normal file
Binary file not shown.
BIN
apps/admin/src/assets/audio/timeout3.mp3
Normal file
BIN
apps/admin/src/assets/audio/timeout3.mp3
Normal file
Binary file not shown.
BIN
apps/admin/src/assets/imgs/user/rank-title.png
Normal file
BIN
apps/admin/src/assets/imgs/user/rank-title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 533 KiB After Width: | Height: | Size: 177 KiB |
@ -135,7 +135,7 @@ function handleClick() {
|
|||||||
.custom-btn {
|
.custom-btn {
|
||||||
background: v-bind(customBg);
|
background: v-bind(customBg);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
min-width: 210px;
|
min-width: 220px;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
@ -151,8 +151,8 @@ function handleClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.is-disabled {
|
&.is-disabled {
|
||||||
color: #eb5e55;
|
color: red;
|
||||||
filter: grayscale(0.5);
|
// filter: grayscale(0.5);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -229,12 +229,12 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="page-header flex items-center px-8 text-[30px] text-[#333333] font-bold">
|
<header class="page-header flex items-center px-8 text-[30px] text-[#333333] font-bold">
|
||||||
<!-- 左侧logo -->
|
<!-- 左侧logo -->
|
||||||
<div class="flex cursor-pointer items-center" @click="handleBackHome">
|
<div class="!z-index-99 flex cursor-pointer items-center" @click="handleBackHome">
|
||||||
<img :src="cqLogo" alt="重庆logo" class="h-20 w-auto">
|
<img :src="cqLogo" alt="重庆logo" class="h-18 w-auto">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间文字 -->
|
<!-- 中间文字 -->
|
||||||
<div class="flex flex-1 justify-center">
|
<div class="!z-index-99 flex flex-1 justify-center">
|
||||||
<img :src="qwLogo" alt="重庆logo" class="h-14 w-auto">
|
<img :src="qwLogo" alt="重庆logo" class="h-14 w-auto">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -249,17 +249,23 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
<section v-if="showTitle" class="title-section">
|
<section v-if="showTitle" class="title-section">
|
||||||
<Transition name="scroll-unfold" appear>
|
<Transition name="scroll-unfold" appear>
|
||||||
<div v-if="showContent" class="title-wrapper">
|
<div v-if="showContent" class="title-wrapper">
|
||||||
<div class="scroll-bg">
|
<div
|
||||||
|
class="scroll-bg"
|
||||||
|
:style="{ width: props.titleBgType === 3 ? '400px' : props.titleBgType === 4 ? '420px' : '480px' }"
|
||||||
|
>
|
||||||
<!-- 预渲染所有背景图,用 opacity 切换避免加载抖动 -->
|
<!-- 预渲染所有背景图,用 opacity 切换避免加载抖动 -->
|
||||||
<img
|
<img
|
||||||
v-for="(url, _key) in titleBgUrlMapping" :key="_key" :src="url" class="scroll-bg-img" :class="{
|
v-for="(url, _key) in titleBgUrlMapping" :key="_key" :src="url" class="scroll-bg-img" :class="{
|
||||||
active: titleBgUrl === url && imageLoadStatus[url] !== false,
|
active: titleBgUrl === url && imageLoadStatus[url] !== false,
|
||||||
loading: imageLoadStatus[url] === undefined,
|
loading: imageLoadStatus[url] === undefined,
|
||||||
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }" aria-hidden="true"
|
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }"
|
||||||
@load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
|
aria-hidden="true" @load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
|
||||||
>
|
>
|
||||||
<Transition name="fade-in-delay" appear>
|
<Transition name="fade-in-delay" appear>
|
||||||
<h1 v-if="showContent" class="main-title" :style="{ color: titleTextColor }">
|
<h1
|
||||||
|
v-if="showContent" class="main-title"
|
||||||
|
:style="{ color: titleTextColor, letterSpacing: props.titleBgType === 2 ? '6px' : props.titleBgType === 4 ? '1px' : '4px', fontSize: props.titleBgType === 4 ? '38px' : '' }"
|
||||||
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -285,7 +291,7 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }" type="custom"
|
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }" type="custom"
|
||||||
:text="backBtnText || ''" :theme="btnTheme" :font-size="fontSize" @click="handleBack"
|
:text="backBtnText || ''" :theme="btnTheme" :font-size="fontSize" @click="handleBack"
|
||||||
>
|
>
|
||||||
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" />
|
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="42" class="arrow-icon" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -294,14 +300,14 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
v-if="showNext" type="custom" :text="nextBtnText || '下一步'" :theme="btnTheme"
|
v-if="showNext" type="custom" :text="nextBtnText || '下一步'" :theme="btnTheme"
|
||||||
:disabled="nextDisabled" :font-size="fontSize" @click="handleNext"
|
:disabled="nextDisabled" :font-size="fontSize" @click="handleNext"
|
||||||
>
|
>
|
||||||
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="40" />
|
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="42" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
v-if="showNextBtn2" :type="nextBtnText2 ? 'text' : 'button'" :text="nextBtnText2 || '下一步'"
|
v-if="showNextBtn2" :type="nextBtnText2 ? 'text' : 'button'" :text="nextBtnText2 || '下一步'"
|
||||||
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext2"
|
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext2"
|
||||||
>
|
>
|
||||||
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="40" />
|
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="42" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@ -330,9 +336,9 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scroll-bg {
|
.scroll-bg {
|
||||||
width: 400px !important;
|
width: 400px;
|
||||||
/* Fixed width to prevent squishing during animation */
|
/* Fixed width to prevent squishing during animation */
|
||||||
height: 155px !important;
|
height: 180px !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -363,10 +369,10 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-title {
|
.main-title {
|
||||||
font-size: 2.6rem;
|
font-size: 2.66rem;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
/* Prevent text wrapping */
|
/* Prevent text wrapping */
|
||||||
@ -444,15 +450,15 @@ watch(titleBgUrl, (newUrl) => {
|
|||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -20px;
|
top: -58px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 196px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 20px 40px;
|
padding: 20px 44px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $secondary-color;
|
color: $secondary-color;
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
|||||||
@ -214,7 +214,7 @@ init()
|
|||||||
<template #prefix>
|
<template #prefix>
|
||||||
<SvgIcon :icon="tab.icon" :local-icon="tab.localIcon" class="inline-block align-text-bottom text-16px" />
|
<SvgIcon :icon="tab.icon" :local-icon="tab.localIcon" class="inline-block align-text-bottom text-16px" />
|
||||||
</template>
|
</template>
|
||||||
<div class="max-w-240px ellipsis-text">
|
<div class="max-w-240px ellipsis-text text-14px">
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</div>
|
</div>
|
||||||
</PageTab>
|
</PageTab>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ body,
|
|||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-autospace: normal;
|
text-autospace: normal;
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ defineOptions({ name: 'QuestionStore' })
|
|||||||
const currentCategory = ref<any>(null)
|
const currentCategory = ref<any>(null)
|
||||||
|
|
||||||
// Resize logic
|
// Resize logic
|
||||||
const siderWidth = ref(525)
|
const siderWidth = ref(535)
|
||||||
const LOCAL_STORAGE_KEY = 'question-store-sider-width'
|
const LOCAL_STORAGE_KEY = 'question-store-sider-width'
|
||||||
const minWidth = 300
|
const minWidth = 300
|
||||||
const maxWidth = 800
|
const maxWidth = 800
|
||||||
|
|||||||
@ -231,10 +231,10 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 overflow-hidden">
|
<div class="flex items-center gap-2 overflow-hidden">
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
icon="carbon:folder" class="flex-shrink-0 text-lg"
|
icon="carbon:folder" class="flex-shrink-0 text-sm"
|
||||||
:class="selectedKey === item.Id ? 'text-primary' : 'text-gray-400'"
|
:class="selectedKey === item.Id ? 'text-primary' : 'text-gray-400'"
|
||||||
/>
|
/>
|
||||||
<span class="truncate font-medium">{{ item.Name }} - {{ item.QuestionContent }}</span>
|
<span class="truncate text-16px font-medium">{{ item.Name }} - {{ item.QuestionContent }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<!-- eslint-disable no-console -->
|
<!-- eslint-disable no-console -->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useIntervalFn, useStorage } from '@vueuse/core'
|
||||||
import { NButton, NCard, NImage, NInputNumber, NTag, useMessage } from 'naive-ui'
|
import { NButton, NCard, NImage, NInputNumber, NTag, useMessage } from 'naive-ui'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@ -33,6 +33,9 @@ const questionInfo = ref<Api.Competition.QuestionListDetailRound>() // 题目详
|
|||||||
const currentQuestionDetailID = ref<number | string>('') // 当前选中的题目详情 ID
|
const currentQuestionDetailID = ref<number | string>('') // 当前选中的题目详情 ID
|
||||||
|
|
||||||
const nowTs = ref(Date.now())
|
const nowTs = ref(Date.now())
|
||||||
|
useIntervalFn(() => {
|
||||||
|
nowTs.value = Date.now()
|
||||||
|
}, 1000)
|
||||||
// 判断当前activityId是正在比赛的活动
|
// 判断当前activityId是正在比赛的活动
|
||||||
const isActiveActivityId = computed(() => {
|
const isActiveActivityId = computed(() => {
|
||||||
if (!currentQuestion.value)
|
if (!currentQuestion.value)
|
||||||
@ -254,7 +257,9 @@ async function fetchCurrentQuestion(shouldSync = true) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentQuestion.value = data?.data || {}
|
currentQuestion.value = data?.data || {}
|
||||||
|
const endTime = new Date(currentQuestion.value.EndTime)
|
||||||
console.warn('current question', currentQuestion.value)
|
console.warn('current question', currentQuestion.value)
|
||||||
|
console.log('当前题目结束时间是:', endTime.toLocaleString())
|
||||||
// 只在初始化时同步选中题,轮询时不同步
|
// 只在初始化时同步选中题,轮询时不同步
|
||||||
if (shouldSync) {
|
if (shouldSync) {
|
||||||
syncCurrentQuestionSelection()
|
syncCurrentQuestionSelection()
|
||||||
@ -265,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取答题卡数据
|
* 获取答题卡数据
|
||||||
*/
|
*/
|
||||||
@ -366,13 +407,22 @@ async function fetchAnswerData() {
|
|||||||
// 强制更新图片 URL,防止缓存
|
// 强制更新图片 URL,防止缓存
|
||||||
// 优先使用 ans 中的图片,如果没有则回退到 answerData 中的图片
|
// 优先使用 ans 中的图片,如果没有则回退到 answerData 中的图片
|
||||||
const imageUrl = ans.AnswerValuePicture || (parsedAnswers.length === 1 && parsedAnswers[0] === ans ? answerData.UserAnswerPicture : '')
|
const imageUrl = ans.AnswerValuePicture || (parsedAnswers.length === 1 && parsedAnswers[0] === ans ? answerData.UserAnswerPicture : '')
|
||||||
|
|
||||||
|
const uniqueId = `${answerData.TeamId}-${ans.Guid}`
|
||||||
let finalImageUrl = imageUrl
|
let finalImageUrl = imageUrl
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
if (oldAns && oldAns._originalUrl === imageUrl) {
|
||||||
|
finalImageUrl = oldAns.AnswerValuePicture
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
finalImageUrl = imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cleanBaseUrl = ''
|
const cleanBaseUrl = ''
|
||||||
const timestamp = new Date().getTime()
|
const timestamp = new Date().getTime()
|
||||||
|
|
||||||
finalImageUrl = `${finalImageUrl}?_t=${timestamp}`
|
|
||||||
console.log(finalImageUrl, 'finalImageUrl')
|
|
||||||
|
|
||||||
// 判断是否手动干预:
|
// 判断是否手动干预:
|
||||||
// 1. 本地存储有状态 (hasStoredStatus) 或有分数 (hasStoredScore)
|
// 1. 本地存储有状态 (hasStoredStatus) 或有分数 (hasStoredScore)
|
||||||
// 2. 内存中有手动标记 (oldAns._isManual)
|
// 2. 内存中有手动标记 (oldAns._isManual)
|
||||||
@ -402,6 +452,7 @@ async function fetchAnswerData() {
|
|||||||
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
||||||
AnswerText: ans.AnswerText,
|
AnswerText: ans.AnswerText,
|
||||||
AnswerValuePicture: finalImageUrl,
|
AnswerValuePicture: finalImageUrl,
|
||||||
|
_originalUrl: imageUrl,
|
||||||
_baseImgUrl: cleanBaseUrl,
|
_baseImgUrl: cleanBaseUrl,
|
||||||
_t: timestamp, // 保存时间戳
|
_t: timestamp, // 保存时间戳
|
||||||
Score: finalScore,
|
Score: finalScore,
|
||||||
@ -410,17 +461,21 @@ async function fetchAnswerData() {
|
|||||||
_aiStatus: aiStatus,
|
_aiStatus: aiStatus,
|
||||||
_aiScore: aiScore,
|
_aiScore: aiScore,
|
||||||
}
|
}
|
||||||
|
if (imageUrl) {
|
||||||
|
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
|
||||||
|
}
|
||||||
return resultItem
|
return resultItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有手动编辑
|
// 如果没有手动编辑
|
||||||
return {
|
const resultItem = {
|
||||||
...ans,
|
...ans,
|
||||||
TeamId: answerData.TeamId, // [新增] 注入 TeamId
|
TeamId: answerData.TeamId, // [新增] 注入 TeamId
|
||||||
QuestionId: answerData.QuestionId, // [新增]
|
QuestionId: answerData.QuestionId, // [新增]
|
||||||
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
||||||
AnswerText: ans.AnswerText,
|
AnswerText: ans.AnswerText,
|
||||||
AnswerValuePicture: finalImageUrl,
|
AnswerValuePicture: finalImageUrl,
|
||||||
|
_originalUrl: imageUrl,
|
||||||
_baseImgUrl: cleanBaseUrl,
|
_baseImgUrl: cleanBaseUrl,
|
||||||
_t: timestamp, // 保存时间戳
|
_t: timestamp, // 保存时间戳
|
||||||
Status: ans.Status,
|
Status: ans.Status,
|
||||||
@ -429,6 +484,10 @@ async function fetchAnswerData() {
|
|||||||
Score: null, // 默认 null
|
Score: null, // 默认 null
|
||||||
_isManual: false,
|
_isManual: false,
|
||||||
}
|
}
|
||||||
|
if (imageUrl) {
|
||||||
|
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
|
||||||
|
}
|
||||||
|
return resultItem
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -466,14 +525,11 @@ async function handleNexCurrentQuestion() {
|
|||||||
currentQuestionId.value = currentQuestion.value?.QuestionID || currentQuestionId.value
|
currentQuestionId.value = currentQuestion.value?.QuestionID || currentQuestionId.value
|
||||||
currentGroupId.value = currentQuestion.value?.TeamGroupID || currentGroupId.value
|
currentGroupId.value = currentQuestion.value?.TeamGroupID || currentGroupId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新当前时间戳,以便重新计算 canPublish
|
|
||||||
nowTs.value = Date.now()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePublish() {
|
function handlePublish() {
|
||||||
const endTimeStr = (currentQuestion.value as any)?.EndTime
|
const endTimeStr = (currentQuestion.value as any)?.EndTime
|
||||||
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 5000) {
|
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 1000) {
|
||||||
window.$message?.warning('倒计时未结束(需等待5秒延迟),暂不能发布')
|
window.$message?.warning('倒计时未结束(需等待5秒延迟),暂不能发布')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -589,7 +645,7 @@ const canPublish = computed(() => {
|
|||||||
if (!endTime)
|
if (!endTime)
|
||||||
return true
|
return true
|
||||||
// 结束时间 + 5秒延迟才能发布
|
// 结束时间 + 5秒延迟才能发布
|
||||||
const end = new Date(endTime).getTime() + 5000
|
const end = new Date(endTime).getTime() + 2000
|
||||||
return nowTs.value >= end
|
return nowTs.value >= end
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -599,8 +655,8 @@ function startPolling() {
|
|||||||
// 检查是否已经结束
|
// 检查是否已经结束
|
||||||
const endTime = (currentQuestion.value as any)?.EndTime
|
const endTime = (currentQuestion.value as any)?.EndTime
|
||||||
if (endTime) {
|
if (endTime) {
|
||||||
// 增加 12s 缓冲时间
|
// 增加 5s 缓冲时间
|
||||||
const end = new Date(endTime).getTime() + 12000
|
const end = new Date(endTime).getTime() + 3000
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
if (now > end) {
|
if (now > end) {
|
||||||
console.log('当前题目已结束(含12s缓冲),仅获取一次最终结果,不启动轮询', endTime)
|
console.log('当前题目已结束(含12s缓冲),仅获取一次最终结果,不启动轮询', endTime)
|
||||||
@ -617,25 +673,23 @@ function startPolling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchAnswerData() // 立即获取
|
fetchAnswerData() // 立即获取
|
||||||
nowTs.value = Date.now()
|
|
||||||
pollingTimer.value = setInterval(() => {
|
pollingTimer.value = setInterval(() => {
|
||||||
// 每次轮询前再次检查是否过期
|
// 每次轮询前再次检查是否过期
|
||||||
const currentEndTime = (currentQuestion.value as any)?.EndTime
|
const currentEndTime = (currentQuestion.value as any)?.EndTime
|
||||||
if (currentEndTime) {
|
if (currentEndTime) {
|
||||||
// 增加 12s 缓冲时间
|
// 增加 1s 缓冲时间
|
||||||
const endTs = new Date(currentEndTime).getTime() + 12000
|
const endTs = new Date(currentEndTime).getTime() + 1000
|
||||||
if (Date.now() > endTs) {
|
if (Date.now() > endTs) {
|
||||||
console.log('题目时间已到(含12s缓冲),停止轮询', currentEndTime)
|
console.log('题目时间已到(含1s缓冲),停止轮询', currentEndTime)
|
||||||
stopPolling()
|
stopPolling()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nowTs.value = Date.now()
|
|
||||||
|
|
||||||
// 轮询时同时更新当前题目信息(获取最新的 EndTime),但不触发同步
|
// 轮询时同时更新当前题目信息(获取最新的 EndTime),但不触发同步
|
||||||
fetchCurrentQuestion(false)
|
fetchCurrentQuestion(false)
|
||||||
fetchAnswerData()
|
fetchAnswerData()
|
||||||
}, 3000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
|
|||||||
@ -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() {
|
async function fetchGetGameStatisticsData() {
|
||||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||||
@ -129,12 +165,25 @@ async function fetchGetGameStatisticsData() {
|
|||||||
const { data } = await fetchGetGameStatistics(_params)
|
const { data } = await fetchGetGameStatistics(_params)
|
||||||
const rawTeams = (data?.data as any[]) || []
|
const rawTeams = (data?.data as any[]) || []
|
||||||
|
|
||||||
// 为每个队伍的图片 URL 添加时间戳,防止缓存
|
|
||||||
teams.value = rawTeams.map((team) => {
|
teams.value = rawTeams.map((team) => {
|
||||||
|
const uniqueId = team.TeamID || team.TeamName || Math.random().toString()
|
||||||
|
team._uniqueId = uniqueId
|
||||||
|
|
||||||
if (team.UserAnswerPicture) {
|
if (team.UserAnswerPicture) {
|
||||||
const timestamp = Date.now()
|
const originalUrl = team.UserAnswerPicture
|
||||||
const separator = team.UserAnswerPicture.includes('?') ? '&' : '?'
|
const oldTeam = teams.value.find(t => t._uniqueId === uniqueId)
|
||||||
team.UserAnswerPicture = `${team.UserAnswerPicture}${separator}_t=${timestamp}`
|
|
||||||
|
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
|
return team
|
||||||
})
|
})
|
||||||
@ -161,14 +210,14 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<CompetitionLayout
|
<CompetitionLayout
|
||||||
:show-title="true" :show-back="false" :show-next="showNextBtn" :title-bg-type="3" title-text-color="#ffffff" title="答题图解" action-position="top"
|
: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"
|
back-btn-text="返回" :next-btn-text="isCompleted ? '已完成,返选组' : '下一题'" btn-theme="light" @back="handleBack"
|
||||||
@next="handleNext"
|
@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 -->
|
<!-- Content -->
|
||||||
<div class="w-full flex flex-col flex-1 overflow-hidden">
|
<div class="w-full flex flex-col flex-1 overflow-hidden">
|
||||||
<div class="mb-2 text-xl text-red-600 font-bold">
|
<div class="mb-2 text-xl text-red-600 font-bold">
|
||||||
备注:评委正在评分中,请等待。。。
|
备注:评委正在评分中,请等待...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="teams && teams.length > 0" class="grid grid-cols-4 h-full gap-6 pb-8">
|
<div v-if="teams && teams.length > 0" class="grid grid-cols-4 h-full gap-6 pb-8">
|
||||||
@ -177,15 +226,17 @@ onBeforeUnmount(() => {
|
|||||||
class="flex flex-col border-2 border-gray-200 rounded-xl bg-white/90 p-4 shadow-sm"
|
class="flex flex-col border-2 border-gray-200 rounded-xl bg-white/90 p-4 shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<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="overflow-hidden text-ellipsis whitespace-nowrap text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
|
||||||
<span class="rounded-full bg-orange-100/60 px-5 py-2 text-sm">
|
<div class="flex shrink-0 gap-2">
|
||||||
AI评分 <span class="text-md text-orange-600 font-bold">{{ team.Points }}</span>
|
<span class="whitespace-nowrap rounded-full bg-green-100/90 px-3 py-1.5 text-sm">
|
||||||
</span>
|
评委评分 <span class="text-lg text-orange-600 font-bold">{{ team.ResultPotins }}</span>
|
||||||
<span class="rounded-full bg-green-100/60 px-5 py-2 text-sm">
|
</span>
|
||||||
评委评分 <span class="text-md text-green-600 font-bold">{{ team.ResultPotins }}</span>
|
<span class="whitespace-nowrap rounded-full bg-orange-100/60 px-3 py-1.5 text-sm">
|
||||||
</span>
|
AI评分 <span class="text-md text-orange-600 font-bold">{{ team.Points }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Answer Image Area -->
|
<!-- Answer Image Area -->
|
||||||
|
|||||||
@ -1,20 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import checkoutAudio from '@/assets/audio/checkout.mp3'
|
||||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||||
import { PublishStatus } from '@/enum/business'
|
import { PublishStatus } from '@/enum/business'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
import { fetchUpdatePublishStatus } from '@/service/api/competition'
|
import { fetchUpdatePublishStatus } from '@/service/api/competition'
|
||||||
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
|
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
|
||||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||||
import { AudioController } from '@/utils/audio'
|
|
||||||
import { filterRepeat } from '@/utils/data'
|
import { filterRepeat } from '@/utils/data'
|
||||||
|
|
||||||
const activityInfoStore = useActivityInfoStore()
|
const activityInfoStore = useActivityInfoStore()
|
||||||
const activityInfo = computed(() => activityInfoStore.activityInfo)
|
const activityInfo = computed(() => activityInfoStore.activityInfo)
|
||||||
|
|
||||||
const { routerPushByKey, routerBack } = useRouterPush()
|
const { routerPushByKey, routerBack } = useRouterPush()
|
||||||
const audioController = new AudioController()
|
|
||||||
|
|
||||||
interface QuestionItem {
|
interface QuestionItem {
|
||||||
id: number
|
id: number
|
||||||
@ -128,7 +127,8 @@ function toggleFlip(index: number) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
flippedCards.value.add(index)
|
flippedCards.value.add(index)
|
||||||
audioController.play('flip') // 翻牌动效
|
const audio = new Audio(checkoutAudio)
|
||||||
|
audio.play().catch(e => console.error('Play audio failed', e))
|
||||||
}
|
}
|
||||||
// 每次点击都播放翻牌音效(无论翻开还是翻回去)
|
// 每次点击都播放翻牌音效(无论翻开还是翻回去)
|
||||||
// audioController.play('flip')
|
// audioController.play('flip')
|
||||||
@ -240,7 +240,7 @@ onMounted(async () => {
|
|||||||
<!-- 文字内容 -->
|
<!-- 文字内容 -->
|
||||||
<div class="relative z-10 transform text-center">
|
<div class="relative z-10 transform text-center">
|
||||||
<div
|
<div
|
||||||
class="card-text text-2xl text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
|
class="card-text text-26px text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
|
||||||
>
|
>
|
||||||
<span class="block">{{ node.moduleName.slice(0, 2) }}</span>
|
<span class="block">{{ node.moduleName.slice(0, 2) }}</span>
|
||||||
<span class="block">{{ node.moduleName.slice(2) }}</span>
|
<span class="block">{{ node.moduleName.slice(2) }}</span>
|
||||||
@ -288,7 +288,7 @@ onMounted(async () => {
|
|||||||
// [过冲状态]
|
// [过冲状态]
|
||||||
// 飞到眼前一点点 (Z=50px)
|
// 飞到眼前一点点 (Z=50px)
|
||||||
// 角度稍微回弹 (rotateY -15deg)
|
// 角度稍微回弹 (rotateY -15deg)
|
||||||
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.05);
|
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
@ -328,8 +328,8 @@ onMounted(async () => {
|
|||||||
// [翻转状态]
|
// [翻转状态]
|
||||||
// rotateY(180deg): 绕Y轴旋转180度,实现翻面
|
// rotateY(180deg): 绕Y轴旋转180度,实现翻面
|
||||||
// scale(1.05): 翻转同时放大1.05倍,产生"向观众逼近"的视觉冲击力
|
// scale(1.05): 翻转同时放大1.05倍,产生"向观众逼近"的视觉冲击力
|
||||||
transform: rotateY(180deg) scale(1.06);
|
transform: rotateY(180deg) scale(1.08);
|
||||||
box-shadow: 0 20px 40px -5px rgba(0, 0, 0, 0.3); // [动态阴影] 翻转浮起时阴影更深、更扩散
|
box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.3); // [动态阴影] 翻转浮起时阴影更深、更扩散
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +344,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// border: 1px solid rgba(255, 255, 255, 0.5); // 增加微妙的边框
|
// border: 1px solid rgba(231, 215, 144, 0.5); // 增加微妙的边框
|
||||||
}
|
}
|
||||||
|
|
||||||
// 正面(实际是卡背图案)
|
// 正面(实际是卡背图案)
|
||||||
@ -367,7 +367,7 @@ onMounted(async () => {
|
|||||||
// 背面(实际是内容面)
|
// 背面(实际是内容面)
|
||||||
.flip-card-back {
|
.flip-card-back {
|
||||||
// background-color: #fff;
|
// background-color: #fff;
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(200deg);
|
||||||
// border: 4px solid #fff;
|
// border: 4px solid #fff;
|
||||||
|
|
||||||
.card-content-design {
|
.card-content-design {
|
||||||
@ -409,7 +409,7 @@ onMounted(async () => {
|
|||||||
// animation-delay: 0.2s; 确保卡片翻转到一半(正对屏幕)时才开始闪光,
|
// animation-delay: 0.2s; 确保卡片翻转到一半(正对屏幕)时才开始闪光,
|
||||||
// 模拟光线随着角度变化掠过卡片表面的物理效果
|
// 模拟光线随着角度变化掠过卡片表面的物理效果
|
||||||
animation: shine-sweep 3s ease-out forwards;
|
animation: shine-sweep 3s ease-out forwards;
|
||||||
animation-delay: 0.2s;
|
animation-delay: 1s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,14 +455,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
10% {
|
10% {
|
||||||
// 刚进入时瞬间变亮
|
// 刚进入时瞬间变亮
|
||||||
opacity: 0.8;
|
opacity: 0.8; // 0.8 是一个半透明值,模拟光束的透明度,值越大越透明
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
// 扫出视野
|
// 扫出视野
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 150%;
|
left: 150%;
|
||||||
opacity: 0;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import start3Audio from '@/assets/audio/timeout3.mp3'
|
||||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
||||||
@ -26,6 +27,9 @@ const loading = ref(false)
|
|||||||
|
|
||||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||||
const isStarted = ref(false)
|
const isStarted = ref(false)
|
||||||
|
const isPreparing = ref(false) // 是否正在准备中(321倒计时)
|
||||||
|
const showCountdown = ref(false) // 控制倒计时弹窗显示
|
||||||
|
const countdownNum = ref(3) // 倒计时数字
|
||||||
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
||||||
|
|
||||||
console.log(activityInfo.value, 'activityInfo.value')
|
console.log(activityInfo.value, 'activityInfo.value')
|
||||||
@ -130,19 +134,10 @@ async function fetchGetQuestionDetailData() {
|
|||||||
|
|
||||||
async function handleNext() {
|
async function handleNext() {
|
||||||
// 点击开始答题
|
// 点击开始答题
|
||||||
if (!isStarted.value) {
|
if (!isStarted.value && !isPreparing.value) {
|
||||||
isStarted.value = true
|
isPreparing.value = true
|
||||||
|
|
||||||
// 获取题目时间,优先从 currentQuestionInfo 获取,兼容大小写
|
// 提交题目使用记录 (调用开始接口)
|
||||||
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
|
|
||||||
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
|
|
||||||
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
|
|
||||||
const duration = Number(infoTime || detailTime || 120)
|
|
||||||
|
|
||||||
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
|
|
||||||
store.startTimer(duration)
|
|
||||||
|
|
||||||
// 提交题目使用记录
|
|
||||||
try {
|
try {
|
||||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||||
const params: Api.Competition.QuestionAddParams = {
|
const params: Api.Competition.QuestionAddParams = {
|
||||||
@ -153,19 +148,45 @@ async function handleNext() {
|
|||||||
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
|
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
|
||||||
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
|
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
|
||||||
}
|
}
|
||||||
console.log(params, 'params')
|
await fetchSubmitQuestionResult(params) // 提交题目使用记录
|
||||||
await fetchSubmitQuestionResult(params)
|
|
||||||
// 立即拉取一次,然后开启轮询
|
// 调用接口成功后,显示 321 倒计时弹窗并播放音乐
|
||||||
await fetchGetGameStatisticsData(params)
|
showCountdown.value = true
|
||||||
startPolling(params)
|
countdownNum.value = 3
|
||||||
|
|
||||||
|
const audio = new Audio(start3Audio)
|
||||||
|
audio.play().catch(e => console.error('Play audio failed', e))
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
countdownNum.value--
|
||||||
|
if (countdownNum.value === 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
showCountdown.value = false
|
||||||
|
isPreparing.value = false
|
||||||
|
isStarted.value = true
|
||||||
|
|
||||||
|
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
|
||||||
|
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
|
||||||
|
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
|
||||||
|
const duration = Number(infoTime || detailTime || 120)
|
||||||
|
|
||||||
|
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
|
||||||
|
store.startTimer(duration)
|
||||||
|
|
||||||
|
// 立即拉取一次,然后开启轮询
|
||||||
|
fetchGetGameStatisticsData(params)
|
||||||
|
startPolling(params)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Failed to submit question result', error)
|
console.error('Failed to submit question result', error)
|
||||||
|
isPreparing.value = false
|
||||||
// 不阻塞流程,仅记录错误
|
// 不阻塞流程,仅记录错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (isStarted.value) {
|
||||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||||
store.stopTimer()
|
store.stopTimer()
|
||||||
stopPolling()
|
stopPolling()
|
||||||
@ -255,8 +276,8 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
|||||||
<CompetitionLayout
|
<CompetitionLayout
|
||||||
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
|
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
|
||||||
:title-bg-type="titleBgTypeNum" title-text-color="#ffffff" back-btn-text="返回"
|
: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-btn-text="(isStarted || isPreparing) ? (isPreparing ? '准备中...' : formattedTime) : '开始答题'"
|
||||||
@next="handleNext"
|
:next-disabled="isStarted || isPreparing" btn-theme="light" @back="handleBack" @next="handleNext"
|
||||||
>
|
>
|
||||||
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
||||||
<!-- 题目说明 -->
|
<!-- 题目说明 -->
|
||||||
@ -270,6 +291,18 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 321 倒计时遮罩层 -->
|
||||||
|
<div
|
||||||
|
v-if="showCountdown"
|
||||||
|
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<Transition name="scale" mode="out-in">
|
||||||
|
<div :key="countdownNum" class="text-[300px] color-#dd0315 font-bold">
|
||||||
|
{{ countdownNum }}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 隐藏的 Next 按钮 (方便演示:右上角小区域) -->
|
<!-- 隐藏的 Next 按钮 (方便演示:右上角小区域) -->
|
||||||
<div class="absolute right-0 top-0 z-50 h-20 w-20 cursor-pointer" title="Next Step (Debug)" @click="handleNext">
|
<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-full w-full flex items-center justify-center">
|
||||||
@ -285,7 +318,10 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
|||||||
>
|
>
|
||||||
<!-- 图表标题栏 -->
|
<!-- 图表标题栏 -->
|
||||||
<div v-if="isStarted" class="z-index-999 relative mb-4">
|
<div v-if="isStarted" class="z-index-999 relative mb-4">
|
||||||
<img src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度" class="z-index-999 absolute left--26px top--22px h-auto max-h-16 w-auto">
|
<img
|
||||||
|
src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度"
|
||||||
|
class="z-index-999 absolute left--30px top--25px h-auto max-h-16 w-auto"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 柱状图(ECharts) -->
|
<!-- 柱状图(ECharts) -->
|
||||||
@ -303,4 +339,16 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
|||||||
/* red-500 */
|
/* red-500 */
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 321 倒计时动画 */
|
||||||
|
.scale-enter-active,
|
||||||
|
.scale-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-enter-from,
|
||||||
|
.scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -40,11 +40,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
|||||||
const correct = list.map(i => i.correct)
|
const correct = list.map(i => i.correct)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 禁用动画,避免频繁刷新时的抖动
|
// 启用动画,让柱子变化更平滑
|
||||||
animation: false,
|
animation: false,
|
||||||
|
animationDuration: 800,
|
||||||
|
animationEasing: 'linear', // 动画缓动函数,平滑过渡,cubicOut|linear|quadOut|quadIn
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: { color: '#1e3a8a' },
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['答题数量', '预估得分'],
|
data: ['答题数量', '预估得分'],
|
||||||
@ -52,17 +58,18 @@ function buildOption(list: BarDatum[]): ECOption {
|
|||||||
right: 0,
|
right: 0,
|
||||||
icon: 'circle',
|
icon: 'circle',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#666',
|
color: '#64748b',
|
||||||
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: { left: 16, right: 16, top: 40, bottom: 0, containLabel: true },
|
grid: { left: 16, right: 16, top: 70, bottom: 0, containLabel: true },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: categories,
|
data: categories,
|
||||||
axisTick: { show: false }, // 隐藏 x 轴刻度线
|
axisTick: { show: false }, // 隐藏 x 轴刻度线
|
||||||
axisLine: { show: false }, // 隐藏 x 轴线
|
axisLine: { show: false }, // 隐藏 x 轴线
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#003366', // 深蓝色字体
|
color: '#1e3a8a', // 深蓝色字体
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
margin: 16,
|
margin: 16,
|
||||||
@ -82,6 +89,7 @@ function buildOption(list: BarDatum[]): ECOption {
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: total,
|
data: total,
|
||||||
barWidth: 24,
|
barWidth: 24,
|
||||||
|
barGap: '40%', // 柱子间的间隙
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
// 从上到下:浅蓝 -> 深蓝
|
// 从上到下:浅蓝 -> 深蓝
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
@ -89,14 +97,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
|||||||
{ offset: 1, color: '#3b82f6' }, // blue-500 (底部深色)
|
{ offset: 1, color: '#3b82f6' }, // blue-500 (底部深色)
|
||||||
]),
|
]),
|
||||||
borderRadius: [4, 4, 0, 0],
|
borderRadius: [4, 4, 0, 0],
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(59, 130, 246, 0.2)',
|
||||||
|
shadowOffsetY: 4,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: '#003366', // 深蓝色
|
color: '#1e3a8a', // 深蓝色
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
offset: [0, 2],
|
offset: [0, -4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -111,14 +122,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
|||||||
{ offset: 1, color: '#f97316' }, // orange-500 (底部深色)
|
{ offset: 1, color: '#f97316' }, // orange-500 (底部深色)
|
||||||
]),
|
]),
|
||||||
borderRadius: [4, 4, 0, 0],
|
borderRadius: [4, 4, 0, 0],
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(249, 115, 22, 0.2)',
|
||||||
|
shadowOffsetY: 4,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: '#ea580c', // orange-600
|
color: '#ea580c', // orange-600
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
offset: [0, 2],
|
offset: [0, -4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -126,7 +126,7 @@ watch(
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 text-gray-800 font-bold leading-relaxed">
|
<div class="mb-1 text-gray-800 font-bold leading-relaxed">
|
||||||
<div class="rich-content content-with-bg pinyin-text ml-3 mt-4 flex text-7xl !max-w-100% !px-16 !py-8 !tracking-[0.08em]">
|
<div class="rich-content content-with-bg pinyin-text ml-3 mt-4 flex text-7xl !max-w-100% !px-16 !py-4 !tracking-[0.08em]">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,8 +135,8 @@ watch(
|
|||||||
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
||||||
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
|
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
|
||||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||||
<div class="rich-content ml-3 mt-4 flex gap-1 text-6xl tracking-[0.06em]">
|
<div class="rich-content ml-3 mt-4 flex gap-1 text-5xl tracking-[0.06em]">
|
||||||
请写出含有“ <span class="text-color-red !text-6xl">{{ content }}</span>”的汉字,书写时间为60秒。
|
请写出含有“ <span class="text-color-red !text-5xl">{{ content }}</span>”的汉字,书写时间为60秒。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -153,7 +153,7 @@ watch(
|
|||||||
<!-- 模板E: 成语-文字要求 -->
|
<!-- 模板E: 成语-文字要求 -->
|
||||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
|
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<img :src="ImageUrl" preview-disabled class="max-h-380px max-w-700px" object-fit="cover">
|
<img :src="ImageUrl" preview-disabled class="max-h-368px max-w-700px" object-fit="cover">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -173,8 +173,8 @@ watch(
|
|||||||
|
|
||||||
<!-- 模板: 场景猜诗句 -->
|
<!-- 模板: 场景猜诗句 -->
|
||||||
<div v-else-if="template === QuestionCategoryEnum.SceneBasedPoemGuessing" class="w-full flex flex-col items-center">
|
<div v-else-if="template === QuestionCategoryEnum.SceneBasedPoemGuessing" class="w-full flex flex-col items-center">
|
||||||
<div class="mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-20">
|
<div class="mb-1px p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-16">
|
||||||
<div class="rich-content" v-html="content" />
|
<div class="rich-content indent-[2em]" v-html="content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ watch(
|
|||||||
>
|
>
|
||||||
<!-- 上方:词组 -->
|
<!-- 上方:词组 -->
|
||||||
<div class="flex flex-wrap justify-center gap-4">
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
<div v-for="(word, index) in associationWords" :key="`word-${index}`" class="association-word text-bold">
|
<div v-for="(word, index) in associationWords" :key="`word-${index}`" class="association-word text-bold text-center text-3xl">
|
||||||
{{ word }}
|
{{ word }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -215,9 +215,9 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模板: 诗词常识 -->
|
<!-- 模板: 加时赛诗词常识 -->
|
||||||
<div v-else-if="template === QuestionCategoryEnum.PoetryCommonKnowledge" class="w-full flex flex-col items-center">
|
<div v-else-if="template === QuestionCategoryEnum.PoetryCommonKnowledge" class="w-full flex flex-col items-center">
|
||||||
<div class="mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-20">
|
<div class="line-height mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed">
|
||||||
<div class="rich-content" v-html="content" />
|
<div class="rich-content" v-html="content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -445,8 +445,7 @@ watch(
|
|||||||
:deep(p),
|
:deep(p),
|
||||||
:deep(span),
|
:deep(span),
|
||||||
:deep(div) {
|
:deep(div) {
|
||||||
font-size: 2.2rem;
|
font-size: 2rem;
|
||||||
/* text-5xl */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,15 +493,14 @@ watch(
|
|||||||
.association-word {
|
.association-word {
|
||||||
background: url('@/assets/imgs/user/dui-kuang.png') no-repeat center;
|
background: url('@/assets/imgs/user/dui-kuang.png') no-repeat center;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
min-width: 160px;
|
min-width: 170px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 32px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0 20px;
|
letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.association-char {
|
.association-char {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { DataTableColumns } from 'naive-ui'
|
|||||||
import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
|
import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import title from '@/assets/imgs/user/rank-title.svg'
|
import title from '@/assets/imgs/user/rank-title.png'
|
||||||
import ActionButton from '@/components/custom/user/ActionButton.vue'
|
import ActionButton from '@/components/custom/user/ActionButton.vue'
|
||||||
import { fetchUserRankList } from '@/service/api/rank'
|
import { fetchUserRankList } from '@/service/api/rank'
|
||||||
import ExtraTimeModal from './components/ExtraTimeModal.vue'
|
import ExtraTimeModal from './components/ExtraTimeModal.vue'
|
||||||
@ -57,6 +57,23 @@ const columns = ref<DataTableColumns<TeamData>>([
|
|||||||
|
|
||||||
const tableData = ref<TeamData[]>([])
|
const tableData = ref<TeamData[]>([])
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
const mockUserRankList: Api.Rank.ActivityRankResList = Array.from({ length: 13 }, (_, teamIndex) => {
|
||||||
|
// eslint-disable-next-line ts/no-shadow
|
||||||
|
const questions = Array.from({ length: 10 }, (_, questionIndex) => {
|
||||||
|
const QuestionNO = questionIndex + 1
|
||||||
|
const ResultPotin = Math.max(0, 10 - teamIndex - (questionIndex % 3))
|
||||||
|
return { QuestionNO, ResultPotin }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
TeamID: 1001 + teamIndex,
|
||||||
|
Name: `测试队伍${teamIndex + 1}`,
|
||||||
|
TotalPoint: questions.reduce((sum, item) => sum + item.ResultPotin, 0),
|
||||||
|
Questions: questions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function getUserRankList() {
|
async function getUserRankList() {
|
||||||
const { data, error } = await fetchUserRankList(MainID, RoundType)
|
const { data, error } = await fetchUserRankList(MainID, RoundType)
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -64,9 +81,11 @@ async function getUserRankList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.data && data.data.length > 0) {
|
const rankSource = data?.data || []
|
||||||
|
|
||||||
|
if (rankSource.length > 0) {
|
||||||
// 动态生成题目列
|
// 动态生成题目列
|
||||||
const firstTeam = data.data[0]
|
const firstTeam = rankSource[0]
|
||||||
const questionColumns = firstTeam.Questions.map(q => ({
|
const questionColumns = firstTeam.Questions.map(q => ({
|
||||||
title: `第${q.QuestionNO}题`,
|
title: `第${q.QuestionNO}题`,
|
||||||
key: `q${q.QuestionNO}`,
|
key: `q${q.QuestionNO}`,
|
||||||
@ -94,12 +113,12 @@ async function getUserRankList() {
|
|||||||
return <span>{rank}</span>
|
return <span>{rank}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '队伍名称', key: 'teamName', align: 'left', width: 150, className: 'team-name' },
|
{ title: '队伍名称', key: 'teamName', align: 'left', width: 190, className: 'team-name' },
|
||||||
...questionColumns,
|
...questionColumns,
|
||||||
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
|
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
|
||||||
]
|
]
|
||||||
|
|
||||||
tableData.value = data.data.map((item, index) => {
|
tableData.value = rankSource.map((item, index) => {
|
||||||
const questions: Record<string, number> = {}
|
const questions: Record<string, number> = {}
|
||||||
item.Questions.forEach((q) => {
|
item.Questions.forEach((q) => {
|
||||||
questions[`q${q.QuestionNO}`] = q.ResultPotin
|
questions[`q${q.QuestionNO}`] = q.ResultPotin
|
||||||
@ -114,7 +133,7 @@ async function getUserRankList() {
|
|||||||
} as TeamData
|
} as TeamData
|
||||||
})
|
})
|
||||||
|
|
||||||
teamOptions.value = data.data.map(item => ({ label: item.Name, value: item.TeamID }))
|
teamOptions.value = rankSource.map(item => ({ label: item.Name, value: item.TeamID }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,21 +156,21 @@ function rowClassName(row: TeamData) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CompetitionLayout :show-back="false" :show-next="false" :show-title="false">
|
<CompetitionLayout :show-back="false" :show-next="false" :show-title="false">
|
||||||
<div class="leaderboard-container">
|
<div class="leaderboard-container h-100%">
|
||||||
<div class="leaderboard-header">
|
<div class="leaderboard-header !z-index-99">
|
||||||
<NImage :src="title" alt="logo" preview-disabled class="mt--60px" object-fit="contain" />
|
<NImage :src="title" alt="logo" preview-disabled class="mt--42px" object-fit="contain" />
|
||||||
<p class="mt--20px text-center text-lg text-#6b778d">
|
<!-- <p class="mt--16px text-center text-18px text-#6b778d">
|
||||||
"勤学如春起之苗,不见其增,日有所长"
|
"勤学如春起之苗,不见其增,日有所长"
|
||||||
</p>
|
</p> -->
|
||||||
<ActionButton
|
<ActionButton
|
||||||
v-if="hasExtraTimeBtn" class="position-absolute bottom-20px right-20px" type="text" text="加时赛"
|
v-if="hasExtraTimeBtn" class="!z-index-100 position-absolute right-20px top-20px" type="text"
|
||||||
@click="handleAddTime"
|
text="加时赛" @click="handleAddTime"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="leaderboard-content">
|
<div class="leaderboard-content">
|
||||||
<NDataTable
|
<NDataTable
|
||||||
:columns="columns" :data="tableData" :bordered="false" :bottom-bordered="false" size="large"
|
:columns="columns" :data="tableData" :bordered="false" max-height="100%" :bottom-bordered="false"
|
||||||
:row-class-name="rowClassName" class="rank-table"
|
size="large" :row-class-name="rowClassName" class="rank-table !h-100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -163,7 +182,7 @@ function rowClassName(row: TeamData) {
|
|||||||
/* 1. 整体容器和背景 */
|
/* 1. 整体容器和背景 */
|
||||||
.leaderboard-container {
|
.leaderboard-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
background-image: url('https://raw.githubusercontent.com/0x3f3f3f3f/figure-bed/main/img/20240524150352.png');
|
background-image: url('https://raw.githubusercontent.com/0x3f3f3f3f/figure-bed/main/img/20240524150352.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@ -193,19 +212,12 @@ function rowClassName(row: TeamData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-header h1 {
|
.leaderboard-header h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a2a44;
|
color: #1a2a44;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .leaderboard-header p {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #6b778d;
|
|
||||||
margin-top: 8px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* 3. 表格内容区 */
|
|
||||||
.leaderboard-content {
|
.leaderboard-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* max-width: 1200px; */
|
/* max-width: 1200px; */
|
||||||
@ -213,6 +225,7 @@ function rowClassName(row: TeamData) {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 24px rgba(26, 42, 68, 0.05);
|
box-shadow: 0 8px 24px rgba(26, 42, 68, 0.05);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: calc(100% - 220px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 4. Naive UI 表格深度定制 */
|
/* 4. Naive UI 表格深度定制 */
|
||||||
@ -221,7 +234,7 @@ function rowClassName(row: TeamData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.rank-table .n-data-table-th) {
|
:deep(.rank-table .n-data-table-th) {
|
||||||
font-size: 14px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
border-bottom: 2px solid #dee2e6 !important;
|
border-bottom: 2px solid #dee2e6 !important;
|
||||||
@ -236,11 +249,11 @@ function rowClassName(row: TeamData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.rank-table .n-data-table-td) {
|
:deep(.rank-table .n-data-table-td) {
|
||||||
font-size: 15px;
|
font-size: 24px;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-bottom: 1px solid #e9ecef !important;
|
border-bottom: 1px solid #e9ecef !important;
|
||||||
padding-top: 16px !important;
|
/* padding-top: 16px !important; */
|
||||||
padding-bottom: 16px !important;
|
/* padding-bottom: 16px !important; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 奇偶行交替色 */
|
/* 奇偶行交替色 */
|
||||||
@ -256,7 +269,7 @@ function rowClassName(row: TeamData) {
|
|||||||
|
|
||||||
:deep(.total-score) {
|
:deep(.total-score) {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.1rem;
|
font-size: 2.1rem;
|
||||||
color: #e67e22;
|
color: #e67e22;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,4 +297,11 @@ function rowClassName(row: TeamData) {
|
|||||||
:deep(.rank-top-3 .total-score) {
|
:deep(.rank-top-3 .total-score) {
|
||||||
color: #cd7f32;
|
color: #cd7f32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 强制表格容器占满父级 */
|
||||||
|
.leaderboard-content :deep(.n-data-table) {
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -116,7 +116,7 @@ onMounted(() => {
|
|||||||
<div class="icon-wrapper">
|
<div class="icon-wrapper">
|
||||||
<img src="@/assets/imgs/user/team-title-icon.webp" alt="team-icon" class="h-full w-full object-cover">
|
<img src="@/assets/imgs/user/team-title-icon.webp" alt="team-icon" class="h-full w-full object-cover">
|
||||||
</div>
|
</div>
|
||||||
<div class="team-name text-3xl">
|
<div class="team-name text-2xl">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,21 +23,6 @@ export default defineConfig<Theme>({
|
|||||||
'icon-xl': '2rem',
|
'icon-xl': '2rem',
|
||||||
|
|
||||||
// 针对 2048x1080 设计基准优化的字体大小
|
// 针对 2048x1080 设计基准优化的字体大小
|
||||||
// 相比默认值适当放大,适配大屏显示
|
|
||||||
// 'xs': '0.8rem', // 12.8px (原 0.75rem = 12px)
|
|
||||||
// 'sm': '0.95rem', // 15.2px (原 0.875rem = 14px)
|
|
||||||
// 'base': '1.1rem', // 17.6px (原 1rem = 16px)
|
|
||||||
// 'lg': '1.3rem', // 20.8px (原 1.125rem = 18px)
|
|
||||||
// 'xl': '1.5rem', // 24px (原 1.25rem = 20px)
|
|
||||||
// '2xl': '1.75rem', // 28px (原 1.5rem = 24px)
|
|
||||||
// '3xl': '2.1rem', // 33.6px (原 1.875rem = 30px)
|
|
||||||
// '4xl': '2.5rem', // 40px (原 2.25rem = 36px)
|
|
||||||
// '5xl': '3.2rem', // 51.2px (原 3rem = 48px)
|
|
||||||
// '6xl': '4rem', // 64px (原 3.75rem = 60px)
|
|
||||||
// '7xl': '5rem', // 80px (原 4.5rem = 72px)
|
|
||||||
// '8xl': '6.5rem', // 104px (原 6rem = 96px)
|
|
||||||
// '9xl': '8.5rem', // 136px (原 8rem = 128px)
|
|
||||||
|
|
||||||
'xs': '0.875rem', // 原 0.75rem
|
'xs': '0.875rem', // 原 0.75rem
|
||||||
'sm': '1rem', // 原 0.875rem
|
'sm': '1rem', // 原 0.875rem
|
||||||
'base': '1.125rem', // 原 1rem
|
'base': '1.125rem', // 原 1rem
|
||||||
|
|||||||
Reference in New Issue
Block a user