Files
photography/frontend/lib/queries.ts
xujiang 48b6a5f4aa feat: 完善 CI/CD 配置并修复代码质量问题
## 修复内容

### 前端 (Frontend)
- 修复 ESLint 错误:未使用变量重命名为下划线前缀
- 修复 TypeScript 类型错误:完善 BackendPhoto 接口定义
- 修复引号转义问题:搜索结果显示优化
- 优化 useEffect 依赖:添加 useCallback 避免无限循环
- 移除未使用的导入和变量

### 后端 (Backend)
- 修复 go vet 错误:测试文件中的字段名称不匹配
- 修复数组访问错误:使用正确的结构体字段路径
- 统一代码格式:go fmt 自动格式化

### 管理后台 (Admin)
- 创建缺失的 ESLint 配置文件
- 修复 React 导入缺失问题
- 确保 TypeScript 编译通过

## CI/CD 改进
- 验证了前端、后端、管理后台的完整构建流程
- 所有 lint 检查、类型检查、测试均通过
- 为自动化部署做好准备

## 技术细节
- 前端:修复 5+ ESLint 错误,完善类型定义
- 后端:修复 3+ go vet 错误,通过所有测试
- 管理后台:创建 ESLint 配置,修复导入问题
- 所有模块均可正常构建和运行
2025-07-14 10:01:48 +08:00

285 lines
8.4 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,
}
// 后端照片数据结构
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 })
},
})
}