后端迁移到fastapi (#3)
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -5,11 +5,12 @@ import { Clock, Location, Search, Settings, User, X } from '@vicons/tabler'
|
||||
import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue'
|
||||
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
|
||||
import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { apiRequest } from '@/lib/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCloudsStore } from '@/stores/clouds'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
|
||||
import type { ApiCloud, ApiPage } from '@/types/api'
|
||||
import type { CloudType } from '@/types/database'
|
||||
|
||||
interface GalleryCloud {
|
||||
@@ -87,135 +88,40 @@ function formatCoordinate(value: number | null) {
|
||||
const normalizedSearch = computed(() => searchQuery.value.trim())
|
||||
const isUserSearch = computed(() => normalizedSearch.value.startsWith('@'))
|
||||
|
||||
function sanitizeSearchTerm(term: string) {
|
||||
return term.replace(/[(),*%]/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function getMatchedCloudTypeIds(term: string) {
|
||||
const lowerTerm = term.toLocaleLowerCase('zh-CN')
|
||||
return cloudsStore.cloudTypes
|
||||
.filter(type => {
|
||||
return type.name.toLocaleLowerCase('zh-CN').includes(lowerTerm) ||
|
||||
type.name_en.toLocaleLowerCase('zh-CN').includes(lowerTerm)
|
||||
})
|
||||
.map(type => type.id)
|
||||
}
|
||||
|
||||
async function fetchUserIdsBySearch(term: string) {
|
||||
const sanitized = sanitizeSearchTerm(term)
|
||||
if (!sanitized) return []
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('id')
|
||||
.ilike('username', `%${sanitized}%`)
|
||||
.limit(100)
|
||||
|
||||
if (error) throw error
|
||||
return ((data || []) as Array<{ id: string }>).map(profile => profile.id)
|
||||
}
|
||||
|
||||
function toGalleryCloud(row: Record<string, unknown>) {
|
||||
const cloudTypes = Array.isArray(row.cloud_types) ? row.cloud_types : row.cloud_types ? [row.cloud_types] : []
|
||||
const profiles = Array.isArray(row.profiles) ? row.profiles : row.profiles ? [row.profiles] : []
|
||||
const cloudType = cloudTypes[0] as Record<string, unknown> | undefined
|
||||
const profile = profiles[0] as Record<string, unknown> | undefined
|
||||
|
||||
function toGalleryCloud(row: ApiCloud) {
|
||||
return {
|
||||
id: row.id as string,
|
||||
user_id: row.user_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,
|
||||
location_name: (row.location_name as string | null) ?? null,
|
||||
description: (row.description as string | null) ?? null,
|
||||
latitude: (row.latitude as number | null) ?? null,
|
||||
longitude: (row.longitude as number | null) ?? null,
|
||||
captured_at: (row.captured_at as string | null) ?? null,
|
||||
created_at: row.created_at as string,
|
||||
status: row.status as GalleryCloud['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',
|
||||
username: (profile?.username as string) || '匿名',
|
||||
id: row.id,
|
||||
user_id: row.user_id,
|
||||
cloud_type_id: row.cloud_type_id,
|
||||
custom_cloud_type: row.custom_cloud_type,
|
||||
image_url: row.image_url,
|
||||
thumbnail_url: row.thumbnail_url,
|
||||
location_name: row.location_name,
|
||||
description: row.description,
|
||||
latitude: row.latitude,
|
||||
longitude: row.longitude,
|
||||
captured_at: row.captured_at,
|
||||
created_at: row.created_at,
|
||||
status: row.status,
|
||||
is_hidden: row.is_hidden,
|
||||
cloudTypeName: row.cloud_type_name || row.custom_cloud_type || '未知',
|
||||
cloudTypeRarity: row.cloud_type_rarity || 'common',
|
||||
username: row.username || '匿名',
|
||||
} satisfies GalleryCloud
|
||||
}
|
||||
|
||||
async function resolveSearchFilters() {
|
||||
const search = normalizedSearch.value
|
||||
const usernameTerm = isUserSearch.value ? sanitizeSearchTerm(search.slice(1)) : ''
|
||||
const cloudTypeTerm = !isUserSearch.value ? sanitizeSearchTerm(search) : ''
|
||||
|
||||
if (isUserSearch.value && !usernameTerm) return null
|
||||
if (search && !isUserSearch.value && !cloudTypeTerm) return null
|
||||
|
||||
let userIds: string[] = []
|
||||
if (usernameTerm) {
|
||||
userIds = await fetchUserIdsBySearch(usernameTerm)
|
||||
if (userIds.length === 0) return null
|
||||
}
|
||||
|
||||
const matchedCloudTypeIds = cloudTypeTerm ? getMatchedCloudTypeIds(cloudTypeTerm) : []
|
||||
|
||||
return { usernameTerm, cloudTypeTerm, userIds, matchedCloudTypeIds }
|
||||
}
|
||||
|
||||
const FULL_SELECT = 'id,user_id,cloud_type_id,image_url,thumbnail_url,location_name,description,latitude,longitude,captured_at,created_at,status,is_hidden,custom_cloud_type,cloud_types(name,rarity),profiles(username)'
|
||||
|
||||
function buildFilteredQuery(selectStr: string, options?: { count?: 'exact'; head?: boolean }) {
|
||||
let query = supabase
|
||||
.from('clouds')
|
||||
.select(selectStr, options as any)
|
||||
.eq('status', 'approved')
|
||||
.eq('is_hidden', false)
|
||||
|
||||
if (selectedTypeId.value !== 'all') {
|
||||
query = query.eq('cloud_type_id', selectedTypeId.value)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
async function loadPage(page: number) {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
|
||||
try {
|
||||
const filters = await resolveSearchFilters()
|
||||
if (filters === null) {
|
||||
galleryItems.value = []
|
||||
totalCount.value = 0
|
||||
currentPage.value = 1
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
const params = new URLSearchParams({ page: String(page), page_size: String(PAGE_SIZE) })
|
||||
if (selectedTypeId.value !== 'all') params.set('type_id', String(selectedTypeId.value))
|
||||
if (normalizedSearch.value) params.set('search', normalizedSearch.value)
|
||||
|
||||
let countQuery = buildFilteredQuery('id', { count: 'exact', head: true })
|
||||
let dataQuery = buildFilteredQuery(FULL_SELECT)
|
||||
.order('created_at', { ascending: false })
|
||||
.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1)
|
||||
|
||||
if (filters.usernameTerm) {
|
||||
countQuery = countQuery.in('user_id', filters.userIds)
|
||||
dataQuery = dataQuery.in('user_id', filters.userIds)
|
||||
} else if (filters.cloudTypeTerm) {
|
||||
if (filters.matchedCloudTypeIds.length) {
|
||||
const orFilter = `cloud_type_id.in.(${filters.matchedCloudTypeIds.join(',')}),custom_cloud_type.ilike.*${filters.cloudTypeTerm}*`
|
||||
countQuery = countQuery.or(orFilter)
|
||||
dataQuery = dataQuery.or(orFilter)
|
||||
} else {
|
||||
countQuery = countQuery.ilike('custom_cloud_type', `%${filters.cloudTypeTerm}%`)
|
||||
dataQuery = dataQuery.ilike('custom_cloud_type', `%${filters.cloudTypeTerm}%`)
|
||||
}
|
||||
}
|
||||
|
||||
const [{ count }, { data, error }] = await Promise.all([countQuery, dataQuery])
|
||||
|
||||
if (error) throw error
|
||||
|
||||
totalCount.value = count ?? 0
|
||||
galleryItems.value = ((data || []) as unknown as Array<Record<string, unknown>>).map(toGalleryCloud)
|
||||
const result = await apiRequest<ApiPage<ApiCloud>>(`/clouds?${params}`, { auth: false })
|
||||
totalCount.value = result.total
|
||||
galleryItems.value = result.items.map(toGalleryCloud)
|
||||
currentPage.value = page
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : '画廊加载失败'
|
||||
|
||||
Reference in New Issue
Block a user