Files
reader-star/apps/admin/src/views/user/teams/index.vue
rjl 11f448cedc feat(admin): 优化用户界面交互与组件功能
- 在抽题页面添加开始抽题按钮并绑定事件
- 调整队伍列表动画延迟和分组页面样式
- 修复结果发布条件逻辑,允许无结束时间时发布
- 增强TypeIt组件,支持动态配置和实例管理
- 更新Competition API类型定义,使用枚举类型
- 优化CompetitionLayout组件按钮显示时机
- 修正分析页面API调用参数类型
- 在拼写检查词典中添加Typeit
2026-03-12 14:33:30 +08:00

284 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetTeamListByGroupId } from '@/service/api/competition'
const { routerPushByKey, routerBack } = useRouterPush()
const route = useRoute()
interface TeamItem {
id: number
name: string
icon: string
}
const teams = ref<TeamItem[]>([])
const loading = ref(false)
const activityId = (route.query?.activityId) as string || ''
const groupsId = (route.query?.groupsId) as string || ''
const teamIds = ref<number[]>([])
const RoundType = computed(() => Number(route.query?.RoundType || 0))
async function getTeams() {
if (!groupsId)
return
loading.value = true
try {
const { data, error } = await fetchGetTeamListByGroupId(Number(groupsId))
if (error) {
window?.$message?.error(error.message)
return
}
const list = data?.data || []
if (list && Array.isArray(list)) {
teams.value = list.map((item: any) => ({
id: item.Id,
name: item.Name,
icon: 'star', // Default icon
}))
teamIds.value = list.map((item: any) => item.Id)
}
}
finally {
loading.value = false
}
}
const showBackground = ref(false)
const showTitle = ref(false)
const showList = ref(false)
function handleBack() {
routerBack()
}
function handleNext() {
// 进入规则页面
// routerPushByKey('user_rules')
routerPushByKey('user_cover', { query: { activityId, groupsId, teamIds: teamIds.value.join(','), RoundType: RoundType.value } })
}
function selectTeam(_id: number) {
// 选择队伍逻辑
routerPushByKey('user_cover', { query: { activityId, groupsId, teamId: _id, teamIds: teamIds.value.join(','), RoundType: RoundType.value } })
}
onMounted(() => {
getTeams()
// 1. 背景卷轴展开 - 稍微慢一点开始,给人一种准备的感觉
setTimeout(() => {
showBackground.value = true
}, 300)
// 2. 标题浮现 - 等卷轴展开一大半再出来 (卷轴动画约 3s这里设为 2000ms)
setTimeout(() => {
showTitle.value = true
}, 2000)
// 3. 列表出现 - 等卷轴完全展开并定住后再出 (卷轴动画结束需 3s这里给 3200ms)
setTimeout(() => {
showList.value = true
}, 2500)
})
</script>
<template>
<CompetitionLayout :show-back="true" :show-next="true" next-btn-text="下一步" :show-title="false" @next="handleNext" @back="handleBack">
<!-- Scroll Content -->
<div class="scroll-container">
<!-- 卷轴背景动画容器 -->
<Transition name="scroll-expand">
<div v-if="showBackground" class="scroll-body">
<!-- Title Section inside Scroll -->
<Transition name="fade-slide-down">
<div v-if="showTitle" class="title-section">
<div class="scroll-bg">
<span class="main-title text-4xl text-#22685a">
队伍名单
</span>
</div>
</div>
</Transition>
<div class="scroll-content">
<TransitionGroup name="list-anim" tag="div" class="teams-grid">
<div
v-for="(item, index) in showList ? teams : []" :key="item.id" class="team-item"
:style="{ '--delay': `${index * 0.4}s` }" @click="selectTeam(item.id)"
>
<div class="icon-wrapper">
<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 }}
</div>
</div>
</TransitionGroup>
</div>
<div class="scroll-right-decor" />
</div>
</Transition>
</div>
</CompetitionLayout>
</template>
<style lang="scss" scoped>
// 卷轴展开动画 - 变得更慢更优雅
.scroll-expand-enter-active {
transition: clip-path 3s cubic-bezier(0.25, 1, 0.5, 1);
overflow: hidden;
}
.scroll-expand-leave-active {
transition: clip-path 1.5s ease;
}
.scroll-expand-enter-from,
.scroll-expand-leave-to {
clip-path: inset(0 50% 0 50%);
}
.scroll-expand-enter-to,
.scroll-expand-leave-from {
clip-path: inset(0 0 0 0);
}
// 标题进场动画 - 更加缓慢浮现
.fade-slide-down-enter-active {
transition: all 1.2s ease-out;
}
.fade-slide-down-enter-from {
opacity: 0;
transform: translateY(-40px);
}
// 列表进场动画 (使用 keyframes 实现更丝滑的渐入)
.list-anim-enter-active {
animation: list-enter 1s cubic-bezier(0.23, 1, 0.32, 1) both; // both 模式确保动画前后状态保留
animation-delay: var(--delay);
}
@keyframes list-enter {
0% {
opacity: 0;
transform: translateY(80px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.title-section {
position: absolute;
top: 18%; // 使用百分比定位,适应不同比例
left: 7%; // 微调位置,根据 1920x1080 下的视觉效果调整
z-index: 10;
.scroll-bg {
.main-title {
margin: 0;
writing-mode: vertical-rl; // 竖排文字
letter-spacing: 20px;
text-align: center;
font-weight: 900;
font-family: 'KaiTi', 'STKaiti', serif; // 尝试使用楷体增加古风感
}
}
}
.competition-layout :deep(.main-content) {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.scroll-container {
width: 80%;
height: 70%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
// border: 1px solid red;
}
.scroll-body {
width: 100%;
height: 781px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: url('@/assets/imgs/user/team-bg-animation.svg') no-repeat center center;
background-size: 100% 100%;
transform-origin: center; // 确保从中心展开
will-change: clip-path; // 优化性能
&::before {
left: -15px;
}
&::after {
right: -15px;
}
}
.scroll-content {
width: 80%;
height: 80%;
display: flex;
align-items: center;
justify-content: center;
}
.teams-grid {
display: flex;
gap: 60px;
justify-content: center;
}
.team-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
.icon-wrapper {
width: 140px;
height: 140px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
// background: url('@/assets/imgs/team-icon-bg.svg') no-repeat center center;
background-size: contain;
margin-bottom: 10px;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
.team-name {
font-size: 1.5rem;
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>