From 7cdf07447c03aee78496110450e5ce65bf48138d Mon Sep 17 00:00:00 2001 From: Mplan Date: Fri, 22 May 2026 00:55:23 +0800 Subject: [PATCH] feat: enhance profile heatmap and caching --- .../profile/ContributionHeatmap.vue | 395 ++++++++++++ src/stores/profile.ts | 122 ++++ src/views/profile/ProfileView.vue | 592 +++++++++++++++++- 3 files changed, 1106 insertions(+), 3 deletions(-) create mode 100644 src/components/profile/ContributionHeatmap.vue create mode 100644 src/stores/profile.ts diff --git a/src/components/profile/ContributionHeatmap.vue b/src/components/profile/ContributionHeatmap.vue new file mode 100644 index 0000000..9a37f93 --- /dev/null +++ b/src/components/profile/ContributionHeatmap.vue @@ -0,0 +1,395 @@ + + + diff --git a/src/stores/profile.ts b/src/stores/profile.ts new file mode 100644 index 0000000..1f81883 --- /dev/null +++ b/src/stores/profile.ts @@ -0,0 +1,122 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import { supabase } from '@/lib/supabase' +import type { CloudType, Profile } from '@/types/database' + +export interface ProfileCloudItem { + id: string + image_url: string + thumbnail_url: string | null + location_name: string | null + description: string | null + captured_at: string | null + created_at: string + status: 'pending' | 'approved' | 'rejected' + is_hidden: boolean + cloudTypeName: string + cloudTypeRarity: CloudType['rarity'] +} + +function toProfileCloud(row: Record) { + const cloudTypes = Array.isArray(row.cloud_types) ? row.cloud_types : row.cloud_types ? [row.cloud_types] : [] + const cloudType = cloudTypes[0] as Record | undefined + + return { + id: row.id as string, + image_url: row.image_url as string, + thumbnail_url: (row.thumbnail_url as string | null) ?? null, + location_name: (row.location_name as string | null) ?? null, + description: (row.description as string | null) ?? null, + captured_at: (row.captured_at as string | null) ?? null, + created_at: row.created_at as string, + status: row.status as ProfileCloudItem['status'], + is_hidden: (row.is_hidden as boolean) ?? false, + cloudTypeName: (cloudType?.name as string) || (row.custom_cloud_type as string) || '未知', + cloudTypeRarity: (cloudType?.rarity as CloudType['rarity']) || 'common', + } satisfies ProfileCloudItem +} + +export const useProfileStore = defineStore('profile-page', () => { + const profilesById = ref>({}) + const cloudsByKey = ref>({}) + const loadingKeys = ref>({}) + const errorByKey = ref>({}) + + const makeKey = (userId: string, isOwnProfile: boolean) => `${userId}:${isOwnProfile ? 'own' : 'public'}` + + function getProfile(userId: string | null) { + if (!userId) return null + return profilesById.value[userId] ?? null + } + + function getClouds(userId: string | null, isOwnProfile: boolean) { + if (!userId) return [] + return cloudsByKey.value[makeKey(userId, isOwnProfile)] ?? [] + } + + function getError(userId: string | null, isOwnProfile: boolean) { + if (!userId) return '' + return errorByKey.value[makeKey(userId, isOwnProfile)] ?? '' + } + + function isLoaded(userId: string | null, isOwnProfile: boolean) { + if (!userId) return false + return makeKey(userId, isOwnProfile) in cloudsByKey.value + } + + async function fetchProfilePage(userId: string, isOwnProfile: boolean, force = false) { + const key = makeKey(userId, isOwnProfile) + if (!force && key in cloudsByKey.value && profilesById.value[userId]) return + + loadingKeys.value[key] = true + errorByKey.value[key] = '' + + try { + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single() + + if (profileError) throw profileError + profilesById.value[userId] = profile as Profile + + let cloudsQuery = supabase + .from('clouds') + .select('id,image_url,thumbnail_url,location_name,description,captured_at,created_at,status,is_hidden,custom_cloud_type,cloud_types(name,rarity)') + .eq('user_id', userId) + .order('captured_at', { ascending: false, nullsFirst: false }) + .order('created_at', { ascending: false }) + + if (!isOwnProfile) { + cloudsQuery = cloudsQuery + .eq('status', 'approved') + .eq('is_hidden', false) + } + + const { data: cloudRows, error: cloudsError } = await cloudsQuery + if (cloudsError) throw cloudsError + + cloudsByKey.value[key] = ((cloudRows || []) as Array>).map(toProfileCloud) + } catch (error) { + errorByKey.value[key] = error instanceof Error ? error.message : '个人主页加载失败' + cloudsByKey.value[key] = [] + } finally { + loadingKeys.value[key] = false + } + } + + function isLoading(userId: string | null, isOwnProfile: boolean) { + if (!userId) return false + return !!loadingKeys.value[makeKey(userId, isOwnProfile)] + } + + return { + getProfile, + getClouds, + getError, + isLoaded, + isLoading, + fetchProfilePage, + } +}) diff --git a/src/views/profile/ProfileView.vue b/src/views/profile/ProfileView.vue index 9e7ed3f..ca82491 100644 --- a/src/views/profile/ProfileView.vue +++ b/src/views/profile/ProfileView.vue @@ -1,9 +1,595 @@