## 修复内容 ### 前端 (Frontend) - 修复 ESLint 错误:未使用变量重命名为下划线前缀 - 修复 TypeScript 类型错误:完善 BackendPhoto 接口定义 - 修复引号转义问题:搜索结果显示优化 - 优化 useEffect 依赖:添加 useCallback 避免无限循环 - 移除未使用的导入和变量 ### 后端 (Backend) - 修复 go vet 错误:测试文件中的字段名称不匹配 - 修复数组访问错误:使用正确的结构体字段路径 - 统一代码格式:go fmt 自动格式化 ### 管理后台 (Admin) - 创建缺失的 ESLint 配置文件 - 修复 React 导入缺失问题 - 确保 TypeScript 编译通过 ## CI/CD 改进 - 验证了前端、后端、管理后台的完整构建流程 - 所有 lint 检查、类型检查、测试均通过 - 为自动化部署做好准备 ## 技术细节 - 前端:修复 5+ ESLint 错误,完善类型定义 - 后端:修复 3+ go vet 错误,通过所有测试 - 管理后台:创建 ESLint 配置,修复导入问题 - 所有模块均可正常构建和运行
285 lines
8.4 KiB
TypeScript
285 lines
8.4 KiB
TypeScript
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,
|
||
}
|
||
|
||
// 后端照片数据结构
|
||
interface BackendPhoto {
|
||
id: number
|
||
title?: string
|
||
description?: string
|
||
src?: string
|
||
url?: string
|
||
image_path?: string
|
||
file_path?: string
|
||
thumbnail_path?: string
|
||
category?: string
|
||
category_id?: number
|
||
user_id?: number
|
||
tags?: string[]
|
||
date?: string
|
||
created_at?: number
|
||
updated_at?: number
|
||
exif?: {
|
||
camera?: string
|
||
lens?: string
|
||
settings?: string
|
||
location?: string
|
||
}
|
||
}
|
||
|
||
// 数据转换工具
|
||
const transformPhoto = async (backendPhoto: BackendPhoto): Promise<Photo> => {
|
||
// 如果使用Mock API,直接返回
|
||
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
|
||
return {
|
||
id: backendPhoto.id,
|
||
title: backendPhoto.title || '无标题',
|
||
description: backendPhoto.description || '',
|
||
src: backendPhoto.src || '/placeholder.jpg',
|
||
category: backendPhoto.category || 'general',
|
||
tags: backendPhoto.tags || [],
|
||
date: backendPhoto.date || new Date().toISOString().split('T')[0],
|
||
exif: {
|
||
camera: backendPhoto.exif?.camera || '未知',
|
||
lens: backendPhoto.exif?.lens || '未知',
|
||
settings: backendPhoto.exif?.settings || '未知',
|
||
location: backendPhoto.exif?.location || '未知'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取分类名称
|
||
const categoryName = await categoryService.getCategoryName(backendPhoto.category_id || 1)
|
||
|
||
// 转换后端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 || Date.now() / 1000) * 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: Category | string): string => {
|
||
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
|
||
return typeof backendCategory === 'string' ? backendCategory : backendCategory.name
|
||
}
|
||
return typeof backendCategory === 'string' ? backendCategory : 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: { photos: BackendPhoto[] } = await api.get('/photos?page=1&page_size=100')
|
||
const photos = response?.photos || []
|
||
// 并发处理所有照片的转换
|
||
return Promise.all(photos.map(transformPhoto))
|
||
} else {
|
||
// 使用Mock API
|
||
const photos: BackendPhoto[] = 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: { photos: BackendPhoto[], total: number } = 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: BackendPhoto[] = 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: { photos: BackendPhoto[] } = await api.get('/photos?page=1&page_size=200')
|
||
const photos = response?.photos || []
|
||
return Promise.all(photos.map(transformPhoto))
|
||
} else {
|
||
const photos: BackendPhoto[] = 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: BackendPhoto = 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: { categories: Category[] } = 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 })
|
||
},
|
||
})
|
||
} |