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:
xujiang
2025-07-08 15:28:26 +08:00
parent d06caee35a
commit 3d197eb7e3
87 changed files with 8329 additions and 0 deletions

42
frontend/lib/api.ts Normal file
View 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
View 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
View 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))
}