All data rendering in rewind

This commit is contained in:
2025-12-27 03:01:16 +08:00
parent 9d6eb5c378
commit cdf177d321
2 changed files with 456 additions and 115 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="py-6 px-5 min-h-screen">
<div class="px-5">
<!-- Loading State -->
<div
v-if="pending"
@@ -49,9 +49,7 @@
<div class="space-y-0">
<!-- Section 1: Pass Data -->
<div
ref="section1"
class="scroll-section min-h-screen flex items-center justify-center"
:class="{ 'animate-in': inView1 }"
>
<n-card class="w-full max-w-4xl">
<template #header>
@@ -101,7 +99,7 @@
<div class="text-2xl font-bold mb-1">
{{ getStreakMessage(rewindData.data.pass.maxCheckInStreak) }}
</div>
<div class="text-lg opacity-80">
<div class="text-md opacity-80">
{{
getStreakDescription(rewindData.data.pass.maxCheckInStreak)
}}
@@ -111,11 +109,130 @@
</n-card>
</div>
<!-- Section 2: Creator Career Overview -->
<!-- Section 2: Lotteries -->
<div
class="scroll-section min-h-screen flex items-center justify-center"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
>
<template #header>
<div class="flex items-center gap-3">
<n-icon
size="28"
:component="PartyPopperIcon"
class="text-purple-600"
/>
<h2 class="text-2xl font-bold">彩票游戏</h2>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 gap-4">
<n-statistic label="获胜次数" tabular-nums>
<n-number-animation
:to="rewindData.data.pass.lotteriesWins"
/>
<template #suffix></template>
</n-statistic>
<n-statistic label="失败次数" tabular-nums>
<n-number-animation
:to="rewindData.data.pass.lotteriesLosses"
/>
<template #suffix></template>
</n-statistic>
<n-statistic label="胜率" tabular-nums>
<n-number-animation
:to="rewindData.data.pass.lotteriesWinRate * 100"
:precision="1"
/>
<template #suffix>%</template>
</n-statistic>
</div>
</div>
<div class="md:text-right pr-4 max-md:order-first">
<div class="text-5xl mb-3">
{{
rewindData.data.pass.lotteriesWinRate >= 0.5 ? "🎉" : "😅"
}}
</div>
<div class="text-2xl font-bold mb-1">
{{ getLotteryMessage(rewindData.data.pass.lotteriesWinRate) }}
</div>
<div class="text-md opacity-80">
{{
getLotteryDescription(rewindData.data.pass.lotteriesWinRate)
}}
</div>
</div>
</div>
</n-card>
</div>
<!-- Section 3: Social Connections -->
<div
class="scroll-section min-h-screen flex items-center justify-center"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
>
<template #header>
<div class="flex items-center gap-3">
<n-icon
size="28"
:component="HeartIcon"
class="text-pink-600"
/>
<h2 class="text-2xl font-bold">社交连接</h2>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 gap-4">
<n-statistic label="新增好友" tabular-nums>
<n-number-animation
:to="rewindData.data.pass.newFriendsCount"
/>
<template #suffix></template>
</n-statistic>
<n-statistic label="新增屏蔽" tabular-nums>
<n-number-animation
:to="rewindData.data.pass.newBlockedCount"
/>
<template #suffix></template>
</n-statistic>
</div>
</div>
<div class="md:text-right pr-4 max-md:order-first">
<div class="text-5xl mb-3">🤝</div>
<div class="text-2xl font-bold mb-1">
{{
getConnectionMessage(
rewindData.data.pass.newFriendsCount,
rewindData.data.pass.newBlockedCount
)
}}
</div>
<div class="text-md opacity-80">
{{
getConnectionDescription(
rewindData.data.pass.newFriendsCount,
rewindData.data.pass.newBlockedCount
)
}}
</div>
</div>
</div>
</n-card>
</div>
<!-- Section 4: Creator Career Overview -->
<div
ref="section2"
class="scroll-section min-h-screen flex items-center justify-center"
:class="{ 'animate-in': inView2 }"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
@@ -210,11 +327,60 @@
</n-card>
</div>
<!-- Section 5: Word Cloud -->
<div
class="scroll-section min-h-screen flex items-center justify-center"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
>
<template #header>
<div class="flex items-center gap-3">
<n-icon
size="28"
:component="MessageCircleIcon"
class="text-blue-600"
/>
<h2 class="text-2xl font-bold">词汇云</h2>
</div>
</template>
<div class="text-center">
<div class="text-5xl mb-6"></div>
<div class="text-2xl font-bold mb-4">你的年度词汇</div>
<div class="text-md opacity-80 mb-8">
这些是你最常使用的词汇反映了你的表达方式和兴趣
</div>
<div
class="flex flex-wrap justify-center items-center gap-4 max-w-3xl mx-auto"
>
<span
v-for="word in rewindData.data.sphere.topWords"
:key="word.word"
:class="getWordCloudClass(word.count)"
class="inline-block transition-all duration-300 hover:scale-110 cursor-default"
:title="`${word.word}: ${word.count} 次`"
>
<n-tooltip>
<template #trigger>
{{ word.word }}
</template>
{{ word.count }} 次使用
</n-tooltip>
</span>
</div>
<div class="mt-8 text-sm opacity-60">
词汇使用频率越高字体越大
</div>
</div>
</n-card>
</div>
<!-- Section 3: Explore History -->
<div
ref="section3"
class="scroll-section min-h-screen flex items-center justify-center"
:class="{ 'animate-in': inView3 }"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
@@ -267,9 +433,9 @@
</nuxt-link>
</div>
<div class="text-right flex flex-col justify-center px-5 gap-2">
<div class="text-4xl">🤔</div>
<div class="text-4xl"></div>
<p class="text-lg">
看起来你真的喜欢他/她呢 (´) <br />
看起来你真的喜欢他/她呢<br />
新的一年不妨试试探索更多优秀创作者吧
</p>
<p class="text-xs opacity-80">
@@ -282,9 +448,7 @@
<!-- Section 4: Chat Summary -->
<div
ref="section4"
class="scroll-section min-h-screen flex items-center justify-center"
:class="{ 'animate-in': inView4 }"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
@@ -312,7 +476,7 @@
<n-avatar
:src="
getChatRoomAvatar(
rewindData.data.sphere.mostCalledChat.chat
rewindData.data.sphere.mostMessagedChat.chat
)
"
>{{
@@ -372,13 +536,111 @@
</div>
</n-card>
</div>
<div>
<h3 class="font-bold mb-2 flex items-center gap-2">
<n-icon :component="PhoneCallIcon" size="16" />
通话时间最长的聊天室
</h3>
<n-card size="small">
<div class="flex items-center gap-4">
<n-avatar
:src="
getChatRoomAvatar(
rewindData.data.sphere.mostCalledChat.chat
)
"
>{{
rewindData.data.sphere.mostCalledChat.chat.name?.substring(
0,
1
)
}}</n-avatar
>
<div class="grow flex flex-col">
<div class="text-md font-bold">
{{ rewindData.data.sphere.mostCalledChat.chat.name }}
</div>
<p>
<n-number-animation
:to="rewindData.data.sphere.mostCalledChat.duration"
/>
分钟
</p>
</div>
</div>
<p class="mt-2 text-opacity-80">
与这些人一起达成这样的成就
</p>
<div
v-if="
rewindData.data.sphere.mostCalledAccounts.length > 0
"
class="flex justify-start gap-4 mt-2"
>
<div
v-for="item in rewindData.data.sphere
.mostCalledChatTopMembers"
:key="item.id"
>
<n-tooltip>
<template #trigger>
<n-avatar
object-fit="cover"
:src="getAccountAvatar(item)"
/>
</template>
{{ item.nick }}
</n-tooltip>
</div>
</div>
</n-card>
</div>
<div>
<h3 class="font-bold mb-2 flex items-center gap-2">
<n-icon :component="WebhookIcon" size="16" />
通话时间前三名
</h3>
<n-card
v-if="rewindData.data.sphere.mostCalledAccounts.length > 0"
size="small"
>
<div class="flex justify-start gap-4 mt-2">
<div
v-for="item in rewindData.data.sphere
.mostCalledAccounts"
:key="item.account.id"
>
<div
class="flex flex-col justify-center items-center text-center gap-2"
>
<n-avatar
object-fit="cover"
:src="getAccountAvatar(item.account)"
/>
<div>
<div class="text-md font-bold">
{{ item.account.nick }}
</div>
<p class="text-sm opacity-80">
{{ item.duration }} 分钟
</p>
</div>
</div>
</div>
</div>
</n-card>
</div>
</div>
<div class="text-right flex flex-col justify-center px-5 gap-2">
<div class="text-4xl">💬</div>
<p class="text-lg">
一眼丁真鉴定为 <br/>
<b>纯纯的话唠</b>
</p>
<div class="text-5xl mb-3">💬</div>
<div class="text-2xl font-bold">
{{ getChatMessage(getTotalMessages(rewindData.data.sphere)) }}
</div>
<div class="text-md opacity-80">
{{
getChatDescription(getTotalMessages(rewindData.data.sphere))
}}
</div>
</div>
</div>
</n-card>
@@ -386,9 +648,7 @@
<!-- Section 6: Summary -->
<div
ref="section6"
class="scroll-section min-h-screen flex items-center justify-center"
:class="{ 'animate-in': inView6 }"
>
<n-card
class="w-full max-w-4xl bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg"
@@ -447,12 +707,11 @@
<p class="text-sm">
<span class="font-medium opacity-80">Favorite Chat:</span>
</p>
<p class="text-sm">
<div class="text-sm">
<span class="font-medium opacity-80"
>Top Connection:</span
>Top Connections:</span
>
{{ rewindData.data.sphere.mostCalledAccounts[0]?.nick }}
</p>
</div>
<p class="text-sm">
<span class="font-medium opacity-80"
>Loved Publisher:</span
@@ -522,13 +781,14 @@ import {
PencilLineIcon,
DownloadIcon,
ShareIcon,
PartyPopperIcon
PartyPopperIcon,
PhoneCallIcon,
WebhookIcon
} from "lucide-vue-next"
import { ref, onMounted, onUnmounted } from "vue"
import { DateTime } from "luxon"
import { ref } from "vue"
import type {
SnAccount,
SnRewind,
SnRewindMostCalledChat,
SnRewindChat,
SnRewindChatMember
} from "~/types/api"
@@ -541,23 +801,7 @@ const pending = ref(true)
const error = ref<unknown>(null)
const rewindData = ref<SnRewind | null>(null)
// Scroll animation refs
const section1 = ref<HTMLElement>()
const section2 = ref<HTMLElement>()
const section3 = ref<HTMLElement>()
const section4 = ref<HTMLElement>()
const section5 = ref<HTMLElement>()
const section6 = ref<HTMLElement>()
// Intersection Observer state
const inView1 = ref(false)
const inView2 = ref(false)
const inView3 = ref(false)
const inView4 = ref(false)
const inView5 = ref(false)
const inView6 = ref(false)
const observers: IntersectionObserver[] = []
// No animation refs needed for CSS-only animations
// Fetch rewind data
const fetchRewindData = async () => {
@@ -573,57 +817,9 @@ const fetchRewindData = async () => {
}
}
// Setup scroll animations
const setupScrollAnimations = () => {
const options = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px"
}
const createObserver = (
element: HTMLElement,
inViewRef: { value: boolean }
) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
inViewRef.value = entry.isIntersecting
})
}, options)
if (element) {
observer.observe(element)
observers.push(observer)
}
}
createObserver(section1.value!, inView1)
createObserver(section2.value!, inView2)
createObserver(section3.value!, inView3)
createObserver(section4.value!, inView4)
createObserver(section5.value!, inView5)
createObserver(section6.value!, inView6)
}
onMounted(() => {
fetchRewindData()
// Delay setup to ensure DOM is ready
setTimeout(setupScrollAnimations, 100)
})
onUnmounted(() => {
observers.forEach((observer) => observer.disconnect())
})
onMounted(() => fetchRewindData())
// Helper methods
const formatDate = (dateString: string): string => {
return DateTime.fromISO(dateString).toFormat("MMM dd, yyyy")
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getChatAvatar = (chat: SnRewindMostCalledChat) => {
// Return default avatar for chat rooms
return "/api/placeholder/48/48"
}
const getChatRoomAvatar = (item: SnRewindChat) => {
const apiBase = useSolarNetworkUrl()
@@ -639,6 +835,13 @@ const getChatMemberAvatar = (member: SnRewindChatMember) => {
: "/api/placeholder/64/64"
}
const getAccountAvatar = (account: SnAccount) => {
const apiBase = useSolarNetworkUrl()
return account?.profile?.picture
? `${apiBase}/drive/files/${account.profile.picture.id}`
: "/api/placeholder/32/32"
}
// Download functionality
const downloadSummary = () => {
// Create a simple text summary for download
@@ -711,6 +914,123 @@ const getStreakDescription = (streak: number): string => {
}
}
// Helper methods for chat messages
const getTotalMessages = (data: SnRewind["data"]["sphere"]): number => {
return (
(data.mostMessagedDirectChat?.messageCounts || 0) +
(data.mostMessagedChat?.messageCounts || 0)
)
}
const getChatMessage = (totalMessages: number): string => {
if (totalMessages >= 10000) {
return "社交达人"
} else if (totalMessages >= 5000) {
return "聊天狂人"
} else if (totalMessages >= 2000) {
return "活跃分子"
} else if (totalMessages >= 1000) {
return "话痨本痨"
} else if (totalMessages >= 500) {
return "健谈人士"
} else if (totalMessages >= 100) {
return "社交新人"
} else {
return "继续聊天"
}
}
const getChatDescription = (totalMessages: number): string => {
if (totalMessages >= 10000) {
return `发送了 ${totalMessages} 条消息,你就是 Solar Network 的社交传奇`
} else if (totalMessages >= 5000) {
return `发送了 ${totalMessages} 条消息,你的聊天热情无人能及`
} else if (totalMessages >= 2000) {
return `发送了 ${totalMessages} 条消息,聊天已经成为你的日常`
} else if (totalMessages >= 1000) {
return `发送了 ${totalMessages} 条消息,你真的很爱在 Solar Network 上聊天`
} else if (totalMessages >= 500) {
return `发送了 ${totalMessages} 条消息,继续保持这个交流习惯`
} else if (totalMessages >= 100) {
return `发送了 ${totalMessages} 条消息,开始享受 Solar Network 的社交功能`
} else {
return `发送了 ${totalMessages} 条消息,多多交流让社区更精彩`
}
}
// Helper methods for lottery messages
const getLotteryMessage = (winRate: number): string => {
if (winRate >= 0.8) {
return "彩票之神"
} else if (winRate >= 0.6) {
return "幸运儿"
} else if (winRate >= 0.4) {
return "运气不错"
} else if (winRate >= 0.2) {
return "继续加油"
} else {
return "试试手气"
}
}
const getLotteryDescription = (winRate: number): string => {
if (winRate >= 0.8) {
return `胜率 ${(winRate * 100).toFixed(1)}%,看来你就是传说中的欧皇`
} else if (winRate >= 0.6) {
return `胜率 ${(winRate * 100).toFixed(1)}%,你的运气真的很不错`
} else if (winRate >= 0.4) {
return `胜率 ${(winRate * 100).toFixed(1)}%,运气还可以,继续保持`
} else if (winRate >= 0.2) {
return `胜率 ${(winRate * 100).toFixed(1)}%,有时候运气就是这样`
} else {
return `胜率 ${(winRate * 100).toFixed(1)}%,新的一年希望你能转运`
}
}
// Helper methods for connection messages
const getConnectionMessage = (friends: number, blocked: number): string => {
if (friends > blocked * 2) {
return "社交之星"
} else if (friends > blocked) {
return "友好使者"
} else if (blocked > friends) {
return "谨慎选择"
} else {
return "平衡发展"
}
}
const getConnectionDescription = (friends: number, blocked: number): string => {
if (friends > blocked * 2) {
return `新增了 ${friends} 位好友,只有 ${blocked} 位屏蔽,你的社交圈在不断扩大`
} else if (friends > blocked) {
return `新增了 ${friends} 位好友,${blocked} 位屏蔽,你善于结识新朋友`
} else if (blocked > friends) {
return `新增了 ${friends} 位好友,但屏蔽了 ${blocked} 位,你对社交比较谨慎`
} else {
return `新增了 ${friends} 位好友,屏蔽了 ${blocked} 位,你的社交选择很平衡`
}
}
// Helper method for word cloud styling
const getWordCloudClass = (count: number): string => {
if (count >= 1000) {
return "text-4xl font-bold text-blue-600"
} else if (count >= 500) {
return "text-3xl font-semibold text-green-600"
} else if (count >= 200) {
return "text-2xl font-medium text-purple-600"
} else if (count >= 100) {
return "text-xl font-medium text-pink-600"
} else if (count >= 50) {
return "text-lg font-normal text-indigo-600"
} else if (count >= 20) {
return "text-base font-normal text-teal-600"
} else {
return "text-sm font-normal text-gray-600"
}
}
useHead({
title: "Solar Network Rewind 2025",
meta: [
@@ -723,34 +1043,43 @@ useHead({
</script>
<style scoped>
.scroll-section {
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.scroll-section.animate-in {
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-section {
animation: fadeInUp 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
opacity: 0;
transform: translateY(30px);
}
/* Staggered animation delays */
.scroll-section:nth-child(1).animate-in {
transition-delay: 0.1s;
.scroll-section:nth-child(1) {
animation-delay: 0.1s;
}
.scroll-section:nth-child(2).animate-in {
transition-delay: 0.2s;
.scroll-section:nth-child(2) {
animation-delay: 0.2s;
}
.scroll-section:nth-child(3).animate-in {
transition-delay: 0.3s;
.scroll-section:nth-child(3) {
animation-delay: 0.3s;
}
.scroll-section:nth-child(4).animate-in {
transition-delay: 0.4s;
.scroll-section:nth-child(4) {
animation-delay: 0.4s;
}
.scroll-section:nth-child(5).animate-in {
transition-delay: 0.5s;
.scroll-section:nth-child(5) {
animation-delay: 0.5s;
}
.scroll-section:nth-child(6).animate-in {
transition-delay: 0.6s;
.scroll-section:nth-child(6) {
animation-delay: 0.6s;
}
.scroll-section:nth-child(7) {
animation-delay: 0.7s;
}
</style>

View File

@@ -9,6 +9,11 @@ export interface SnRewindActiveData {
mostActiveWeekday: string
latestActiveTime: string
checkInCompleteness: number
lotteriesWins: number
lotteriesLosses: number
lotteriesWinRate: number
newBlockedCount: number
newFriendsCount: number
}
export interface SnRewindMostCalledChat {
@@ -94,6 +99,11 @@ export interface SnRewindMostLovedAudience {
upvoteCounts: number
}
export interface SnAccountCallRewind {
account: SnAccount
duration: number
}
export interface SnRewindSocialData {
totalPostCount: number
totalUpvoteCount: number
@@ -101,10 +111,12 @@ export interface SnRewindSocialData {
mostMessagedDirectChat: SnRewindChatSummary
mostPopularPost: SnRewindMostPopularPost
mostProductiveDay: SnRewindMostProductiveDay
mostCalledAccounts: SnAccount[]
mostCalledAccounts: SnAccountCallRewind[]
mostCalledChatTopMembers: SnAccount[]
mostLovedPublisher: SnRewindMostLovedPublisher
mostLovedAudience: SnRewindMostLovedAudience
mostMessagedChat: SnRewindChatSummary
topWords: { word: string; count: number }[]
}
export interface SnRewind {