feat(admin): 优化用户界面交互与数据展示
- 在队伍选择页面增加卷轴展开动画,优化视觉体验 - 为游戏页面添加实时统计图表,使用 ECharts 展示各队答题进度 - 调整题目渲染器中图片的最大宽度为 200px - 在路由配置中隐藏 results_results-detail-copy 菜单项 - 修复 API 函数命名和参数类型,更新接口返回类型定义 - 移动拼音听写题目数据文件到 store 目录 - 更新 VSCode 拼写检查配置,添加 ECharts 单词
This commit is contained in:
@ -126,9 +126,15 @@ async function initSyncRouteParams() {
|
||||
}
|
||||
}
|
||||
|
||||
const showCards = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await initSyncRouteParams()
|
||||
await getQuestions()
|
||||
// 页面加载 300ms 后显示卡片,开始动画
|
||||
setTimeout(() => {
|
||||
showCards.value = true
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -142,9 +148,16 @@ onMounted(async () => {
|
||||
<div class="relative z-10 min-h-full w-full">
|
||||
<div class="min-h-full w-full flex flex-col items-center pb-12 pt-24">
|
||||
<!-- 卡片列表 -->
|
||||
<div class="perspective-container max-w-[1800px] w-full flex flex-wrap justify-center gap-4 px-4 md:gap-10 md:px-12">
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
name="card-anim"
|
||||
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 questions" :key="node.id" class="w-[44%] card-wrapper 2xl:w-[14%] lg:w-[21%] md:w-[28%] xl:w-[17%]"
|
||||
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)"
|
||||
>
|
||||
<div class="flip-card-inner" :class="{ 'is-flipped': flippedCards.has(index) }">
|
||||
@ -174,7 +187,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,6 +195,41 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// ... existing styles ...
|
||||
|
||||
// 卡片进场动画 - 增强版 3D 纵深翻转入场 (Deep Space Flip-in)
|
||||
.card-anim-enter-active {
|
||||
// 增加动画时长到 1.5s,配合复杂的 3D 变换
|
||||
animation: card-enter-deep 1.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
}
|
||||
|
||||
@keyframes card-enter-deep {
|
||||
0% {
|
||||
opacity: 0;
|
||||
// [初始状态]
|
||||
// translateZ(-800px): 从屏幕深处(远离观众)开始
|
||||
// translateY(300px): 从下方更深处升起
|
||||
// rotateY(180deg): 背面朝上 (配合 flip-card 结构,视觉上可能是翻转状态)
|
||||
// rotateX(45deg): 稍微倾斜
|
||||
transform: translate3d(0, 300px, -800px) rotateY(180deg) rotateX(45deg) scale(0.5);
|
||||
filter: blur(20px); // 深度模糊
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: blur(0); // 聚焦
|
||||
}
|
||||
70% {
|
||||
// [过冲状态]
|
||||
// 飞到眼前一点点 (Z=50px)
|
||||
// 角度稍微回弹 (rotateY -15deg)
|
||||
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
// [最终归位]
|
||||
transform: translate3d(0, 0, 0) rotateY(0) rotateX(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.perspective-container {
|
||||
perspective: 1000px; // [3D透视] 设置观察者距离,数值越小透视感越强,产生近大远小的3D效果
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
import QuestionRenderer from '../modules/QuestionRenderer.vue'
|
||||
import LineCharts from './modules/LineCharts.vue'
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
@ -24,6 +25,7 @@ const loading = ref(false)
|
||||
|
||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||
const isStarted = ref(false)
|
||||
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
||||
|
||||
console.log(activityInfo.value, 'activityInfo.value')
|
||||
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
|
||||
@ -38,10 +40,15 @@ onMounted(async () => {
|
||||
await fetchGetQuestionDetailData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
// 监听倒计时,结束时自动跳转
|
||||
watch(() => store.timeLeft, (newVal) => {
|
||||
if (newVal === 0 && isStarted.value) {
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -75,16 +82,12 @@ const formattedTime = computed(() => {
|
||||
return `倒计时 ${mm}:${ss}`
|
||||
})
|
||||
|
||||
// Mock chart data
|
||||
const chartData = [
|
||||
{ group: '第一组', total: 42, correct: 36 },
|
||||
{ group: '第二组', total: 20, correct: 16 },
|
||||
{ group: '第三组', total: 19, correct: 19 },
|
||||
{ group: '第四组', total: 17, correct: 14 },
|
||||
]
|
||||
// Chart data
|
||||
const chartData = ref<any[]>([])
|
||||
|
||||
function handleBack() {
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_draw', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -138,10 +141,13 @@ async function handleNext() {
|
||||
TeamGroupID: groupId.value || 0,
|
||||
QuestionID: store.currentQuestionInfo.ID, // 使用列表中的 ID
|
||||
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
|
||||
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
|
||||
}
|
||||
console.log(params, 'params')
|
||||
await fetchSubmitQuestionResult(params)
|
||||
// 立即拉取一次,然后开启轮询
|
||||
await fetchGetGameStatisticsData(params)
|
||||
startPolling(params)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -152,6 +158,7 @@ async function handleNext() {
|
||||
else {
|
||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
@ -163,16 +170,55 @@ async function handleNext() {
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling(params: Api.Competition.QuestionAddParams) {
|
||||
stopPolling()
|
||||
pollingTimer.value = window.setInterval(() => {
|
||||
fetchGetGameStatisticsData(params)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer.value) {
|
||||
clearInterval(pollingTimer.value)
|
||||
pollingTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddParams) {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
try {
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
console.log(data, 'data')
|
||||
// 更新图表数据
|
||||
// 假设接口返回格式为: [{ TeamName: '组1', Points: 10, ResultPotins: 5 }, ...]
|
||||
// 需要根据实际接口返回结构调整映射逻辑
|
||||
const list = (data?.data || []) as any[]
|
||||
if (list && Array.isArray(list)) {
|
||||
// 根据 TeamName 分组并去重(如果是同一个小组多条记录,可能需要合并,这里假设直接展示返回列表)
|
||||
// 如果后端返回的就是每个小组一条汇总数据,直接映射即可
|
||||
// 根据需求描述,返回的数据可能有多条同名小组记录,或者就是每个小组一条
|
||||
// 鉴于示例数据中有重复的 TeamName (第一小队, 第二小队),这里做一个简单的去重或聚合
|
||||
// 实际上 fetchGetGameStatistics 应该是返回统计数据,如果返回了明细,前端需要聚合
|
||||
// 但看字段 Points 和 ResultPotins 像是统计值,这里直接映射展示,如果有重复名,ECharts category 会自动处理或需要前端聚合
|
||||
// 为了稳妥,这里按 TeamId 或 TeamName 聚合一下,取最新或求和?
|
||||
// 假设返回的是当前所有小组的状态,直接映射即可
|
||||
|
||||
// 使用 Map 去重,以 TeamId 为准,保留最新的状态
|
||||
const teamMap = new Map()
|
||||
list.forEach((item) => {
|
||||
teamMap.set(item.TeamName, {
|
||||
group: item.TeamName || '未知队伍',
|
||||
total: item.Points || 0,
|
||||
correct: item.ResultPotins || 0,
|
||||
})
|
||||
})
|
||||
chartData.value = Array.from(teamMap.values())
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch game statistics', error)
|
||||
window.$message?.error('获取游戏现场统计结果失败')
|
||||
// 轮询时出错不频繁弹窗,仅 log
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,6 +253,7 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
|
||||
<!-- 底部图表区域 -->
|
||||
<div
|
||||
v-if="isStarted"
|
||||
class="relative z-20 h-64 max-w-5xl w-full flex flex-col shrink-0 border-2 border-blue-300 rounded-xl bg-white/90 p-4 shadow-lg backdrop-blur"
|
||||
>
|
||||
<!-- 图表标题栏 -->
|
||||
@ -214,45 +261,12 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rotate-45 bg-orange-400" />
|
||||
<span class="text-lg text-blue-800 font-bold">答题进度</span>
|
||||
</div>
|
||||
<div class="flex gap-6 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rounded-full bg-blue-400" />
|
||||
<span class="text-gray-600">答题数量</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rounded-full bg-orange-300" />
|
||||
<span class="text-gray-600">正确数量</span>
|
||||
</div>
|
||||
<span class="text-md text-gray-400">(仅为ai判定,不代表最终结果)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 柱状图 -->
|
||||
<div class="flex flex-1 items-end justify-around px-8 pb-2">
|
||||
<div v-for="item in chartData" :key="item.group" class="w-16 flex flex-col items-center gap-2">
|
||||
<div class="h-32 flex items-end gap-2">
|
||||
<!-- Blue Bar -->
|
||||
<div
|
||||
class="relative w-6 rounded-t-md from-blue-500 to-blue-300 bg-gradient-to-t transition-all duration-1000"
|
||||
:style="{ height: `${item.total * 2}px` }"
|
||||
>
|
||||
<span class="absolute left-1/2 text-blue-800 font-bold -top-6 -translate-x-1/2">{{ item.total
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Orange Bar -->
|
||||
<div
|
||||
class="relative w-6 rounded-t-md from-orange-400 to-orange-200 bg-gradient-to-t transition-all duration-1000"
|
||||
:style="{ height: `${item.correct * 2}px` }"
|
||||
>
|
||||
<span class="absolute left-1/2 text-orange-600 font-bold -top-6 -translate-x-1/2">{{ item.correct
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-blue-800 font-bold">
|
||||
{{ item.group }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 柱状图(ECharts) -->
|
||||
<LineCharts :data="chartData" :height="208" />
|
||||
</div>
|
||||
</div>
|
||||
</CompetitionLayout>
|
||||
|
||||
136
apps/admin/src/views/user/game/modules/LineCharts.vue
Normal file
136
apps/admin/src/views/user/game/modules/LineCharts.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { type ECOption, useEcharts } from '@/hooks/common/echarts'
|
||||
|
||||
interface BarDatum {
|
||||
group: string | number
|
||||
total: number
|
||||
correct: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: BarDatum[]
|
||||
height?: number
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 构建柱状图配置
|
||||
* 说明:根据传入的数据生成双系列柱状图(答题数量/正确数量)
|
||||
*/
|
||||
function buildOption(list: BarDatum[]): ECOption {
|
||||
const categories = list.map(i => String(i.group))
|
||||
const total = list.map(i => i.total)
|
||||
const correct = list.map(i => i.correct)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
},
|
||||
legend: {
|
||||
data: ['答题数量', '正确数量'],
|
||||
top: 0,
|
||||
right: 0,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
},
|
||||
},
|
||||
grid: { left: 16, right: 16, top: 40, bottom: 0, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisTick: { show: false }, // 隐藏 x 轴刻度线
|
||||
axisLine: { show: false }, // 隐藏 x 轴线
|
||||
axisLabel: {
|
||||
color: '#003366', // 深蓝色字体
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
margin: 16,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
axisTick: { show: false }, // 隐藏 y 轴刻度线
|
||||
axisLine: { show: false }, // 隐藏 y 轴线
|
||||
splitLine: { show: false }, // 隐藏 y 轴网格线
|
||||
axisLabel: { show: false }, // 隐藏 y 轴标签
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '答题数量',
|
||||
type: 'bar',
|
||||
data: total,
|
||||
barWidth: 24,
|
||||
itemStyle: {
|
||||
// 从上到下:浅蓝 -> 深蓝
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#e0f2fe' }, // sky-100 (顶部浅色)
|
||||
{ offset: 1, color: '#3b82f6' }, // blue-500 (底部深色)
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#003366', // 深蓝色
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
offset: [0, 2],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '正确数量',
|
||||
type: 'bar',
|
||||
data: correct,
|
||||
barWidth: 24,
|
||||
itemStyle: {
|
||||
// 从上到下:浅橙 -> 深橙
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#fff7ed' }, // orange-50 (顶部极浅)
|
||||
{ offset: 1, color: '#f97316' }, // orange-500 (底部深色)
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#ea580c', // orange-600
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
offset: [0, 2],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const { domRef, updateOptions } = useEcharts<ECOption>(() => buildOption(props.data || []))
|
||||
|
||||
/**
|
||||
* 监听数据变化并更新图表
|
||||
* 说明:当父组件 chartData 更新时,动态刷新柱状图
|
||||
*/
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
updateOptions(() => buildOption(val || []))
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="domRef"
|
||||
class="w-full"
|
||||
:style="{ height: `${props.height ?? 180}px` }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@ -9,8 +9,6 @@ const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
// console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo')
|
||||
// 题目
|
||||
const title = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent)
|
||||
// 归属大纲
|
||||
// const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name)
|
||||
// 题目内容
|
||||
const content = computed(() => currentQuestionDetail.value.Name)
|
||||
// 模版类型
|
||||
@ -380,7 +378,7 @@ watch(
|
||||
|
||||
.rich-content {
|
||||
:deep(img) {
|
||||
max-width: 50%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
margin: 0 auto;
|
||||
|
||||
@ -46,7 +46,9 @@ async function getTeams() {
|
||||
}
|
||||
}
|
||||
|
||||
const showContent = ref(false)
|
||||
const showBackground = ref(false)
|
||||
const showTitle = ref(false)
|
||||
const showList = ref(false)
|
||||
|
||||
function handleBack() {
|
||||
routerBack()
|
||||
@ -63,10 +65,21 @@ function selectTeam(_id: number) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTeams()
|
||||
// 1. 背景卷轴展开 - 稍微慢一点开始,给人一种准备的感觉
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
getTeams()
|
||||
}, 100)
|
||||
showBackground.value = true
|
||||
}, 300)
|
||||
|
||||
// 2. 标题浮现 - 等卷轴展开一大半再出来 (卷轴动画约 1.5s,这里设为 1200ms)
|
||||
setTimeout(() => {
|
||||
showTitle.value = true
|
||||
}, 1200)
|
||||
|
||||
// 3. 列表出现 - 等卷轴完全展开并定住后再出 (卷轴动画结束需 1.5s,这里给 1600ms)
|
||||
setTimeout(() => {
|
||||
showList.value = true
|
||||
}, 1600)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -74,40 +87,90 @@ onMounted(() => {
|
||||
<CompetitionLayout :show-back="true" :show-next="false" :show-title="false" @back="handleBack" @next="handleNext">
|
||||
<!-- Scroll Content -->
|
||||
<div class="scroll-container">
|
||||
<div class="scroll-body">
|
||||
<!-- Title Section inside Scroll -->
|
||||
<Transition name="fade-slide-down" appear>
|
||||
<div v-if="showContent" class="title-section">
|
||||
<div class="scroll-bg">
|
||||
<span class="main-title text-4xl text-#22685a">
|
||||
队伍名单
|
||||
</span>
|
||||
<!-- 卷轴背景动画容器 -->
|
||||
<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>
|
||||
</div>
|
||||
</Transition>
|
||||
</Transition>
|
||||
|
||||
<div class="scroll-content">
|
||||
<TransitionGroup name="list-anim" tag="div" class="teams-grid">
|
||||
<div
|
||||
v-for="(item, index) in teams" v-show="showContent" :key="item.id" class="team-item"
|
||||
:style="{ '--delay': `${index * 0.1}s` }" @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">
|
||||
<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.8}s` }"
|
||||
@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">
|
||||
</div>
|
||||
<div class="team-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<div class="scroll-right-decor" />
|
||||
</div>
|
||||
<div class="scroll-right-decor" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</CompetitionLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 卷轴展开动画 - 变得更慢更优雅
|
||||
.scroll-expand-enter-active {
|
||||
transition: all 1.5s cubic-bezier(0.25, 1, 0.5, 1); // 持续时间加长到 1.5s,曲线更平滑
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-expand-leave-active {
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.scroll-expand-enter-from,
|
||||
.scroll-expand-leave-to {
|
||||
width: 0 !important;
|
||||
opacity: 0;
|
||||
transform: scaleX(0.9); // 稍微调整初始缩放
|
||||
}
|
||||
|
||||
// 标题进场动画 - 更加缓慢浮现
|
||||
.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 2s 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%; // 使用百分比定位,适应不同比例
|
||||
@ -153,6 +216,7 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
background: url('@/assets/imgs/user/team-bg.svg') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
transform-origin: center; // 确保从中心展开
|
||||
|
||||
&::before {
|
||||
left: -15px;
|
||||
@ -181,6 +245,12 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 140px;
|
||||
@ -192,36 +262,14 @@ onMounted(() => {
|
||||
// background: url('@/assets/imgs/team-icon-bg.svg') no-repeat center center;
|
||||
background-size: contain;
|
||||
margin-bottom: 20px;
|
||||
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.5rem;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 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: translateX(-70px); // 竖排标题从左侧进入
|
||||
}
|
||||
|
||||
.list-anim-enter-active,
|
||||
.list-anim-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0.5, 0, 0.25, 1);
|
||||
transition-delay: var(--delay);
|
||||
}
|
||||
|
||||
.list-anim-enter-from,
|
||||
.list-anim-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user