Files
photography/frontend/lib/queries.ts
xujiang 9046befcf1 feat: 完成前端展示网站核心功能开发
 新增功能:
- 增强照片展示页面,支持3种视图模式(网格/瀑布流/列表)
- 实现分页加载和无限滚动功能
- 完整的搜索和过滤系统,支持实时搜索、标签筛选、排序
- 新增分类浏览页面,提供分类统计和预览
- 新增标签云页面,热度可视化显示
- 面包屑导航和页面间无缝跳转

🎨 用户体验优化:
- 响应式设计,完美适配移动端和桌面端
- 智能loading状态和空状态处理
- 悬停效果和交互动画
- 视觉化统计仪表盘

 性能优化:
- 图片懒加载和智能分页
- 优化的组件渲染和状态管理
- 构建大小优化(187kB gzipped)

📝 更新任务进度文档,完成率达到32.5%

Phase 3核心功能基本完成,前端展示网站达到完全可用状态。
2025-07-11 12:27:36 +08:00

258 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from './api'
import { categoryService } from './categoryService'
// 照片数据类型 - 统一的显示格式
export interface Photo {
id: number
title: string
description: string
src: string
category: string
tags: string[]
date: string
exif: {
camera: string
lens: string
settings: string
location: string
}
// 后端原始数据 (仅供内部使用)
file_path?: string
thumbnail_path?: string
user_id?: number
category_id?: number
created_at?: number
updated_at?: number
}
// 分类数据类型
export interface Category {
id: number
name: string
description: string
created_at: number
updated_at: number
}
// 后端分页响应格式
export interface PageResponse<T> {
total: number
page: number
size: number
photos?: T[]
categories?: T[]
}
// 后端API基础响应格式
export interface ApiResponse<T> {
code: number
message: string
data: T
}
// 查询键
export const queryKeys = {
photos: ['photos'] as const,
photo: (id: number) => ['photo', id] as const,
categories: ['categories'] as const,
}
// 数据转换工具
const transformPhoto = async (backendPhoto: any): Promise<Photo> => {
// 如果使用Mock API直接返回
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
return {
...backendPhoto,
src: backendPhoto.src || '/placeholder.jpg',
category: backendPhoto.category || 'general',
tags: backendPhoto.tags || [],
date: backendPhoto.date || new Date().toISOString().split('T')[0],
exif: backendPhoto.exif || {
camera: '未知',
lens: '未知',
settings: '未知',
location: '未知'
}
}
}
// 获取分类名称
const categoryName = await categoryService.getCategoryName(backendPhoto.category_id)
// 转换后端API数据格式
return {
id: backendPhoto.id,
title: backendPhoto.title || '无标题',
description: backendPhoto.description || '',
src: backendPhoto.file_path ? `http://localhost:8080${backendPhoto.file_path}` : '/placeholder.jpg',
category: categoryName,
tags: [], // 后端暂无标签系统,使用空数组
date: new Date(backendPhoto.created_at * 1000).toISOString().split('T')[0],
exif: {
camera: '未知',
lens: '未知',
settings: '未知',
location: '未知'
},
// 保留原始数据供内部使用
file_path: backendPhoto.file_path,
thumbnail_path: backendPhoto.thumbnail_path,
user_id: backendPhoto.user_id,
category_id: backendPhoto.category_id,
created_at: backendPhoto.created_at,
updated_at: backendPhoto.updated_at
}
}
const transformCategory = (backendCategory: any): string => {
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
return backendCategory
}
return backendCategory.name
}
// 获取所有照片
export const usePhotos = () => {
return useQuery({
queryKey: queryKeys.photos,
queryFn: async (): Promise<Photo[]> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
// 使用真实API带分页参数
const response: any = await api.get('/photos?page=1&page_size=100')
const photos = response?.photos || []
// 并发处理所有照片的转换
return Promise.all(photos.map(transformPhoto))
} else {
// 使用Mock API
const photos: any[] = await api.get('/photos')
return Promise.all(photos.map(transformPhoto))
}
},
staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
})
}
// 分页获取照片
export const usePhotosPaginated = (page: number = 1, pageSize: number = 12) => {
return useQuery({
queryKey: [...queryKeys.photos, 'paginated', page, pageSize],
queryFn: async (): Promise<{ photos: Photo[], total: number, hasMore: boolean }> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response: any = await api.get(`/photos?page=${page}&page_size=${pageSize}`)
const photos = response?.photos || []
const total = response?.total || 0
const transformedPhotos = await Promise.all(photos.map(transformPhoto))
return {
photos: transformedPhotos,
total,
hasMore: (page * pageSize) < total
}
} else {
// 使用Mock API - 模拟分页
const allPhotos: any[] = await api.get('/photos')
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedPhotos = allPhotos.slice(startIndex, endIndex)
const transformedPhotos = await Promise.all(paginatedPhotos.map(transformPhoto))
return {
photos: transformedPhotos,
total: allPhotos.length,
hasMore: endIndex < allPhotos.length
}
}
},
staleTime: 5 * 60 * 1000,
})
}
// 无限滚动照片查询
export const useInfinitePhotos = (pageSize: number = 12) => {
return useQuery({
queryKey: [...queryKeys.photos, 'infinite'],
queryFn: async (): Promise<Photo[]> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
// 获取所有照片用于前端分页
const response: any = await api.get('/photos?page=1&page_size=200')
const photos = response?.photos || []
return Promise.all(photos.map(transformPhoto))
} else {
const photos: any[] = await api.get('/photos')
return Promise.all(photos.map(transformPhoto))
}
},
staleTime: 5 * 60 * 1000,
})
}
// 获取单张照片
export const usePhoto = (id: number) => {
return useQuery({
queryKey: queryKeys.photo(id),
queryFn: async (): Promise<Photo> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response = await api.get(`/photos/${id}`)
return await transformPhoto(response)
} else {
return api.get(`/photos/${id}`)
}
},
enabled: !!id,
})
}
// 获取分类列表
export const useCategories = () => {
return useQuery({
queryKey: queryKeys.categories,
queryFn: async (): Promise<string[]> => {
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
const response: any = await api.get('/categories?page=1&page_size=100')
const categories = response?.categories || []
return categories.map((cat: Category) => cat.name)
} else {
const categories: string[] = await api.get('/categories')
return categories
}
},
staleTime: 10 * 60 * 1000, // 10分钟内不重新获取
})
}
// 添加照片
export const useAddPhoto = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (photo: Omit<Photo, 'id'>) => api.post('/photos', photo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.photos })
},
})
}
// 更新照片
export const useUpdatePhoto = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, ...photo }: Partial<Photo> & { id: number }) =>
api.put(`/photos/${id}`, photo),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: queryKeys.photos })
queryClient.invalidateQueries({ queryKey: queryKeys.photo(variables.id) })
},
})
}
// 删除照片
export const useDeletePhoto = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => api.delete(`/photos/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.photos })
},
})
}