feat: 优化竞赛流程并修复题目禁用功能

- 调整倒计时音效触发时间为剩余10秒,并更新音频文件
- 修复题目禁用功能,将IsDisabled类型从number改为boolean
- 优化抽题页面,替换为静态图片并禁用放大预览
- 为卡片添加3D悬停效果,提升交互体验
- 在结果发布前检查倒计时是否结束,防止提前发布
- 优化团队选择页面样式,更新图标和背景
- 修复Typewriter组件内容重复累加的问题
- 更新图表标签从"正确数量"改为"预估得分"
- 优化竞赛布局,统一"下一步"按钮文本
This commit is contained in:
2026-03-10 17:55:45 +08:00
parent 11fb5481d3
commit b9c5886437
24 changed files with 331 additions and 113 deletions

View File

@ -128,8 +128,10 @@ function toggleFlip(index: number) {
}
else {
flippedCards.value.add(index)
audioController.play('flip') // 播放翻牌
audioController.play('flip') // 翻牌
}
// 每次点击都播放翻牌音效(无论翻开还是翻回去)
// audioController.play('flip')
}
async function initSyncRouteParams() {
@ -148,6 +150,39 @@ async function initSyncRouteParams() {
const showCards = ref(false)
// 3D 悬停效果逻辑
const cardStyles = ref<Record<number, any>>({})
function handleMouseMove(e: MouseEvent, index: number) {
const card = e.currentTarget as HTMLElement
const rect = card.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const centerX = rect.width / 2
const centerY = rect.height / 2
// 计算旋转角度
// 鼠标在上方(y < centerY) -> rotateX > 0 (上半部分凸起)
// 鼠标在下方(y > centerY) -> rotateX < 0 (下半部分凸起)
const rotateX = ((centerY - y) / centerY) * 10
// 鼠标在左方(x < centerX) -> rotateY < 0 (左半部分凸起)
// 鼠标在右方(x > centerX) -> rotateY > 0 (右半部分凸起)
const rotateY = ((x - centerX) / centerX) * 10
cardStyles.value[index] = {
transform: `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.02, 1.02, 1.02)`,
transition: 'transform 0.1s ease-out', // 移动时响应要快
}
}
function handleMouseLeave(index: number) {
cardStyles.value[index] = {
transform: 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)',
transition: 'transform 0.5s ease', // 离开时平滑复位
}
}
onMounted(async () => {
await initSyncRouteParams()
await getQuestions()
@ -160,8 +195,8 @@ onMounted(async () => {
<template>
<CompetitionLayout
:show-back="true" :show-next="showNextButton" :title="activityName" :title-bg-type="2"
@back="handleBack" @next="handleNext"
:show-back="true" :show-next="showNextButton" next-btn-text="下一步" :title="activityName"
:title-bg-type="2" @back="handleBack" @next="handleNext"
>
<div class="h-screen w-full overflow-y-auto font-sans">
<div class="relative z-10 min-h-full w-full">
@ -172,9 +207,12 @@ onMounted(async () => {
class="perspective-container max-w-[1800px] w-full flex flex-wrap justify-center gap-4 px-4 md:gap-10 md:px-12"
>
<div
v-for="(node, index) in showCards ? questions : []" :key="node.id"
class="w-[44%] card-wrapper 2xl:w-[14%] lg:w-[21%] md:w-[28%] xl:w-[17%]"
:style="{ animationDelay: `${index * 0.2}s` }" @click="toggleFlip(index)"
v-for="(node, index) in showCards ? questions : []" :key="index"
class="w-[44%] card-wrapper 2xl:w-[14%] lg:w-[21%] md:w-[28%] xl:w-[17%]" :style="{
animationDelay: `${index * 0.2}s`,
...(cardStyles[index] || {}),
}" @click="toggleFlip(index)" @mousemove="(e) => handleMouseMove(e, index)"
@mouseleave="handleMouseLeave(index)"
>
<div class="flip-card-inner" :class="{ 'is-flipped': flippedCards.has(index) }">
<!-- 正面 (Front) - 显示背面图案 -->
@ -259,10 +297,6 @@ onMounted(async () => {
cursor: pointer;
transition: transform 0.3s ease;
box-shadow: none !important;
&:hover {
transform: translateY(-10px); // [悬停效果] 鼠标移上去时卡片轻微上浮
}
}
.flip-card-inner {
@ -278,7 +312,7 @@ onMounted(async () => {
transform-style: preserve-3d; // [3D空间] 确保子元素正反面在3D空间中渲染而不是压平在平面上
border-radius: 20px;
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.2); // [静态阴影] 默认状态下的投影
// box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.2); // [静态阴影] 默认状态下的投影
&.is-flipped {
// [翻转状态]
@ -300,7 +334,7 @@ onMounted(async () => {
border-radius: 20px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.5); // 增加微妙的边框
// border: 1px solid rgba(255, 255, 255, 0.5); // 增加微妙的边框
}
// 正面(实际是卡背图案)
@ -370,8 +404,8 @@ onMounted(async () => {
}
.inner-circle-decoration {
width: 100px;
height: 100px;
width: 98px;
height: 98px;
border-radius: 50%;
display: flex;
align-items: center;

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import drawImg from '@/assets/imgs/user/draw.svg'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetCurrentQuestion } from '@/service/api/game'
@ -130,29 +131,29 @@ function handleBack() {
}
const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '')
const questionTitle = computed(() => question.value?.QuestionList?.QuestionContent || question.value?.QuestionList?.Name || '')
</script>
<template>
<CompetitionLayout :show-back="true" :show-next="false" :title="questionMainTitle || '抽题'" @back="handleBack">
<div class="h-full w-full flex flex-col items-center pt-10 font-sans">
<!-- <div v-if="question" class="mb-10 max-w-4xl text-center">
<div v-if="question" class="mb-10 max-w-4xl text-center">
<div class="text-2xl text-gray-800 font-medium leading-relaxed tracking-wide">
{{ questionMainTitle || '' }}
{{ questionTitle || '' }}
</div>
</div> -->
</div>
<!-- 题目卡片列表 -->
<div v-if="loading" class="text-xl text-gray-500">
加载中...
</div>
<div v-else class="w-full flex flex-1 items-center justify-center pb-10">
<div v-else class="w-full flex items-center justify-center pb-10">
<div v-if="question" class="lottery-stick-container">
<div
class="lottery-stick" :class="{ 'is-selected': flippedId === question?.Activity_Question?.ID }"
@click="handleCardClick(question)"
>
<!-- 签身 (默认显示) -->
<div class="stick-face stick-body">
<!-- <div class="stick-face stick-body">
<div class="stick-top-mark" />
<div
class="writing-mode-vertical-rl h-full flex items-center justify-center text-xl text-yellow-900 font-bold tracking-widest opacity-80"
@ -160,11 +161,12 @@ const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '
</div>
</div>
<!-- 签面 (翻转后显示 - 实际上是放大显示的) -->
<div class="stick-face stick-content">
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ questionMainTitle
}}</span>
</div>
</div> -->
<!-- 禁用放大 -->
<NImage :src="drawImg" class="h-full w-full object-cover" :preview-disabled="true" />
</div>
</div>
</div>
@ -178,8 +180,8 @@ const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '
<style scoped>
.lottery-stick-container {
width: 50px;
height: 240px;
/* width: 50px;
height: 240px; */
perspective: 1000px;
cursor: pointer;
transition: transform 0.3s;
@ -236,7 +238,7 @@ const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '
/* 选中(抽出)动画状态 */
.lottery-stick.is-selected {
transform: translateY(-100px) scale(2) rotateY(180deg);
transform: translateY(-100px) scale(1.4) rotateY(180deg);
z-index: 50;
}

View File

@ -83,7 +83,7 @@ function buildOption(list: BarDatum[]): ECOption {
},
},
{
name: '正确数量',
name: '预估得分',
type: 'bar',
data: correct,
barWidth: 24,

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import startIcon from '@/assets/imgs/user/star-icon.svg'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetGroupListByActivityID } from '@/service/api/game'
@ -94,7 +95,7 @@ onMounted(() => {
<div class="icon-wrapper">
<div class="flower-bg" />
<div class="inner-icon">
<img src="@/assets/imgs/user/star-icon.svg" alt="star" class="star-img">
<NImage :src="startIcon" alt="star" class="star-img" width="180" height="187" :preview-disabled="true" object-fit="contain" />
</div>
</div>
<div class="group-name-tag">
@ -153,8 +154,8 @@ onMounted(() => {
}
.icon-wrapper {
width: 100px;
height: 100px;
// width: 100px;
// height: 100px;
position: relative;
display: flex;
align-items: center;

View File

@ -49,10 +49,7 @@ function handleNext() {
<template>
<CompetitionLayout
:show-back="true"
:show-next="true"
:title="activityName"
@back="handleBack"
:show-back="true" :show-next="true" :title="activityName" next-btn-text="下一步" @back="handleBack"
@next="handleNext"
>
<div class="items-flex-start h-full w-full flex justify-center font-sans">

View File

@ -113,7 +113,7 @@ onMounted(() => {
@click="selectTeam(item.id)"
>
<div class="icon-wrapper">
<img src="@/assets/imgs/user/star-icon.svg" alt="team-icon" class="h-full w-full object-cover">
<img src="@/assets/imgs/user/team-title-icon.svg" alt="team-icon" class="h-full w-full object-cover">
</div>
<div class="team-name">
{{ item.name }}
@ -263,7 +263,7 @@ onMounted(() => {
justify-content: center;
// background: url('@/assets/imgs/team-icon-bg.svg') no-repeat center center;
background-size: contain;
margin-bottom: 20px;
margin-bottom: 10px;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
@ -272,6 +272,10 @@ onMounted(() => {
color: #333;
font-weight: bold;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
background: url('@/assets/imgs/user/team-title-bg.svg') no-repeat center center;
background-size: contain;
padding: 15px 30px;
border-radius: 10px;
}
}
</style>