feat: enhance cloud image management

This commit is contained in:
2026-05-22 10:34:35 +08:00
parent 7cdf07447c
commit 7e4ee3d699
11 changed files with 1468 additions and 216 deletions
+148 -1
View File
@@ -5,8 +5,12 @@ import type { CloudType, Profile } from '@/types/database'
export interface ProfileCloudItem {
id: string
cloud_type_id: number | null
custom_cloud_type: string | null
image_url: string
thumbnail_url: string | null
latitude: number | null
longitude: number | null
location_name: string | null
description: string | null
captured_at: string | null
@@ -23,8 +27,12 @@ function toProfileCloud(row: Record<string, unknown>) {
return {
id: row.id as string,
cloud_type_id: (row.cloud_type_id as number | null) ?? null,
custom_cloud_type: (row.custom_cloud_type as string | null) ?? null,
image_url: row.image_url as string,
thumbnail_url: (row.thumbnail_url as string | null) ?? null,
latitude: (row.latitude as number | null) ?? null,
longitude: (row.longitude as number | 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,
@@ -36,6 +44,28 @@ function toProfileCloud(row: Record<string, unknown>) {
} satisfies ProfileCloudItem
}
function getSupabaseErrorCode(error: unknown) {
if (!error || typeof error !== 'object' || !('code' in error)) return null
const code = (error as { code?: unknown }).code
return typeof code === 'string' ? code : null
}
function getCloudStoragePath(publicUrl: string | null) {
if (!publicUrl) return null
try {
const url = new URL(publicUrl)
const marker = '/storage/v1/object/public/clouds/'
const markerIndex = url.pathname.indexOf(marker)
if (markerIndex === -1) return null
const path = url.pathname.slice(markerIndex + marker.length)
return path ? decodeURIComponent(path) : null
} catch {
return null
}
}
export const useProfileStore = defineStore('profile-page', () => {
const profilesById = ref<Record<string, Profile>>({})
const cloudsByKey = ref<Record<string, ProfileCloudItem[]>>({})
@@ -83,7 +113,7 @@ export const useProfileStore = defineStore('profile-page', () => {
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)')
.select('id,cloud_type_id,image_url,thumbnail_url,latitude,longitude,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 })
@@ -111,6 +141,119 @@ export const useProfileStore = defineStore('profile-page', () => {
return !!loadingKeys.value[makeKey(userId, isOwnProfile)]
}
function patchCachedCloud(userId: string, cloudId: string, patch: Partial<ProfileCloudItem>) {
for (const isOwnProfile of [true, false]) {
const key = makeKey(userId, isOwnProfile)
const current = cloudsByKey.value[key]
if (!current) continue
cloudsByKey.value[key] = current
.map(item => (item.id === cloudId ? { ...item, ...patch } : item))
.filter(item => isOwnProfile || (item.status === 'approved' && !item.is_hidden))
}
}
function removeCachedClouds(userId: string, cloudIds: string[]) {
const idSet = new Set(cloudIds)
for (const isOwnProfile of [true, false]) {
const key = makeKey(userId, isOwnProfile)
const current = cloudsByKey.value[key]
if (!current) continue
cloudsByKey.value[key] = current.filter(item => !idSet.has(item.id))
}
}
function invalidateUser(userId: string) {
delete cloudsByKey.value[makeKey(userId, true)]
delete cloudsByKey.value[makeKey(userId, false)]
}
async function updateCloud(userId: string, cloudId: string, patch: {
cloud_type_id: number | null
custom_cloud_type: string | null
latitude: number | null
longitude: number | null
location_name: string | null
description: string | null
captured_at: string | null
is_hidden: boolean
}) {
const { data, error } = await supabase
.from('clouds')
.update(patch)
.eq('id', cloudId)
.eq('user_id', userId)
.select('id,cloud_type_id,image_url,thumbnail_url,latitude,longitude,location_name,description,captured_at,created_at,status,is_hidden,custom_cloud_type,cloud_types(name,rarity)')
.single()
if (error) throw error
const updated = toProfileCloud(data as Record<string, unknown>)
patchCachedCloud(userId, cloudId, updated)
return updated
}
async function updateCloudVisibility(userId: string, cloudId: string, isHidden: boolean) {
const { data, error } = await supabase
.from('clouds')
.update({ is_hidden: isHidden })
.eq('id', cloudId)
.eq('user_id', userId)
.select('id')
if (error) throw error
if (!data?.length) {
throw new Error('私密状态没有写入数据库,请检查 clouds 表的 UPDATE RLS policy。')
}
patchCachedCloud(userId, cloudId, { is_hidden: isHidden })
}
async function deleteClouds(userId: string, cloudIds: string[]) {
if (!cloudIds.length) return 0
const { data, error } = await supabase
.from('clouds')
.delete()
.eq('user_id', userId)
.in('id', cloudIds)
.select('id,image_url,thumbnail_url')
if (error) {
if (getSupabaseErrorCode(error) === '23503') {
throw new Error('这张照片仍被图鉴收藏记录引用,请先在数据库把 user_collections.first_cloud_id 外键改为 ON DELETE SET NULL。')
}
throw error
}
const deletedIds = (data || []).map(item => item.id as string)
if (deletedIds.length !== cloudIds.length) {
throw new Error('图片没有真正从数据库删除,请检查 clouds 表的 DELETE RLS policy。')
}
const storagePaths = Array.from(new Set(
(data || [])
.flatMap(item => [
getCloudStoragePath((item.image_url as string | null) ?? null),
getCloudStoragePath((item.thumbnail_url as string | null) ?? null),
])
.filter((path): path is string => !!path),
))
if (storagePaths.length) {
const { error: storageError } = await supabase.storage
.from('clouds')
.remove(storagePaths)
if (storageError) {
throw new Error(`图片数据库记录已删除,但 Supabase Storage 文件清理失败:${storageError.message}`)
}
}
removeCachedClouds(userId, deletedIds)
return deletedIds.length
}
return {
getProfile,
getClouds,
@@ -118,5 +261,9 @@ export const useProfileStore = defineStore('profile-page', () => {
isLoaded,
isLoading,
fetchProfilePage,
invalidateUser,
updateCloud,
updateCloudVisibility,
deleteClouds,
}
})