|
|
@ -1,50 +1,47 @@ |
|
|
|
<script setup lang="ts"> |
|
|
|
import { useI18n } from 'vue-i18n'; |
|
|
|
import { ref, onMounted, nextTick } from 'vue'; |
|
|
|
import { ref, onMounted, reactive } from 'vue'; |
|
|
|
import { queryCourseList } from '@/api'; |
|
|
|
import type { CourseItem } from '@/api'; |
|
|
|
import gsap from 'gsap'; |
|
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'; |
|
|
|
import { useConfig } from '@/utils/config'; |
|
|
|
|
|
|
|
const { getConfigImage } = useConfig(); |
|
|
|
|
|
|
|
// 注册GSAP插件 |
|
|
|
gsap.registerPlugin(ScrollTrigger); |
|
|
|
|
|
|
|
const { t } = useI18n(); |
|
|
|
|
|
|
|
// 默认里程碑数据 |
|
|
|
const defaultMilestones: CourseItem[] = [ |
|
|
|
{ |
|
|
|
id: '1', |
|
|
|
title: t('about.milestones.founded'), |
|
|
|
description: t('about.milestones.founded_desc') |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: '2', |
|
|
|
title: t('about.milestones.testnet'), |
|
|
|
description: t('about.milestones.testnet_desc') |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: '3', |
|
|
|
title: t('about.milestones.mainnet'), |
|
|
|
description: t('about.milestones.mainnet_desc') |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: '4', |
|
|
|
title: t('about.milestones.growth'), |
|
|
|
description: t('about.milestones.growth_desc') |
|
|
|
} |
|
|
|
]; |
|
|
|
|
|
|
|
// 里程碑数据 |
|
|
|
const milestones = ref<CourseItem[]>(defaultMilestones); |
|
|
|
const loading = ref(false); |
|
|
|
// 里程碑数据 - 使用API返回的真实数据 |
|
|
|
const milestones = ref<CourseItem[]>([]); |
|
|
|
const loading = ref(true); |
|
|
|
const error = ref<string | null>(null); |
|
|
|
const activeIndex = ref(0); |
|
|
|
const timelineRef = ref<HTMLElement | null>(null); |
|
|
|
const milestonesContainer = ref<HTMLElement | null>(null); |
|
|
|
|
|
|
|
// 为里程碑分配布局和样式 |
|
|
|
const getMilestoneStyle = (index: number) => { |
|
|
|
const layouts = ['left', 'right', 'center']; |
|
|
|
const styles = ['large', 'medium', 'full']; |
|
|
|
const overlayColors = [ |
|
|
|
'from-indigo-900/80 to-transparent', |
|
|
|
'from-emerald-900/80 to-transparent', |
|
|
|
'from-purple-900/70 via-purple-900/40 to-transparent', |
|
|
|
'from-blue-900/80 to-transparent', |
|
|
|
'from-gray-900/80 to-transparent', |
|
|
|
'from-amber-900/70 via-amber-900/40 to-transparent' |
|
|
|
]; |
|
|
|
const bgColors = [ |
|
|
|
'bg-gradient-to-br from-black to-indigo-900/30', |
|
|
|
'bg-gradient-to-tl from-black to-emerald-900/30', |
|
|
|
'bg-gradient-to-r from-black via-purple-900/20 to-black', |
|
|
|
'bg-gradient-to-br from-black to-blue-900/30', |
|
|
|
'bg-gradient-to-tl from-black to-gray-900/30', |
|
|
|
'bg-gradient-to-r from-black via-amber-900/20 to-black' |
|
|
|
]; |
|
|
|
|
|
|
|
return { |
|
|
|
layout: layouts[index % layouts.length], |
|
|
|
style: styles[index % styles.length], |
|
|
|
overlayColor: overlayColors[index % overlayColors.length], |
|
|
|
bgColor: bgColors[index % bgColors.length] |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
// 获取里程碑数据 |
|
|
|
const fetchMilestones = async () => { |
|
|
@ -61,304 +58,325 @@ const fetchMilestones = async () => { |
|
|
|
error.value = '获取发展历程数据失败'; |
|
|
|
} finally { |
|
|
|
loading.value = false; |
|
|
|
nextTick(() => { |
|
|
|
initAnimations(); |
|
|
|
}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 设置活跃的里程碑 |
|
|
|
const setActiveMilestone = (index: number) => { |
|
|
|
activeIndex.value = index; |
|
|
|
}; |
|
|
|
|
|
|
|
// 初始化动画 |
|
|
|
const initAnimations = () => { |
|
|
|
if (!milestonesContainer.value) return; |
|
|
|
|
|
|
|
// 为每个里程碑项设置动画 |
|
|
|
const items = document.querySelectorAll('.milestone-item'); |
|
|
|
|
|
|
|
// 时间轴进度条动画 |
|
|
|
gsap.to('.timeline-progress', { |
|
|
|
height: '100%', |
|
|
|
scrollTrigger: { |
|
|
|
trigger: milestonesContainer.value, |
|
|
|
start: 'top 80%', |
|
|
|
end: 'bottom 20%', |
|
|
|
scrub: 0.6, |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 为每个里程碑项设置动画 |
|
|
|
items.forEach((item, index) => { |
|
|
|
// 图片动画 |
|
|
|
gsap.from(item.querySelector('.milestone-image'), { |
|
|
|
y: 100, |
|
|
|
opacity: 0, |
|
|
|
duration: 1, |
|
|
|
scrollTrigger: { |
|
|
|
trigger: item, |
|
|
|
start: 'top 80%', |
|
|
|
toggleActions: 'play none none reverse' |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 内容动画 |
|
|
|
gsap.from(item.querySelector('.milestone-content'), { |
|
|
|
x: index % 2 === 0 ? -50 : 50, |
|
|
|
opacity: 0, |
|
|
|
duration: 1, |
|
|
|
delay: 0.3, |
|
|
|
scrollTrigger: { |
|
|
|
trigger: item, |
|
|
|
start: 'top 80%', |
|
|
|
toggleActions: 'play none none reverse' |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 标记点动画 |
|
|
|
gsap.from(item.querySelector('.milestone-marker'), { |
|
|
|
scale: 0, |
|
|
|
opacity: 0, |
|
|
|
duration: 0.6, |
|
|
|
delay: 0.6, |
|
|
|
scrollTrigger: { |
|
|
|
trigger: item, |
|
|
|
start: 'top 80%', |
|
|
|
toggleActions: 'play none none reverse' |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// 设置滚动触发器,根据滚动位置更新活跃的里程碑 |
|
|
|
items.forEach((item, index) => { |
|
|
|
ScrollTrigger.create({ |
|
|
|
trigger: item, |
|
|
|
start: 'top center', |
|
|
|
end: 'bottom center', |
|
|
|
onEnter: () => setActiveMilestone(index), |
|
|
|
onEnterBack: () => setActiveMilestone(index) |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
// 视差滚动效果 |
|
|
|
const parallaxOffset = ref({ x: 0, y: 0 }); |
|
|
|
|
|
|
|
// 获取随机图片 - 实际项目中应该使用真实图片 |
|
|
|
const getMilestoneImage = (index: number) => { |
|
|
|
// 这里应该返回实际的图片路径,现在使用LOGO作为占位符 |
|
|
|
return '/LOGO.png'; |
|
|
|
// 鼠标移动事件处理 |
|
|
|
const handleMouseMove = (event: MouseEvent) => { |
|
|
|
const x = (event.clientX / window.innerWidth - 0.5) * 20; |
|
|
|
const y = (event.clientY / window.innerHeight - 0.5) * 20; |
|
|
|
parallaxOffset.value = { x, y }; |
|
|
|
}; |
|
|
|
|
|
|
|
// 获取年份 - 实际项目中应该从数据中获取 |
|
|
|
const getMilestoneYear = (index: number) => { |
|
|
|
const currentYear = new Date().getFullYear(); |
|
|
|
return (currentYear - (milestones.value.length - 1) + index).toString(); |
|
|
|
// 滚动到下一个里程碑 |
|
|
|
const scrollToNext = (currentIndex: number) => { |
|
|
|
const nextElement = document.querySelectorAll('.milestone-section')[currentIndex + 1]; |
|
|
|
if (nextElement) { |
|
|
|
nextElement.scrollIntoView({ behavior: 'smooth' }); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
fetchMilestones(); |
|
|
|
|
|
|
|
// 初始化WOW.js动画 |
|
|
|
try { |
|
|
|
const WOW = (window as any).WOW; |
|
|
|
if (WOW) { |
|
|
|
new WOW({ |
|
|
|
boxClass: 'wow', |
|
|
|
animateClass: 'animate__animated', |
|
|
|
offset: 100, |
|
|
|
mobile: true, |
|
|
|
live: true |
|
|
|
}).init(); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('Failed to initialize WOW.js:', error); |
|
|
|
} |
|
|
|
|
|
|
|
// 监听鼠标移动 |
|
|
|
window.addEventListener('mousemove', handleMouseMove); |
|
|
|
}); |
|
|
|
</script> |
|
|
|
|
|
|
|
<template> |
|
|
|
<section class="py-24 px-6 md:px-12 lg:px-24 bg-background-dark relative overflow-hidden" :style="{ backgroundImage: `url(${getConfigImage('about_process_bg')})` }"> |
|
|
|
<!-- 背景装饰 --> |
|
|
|
<div class="absolute inset-0 overflow-hidden"> |
|
|
|
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background-dark via-background to-background-dark opacity-40"></div> |
|
|
|
<div class="absolute -top-24 -left-24 w-64 h-64 rounded-full bg-primary-light blur-3xl opacity-20"></div> |
|
|
|
<div class="absolute bottom-0 right-0 w-80 h-80 rounded-full bg-secondary blur-3xl opacity-20"></div> |
|
|
|
<div class="milestone-gallery"> |
|
|
|
<!-- 加载状态 --> |
|
|
|
<div v-if="loading" class="h-screen flex items-center justify-center"> |
|
|
|
<div class="text-center"> |
|
|
|
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary mx-auto mb-4"></div> |
|
|
|
<p class="text-text-secondary">加载里程碑数据中...</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="container mx-auto relative z-10"> |
|
|
|
<!-- 标题部分 --> |
|
|
|
<div class="max-w-3xl mx-auto text-center mb-20"> |
|
|
|
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-text mb-6 wow animate__animated animate__fadeInDown animate__duration-fast"> |
|
|
|
{{ t('about.milestones.title') }} |
|
|
|
</h2> |
|
|
|
<p class="text-lg md:text-xl text-text-secondary wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast"> |
|
|
|
见证MOSE的成长历程,从创立之初到现在的每一步 |
|
|
|
</p> |
|
|
|
|
|
|
|
<!-- 装饰线 --> |
|
|
|
<div class="w-24 h-1 bg-primary mx-auto mt-8"></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 加载状态 --> |
|
|
|
<div v-if="loading" class="flex justify-center py-20"> |
|
|
|
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary"></div> |
|
|
|
<!-- 错误状态 --> |
|
|
|
<div v-else-if="error" class="h-screen flex items-center justify-center"> |
|
|
|
<div class="text-center"> |
|
|
|
<p class="text-red-500 mb-4">{{ error }}</p> |
|
|
|
<button @click="fetchMilestones" class="px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"> |
|
|
|
重新加载 |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 错误提示 --> |
|
|
|
<div v-else-if="error" class="text-center py-20 text-red-500"> |
|
|
|
{{ error }} |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 里程碑展示 --> |
|
|
|
<div |
|
|
|
v-else |
|
|
|
v-for="(milestone, index) in milestones" |
|
|
|
:key="milestone.id" |
|
|
|
class="milestone-section relative w-full overflow-hidden" |
|
|
|
:class="[ |
|
|
|
getMilestoneStyle(index).bgColor, |
|
|
|
getMilestoneStyle(index).style === 'full' ? 'h-screen' : (getMilestoneStyle(index).style === 'large' ? 'h-[85vh]' : 'h-[70vh]') |
|
|
|
]" |
|
|
|
> |
|
|
|
<!-- 图片容器 - 视差效果 --> |
|
|
|
<div class="absolute inset-0 w-full h-full overflow-hidden"> |
|
|
|
<!-- 图片 - 添加视差效果 --> |
|
|
|
<img |
|
|
|
:src="'/LOGO.png'" |
|
|
|
:alt="milestone.title" |
|
|
|
class="w-full h-full object-cover transition-transform duration-700 ease-out" |
|
|
|
:style="{ |
|
|
|
transform: `scale(1.1) translate(${parallaxOffset.x * (index % 3 - 1) * 0.1}px, ${parallaxOffset.y * (index % 2 ? 1 : -1) * 0.1}px)` |
|
|
|
}" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 渐变叠加层 - 每个里程碑有不同的渐变 --> |
|
|
|
<div |
|
|
|
class="absolute inset-0 bg-gradient-to-r" |
|
|
|
:class="getMilestoneStyle(index).overlayColor" |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 里程碑时间轴 --> |
|
|
|
<!-- 内容区域 - 根据布局调整位置 --> |
|
|
|
<div |
|
|
|
v-else |
|
|
|
ref="milestonesContainer" |
|
|
|
class="relative max-w-7xl mx-auto" |
|
|
|
class="absolute inset-0 flex items-center" |
|
|
|
:class="{ |
|
|
|
'justify-start': getMilestoneStyle(index).layout === 'left', |
|
|
|
'justify-end': getMilestoneStyle(index).layout === 'right', |
|
|
|
'justify-center': getMilestoneStyle(index).layout === 'center' |
|
|
|
}" |
|
|
|
> |
|
|
|
<!-- 时间轴线 --> |
|
|
|
<div class="absolute top-0 left-1/2 w-1 h-full bg-gray-800/30 transform -translate-x-1/2 z-10"> |
|
|
|
<div class="timeline-progress absolute top-0 left-0 w-full bg-primary origin-top" style="height: 0%"></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 里程碑项 --> |
|
|
|
<div class="space-y-32 md:space-y-64 pb-20"> |
|
|
|
<div |
|
|
|
class="max-w-xl p-8 md:p-16 backdrop-blur-sm bg-black/10 rounded-xl border border-white/10" |
|
|
|
:class="{ |
|
|
|
'ml-0 md:ml-16': getMilestoneStyle(index).layout === 'left', |
|
|
|
'mr-0 md:mr-16': getMilestoneStyle(index).layout === 'right', |
|
|
|
'mx-auto text-center': getMilestoneStyle(index).layout === 'center' |
|
|
|
}" |
|
|
|
> |
|
|
|
<!-- 年份编号 - 不同样式 --> |
|
|
|
<div |
|
|
|
v-for="(milestone, index) in milestones" |
|
|
|
:key="milestone.id" |
|
|
|
class="milestone-item relative" |
|
|
|
:class="{ 'active': activeIndex === index }" |
|
|
|
class="mb-6" |
|
|
|
:class="{'flex items-center': getMilestoneStyle(index).layout !== 'center'}" |
|
|
|
> |
|
|
|
<!-- 时间标记 --> |
|
|
|
<div |
|
|
|
class="milestone-marker absolute left-1/2 w-8 h-8 transform -translate-x-1/2 z-20" |
|
|
|
:class="{ 'active': activeIndex === index }" |
|
|
|
class="rounded-full backdrop-blur-sm flex items-center justify-center border border-white/30" |
|
|
|
:class="{ |
|
|
|
'w-16 h-16 bg-white/10': getMilestoneStyle(index).style !== 'full', |
|
|
|
'w-20 h-20 bg-white/20': getMilestoneStyle(index).style === 'full', |
|
|
|
'mx-auto': getMilestoneStyle(index).layout === 'center' |
|
|
|
}" |
|
|
|
> |
|
|
|
<div class="relative"> |
|
|
|
<!-- 外圈 --> |
|
|
|
<div |
|
|
|
class="absolute w-8 h-8 rounded-full border-2 transition-all duration-500" |
|
|
|
:class="activeIndex === index ? 'border-primary scale-125' : 'border-gray-500'" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 内圈 --> |
|
|
|
<div |
|
|
|
class="absolute w-4 h-4 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full transition-all duration-500" |
|
|
|
:class="activeIndex === index ? 'bg-primary scale-100' : 'bg-gray-500 scale-75'" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 年份标签 --> |
|
|
|
<div |
|
|
|
class="absolute top-1/2 transform -translate-y-1/2 whitespace-nowrap font-bold text-xl transition-all duration-500" |
|
|
|
:class="[ |
|
|
|
index % 2 === 0 ? 'left-12' : 'right-12', |
|
|
|
activeIndex === index ? 'text-primary' : 'text-text-secondary' |
|
|
|
]" |
|
|
|
> |
|
|
|
{{ getMilestoneYear(index) }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 内容区域 - 左右交替 --> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center"> |
|
|
|
<!-- 左侧内容 (偶数索引) 或 右侧内容 (奇数索引) --> |
|
|
|
<div |
|
|
|
class="milestone-content order-2 md:order-none" |
|
|
|
:class="{ 'md:order-1': index % 2 !== 0 }" |
|
|
|
> |
|
|
|
<div |
|
|
|
class="p-8 rounded-2xl transition-all duration-500 transform hover:-translate-y-2" |
|
|
|
:class="[ |
|
|
|
activeIndex === index |
|
|
|
? 'bg-gradient-to-br from-background-light to-background-dark shadow-xl' |
|
|
|
: 'bg-background-light/50 shadow-md' |
|
|
|
]" |
|
|
|
> |
|
|
|
<!-- 标题 --> |
|
|
|
<h3 |
|
|
|
class="text-2xl md:text-3xl font-bold mb-4 transition-colors duration-500" |
|
|
|
:class="activeIndex === index ? 'text-primary' : 'text-text'" |
|
|
|
> |
|
|
|
{{ milestone.title }} |
|
|
|
</h3> |
|
|
|
|
|
|
|
<!-- 描述 --> |
|
|
|
<p |
|
|
|
class="text-base md:text-lg leading-relaxed" |
|
|
|
:class="activeIndex === index ? 'text-text' : 'text-text-secondary'" |
|
|
|
v-html="milestone.description" |
|
|
|
></p> |
|
|
|
|
|
|
|
<!-- 装饰线 --> |
|
|
|
<div |
|
|
|
class="w-16 h-1 mt-6 transition-all duration-500" |
|
|
|
:class="activeIndex === index ? 'bg-primary w-24' : 'bg-gray-400 w-16'" |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 图片区域 --> |
|
|
|
<div |
|
|
|
class="milestone-image order-1 md:order-none" |
|
|
|
:class="{ 'md:order-2': index % 2 === 0 }" |
|
|
|
> |
|
|
|
<div |
|
|
|
class="relative overflow-hidden rounded-2xl shadow-xl transition-all duration-500 transform" |
|
|
|
:class="activeIndex === index ? 'scale-105' : 'scale-100'" |
|
|
|
> |
|
|
|
<!-- 图片 --> |
|
|
|
<img |
|
|
|
:src="getMilestoneImage(index)" |
|
|
|
:alt="milestone.title" |
|
|
|
class="w-full h-64 md:h-80 object-cover transition-all duration-700 transform" |
|
|
|
:class="activeIndex === index ? 'scale-110' : 'scale-100'" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 渐变叠加 --> |
|
|
|
<div |
|
|
|
class="absolute inset-0 transition-opacity duration-500" |
|
|
|
:class="[ |
|
|
|
index % 4 === 0 ? 'bg-gradient-to-tr from-primary/70 to-transparent' : '', |
|
|
|
index % 4 === 1 ? 'bg-gradient-to-tr from-secondary/70 to-transparent' : '', |
|
|
|
index % 4 === 2 ? 'bg-gradient-to-tr from-accent/70 to-transparent' : '', |
|
|
|
index % 4 === 3 ? 'bg-gradient-to-tr from-primary-light/70 to-transparent' : '', |
|
|
|
activeIndex === index ? 'opacity-100' : 'opacity-70' |
|
|
|
]" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 序号装饰 --> |
|
|
|
<div class="absolute top-4 right-4 w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center"> |
|
|
|
<span class="text-white font-bold text-xl">{{ index + 1 }}</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<span |
|
|
|
class="font-bold text-white" |
|
|
|
:class="{ |
|
|
|
'text-3xl': getMilestoneStyle(index).style !== 'full', |
|
|
|
'text-4xl': getMilestoneStyle(index).style === 'full' |
|
|
|
}" |
|
|
|
>{{ index + 1 }}</span> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
v-if="getMilestoneStyle(index).layout !== 'center'" |
|
|
|
class="ml-4 h-px bg-gradient-to-r from-white to-transparent flex-grow" |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 标题 - 不同大小和动画 --> |
|
|
|
<h2 |
|
|
|
class="font-bold text-white mb-6 wow animate__animated" |
|
|
|
:class="{ |
|
|
|
'text-4xl md:text-5xl': getMilestoneStyle(index).style === 'medium', |
|
|
|
'text-5xl md:text-6xl': getMilestoneStyle(index).style === 'large', |
|
|
|
'text-6xl md:text-7xl': getMilestoneStyle(index).style === 'full', |
|
|
|
'animate__fadeInUp': getMilestoneStyle(index).layout === 'left' || getMilestoneStyle(index).layout === 'center', |
|
|
|
'animate__fadeInRight': getMilestoneStyle(index).layout === 'right' |
|
|
|
}" |
|
|
|
> |
|
|
|
{{ milestone.title }} |
|
|
|
</h2> |
|
|
|
|
|
|
|
<!-- 描述 - 不同样式和动画 --> |
|
|
|
<div |
|
|
|
class="text-white/80 mb-8 wow animate__animated" |
|
|
|
:class="{ |
|
|
|
'text-base': getMilestoneStyle(index).style === 'medium', |
|
|
|
'text-lg': getMilestoneStyle(index).style === 'large', |
|
|
|
'text-xl': getMilestoneStyle(index).style === 'full', |
|
|
|
'animate__fadeInUp animate__delay-xs': getMilestoneStyle(index).layout === 'left' || getMilestoneStyle(index).layout === 'center', |
|
|
|
'animate__fadeInRight animate__delay-xs': getMilestoneStyle(index).layout === 'right' |
|
|
|
}" |
|
|
|
> |
|
|
|
<div v-html="milestone.description"></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 里程碑编号 --> |
|
|
|
<div |
|
|
|
class="flex items-center space-x-2 wow animate__animated animate__fadeInUp animate__delay-sm" |
|
|
|
:class="{ |
|
|
|
'justify-start': getMilestoneStyle(index).layout !== 'center', |
|
|
|
'justify-center': getMilestoneStyle(index).layout === 'center' |
|
|
|
}" |
|
|
|
> |
|
|
|
<div class="w-3 h-3 rounded-full bg-white/40 animate-pulse"></div> |
|
|
|
<span class="text-white/70 text-sm">里程碑 {{ index + 1 }}/{{ milestones.length }}</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 底部装饰 --> |
|
|
|
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2"> |
|
|
|
<div class="w-4 h-4 bg-primary rounded-full animate-ping"></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 装饰元素 - 随机位置 --> |
|
|
|
<div |
|
|
|
class="absolute" |
|
|
|
:class="{ |
|
|
|
'bottom-8 right-8': index % 3 === 0, |
|
|
|
'top-8 right-8': index % 3 === 1, |
|
|
|
'bottom-8 left-8': index % 3 === 2 |
|
|
|
}" |
|
|
|
> |
|
|
|
<div class="flex items-center space-x-4"> |
|
|
|
<div class="w-3 h-3 rounded-full bg-white/40 animate-pulse"></div> |
|
|
|
<div class="w-3 h-3 rounded-full bg-white/60 animate-pulse" style="animation-delay: 0.5s"></div> |
|
|
|
<div class="w-3 h-3 rounded-full bg-white/80 animate-pulse" style="animation-delay: 1s"></div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 浮动装饰 --> |
|
|
|
<div |
|
|
|
v-if="index % 2 === 0" |
|
|
|
class="absolute top-1/4 right-1/4 w-32 h-32 rounded-full border border-white/20 opacity-50 animate-float" |
|
|
|
:style="{animationDelay: `${index * 0.2}s`}" |
|
|
|
></div> |
|
|
|
|
|
|
|
<div |
|
|
|
v-if="index % 2 === 1" |
|
|
|
class="absolute bottom-1/4 left-1/4 w-24 h-24 rounded-full border border-white/20 opacity-50 animate-float-reverse" |
|
|
|
:style="{animationDelay: `${index * 0.2}s`}" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 滚动指示器 (除了最后一个) --> |
|
|
|
<div |
|
|
|
v-if="index < milestones.length - 1" |
|
|
|
class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white/70 text-sm flex flex-col items-center cursor-pointer hover:text-white transition-colors duration-300" |
|
|
|
@click="scrollToNext(index)" |
|
|
|
> |
|
|
|
<span class="mb-1">继续探索</span> |
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 animate-bounce-soft" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" /> |
|
|
|
</svg> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
/* 时间轴动画 */ |
|
|
|
@keyframes progress { |
|
|
|
from { height: 0; } |
|
|
|
to { height: 100%; } |
|
|
|
.milestone-gallery { |
|
|
|
scroll-snap-type: y proximity; |
|
|
|
overflow-y: auto; |
|
|
|
scroll-behavior: smooth; |
|
|
|
} |
|
|
|
|
|
|
|
.milestone-item { |
|
|
|
.milestone-section { |
|
|
|
scroll-snap-align: start; |
|
|
|
position: relative; |
|
|
|
} |
|
|
|
|
|
|
|
/* 活跃状态的样式 */ |
|
|
|
.milestone-marker.active .outer-circle { |
|
|
|
transform: scale(1.5); |
|
|
|
border-color: var(--color-primary); |
|
|
|
/* 动画延迟类 */ |
|
|
|
.animate__delay-xs { |
|
|
|
animation-delay: 0.2s; |
|
|
|
} |
|
|
|
|
|
|
|
.animate__delay-sm { |
|
|
|
animation-delay: 0.4s; |
|
|
|
} |
|
|
|
|
|
|
|
.animate__delay-md { |
|
|
|
animation-delay: 0.6s; |
|
|
|
} |
|
|
|
|
|
|
|
/* 脉冲动画 */ |
|
|
|
@keyframes pulse { |
|
|
|
0%, 100% { |
|
|
|
opacity: 0.6; |
|
|
|
transform: scale(1); |
|
|
|
} |
|
|
|
50% { |
|
|
|
opacity: 1; |
|
|
|
transform: scale(1.2); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.animate-pulse { |
|
|
|
animation: pulse 2s infinite; |
|
|
|
} |
|
|
|
|
|
|
|
/* 更柔和的弹跳动画 */ |
|
|
|
@keyframes bounce-soft { |
|
|
|
0%, 20%, 50%, 80%, 100% { |
|
|
|
transform: translateY(0); |
|
|
|
} |
|
|
|
40% { |
|
|
|
transform: translateY(-8px); |
|
|
|
} |
|
|
|
60% { |
|
|
|
transform: translateY(-4px); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.animate-bounce-soft { |
|
|
|
animation: bounce-soft 2s infinite; |
|
|
|
} |
|
|
|
|
|
|
|
.milestone-marker.active .inner-circle { |
|
|
|
background-color: var(--color-primary); |
|
|
|
/* 浮动动画 */ |
|
|
|
@keyframes float { |
|
|
|
0%, 100% { |
|
|
|
transform: translateY(0) translateX(0); |
|
|
|
} |
|
|
|
25% { |
|
|
|
transform: translateY(-10px) translateX(5px); |
|
|
|
} |
|
|
|
50% { |
|
|
|
transform: translateY(0) translateX(10px); |
|
|
|
} |
|
|
|
75% { |
|
|
|
transform: translateY(10px) translateX(5px); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* 悬停效果 */ |
|
|
|
.milestone-content:hover .milestone-title { |
|
|
|
color: var(--color-primary); |
|
|
|
.animate-float { |
|
|
|
animation: float 8s ease-in-out infinite; |
|
|
|
} |
|
|
|
|
|
|
|
/* 响应式调整 */ |
|
|
|
@media (max-width: 768px) { |
|
|
|
.milestone-marker .year-label { |
|
|
|
display: none; |
|
|
|
@keyframes float-reverse { |
|
|
|
0%, 100% { |
|
|
|
transform: translateY(0) translateX(0); |
|
|
|
} |
|
|
|
25% { |
|
|
|
transform: translateY(10px) translateX(-5px); |
|
|
|
} |
|
|
|
50% { |
|
|
|
transform: translateY(0) translateX(-10px); |
|
|
|
} |
|
|
|
75% { |
|
|
|
transform: translateY(-10px) translateX(-5px); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.animate-float-reverse { |
|
|
|
animation: float-reverse 8s ease-in-out infinite; |
|
|
|
} |
|
|
|
</style> |