feat(user): 重构用户端竞赛流程并添加新页面

- 将原单页竞赛流程拆分为独立路由页面(首页、封面、规则、抽题、答题、图解、组别、队伍)
- 新增 Pinia store 管理竞赛状态、计时器和音频控制
- 添加 SCSS 变量文件定义主题色
- 更新路由配置和类型定义以支持新页面结构
- 重构 QuestionRenderer 组件优化样式
- 添加图标依赖并更新国际化配置
- 移动图片资源到用户目录并清理旧文件
This commit is contained in:
2026-02-06 17:45:44 +08:00
parent ca42c6a876
commit b71a758474
39 changed files with 1559 additions and 486 deletions

View File

@ -0,0 +1,240 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
const { routerPushByKey } = useRouterPush()
interface EventItem {
id: number
title: string
subTitle?: string
icon: string
status: 'active' | 'pending' | 'finished'
}
const events = ref<EventItem[]>([
{ id: 1, title: '第九届阅读之星', subTitle: '第一轮', icon: 'activity', status: 'active' },
{ id: 2, title: '第九届阅读之星', subTitle: '第二轮', icon: 'cast', status: 'pending' },
{ id: 3, title: '第一届', subTitle: '星际知识大赛', icon: 'chrome', status: 'active' },
{ id: 4, title: '第九届友谊赛', subTitle: '', icon: 'heart', status: 'finished' },
])
function handleEnter(id: number) {
routerPushByKey('user_groups', { query: { id: id.toString() } })
}
function handleBack() {
// console.log('back')
}
function handleNext() {
// 首页的下一步操作,可以根据需求实现
routerPushByKey('user_groups')
}
const showContent = ref(false)
onMounted(() => {
setTimeout(() => {
showContent.value = true
}, 100)
})
</script>
<template>
<CompetitionLayout
:show-back="false"
:show-next="false"
title="赛事总览"
action-position="top"
back-btn-text="返回"
btn-theme="light"
@back="handleBack"
@next="handleNext"
>
<!-- Cards Section -->
<div class="cards-container">
<TransitionGroup name="list-anim" tag="div" class="cards-wrapper">
<div
v-for="(item, index) in events" v-show="showContent" :key="item.id" class="event-card"
:style="{ '--delay': `${index * 0.1}s` }" @click="handleEnter(item.id)"
>
<div class="card-top-bar" />
<div class="card-body">
<div class="icon-wrapper">
<SvgIcon :local-icon="item.icon" class="event-icon" />
</div>
<div class="text-content">
<h3 class="event-title">
{{ item.title }}
</h3>
<p v-if="item.subTitle" class="event-subtitle">
{{ item.subTitle }}
</p>
</div>
</div>
<div class="tassel" />
</div>
</TransitionGroup>
</div>
</CompetitionLayout>
</template>
<style lang="scss" scoped>
@import '@/styles/scss/variables.scss';
.cards-container {
width: 100%;
padding: 0 40px;
}
.cards-wrapper {
display: flex;
justify-content: center;
gap: 40px;
flex-wrap: wrap;
}
.event-card {
width: 340px;
height: 480px;
background: url('@/assets/imgs/user/event-card-bg.svg') no-repeat center center;
border-radius: 8px 8px 40px 40px;
position: relative;
cursor: pointer;
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
.card-body {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
text-align: center;
}
.icon-wrapper {
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
.event-icon {
font-size: 48px;
color: $secondary-color;
}
}
.text-content {
.event-title {
font-size: 1.4rem;
font-weight: bold;
color: $text-color;
margin-bottom: 8px;
line-height: 1.4;
}
.event-subtitle {
font-size: 1.1rem;
color: lighten($text-color, 15%);
}
}
&:hover {
transform: translateY(-10px);
.icon-wrapper {
transform: scale(1.1) rotate(5deg);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
}
}
// Transitions
.fade-slide-down-enter-active,
.fade-slide-down-leave-active {
transition: all 0.8s ease;
}
.fade-slide-down-enter-from,
.fade-slide-down-leave-to {
opacity: 0;
transform: translateY(-30px);
}
.list-anim-enter-active,
.list-anim-leave-active {
transition: all 0.6s cubic-bezier(0.5, 0, 0.25, 1);
transition-delay: var(--delay);
}
.list-anim-enter-from,
.list-anim-leave-to {
opacity: 0;
transform: translateY(50px);
}
// Responsive
@media (max-width: 1024px) {
.cards-wrapper {
gap: 20px;
}
.event-card {
width: 220px;
height: 320px;
}
}
@media (max-width: 768px) {
.cards-wrapper {
flex-direction: column;
align-items: center;
}
.event-card {
width: 90%;
height: 120px;
flex-direction: row;
border-radius: 8px;
.card-top-bar {
width: 16px;
height: 110%;
top: -5%;
left: -8px;
}
.tassel {
display: none;
}
.card-body {
flex-direction: row;
text-align: left;
padding: 10px 20px;
}
.icon-wrapper {
width: 60px;
height: 60px;
margin-bottom: 0;
margin-right: 20px;
.event-icon {
font-size: 32px;
}
}
}
}
</style>