Skip to content

Commit ec82fc1

Browse files
committed
feat(annual-report): 新增年度报告组件及页面
添加年度报告相关组件,包括封面、时间轴、账户统计、高光时刻、分类图表、情感记忆、四季回顾、最远城市和足迹地图等模块。实现平滑滚动、动画效果和数据可视化功能,支持暗黑模式。 - 新增 AnnualContainer 作为报告容器 - 添加 ReportPage 作为基础页面组件 - 实现封面、结束页和消息页 - 添加时间、账户、高光时刻等数据展示模块 - 实现分类饼图和情感记忆轮播 - 添加四季回顾和城市足迹地图 - 支持暗黑模式切换 - 优化动画效果和响应式设计
1 parent 4ffcd7e commit ec82fc1

16 files changed

+2375
-20
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<div class="annual-container w-full h-screen overflow-hidden bg-bg-light dark:bg-dark-navy text-light-text1 dark:text-dark-text-warm-gray">
3+
<!-- Scroll Wrapper -->
4+
<div ref="scrollWrapper" class="scroll-wrapper snap-y snap-mandatory overflow-y-auto h-screen w-full relative">
5+
<slot />
6+
</div>
7+
</div>
8+
</template>
9+
10+
<style scoped>
11+
.scroll-wrapper {
12+
scroll-behavior: smooth;
13+
scrollbar-width: none;
14+
}
15+
.scroll-wrapper::-webkit-scrollbar { display: none; }
16+
</style>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<template>
2+
<div class="report-page page-item relative w-full h-full flex flex-col justify-center items-center overflow-hidden" :class="props.maxWidth ? 'p-0' : ''">
3+
<!-- Gradient Background (Global) -->
4+
<div class="absolute inset-0 bg-gradient-to-b from-bg-top to-bg-bottom dark:from-dark-navy dark:to-dark-gray-blue z-0"></div>
5+
6+
<!-- Content Wrapper -->
7+
<div class="content-wrapper relative z-10 w-full flex flex-col gap-6 md:px-0 h-full justify-center" :class="props.maxWidth ? '' : 'max-w-lg px-4'">
8+
<transition name="page-fade" appear>
9+
<div class="w-full h-full flex flex-col justify-center relative">
10+
<slot />
11+
</div>
12+
</transition>
13+
</div>
14+
</div>
15+
</template>
16+
<script setup lang="ts">
17+
import { ref, onMounted, onUnmounted, watch } from 'vue';
18+
19+
const props = defineProps<{
20+
maxWidth?: string;
21+
}>();
22+
23+
</script>
24+
25+
<style scoped>
26+
.page-fade-enter-active { transition: all 0.8s ease-out; }
27+
.page-fade-leave-active { transition: all 0.5s ease-in; }
28+
.page-fade-enter-from { opacity: 0; transform: translateY(20px); }
29+
.page-fade-leave-to { opacity: 0; transform: translateY(-20px); }
30+
</style>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
<template>
3+
<ReportPage>
4+
<div class="flex flex-col justify-center h-full w-full">
5+
<h2 class="text-2xl font-bold text-center mb-8 text-light-text1 dark:text-white">
6+
时光账本
7+
</h2>
8+
9+
<div class="grid grid-cols-2 gap-4 md:gap-6 w-full">
10+
<div
11+
v-for="(item, index) in items"
12+
:key="index"
13+
class="bg-white/60 dark:bg-white/5 backdrop-blur-md rounded-2xl p-4 md:p-6 flex flex-col items-center text-center gap-3 border border-white/20 shadow-sm hover:shadow-md transition-all duration-300 hover:-translate-y-1"
14+
:style="{ animationDelay: `${index * 100}ms` }"
15+
>
16+
<component :is="item.icon" class="w-8 h-8" :class="item.color" />
17+
<div class="text-2xl font-bold text-light-text1 dark:text-white">{{ item.value }}</div>
18+
<div class="text-xs text-light-text3 dark:text-gray-400 font-medium">{{ item.label }}</div>
19+
<div class="text-[10px] text-light-text3/70 dark:text-gray-500">{{ item.desc }}</div>
20+
</div>
21+
</div>
22+
</div>
23+
</ReportPage>
24+
</template>
25+
26+
<script setup lang="ts">
27+
import ReportPage from './ReportPage.vue';
28+
import type { TimeMetrics, EmotionMetrics } from '@/types/annualReport';
29+
import { Camera, Star, Video, Tag, User, Clock } from 'lucide-vue-next';
30+
31+
const props = defineProps<{
32+
time: TimeMetrics;
33+
emotion: EmotionMetrics;
34+
}>();
35+
36+
const items = [
37+
{
38+
icon: Camera,
39+
value: props.time.totalPhotos,
40+
label: '年度留存总数',
41+
desc: '每一帧,都是时光的印记',
42+
color: 'text-blue-500'
43+
},
44+
{
45+
icon: User,
46+
value: props.emotion.starredPhotos,
47+
label: '年度人物同框数',
48+
desc: '藏起的,都是心头的偏爱',
49+
color: 'text-yellow-500'
50+
},
51+
{
52+
icon: Video,
53+
value: `${(props.emotion.totalVideoDuration / 60).toFixed(0)}分钟`,
54+
label: '年度拍摄视频时长',
55+
desc: '流动的时光,都被温柔留存',
56+
color: 'text-cyan-500'
57+
},
58+
{
59+
icon: Tag,
60+
value: props.emotion.totalOpenTimes,
61+
label: '年度高频标签数',
62+
desc: '镜头的偏爱,藏着生活模样',
63+
color: 'text-purple-500'
64+
}
65+
];
66+
</script>
67+
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<template>
2+
<ReportPage>
3+
<div class="flex flex-col w-full">
4+
<!-- Title -->
5+
<div class="text-center mb-2">
6+
<h2 class="text-2xl font-bold text-light-text1 dark:text-white">你的年度生活图谱</h2>
7+
<p class="text-sm text-light-text3 dark:text-gray-400 mt-2">镜头里的世界,藏着你的热爱</p>
8+
</div>
9+
10+
<!-- Chart -->
11+
<div ref="chartRef" class="w-full h-[250px] md:h-[350px]"></div>
12+
13+
<!-- Top List -->
14+
<div class="flex flex-col gap-3 mt-0">
15+
<div
16+
v-for="(cat, index) in topCategories"
17+
:key="cat.name"
18+
class="relative overflow-hidden flex items-center justify-between bg-white/50 dark:bg-white/5 p-3 rounded-xl backdrop-blur-sm"
19+
>
20+
<!-- Hand-drawn style progress bar -->
21+
<div
22+
class="absolute top-0 left-0 h-full transition-all duration-1000 ease-out z-0"
23+
:style="{
24+
width: `calc(${cat.percent}% + 10px)`,
25+
backgroundColor: ['#F97316', '#FBBF24', '#EC4899'][index] || '#ccc',
26+
opacity: 0.2,
27+
borderRadius: '0 12px 12px 0',
28+
transform: 'skewX(-10deg) translateX(-5px)'
29+
}"
30+
></div>
31+
32+
<!-- Content -->
33+
<div class="relative z-10 flex items-center gap-2">
34+
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: ['#F97316', '#FBBF24', '#EC4899'][index] || '#ccc' }"></div>
35+
<span class="font-medium text-sm text-light-text1 dark:text-gray-200">{{ cat.name }}</span>
36+
<span class="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5 ml-1">{{ cat.percent }}%</span>
37+
</div>
38+
<div class="relative z-10 flex items-center">
39+
<span class="text-primary-amber font-bold text-sm">{{ cat.value }}</span>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
</ReportPage>
45+
</template>
46+
47+
48+
<script setup lang="ts">
49+
import ReportPage from './ReportPage.vue';
50+
import type { MemoryMetrics } from '@/types/annualReport';
51+
import { onMounted, ref, computed } from 'vue';
52+
import * as echarts from 'echarts';
53+
54+
const props = defineProps<{
55+
data: MemoryMetrics;
56+
}>();
57+
58+
const chartRef = ref<HTMLElement | null>(null);
59+
let chartInstance: echarts.ECharts | null = null;
60+
61+
const initChart = () => {
62+
if (!chartRef.value) return;
63+
chartInstance = echarts.init(chartRef.value);
64+
const isMobile = window.innerWidth < 768;
65+
const option = {
66+
series: [
67+
{
68+
type: 'pie',
69+
radius: ['50%', '70%'],
70+
center: ['50%', '40%'],
71+
data: props.data.categoryDistribution,
72+
color: ['#F97316', '#FBBF24', '#EC4899', '#8B5CF6', '#10B981'],
73+
label: { show: !isMobile },
74+
emphasis: {
75+
itemStyle: {
76+
scale: true,
77+
scaleSize: 10,
78+
shadowBlur: 10,
79+
shadowOffsetX: 0,
80+
shadowColor: 'rgba(0, 0, 0, 0.5)'
81+
}
82+
}
83+
}
84+
],
85+
tooltip: {
86+
trigger: 'item',
87+
formatter: '{b}: {c} ({d}%)'
88+
}
89+
};
90+
chartInstance.setOption(option);
91+
};
92+
93+
onMounted(() => {
94+
// Delay init to ensure transition is done or use IntersectionObserver
95+
setTimeout(initChart, 500);
96+
window.addEventListener('resize', () => chartInstance?.resize());
97+
});
98+
99+
// Top 3 Categories
100+
const topCategories = computed(() => {
101+
const total = props.data.categoryDistribution.reduce((acc, cur) => acc + cur.value, 0);
102+
return [...props.data.categoryDistribution]
103+
.sort((a, b) => b.value - a.value)
104+
.slice(0, 3)
105+
.map(cat => ({
106+
...cat,
107+
percent: total ? ((cat.value / total) * 100).toFixed(1) : '0'
108+
}));
109+
});
110+
</script>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<template>
2+
<ReportPage>
3+
<div class="flex flex-col items-center justify-center h-full relative text-center">
4+
<!-- Background Watermark -->
5+
<div class="absolute inset-0 opacity-5 pointer-events-none flex items-center justify-center">
6+
<div class="w-full h-full bg-[url('/icon/Report.svg')] bg-repeat opacity-10"></div>
7+
</div>
8+
9+
<!-- Main Content -->
10+
<div class="z-10 flex flex-col items-center gap-6 animate-fade-in-up">
11+
<!-- Logo/Icon -->
12+
<div class="w-16 h-16 bg-primary-amber rounded-2xl flex items-center justify-center shadow-lg mb-4">
13+
<Camera class="w-10 h-10 text-white" />
14+
</div>
15+
16+
<!-- Titles -->
17+
<div class="space-y-2">
18+
<h1 class="text-[clamp(2rem,5vw,2.8rem)] font-bold text-primary-amber tracking-wider">
19+
TrailSnap
20+
</h1>
21+
<p class="text-lg font-light text-light-text3 dark:text-dark-blue-gray">
22+
一帧一画,定格步履与温柔
23+
</p>
24+
</div>
25+
26+
<!-- User Info -->
27+
<div class="flex items-center gap-4 mt-8 bg-white/50 dark:bg-black/20 p-4 rounded-full backdrop-blur-sm border border-white/20">
28+
<img
29+
:src="user.avatarUrl || defaultAvatar"
30+
class="w-16 h-16 rounded-full border-2 border-primary-amber shadow-sm object-cover"
31+
alt="Avatar"
32+
/>
33+
<div class="text-left pr-4">
34+
<div class="text-sm text-light-text3 dark:text-gray-400">时光旅人</div>
35+
<div class="text-xl font-bold text-light-text1 dark:text-white">{{ user.nickname }}</div>
36+
</div>
37+
</div>
38+
39+
<!-- Year -->
40+
<div class="text-6xl font-black text-light-text1/10 dark:text-white/10 mt-4">
41+
{{ year }}
42+
</div>
43+
</div>
44+
45+
<!-- Bottom Hint -->
46+
<div class="absolute bottom-10 flex flex-col items-center gap-2 animate-bounce">
47+
<p class="text-xs text-light-text3 dark:text-gray-400">向上滑动,开启你的时光回忆录</p>
48+
<ArrowUp class="w-5 h-5 text-primary-amber" />
49+
</div>
50+
</div>
51+
</ReportPage>
52+
</template>
53+
54+
<script setup lang="ts">
55+
import ReportPage from './ReportPage.vue';
56+
import { computed } from 'vue';
57+
import type { UserInfo } from '@/types/annualReport';
58+
import { Camera, ArrowUp } from 'lucide-vue-next';
59+
60+
const props = defineProps<{
61+
user: UserInfo;
62+
year: number;
63+
}>();
64+
65+
const defaultAvatar = '/avatar.png'; // Placeholder
66+
</script>
67+
68+
<style scoped>
69+
.animate-fade-in-up {
70+
animation: fadeInUp 1s ease-out forwards;
71+
}
72+
@keyframes fadeInUp {
73+
from { opacity: 0; transform: translateY(20px); }
74+
to { opacity: 1; transform: translateY(0); }
75+
}
76+
</style>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<template>
2+
<ReportPage>
3+
<div class="flex flex-col h-full w-full items-center justify-center text-center">
4+
<!-- Main Tag -->
5+
<div class="relative mb-8 animate-scale-in">
6+
<div class="absolute -inset-4 bg-primary-amber/10 rounded-full blur-xl"></div>
7+
<h2 class="relative text-[clamp(2.5rem,8vw,3.5rem)] font-bold text-primary-amber">
8+
{{ data.tags.main }}
9+
</h2>
10+
</div>
11+
12+
<!-- Sub Tags -->
13+
<div class="flex flex-wrap gap-3 justify-center mb-10">
14+
<span
15+
v-for="tag in data.tags.sub"
16+
:key="tag"
17+
class="px-4 py-1.5 bg-white dark:bg-white/10 rounded-full text-sm text-light-text2 dark:text-gray-300 shadow-sm border border-gray-100 dark:border-gray-700"
18+
>
19+
{{ tag }}
20+
</span>
21+
</div>
22+
23+
<!-- Best Photo -->
24+
<div class="relative w-64 h-64 md:w-72 md:h-72 mb-6 group">
25+
<div class="absolute inset-0 bg-primary-amber rounded-2xl rotate-3 opacity-20 group-hover:rotate-6 transition-transform"></div>
26+
<img
27+
:src="data.bestPhotoUrl"
28+
class="relative w-full h-full object-cover rounded-2xl shadow-xl border-4 border-white dark:border-gray-700 transform transition-transform group-hover:-rotate-1"
29+
alt="Best Photo"
30+
/>
31+
<div class="absolute -bottom-6 left-0 right-0 text-center">
32+
<span class="inline-block bg-white/90 dark:bg-black/60 backdrop-blur px-3 py-1 rounded-full text-xs text-gray-500 shadow-sm">
33+
{{ data.bestPhotoDate }} · 年度最佳瞬间
34+
</span>
35+
</div>
36+
</div>
37+
38+
</div>
39+
</ReportPage>
40+
</template>
41+
42+
<script setup lang="ts">
43+
import ReportPage from './ReportPage.vue';
44+
import type { EasterEgg } from '@/types/annualReport';
45+
import { Quote } from 'lucide-vue-next';
46+
47+
const props = defineProps<{
48+
data: EasterEgg;
49+
}>();
50+
</script>
51+
52+
<style scoped>
53+
.animate-scale-in {
54+
animation: scaleIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
55+
}
56+
@keyframes scaleIn {
57+
from { transform: scale(0.5); opacity: 0; }
58+
to { transform: scale(1); opacity: 1; }
59+
}
60+
</style>

0 commit comments

Comments
 (0)