Browse Source

feat(about): 重构关于页面组件,优化视觉效果和交互体验

重构CompanyModule、TeamModule和MilestoneModule组件,主要变更包括:
1. CompanyModule改为从API获取公司位置数据并实现错落布局
2. TeamModule使用Swiper实现卡片轮播效果并优化样式
3. MilestoneModule重新设计里程碑展示方式,增加视差滚动效果
4. 统一添加加载状态和错误处理
5. 优化动画效果和响应式设计
前端-胡立永 1 week ago
parent
commit
86525562dd
3 changed files with 750 additions and 451 deletions
  1. +193
    -72
      src/components/about/CompanyModule.vue
  2. +307
    -289
      src/components/about/MilestoneModule.vue
  3. +250
    -90
      src/components/about/TeamModule.vue

+ 193
- 72
src/components/about/CompanyModule.vue View File

@ -1,89 +1,210 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { queryCompanyList, type CompanyItem } from '@/api';
import { useConfig } from '@/utils/config';
const { getConfigImage } = useConfig();
const { t } = useI18n();
//
const companyLocations = ref([
{
id: 1,
city: '香港',
country: '中国',
address: '香港中环金融街88号',
role: '区块链研发中心',
image: '/LOGO.png'
},
{
id: 2,
city: '新加坡',
country: '新加坡',
address: '新加坡金融区10号',
role: '全球运营总部',
image: '/LOGO.png'
},
{
id: 3,
city: '伦敦',
country: '英国',
address: '伦敦金融城15号',
role: '欧洲市场拓展中心',
image: '/LOGO.png'
},
{
id: 4,
city: '迪拜',
country: '阿联酋',
address: '迪拜国际金融中心28号',
role: '中东市场拓展中心',
image: '/LOGO.png'
},
{
id: 5,
city: '东京',
country: '日本',
address: '东京涩谷区105号',
role: '亚太技术中心',
image: '/LOGO.png'
// -
const getLocationPosition = (index: number) => {
const positions = [
{ x: 35, y: 15 }, //
{ x: 45, y: 25 }, //
{ x: 55, y: 15 }, //
{ x: 65, y: 35 }, // 西
{ x: 75, y: 20 }, //
{ x: 85, y: 30 }, // 西
{ x: 75, y: 45 }, //
{ x: 40, y: 45 }, //
{ x: 50, y: 55 }, //
{ x: 60, y: 50 }, //
{ x: 70, y: 60 } //
];
return positions[index] || { x: 50, y: 50 };
};
//
const companyLocations = ref<CompanyItem[]>([]);
const loading = ref(false);
//
const fetchCompanyList = async () => {
try {
loading.value = true;
const data = await queryCompanyList();
companyLocations.value = data;
} catch (error) {
console.error('获取公司列表数据失败:', error);
} finally {
loading.value = false;
}
]);
};
//
onMounted(() => {
fetchCompanyList();
});
</script>
<template>
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
全球战略部署
</h2>
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background-dark relative overflow-hidden">
<!-- 背景装饰 -->
<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>
<div class="container mx-auto relative z-10">
<!-- 标题部分 -->
<div class="max-w-3xl mx-auto text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-text mb-6 wow animate__animated animate__fadeInDown animate__duration-fast">
全球战略部署
</h2>
<p class="text-lg md:text-xl text-text-secondary wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast">
全球战略部署构建全球战略网络
</p>
<!-- 装饰线 -->
<div class="w-24 h-1 bg-primary mx-auto mt-8"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
v-for="(location, index) in companyLocations"
:key="location.id"
class="flex bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
:class="{
'animate__delay-xs': index % 5 === 1,
'animate__delay-sm': index % 5 === 2,
'animate__delay-md': index % 5 === 3,
'animate__delay-lg': index % 5 === 4
}"
>
<!-- 地点图片 -->
<div class="w-1/3">
<img :src="location.image" :alt="location.city" class="w-full h-full object-cover" />
<!-- 全球办公室展示 - 错乱布局 -->
<div class="relative max-w-7xl mx-auto h-96 md:h-[500px] lg:h-[600px]">
<!-- 背景图片 -->
<div class="absolute inset-0 z-0">
<img
:src="getConfigImage('about_CompanyModule_bg')"
alt="全球地图背景"
class="w-full h-full object-cover rounded-2xl"
/>
<div class="absolute inset-0 bg-black/40 rounded-2xl"></div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="absolute inset-0 flex items-center justify-center z-30">
<div class="text-white text-lg">加载中...</div>
</div>
<!-- 左侧Logo -->
<div class="absolute top-1/2 left-8 transform -translate-y-1/2 z-20">
<div class="w-32 h-32 md:w-40 md:h-40 rounded-full bg-white/10 backdrop-blur-sm flex items-center justify-center border-2 border-white/20">
<div class="text-white text-2xl md:text-3xl font-bold">MOSE</div>
</div>
<!-- 地点信息 -->
<div class="w-2/3 p-5">
<div class="flex items-center mb-2">
<h3 class="text-lg font-bold text-text">{{ location.city }}</h3>
<span class="text-text-secondary text-sm ml-2">{{ location.country }}</span>
</div>
<!-- 连接线 -->
<svg class="absolute inset-0 w-full h-full z-10" style="pointer-events: none;">
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3B82F6;stop-opacity:0.5" />
</linearGradient>
</defs>
<line
v-for="(location, index) in companyLocations"
:key="`line-${location.id}`"
:x1="15"
:y1="50"
:x2="getLocationPosition(index).x"
:y2="getLocationPosition(index).y"
stroke="url(#lineGradient)"
stroke-width="2"
stroke-dasharray="5,5"
class="animate-pulse"
/>
</svg>
<!-- 办公室位置 -->
<div class="relative w-full h-full">
<div
v-for="(location, index) in companyLocations"
:key="location.id"
class="absolute transform -translate-x-1/2 -translate-y-1/2 wow animate__animated animate__fadeInUp"
:class="`animate__delay-${(index % 5) * 100}ms`"
:style="{
left: getLocationPosition(index).x + '%',
top: getLocationPosition(index).y + '%'
}"
>
<div class="group cursor-pointer">
<!-- 办公室卡片 -->
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20 hover:bg-white/20 transition-all duration-300 transform hover:scale-110">
<!-- 国旗和状态 -->
<div class="flex items-center mb-2">
<img :src="location.image" :alt="location.title + ' flag'" class="w-6 h-4 mr-2 rounded object-cover">
<div class="flex-1">
<h3 class="text-white font-bold text-sm">{{ location.title }}</h3>
<div class="text-white/70 text-xs" v-html="location.description"></div>
</div>
</div>
<!-- 装饰点 -->
<div class="w-2 h-2 bg-orange-400 rounded-full mx-auto animate-pulse"></div>
</div>
<!-- 悬停时的详细信息 -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
<div class="bg-black/80 backdrop-blur-sm rounded-lg p-3 text-white text-xs whitespace-nowrap">
<div class="font-bold">{{ location.title }}, {{ location.description }}</div>
</div>
<div class="w-2 h-2 bg-black/80 transform rotate-45 mx-auto -mt-1"></div>
</div>
</div>
<p class="text-primary-light text-sm font-medium mb-2">{{ location.role }}</p>
<p class="text-text-secondary text-sm">{{ location.address }}</p>
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="text-center mt-16">
<p class="text-text-secondary mb-6">
通过全球办公室网络MOSE为不同地区的用户提供本地化服务和支持
</p>
<div class="flex justify-center space-x-2">
<span v-for="i in 10" :key="i"
class="w-2 h-2 rounded-full transition-all duration-300"
:class="i <= 6 ? 'bg-primary' : 'bg-primary/30'"></span>
</div>
</div>
</div>
</section>
</template>
</template>
<style scoped>
/* 动画延迟类 */
.animate__delay-0ms {
animation-delay: 0ms;
}
.animate__delay-100ms {
animation-delay: 100ms;
}
.animate__delay-200ms {
animation-delay: 200ms;
}
.animate__delay-300ms {
animation-delay: 300ms;
}
.animate__delay-400ms {
animation-delay: 400ms;
}
/* 脉冲动画 */
@keyframes pulse {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
.animate-pulse {
animation: pulse 2s infinite;
}
</style>

+ 307
- 289
src/components/about/MilestoneModule.vue View File

@ -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>

+ 250
- 90
src/components/about/TeamModule.vue View File

@ -2,52 +2,125 @@
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useConfig } from '@/utils/config';
import { Icon } from '@iconify/vue';
import { queryTeamList } from '@/api';
import type { TeamMember } from '@/api';
const { t } = useI18n();
const { getConfigImage } = useConfig();
//
const teamMembers = ref([
{
id: 1,
name: '张明宇',
title: '创始人 & CEO',
bio: '张明宇拥有10年区块链技术经验,曾在多家知名区块链项目担任技术负责人,主导了多个成功的跨链项目,对密码学和分布式系统有深入研究。',
image: '/LOGO.png'
},
{
id: 2,
name: '李华',
title: '首席技术官 (CTO)',
bio: '李华是密码学专家,拥有计算机科学博士学位,在零知识证明和安全多方计算领域发表过多篇论文,曾负责设计多个区块链项目的核心安全架构。',
image: '/LOGO.png'
},
{
id: 3,
name: '王建国',
title: '首席运营官 (COO)',
bio: '王建国拥有20年互联网和金融科技公司运营经验,曾在多家上市公司担任高管职位,对区块链行业商业模式和市场策略有独到见解。',
image: '/LOGO.png'
}
]);
//
const currentMemberIndex = ref(0);
//
const teamMembers = ref<TeamMember[]>([]);
const loading = ref(true);
//
const showNextMember = () => {
currentMemberIndex.value = (currentMemberIndex.value + 1) % teamMembers.value.length;
//
const getCardBackground = () => {
return 'bg-slate-800';
};
//
const showPrevMember = () => {
currentMemberIndex.value = (currentMemberIndex.value - 1 + teamMembers.value.length) % teamMembers.value.length;
//
const fetchTeamData = async () => {
try {
loading.value = true;
const response = await queryTeamList({
pageNo: 1,
pageSize: 10
});
if (response && Array.isArray(response)) {
teamMembers.value = response;
}
} catch (error) {
console.error('Failed to fetch team data:', error);
} finally {
loading.value = false;
}
};
//
const currentMember = computed(() => {
return teamMembers.value[currentMemberIndex.value];
// Swiper
let swiper = null;
//
onMounted(async () => {
//
await fetchTeamData();
// 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);
}
// Swiper
initSwiper();
});
// Swiper
const initSwiper = () => {
// Swiper
import('swiper/bundle').then(({ Swiper }) => {
import('swiper/css/bundle').then(() => {
// DOM
setTimeout(() => {
swiper = new Swiper('.team-swiper-container', {
effect: 'coverflow',
grabCursor: true,
centeredSlides: true,
slidesPerView: 'auto',
loop: true, //
speed: 800,
autoplay: {
delay: 5000,
disableOnInteraction: false,
},
pagination: {
el: '.swiper-pagination',
clickable: true,
dynamicBullets: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
coverflowEffect: {
rotate: 0,
stretch: 0,
depth: 100,
modifier: 1,
slideShadows: false,
},
breakpoints: {
320: {
slidesPerView: 1,
spaceBetween: 20
},
768: {
slidesPerView: 2,
spaceBetween: 30
},
1024: {
slidesPerView: 3,
spaceBetween: 30
}
}
});
}, 500);
}).catch(err => {
console.error('Failed to load Swiper CSS:', err);
});
}).catch(err => {
console.error('Failed to load Swiper:', err);
});
};
</script>
<template>
@ -58,71 +131,158 @@ const currentMember = computed(() => {
</h2>
<!-- 领导团队轮播展示 -->
<div class="relative max-w-4xl mx-auto">
<div class="bg-background-light rounded-2xl overflow-hidden shadow-card">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
<!-- 成员照片 -->
<div class="aspect-square relative overflow-hidden rounded-xl">
<img
:src="currentMember.image"
:alt="currentMember.name"
class="w-full h-full object-cover transition-all duration-500"
/>
</div>
<!-- 成员信息 -->
<div class="flex flex-col justify-center">
<h3 class="text-2xl font-bold text-text mb-2">{{ currentMember.name }}</h3>
<p class="text-primary-light font-medium mb-4">{{ currentMember.title }}</p>
<p class="text-text-secondary">{{ currentMember.bio }}</p>
</div>
</div>
</div>
<!-- 左右切换控制器 -->
<div class="absolute top-1/2 -left-5 transform -translate-y-1/2 z-10">
<button
@click="showPrevMember"
class="w-10 h-10 rounded-full bg-background-dark flex items-center justify-center hover:bg-primary hover:bg-opacity-20 transition-colors shadow-lg"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div class="relative wow animate__animated animate__fadeIn animate__duration-fast overflow-hidden">
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-16">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
<span class="ml-3 text-text-secondary">加载中...</span>
</div>
<div class="absolute top-1/2 -right-5 transform -translate-y-1/2 z-10">
<button
@click="showNextMember"
class="w-10 h-10 rounded-full bg-background-dark flex items-center justify-center hover:bg-primary hover:bg-opacity-20 transition-colors shadow-lg"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Swiper轮播 -->
<div v-else class="team-swiper-container">
<div class="swiper-wrapper">
<!-- 团队成员卡片 -->
<div
v-for="(member, index) in teamMembers"
:key="member.id"
class="swiper-slide"
>
<div class="team-card relative overflow-hidden rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 mx-2 my-8 h-[450px]">
<!-- 背景 -->
<div class="absolute inset-0" :class="getCardBackground()"></div>
<!-- 成员照片 - 撑满顶部 -->
<div class="absolute top-0 left-0 right-0 h-2/5 overflow-hidden z-10">
<img
:src="member.image"
:alt="member.name"
class="w-full h-full object-cover"
/>
<!-- 渐变遮罩 -->
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/50"></div>
</div>
<!-- 内容 -->
<div class="relative p-6 flex flex-col h-full pt-[52%]">
<!-- 成员姓名 -->
<h3 class="text-xl font-black text-white text-center mb-2">{{ member.name }}</h3>
<!-- 成员职位 -->
<p class="text-white/90 text-center text-sm mb-4">{{ member.post }}</p>
<!-- 装饰线 -->
<div class="w-16 h-1 bg-white/30 mx-auto mb-4"></div>
<!-- 成员简介 -->
<div class="text-white/90 text-center text-sm leading-relaxed flex-grow">
<div v-html="member.resume"></div>
</div>
<!-- 成员编号 -->
<div class="absolute top-4 right-4 bg-white/20 backdrop-blur-sm w-8 h-8 rounded-full flex items-center justify-center">
<span class="text-white font-bold text-sm">{{ index + 1 }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 分页器 -->
<div class="swiper-pagination mt-8"></div>
</div>
<!-- 指示器 -->
<div class="flex justify-center mt-6 gap-2">
<button
v-for="(member, index) in teamMembers"
:key="member.id"
@click="currentMemberIndex = index"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="currentMemberIndex === index ? 'bg-primary' : 'bg-background-dark'"
></button>
</div>
<!-- 导航按钮 -->
<div v-if="!loading && teamMembers.length > 0" class="swiper-button-prev !text-primary after:!text-lg"></div>
<div v-if="!loading && teamMembers.length > 0" class="swiper-button-next !text-primary after:!text-lg"></div>
</div>
<div class="text-center mt-8">
<p class="text-text-secondary mb-6">{{ t('about.team.description') }}</p>
<a href="#" class="inline-flex items-center text-primary-light hover:text-primary-dark transition-colors">
<span>{{ t('about.team.viewAll') }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<Icon icon="carbon:arrow-right" class="h-5 w-5 ml-1" />
</a>
</div>
</div>
</section>
</template>
</template>
<style scoped>
/* Swiper样式覆盖 */
:deep(.team-swiper-container) {
padding: 30px 0;
overflow: hidden;
width: 100%;
}
:deep(.swiper-slide) {
width: 350px;
transition: transform 0.3s;
opacity: 0.4;
}
:deep(.swiper-slide-active) {
transform: scale(1.05);
z-index: 2;
opacity: 1;
}
:deep(.swiper-slide-prev),
:deep(.swiper-slide-next) {
opacity: 0.7;
}
:deep(.swiper-pagination-bullet) {
width: 10px;
height: 10px;
background: rgba(255, 255, 255, 0.5);
opacity: 1;
}
:deep(.swiper-pagination-bullet-active) {
background: var(--color-primary);
transform: scale(1.2);
}
:deep(.swiper-button-prev),
:deep(.swiper-button-next) {
color: var(--color-primary);
background: rgba(255, 255, 255, 0.3);
width: 50px;
height: 50px;
border-radius: 50%;
backdrop-filter: blur(4px);
transition: all 0.3s;
}
:deep(.swiper-button-prev:hover),
:deep(.swiper-button-next:hover) {
background: rgba(255, 255, 255, 0.5);
}
:deep(.swiper-button-prev:after),
:deep(.swiper-button-next:after) {
font-size: 20px;
font-weight: bold;
}
/* 卡片悬停效果 */
.team-card {
transition: all 0.5s;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
}
.team-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
/* 确保整个section不出现横向滚动 */
section {
overflow-x: hidden;
}
</style>

Loading…
Cancel
Save