feat: add cloud encyclopedia system

This commit is contained in:
2026-05-21 17:15:52 +08:00
parent 2b7f393d7c
commit 76eb4dcfe1
6 changed files with 943 additions and 19 deletions
+111 -6
View File
@@ -1,5 +1,6 @@
import { ref } from 'vue'
import { supabase } from '@/lib/supabase'
import type { CloudType } from '@/types/database'
export interface UploadItem {
id: string
@@ -16,6 +17,19 @@ export interface UploadItem {
errors: Record<string, string>
}
export interface UnlockedBadge {
cloudTypeId: number
cloudName: string
cloudNameEn: string
rarity: CloudType['rarity']
unlockedAt: string
}
export interface UploadResult {
ok: boolean
unlockedBadges: UnlockedBadge[]
}
let nextId = 0
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
@@ -75,6 +89,50 @@ function extractExifDate(buffer: ArrayBuffer): string | null {
return null
}
async function fetchUnlockedTypeIds(userId: string) {
const { data, error } = await supabase
.from('user_collections')
.select('cloud_type_id')
.eq('user_id', userId)
if (error) throw error
return new Set(
(data || [])
.map(row => row.cloud_type_id)
.filter((cloudTypeId): cloudTypeId is number => typeof cloudTypeId === 'number'),
)
}
async function fetchBadgeDetails(unlockedRows: Array<{ cloudTypeId: number; unlockedAt: string }>) {
const ids = unlockedRows.map(item => item.cloudTypeId)
const { data, error } = await supabase
.from('cloud_types')
.select('id,name,name_en,rarity')
.in('id', ids)
if (error) throw error
const typeMap = new Map(
((data || []) as Array<Pick<CloudType, 'id' | 'name' | 'name_en' | 'rarity'>>).map(item => [item.id, item]),
)
return unlockedRows
.map(item => {
const cloudType = typeMap.get(item.cloudTypeId)
if (!cloudType) return null
return {
cloudTypeId: cloudType.id,
cloudName: cloudType.name,
cloudNameEn: cloudType.name_en,
rarity: cloudType.rarity,
unlockedAt: item.unlockedAt,
} satisfies UnlockedBadge
})
.filter((item): item is UnlockedBadge => item !== null)
}
export function useUpload() {
const items = ref<UploadItem[]>([])
const uploading = ref(false)
@@ -163,8 +221,10 @@ export function useUpload() {
return Math.round(value * 100) / 100
}
async function uploadAll(): Promise<boolean> {
if (!validateAll()) return false
async function uploadAll(): Promise<UploadResult> {
if (!validateAll()) {
return { ok: false, unlockedBadges: [] }
}
uploading.value = true
totalItems.value = items.value.length
@@ -174,10 +234,19 @@ export function useUpload() {
const userId = (await supabase.auth.getUser()).data.user?.id
if (!userId) {
uploading.value = false
return false
return { ok: false, unlockedBadges: [] }
}
try {
let unlockedTypeIds = new Set<number>()
try {
unlockedTypeIds = await fetchUnlockedTypeIds(userId)
} catch {
unlockedTypeIds = new Set<number>()
}
const newlyUnlockedRows: Array<{ cloudTypeId: number; unlockedAt: string }> = []
for (let i = 0; i < items.value.length; i++) {
const item = items.value[i]
currentItemIndex.value = i + 1
@@ -200,7 +269,7 @@ export function useUpload() {
const latitude = item.latitude ? blurCoordinate(item.latitude) : null
const longitude = item.longitude ? blurCoordinate(item.longitude) : null
const { error: dbError } = await supabase
const { data: insertedCloud, error: dbError } = await supabase
.from('clouds')
.insert({
user_id: userId,
@@ -216,8 +285,41 @@ export function useUpload() {
status: 'approved',
is_hidden: item.isHidden,
})
.select('id,cloud_type_id')
.single()
if (dbError) throw dbError
if (
insertedCloud &&
typeof insertedCloud.cloud_type_id === 'number' &&
!unlockedTypeIds.has(insertedCloud.cloud_type_id)
) {
try {
const { data: collectionRow, error: collectionError } = await supabase
.from('user_collections')
.insert({
user_id: userId,
cloud_type_id: insertedCloud.cloud_type_id,
first_cloud_id: insertedCloud.id,
})
.select('cloud_type_id,unlocked_at')
.single()
if (collectionError) {
const duplicate = collectionError.code === '23505'
if (!duplicate) throw collectionError
} else if (collectionRow) {
unlockedTypeIds.add(collectionRow.cloud_type_id)
newlyUnlockedRows.push({
cloudTypeId: collectionRow.cloud_type_id,
unlockedAt: collectionRow.unlocked_at,
})
}
} catch {
// Ignore collection sync failures so uploads can still complete.
}
}
overallProgress.value = Math.round((i + 1) / items.value.length * 100)
}
@@ -225,9 +327,12 @@ export function useUpload() {
URL.revokeObjectURL(item.preview)
}
items.value = []
return true
return {
ok: true,
unlockedBadges: newlyUnlockedRows.length ? await fetchBadgeDetails(newlyUnlockedRows) : [],
}
} catch {
return false
return { ok: false, unlockedBadges: [] }
} finally {
uploading.value = false
}