hookehuyr

feat(recall): 添加垂直滑动页面和动画效果

为回忆页面添加垂直滑动功能,使用Swiper实现多屏滑动效果
添加元素进入动画和过渡效果,提升用户体验
新增第二屏内容展示用户活动数据和义工信息
<!--
* @Date: 2025-12-19 15:40:34
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-19 16:00:08
* @LastEditTime: 2025-12-19 16:24:51
* @FilePath: /mlaj/src/views/recall/Index.vue
* @Description: 文件描述
-->
<template>
<div class="min-h-screen bg-[#F0FBF9] flex flex-col items-center pt-20 pb-10 px-6 relative overflow-hidden">
<!-- 头像 -->
<div class="relative mb-6">
<img
:src="userAvatar || defaultAvatar"
class="w-24 h-24 rounded-full border-4 border-white shadow-lg object-cover"
alt="User Avatar"
/>
</div>
<!-- 欢迎语 -->
<h1 class="text-2xl font-bold text-[#1A3B34] mb-2">{{ userName }}</h1>
<h2 class="text-xl font-bold text-[#1A3B34] mb-4">欢迎回归美乐爱觉宇宙</h2>
<p class="text-slate-500 text-sm mb-12">您与Behalo的故事从这里开始</p>
<!-- 时间卡片 -->
<div class="bg-white rounded-3xl p-8 shadow-xl w-full max-w-sm text-center mb-auto relative z-10">
<p class="text-slate-500 mb-4">您加入Behalo的时间</p>
<div class="text-3xl font-bold text-[#1A3B34] mb-4 tracking-wide">{{ joinDateFormatted }}</div>
<div class="text-slate-500">
已有 <span class="text-[#1A3B34] font-bold text-lg">{{ durationString }}</span>
</div>
</div>
<!-- 底部引导 -->
<div class="flex flex-col items-center mt-12 animate-bounce cursor-pointer">
<ChevronDoubleDownIcon class="w-6 h-6 text-[#4ADE80] mb-2" />
<span class="text-[#4ADE80] font-medium text-sm">向上滑动,探索更多历程</span>
</div>
<div class="h-screen bg-[#F0FBF9] overflow-hidden relative">
<!-- Swiper Container -->
<swiper
:direction="'vertical'"
:modules="[Mousewheel]"
:mousewheel="true"
class="h-full w-full"
@slideChange="onSlideChange"
:speed="800"
@swiper="onSwiper"
>
<!-- 第一屏 -->
<swiper-slide>
<div class="flex flex-col items-center pt-20 pb-10 px-6 relative h-full w-full">
<!-- 头像 -->
<div class="relative mb-6 opacity-0 translate-y-4 transition-all duration-700 delay-100" :class="{ 'opacity-100 translate-y-0': activeIndex === 0 }">
<img
:src="userAvatar || defaultAvatar"
class="w-24 h-24 rounded-full border-4 border-white shadow-lg object-cover"
alt="User Avatar"
/>
</div>
<!-- 欢迎语 -->
<h1 class="text-2xl font-bold text-[#1A3B34] mb-2 text-center opacity-0 translate-y-4 transition-all duration-700 delay-200" :class="{ 'opacity-100 translate-y-0': activeIndex === 0 }">{{ userName }}</h1>
<h2 class="text-xl font-bold text-[#1A3B34] mb-4 text-center opacity-0 translate-y-4 transition-all duration-700 delay-300" :class="{ 'opacity-100 translate-y-0': activeIndex === 0 }">欢迎回归美乐爱觉宇宙</h2>
<p class="text-slate-500 text-sm mb-12 text-center opacity-0 translate-y-4 transition-all duration-700 delay-400" :class="{ 'opacity-100 translate-y-0': activeIndex === 0 }">您与Behalo的故事从这里开始</p>
<!-- 时间卡片 -->
<div class="bg-white rounded-3xl p-8 shadow-xl w-full max-w-sm text-center mb-auto relative z-10 opacity-0 scale-95 transition-all duration-700 delay-500" :class="{ 'opacity-100 scale-100': activeIndex === 0 }">
<p class="text-slate-500 mb-4">您加入Behalo的时间</p>
<div class="text-3xl font-bold text-[#1A3B34] mb-4 tracking-wide">{{ joinDateFormatted }}</div>
<div class="text-slate-500">
已有 <span class="text-[#1A3B34] font-bold text-lg">{{ durationString }}</span>
</div>
</div>
<!-- 底部引导 -->
<div class="flex flex-col items-center mt-12 animate-bounce cursor-pointer opacity-0 transition-opacity duration-1000 delay-1000" :class="{ 'opacity-100': activeIndex === 0 }" @click="slideNext">
<ChevronDoubleDownIcon class="w-6 h-6 text-[#4ADE80] mb-2" />
<span class="text-[#4ADE80] font-medium text-sm">向上滑动,探索更多历程</span>
</div>
</div>
</swiper-slide>
<!-- 第二屏 -->
<swiper-slide>
<div class="flex flex-col items-center pt-10 pb-10 px-6 relative h-full w-full overflow-y-auto overflow-x-hidden">
<h2 class="text-2xl font-bold text-[#1A3B34] mb-2 text-center opacity-0 translate-y-4 transition-all duration-700 delay-100" :class="{ 'opacity-100 translate-y-0': activeIndex === 1 }">您的Behalo足迹</h2>
<p class="text-slate-500 text-sm mb-8 text-center opacity-0 translate-y-4 transition-all duration-700 delay-200" :class="{ 'opacity-100 translate-y-0': activeIndex === 1 }">2016~2025,您在Behalo留下的印记</p>
<!-- 活动参与卡片 -->
<div class="bg-white rounded-3xl p-6 shadow-md w-full max-w-sm mb-4 opacity-0 translate-y-4 transition-all duration-700 delay-300" :class="{ 'opacity-100 translate-y-0': activeIndex === 1 }">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-[#E0F2F1] flex items-center justify-center mr-3 text-[#26A69A]">
<CalendarIcon class="w-5 h-5" />
</div>
<span class="font-bold text-[#1A3B34]">参与宇宙活动</span>
</div>
<div class="mb-2">
<span class="text-5xl font-bold text-[#1A3B34]">{{ activityCount }}</span>
<span class="text-slate-500 ml-1">次</span>
</div>
<p class="text-slate-500 text-xs">您积极参与各类教育活动,累计参与{{ activityCount }}次</p>
</div>
<!-- 义工服务卡片 -->
<div class="bg-white rounded-3xl p-6 shadow-md w-full max-w-sm mb-8 opacity-0 translate-y-4 transition-all duration-700 delay-500" :class="{ 'opacity-100 translate-y-0': activeIndex === 1 }">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-[#E8F5E9] flex items-center justify-center mr-3 text-[#4CAF50]">
<HeartIcon class="w-5 h-5" />
</div>
<span class="font-bold text-[#1A3B34]">义工服务活动</span>
</div>
<div class="mb-2">
<span class="text-5xl font-bold text-[#1A3B34]">{{ volunteerCount }}</span>
<span class="text-slate-500 ml-1">次</span>
</div>
<p class="text-slate-500 text-xs">您热心公益,作为义工服务了{{ volunteerCount }}次活动,贡献时长超过{{ volunteerHours }}小时</p>
</div>
<!-- 荣誉徽章 -->
<!--<div class="w-full max-w-sm opacity-0 translate-y-4 transition-all duration-700 delay-700" :class="{ 'opacity-100 translate-y-0': activeIndex === 1 }">
<h3 class="font-bold text-[#1A3B34] mb-4">获得的荣誉徽章</h3>
<div class="flex justify-between px-2">
<!~~ 徽章1 ~~>
<div class="w-16 h-16 rounded-full bg-[#FFF9C4] flex items-center justify-center shadow-sm">
<TrophyIcon class="w-8 h-8 text-[#FBC02D]" />
</div>
<!~~ 徽章2 ~~>
<div class="w-16 h-16 rounded-full bg-[#F3E5F5] flex items-center justify-center shadow-sm">
<StarIcon class="w-8 h-8 text-[#AB47BC]" />
</div>
<!~~ 徽章3 ~~>
<div class="w-16 h-16 rounded-full bg-[#E3F2FD] flex items-center justify-center shadow-sm">
<CheckBadgeIcon class="w-8 h-8 text-[#42A5F5]" />
</div>
<!~~ 徽章4 (锁定) ~~>
<div class="w-16 h-16 rounded-full bg-[#F5F5F5] flex items-center justify-center shadow-sm">
<LockClosedIcon class="w-8 h-8 text-[#BDBDBD]" />
</div>
</div>
</div>-->
</div>
</swiper-slide>
</swiper>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useAuth } from '@/contexts/auth';
import { ChevronDoubleDownIcon } from '@heroicons/vue/24/solid';
import { ChevronDoubleDownIcon, CalendarIcon, HeartIcon, TrophyIcon, StarIcon, CheckBadgeIcon, LockClosedIcon } from '@heroicons/vue/24/solid'; // Added icons
import dayjs from 'dayjs';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { Mousewheel } from 'swiper/modules';
import 'swiper/css';
// 状态
const { currentUser } = useAuth();
// 使用一个通用的默认头像URL,实际项目中应替换为本地资源或配置的默认图
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/default_avatar.png';
// Swiper 实例和状态
const swiperInstance = ref(null);
const activeIndex = ref(0);
const onSwiper = (swiper) => {
swiperInstance.value = swiper;
};
const onSlideChange = (swiper) => {
activeIndex.value = swiper.activeIndex;
};
const slideNext = () => {
swiperInstance.value?.slideNext();
};
const userAvatar = computed(() => {
let avatar = currentUser.value?.avatar;
if (avatar && avatar.includes('cdn.ipadbiz.cn')) {
......@@ -60,7 +156,6 @@ const userAvatar = computed(() => {
const userName = computed(() => currentUser.value?.name || '旅行者');
// 加入时间逻辑
// 优先使用 created_at,如果没有则尝试 reg_time,最后回退到当前时间(防止报错)
const joinDate = computed(() => {
return currentUser.value?.created_at || currentUser.value?.reg_time || new Date();
});
......@@ -75,7 +170,6 @@ const durationString = computed(() => {
const years = now.diff(start, 'year');
const months = now.diff(start, 'month') % 12;
// 如果不满一个月,显示天数
if (years === 0 && months === 0) {
const days = now.diff(start, 'day');
return `${days} 天`;
......@@ -88,8 +182,13 @@ const durationString = computed(() => {
}
});
// Mock Data for Second Screen
const activityCount = ref(28);
const volunteerCount = ref(12);
const volunteerHours = ref(100);
onMounted(() => {
// 调试用,确认用户信息字段
console.log('Recall Page - Current User:', currentUser.value);
});
</script>
......