feat: 优化竞赛流程并修复题目禁用功能
- 调整倒计时音效触发时间为剩余10秒,并更新音频文件 - 修复题目禁用功能,将IsDisabled类型从number改为boolean - 优化抽题页面,替换为静态图片并禁用放大预览 - 为卡片添加3D悬停效果,提升交互体验 - 在结果发布前检查倒计时是否结束,防止提前发布 - 优化团队选择页面样式,更新图标和背景 - 修复Typewriter组件内容重复累加的问题 - 更新图表标签从"正确数量"改为"预估得分" - 优化竞赛布局,统一"下一步"按钮文本
This commit is contained in:
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '正确数量',
|
||||
name: '预估得分',
|
||||
type: 'bar',
|
||||
data: correct,
|
||||
barWidth: 24,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user