@@ -132,7 +132,7 @@ onUnmounted(() => {
Reset Request
忘记密码
-
输入你的注册邮箱,我们会发送一封密码重置邮件。
+
当前版本暂未启用邮件发送;提交后会显示处理说明。
diff --git a/src/views/auth/RegisterView.vue b/src/views/auth/RegisterView.vue
index f5a8bdb..22f16c3 100644
--- a/src/views/auth/RegisterView.vue
+++ b/src/views/auth/RegisterView.vue
@@ -77,13 +77,13 @@ async function handleRegister() {
- 目标邮箱:
+ 注册邮箱:
{{ email }}
diff --git a/src/views/auth/ResetPasswordView.vue b/src/views/auth/ResetPasswordView.vue
index 4580f6c..14cded0 100644
--- a/src/views/auth/ResetPasswordView.vue
+++ b/src/views/auth/ResetPasswordView.vue
@@ -1,118 +1,5 @@
@@ -120,88 +7,21 @@ onUnmounted(() => {
-
-
{{ countdown }} 秒后自动跳转地图页面...
-
进入地图
+
+
+ 返回登录
+
+
+ 个人设置
+
-
-
-
-
-
-
-
-
Password Recovery
-
设置新密码
-
- {{
- state === 'invalid'
- ? '当前重置链接不可用,请重新发送密码重置邮件。'
- : '请输入新的登录密码,提交后当前重置链接会失效。'
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
- 返回重新发送重置邮件
-
-
-
- 保存新密码
-
-
-
diff --git a/src/views/encyclopedia/CloudTypeView.vue b/src/views/encyclopedia/CloudTypeView.vue
index e50a746..bd9d931 100644
--- a/src/views/encyclopedia/CloudTypeView.vue
+++ b/src/views/encyclopedia/CloudTypeView.vue
@@ -4,7 +4,7 @@ import { NAlert, NButton, NCard, NEmpty, NSkeleton, NTag } from 'naive-ui'
import { RouterLink, useRoute } from 'vue-router'
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
-import { supabase } from '@/lib/supabase'
+import { api } from '@/lib/api'
import { useAuthStore } from '@/stores/auth'
import { useEncyclopediaStore } from '@/stores/encyclopedia'
import type { CloudType } from '@/types/database'
@@ -82,32 +82,9 @@ function openGalleryDetail(item: CloudGalleryItem) {
}
async function loadGallery(typeId: number) {
- const galleryQuery = supabase
- .from('clouds')
- .select('id,image_url,thumbnail_url,latitude,longitude,location_name,description,captured_at,created_at,profiles(username)')
- .eq('cloud_type_id', typeId)
- .eq('status', 'approved')
- .eq('is_hidden', false)
- .order('captured_at', { ascending: false, nullsFirst: false })
- .order('created_at', { ascending: false })
- .limit(24)
+ const { data: galleryData, count } = await api.cloudTypes.gallery(typeId)
- const countQuery = supabase
- .from('clouds')
- .select('*', { head: true, count: 'exact' })
- .eq('cloud_type_id', typeId)
- .eq('status', 'approved')
- .eq('is_hidden', false)
-
- const [{ data: galleryData, error: galleryError }, { count, error: countError }] = await Promise.all([
- galleryQuery,
- countQuery,
- ])
-
- if (galleryError) throw galleryError
- if (countError) throw countError
-
- gallery.value = ((galleryData || []) as Array>).map(row => {
+ gallery.value = ((galleryData || []) as unknown as Array>).map(row => {
const profiles = Array.isArray(row.profiles) ? row.profiles : row.profiles ? [row.profiles] : []
const profile = profiles[0] as Record | undefined
diff --git a/src/views/gallery/GalleryView.vue b/src/views/gallery/GalleryView.vue
index 8b46eb6..293c2b1 100644
--- a/src/views/gallery/GalleryView.vue
+++ b/src/views/gallery/GalleryView.vue
@@ -5,7 +5,7 @@ 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 { api } from '@/lib/api'
import { useAuthStore } from '@/stores/auth'
import { useCloudsStore } from '@/stores/clouds'
import { useProfileStore } from '@/stores/profile'
@@ -87,34 +87,6 @@ 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) {
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] : []
@@ -142,48 +114,12 @@ function toGalleryCloud(row: Record) {
} 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) {
+ if (isUserSearch.value && !normalizedSearch.value.slice(1).trim()) {
galleryItems.value = []
totalCount.value = 0
currentPage.value = 1
@@ -191,29 +127,12 @@ async function loadPage(page: number) {
return
}
- 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
-
+ const { data, count } = await api.clouds.list({
+ page,
+ pageSize: PAGE_SIZE,
+ typeId: selectedTypeId.value,
+ search: normalizedSearch.value,
+ })
totalCount.value = count ?? 0
galleryItems.value = ((data || []) as unknown as Array>).map(toGalleryCloud)
currentPage.value = page
diff --git a/src/views/map/MapView.vue b/src/views/map/MapView.vue
index 2461b70..4001cbd 100644
--- a/src/views/map/MapView.vue
+++ b/src/views/map/MapView.vue
@@ -3,7 +3,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue'
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
import QuickUploadModal from '@/components/cloud/QuickUploadModal.vue'
-import { supabase } from '@/lib/supabase'
+import { api } from '@/lib/api'
import { loadAMap } from '@/lib/amap'
import { useAuthStore } from '@/stores/auth'
import { NIcon } from 'naive-ui'
@@ -232,24 +232,17 @@ function toCloudMarker(row: Record): CloudMarkerData {
}
async function fetchCloudsByRange(field: 'captured_at' | 'created_at', start: Date, end: Date): Promise {
- const { data, error } = await supabase
- .from('clouds')
- .select('id,image_url,thumbnail_url,latitude,longitude,location_name,description,captured_at,created_at,custom_cloud_type,cloud_types(name,rarity),profiles(username)')
- .eq('status', 'approved')
- .eq('is_hidden', false)
- .not('latitude', 'is', null)
- .not('longitude', 'is', null)
- .gte(field, start.toISOString())
- .lt(field, end.toISOString())
- .order(field, { ascending: true })
- .limit(1000)
-
- if (error) {
- statusText.value = `查询失败: ${error.message}`
+ try {
+ const { data } = await api.clouds.map({
+ field,
+ start: start.toISOString(),
+ end: end.toISOString(),
+ })
+ return ((data || []) as unknown as Array>).map(toCloudMarker)
+ } catch (error) {
+ statusText.value = `查询失败: ${error instanceof Error ? error.message : '地图数据加载失败'}`
return []
}
-
- return ((data || []) as Array>).map(toCloudMarker)
}
async function loadRealtimeClouds() {
diff --git a/vite.config.ts b/vite.config.ts
index aeca413..48492b4 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -57,6 +57,12 @@ function seoFilesPlugin() {
export default defineConfig({
plugins: [vue(), tailwindcss(), seoFilesPlugin()],
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3001',
+ '/uploads': 'http://localhost:3001',
+ },
+ },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),