<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n';
|
|
import { ref, onMounted, reactive } from 'vue';
|
|
import { queryCourseList } from '@/api';
|
|
import type { CourseItem } from '@/api';
|
|
import { useConfig } from '@/utils/config';
|
|
|
|
const { getConfigImage } = useConfig();
|
|
|
|
const { t } = useI18n();
|
|
|
|
// 里程碑数据 - 使用API返回的真实数据
|
|
const milestones = ref<CourseItem[]>([]);
|
|
const loading = ref(true);
|
|
const error = ref<string | 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 () => {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const response = await queryCourseList();
|
|
if (response && Array.isArray(response) && response.length > 0) {
|
|
milestones.value = response;
|
|
}
|
|
} catch (err) {
|
|
console.error('获取发展历程数据失败:', err);
|
|
error.value = '获取发展历程数据失败';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// 视差滚动效果
|
|
const parallaxOffset = ref({ x: 0, y: 0 });
|
|
|
|
// 鼠标移动事件处理
|
|
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 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>
|
|
<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 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>
|
|
|
|
<!-- 里程碑展示 -->
|
|
<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
|
|
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="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
|
|
class="mb-6"
|
|
:class="{'flex items-center': getMilestoneStyle(index).layout !== 'center'}"
|
|
>
|
|
<div
|
|
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'
|
|
}"
|
|
>
|
|
<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>
|
|
|
|
<!-- 装饰元素 - 随机位置 -->
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.milestone-gallery {
|
|
scroll-snap-type: y proximity;
|
|
overflow-y: auto;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
.milestone-section {
|
|
scroll-snap-align: start;
|
|
position: relative;
|
|
}
|
|
|
|
/* 动画延迟类 */
|
|
.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;
|
|
}
|
|
|
|
/* 浮动动画 */
|
|
@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);
|
|
}
|
|
}
|
|
|
|
.animate-float {
|
|
animation: float 8s ease-in-out infinite;
|
|
}
|
|
|
|
@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>
|