feat: setup frontend project with bun and dynamic data fetching
- Create new frontend project directory with Next.js 15 + React 19 - Migrate from pnpm to bun for faster package management - Add TanStack Query + Axios for dynamic data fetching - Create comprehensive Makefile with development commands - Setup API layer with query hooks and error handling - Configure environment variables and bun settings - Add TypeScript type checking and project documentation - Update CLAUDE.md with bun-specific development workflow
This commit is contained in:
42
frontend/lib/api.ts
Normal file
42
frontend/lib/api.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加token等认证信息
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// 处理未授权
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
91
frontend/lib/queries.ts
Normal file
91
frontend/lib/queries.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import api from './api'
|
||||
|
||||
// 照片数据类型
|
||||
export interface Photo {
|
||||
id: number
|
||||
src: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
tags: string[]
|
||||
date: string
|
||||
exif: {
|
||||
camera: string
|
||||
lens: string
|
||||
settings: string
|
||||
location: string
|
||||
}
|
||||
}
|
||||
|
||||
// 查询键
|
||||
export const queryKeys = {
|
||||
photos: ['photos'] as const,
|
||||
photo: (id: number) => ['photo', id] as const,
|
||||
categories: ['categories'] as const,
|
||||
}
|
||||
|
||||
// 获取所有照片
|
||||
export const usePhotos = () => {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.photos,
|
||||
queryFn: () => api.get('/photos'),
|
||||
staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
|
||||
})
|
||||
}
|
||||
|
||||
// 获取单张照片
|
||||
export const usePhoto = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.photo(id),
|
||||
queryFn: () => api.get(`/photos/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
export const useCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.categories,
|
||||
queryFn: () => api.get('/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 })
|
||||
},
|
||||
})
|
||||
}
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user