Files
aida_front/src/views/AwardPage/components/TimeLine.vue
2026-02-04 10:57:25 +08:00

371 lines
7.8 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.
<template>
<div
ref="containerRef"
class="timeline-container container flex flex-col align-center"
>
<div class="timeline-title">{{ $t('AwardsPage.competitionTimeline') }}</div>
<div class="desc">{{ $t('AwardsPage.shapingTheFuture') }}</div>
<div
class="timeline-point"
ref="timelineRef"
>
<!-- 顶部标签行 -->
<div class="grid-row labels-row">
<div
class="grid-cell label-cell"
v-for="item in points"
:key="'label-' + item.time"
>
<div class="main-label">{{ $t(item.label) }}</div>
<div
class="sub-label"
v-if="item.subLabel"
>
{{ $t(item.subLabel) }}
</div>
</div>
</div>
<!-- 图标行 -->
<div class="grid-row icons-row">
<div class="timeline-line"></div>
<div
class="grid-cell icon-cell"
v-for="item in points"
:key="'icon-' + item.time"
>
<img
src="@/assets/images/award/point.png"
class="point-icon"
/>
</div>
</div>
<!-- 时间行 -->
<div class="grid-row times-row">
<div
class="grid-cell time-cell"
v-for="item in points"
:key="'time-' + item.time"
>
{{ $t(item.time) }}
</div>
</div>
<!-- 描述行 -->
<div class="grid-row descs-row">
<div
class="grid-cell desc-cell"
v-for="item in points"
:key="'desc-' + item.time"
>
<div class="txt">
{{ $t(item.desc) }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { gsap } from 'gsap'
const { t } = useI18n()
const containerRef = ref<HTMLElement | null>(null)
const timelineRef = ref<HTMLElement | null>(null)
const hasAnimated = ref(false)
const points = ref([
{
label: 'AwardsPage.timelineApplicationLabel',
subLabel: 'AwardsPage.timelineDeadlineLabel',
time: 'AwardsPage.timeJul15',
desc: 'AwardsPage.applicationDeadlineDesc'
},
{
label: 'AwardsPage.twentyFinalistsAnnounced',
subLabel: 'AwardsPage.announcedLabel',
time: 'AwardsPage.timeAug30',
desc: 'AwardsPage.twentyFinalistsDesc'
},
{
label: 'AwardsPage.finalistSubmission',
subLabel: 'AwardsPage.submissionLabel',
time: 'AwardsPage.timeSept30',
desc: 'AwardsPage.finalistSubmissionDesc'
},
{
label: 'AwardsPage.receivingOutfits',
subLabel: 'AwardsPage.fromFinalistsLabel',
time: 'AwardsPage.timeOctober',
desc: 'AwardsPage.receivingOutfitsDesc'
},
{
label: 'AwardsPage.awardCeremony',
subLabel: 'AwardsPage.ceremonyLabel',
time: 'AwardsPage.timeNov12',
desc: 'AwardsPage.awardCeremonyDesc'
}
])
const playAnimation = () => {
if (!containerRef.value || hasAnimated.value) return
const title = containerRef.value.querySelector('.timeline-title')
const subtitle = containerRef.value.querySelector('.desc')
const line = containerRef.value.querySelector('.timeline-line')
const timeline = containerRef.value.querySelector('.timeline-point')
const tl = gsap.timeline()
// 我们使用一个统一的开始 label使横线、timeline 裁剪与所有文字同时启动,
// 点图标在它们完成后立即开始。
tl.addLabel('start')
// 整体 timeline 的裁剪展开(与 start 同步)
if (timeline) {
tl.fromTo(
timeline,
{
clipPath: 'inset(0 100% 0 0)'
},
{
clipPath: 'inset(0 0% 0 0)',
duration: 1.3,
ease: 'power1.out'
},
'start'
)
}
// 线条动画(与 start 同步)
if (line) {
tl.from(
line,
{
scaleX: 0,
transformOrigin: '0% 50%',
duration: 1.3,
ease: 'power1.out'
},
'start'
)
}
// 标题与副标题(与 start 同步)
if (title && subtitle) {
tl.from(
[title, subtitle],
{
scaleX: 0,
autoAlpha: 0.5,
transformOrigin: '50% 50%',
duration: 0.6,
stagger: 0.1,
ease: 'power2.out'
},
'start'
)
}
// 行内文字(标签、时间、描述、图标)与 start 同步开始
const textItems = containerRef.value.querySelectorAll('.grid-cell')
if (textItems && textItems.length) {
tl.from(
textItems,
{
// autoAlpha: 0.5,
duration: 0.7,
stagger: 0.08,
ease: 'power2.out'
},
'start'
)
}
hasAnimated.value = true
}
let observer: IntersectionObserver | null = null
onMounted(async () => {
await nextTick()
if (!containerRef.value) return
observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
playAnimation()
}
})
},
{ threshold: 0.3 }
)
observer.observe(containerRef.value)
})
onBeforeUnmount(() => {
if (observer && containerRef.value) {
observer.unobserve(containerRef.value)
}
observer = null
})
</script>
<style scoped lang="less">
.timeline-container {
background: url('@/assets/images/award/timeline_bg.png') no-repeat;
background-size: 100% 100%;
position: relative;
padding: 12.8rem 0 15.9rem;
width: 100%;
color: #fff;
.timeline-title {
font-family: 'PoppinsBold';
font-weight: 600;
font-size: 4rem;
text-align: center;
vertical-align: middle;
}
.logo {
margin: 2.4rem 0 2.2rem 0;
}
.desc {
font-family: 'Arial';
font-size: 3rem;
font-weight: 400;
color: #f95750;
}
.timeline-point {
overflow: hidden;
will-change: clip-path;
flex: 1;
width: 100%;
margin-top: 11rem;
padding: 0 13.8rem;
position: relative;
z-index: 2;
// 主网格布局5列
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: auto auto auto auto;
grid-column-gap: 0;
grid-row-gap: 0;
// 所有 grid 子行的通用样式
.grid-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-column: 1 / -1;
}
.grid-cell {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
// 图标行
.icons-row {
align-items: center;
height: 6.4rem;
position: relative;
z-index: 2;
margin-bottom: 1.6rem;
.timeline-line {
position: absolute;
top: 50%;
left: -22rem;
right: -21.2rem;
height: 0.15rem;
background: linear-gradient(
90deg,
rgba(199, 52, 44, 0) 0%,
rgba(199, 52, 44, 0.719626) 25.96%,
#c7342c 51.44%,
rgba(199, 52, 44, 0.762376) 75.96%,
rgba(199, 52, 44, 0) 100%
);
transform: translateY(-50%);
z-index: 1;
pointer-events: none;
}
.icon-cell {
position: relative;
.point-icon {
width: 6.4rem;
height: 6.4rem;
display: block;
position: relative;
z-index: 2;
}
}
}
// 标签行
.labels-row {
margin-bottom: 8rem;
position: relative;
z-index: 2;
.label-cell {
flex-direction: column;
color: #fff;
font-family: 'PoppinsBold';
font-weight: 600;
font-size: 2.8rem;
white-space: pre-line;
justify-content: center;
min-height: 6rem;
// .sub-label {
// font-family: 'Arial';
// font-weight: 400;
// font-size: 1.4rem;
// color: rgba(255, 255, 255, 0.8);
// margin-top: 0.4rem;
// }
}
}
// 时间行
.times-row {
margin-bottom: 6rem;
z-index: 2;
position: relative;
.time-cell {
color: #f95750;
font-family: 'Arial';
font-weight: 400;
font-size: 2.8rem;
line-height: 4.5rem;
}
}
// 描述行
.descs-row {
.desc-cell {
.txt {
font-family: 'Arial';
font-weight: 400;
font-size: 2rem;
text-align: center;
color: #e0e0e0;
width: 100%;
max-width: 31.2rem;
min-height: 10.2rem;
white-space: pre-line;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>