--- globs: frontend/**/* alwaysApply: false --- # Next.js 前端项目结构规则 ## 🏗️ 项目架构 ### 目录结构 ``` frontend/ ├── app/ # Next.js 14 App Router │ ├── globals.css # 全局样式 │ ├── layout.tsx # 根布局 │ ├── page.tsx # 首页 │ ├── gallery/ # 照片画廊页面 │ ├── about/ # 关于页面 │ └── contact/ # 联系页面 ├── components/ # React 组件 │ ├── ui/ # shadcn/ui 组件 │ ├── photo-gallery.tsx # 照片画廊组件 │ ├── photo-modal.tsx # 照片弹窗 │ ├── navigation.tsx # 导航组件 │ └── filter-bar.tsx # 筛选栏 ├── lib/ # 工具库 │ ├── api.ts # API 客户端 │ ├── queries.ts # TanStack Query │ └── utils.ts # 工具函数 ├── hooks/ # 自定义 Hooks ├── styles/ # 样式文件 └── public/ # 静态资源 ``` ## 🎯 开发规范 ### App Router 页面组件 ```tsx // app/gallery/page.tsx import { Metadata } from 'next' import { PhotoGallery } from '@/components/photo-gallery' import { FilterBar } from '@/components/filter-bar' export const metadata: Metadata = { title: '照片画廊 | Photography Portfolio', description: '浏览我的摄影作品集', } export default function GalleryPage() { return (

照片画廊

) } ``` ### 布局组件 ```tsx // app/layout.tsx import type { Metadata } from 'next' import { Inter } from 'next/font/google' import { Navigation } from '@/components/navigation' import { QueryProvider } from '@/components/providers/query-provider' import { ThemeProvider } from '@/components/theme-provider' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Photography Portfolio', description: '专业摄影作品展示', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` ## 🧩 组件开发规范 ### 功能组件模式 ```tsx // components/photo-gallery.tsx 'use client' import { useState } from 'react' import { usePhotos } from '@/hooks/use-photos' import { PhotoCard } from './photo-card' import { LoadingSpinner } from './loading-spinner' interface PhotoGalleryProps { categoryId?: string limit?: number } export function PhotoGallery({ categoryId, limit = 20 }: PhotoGalleryProps) { const [page, setPage] = useState(1) const { data, isLoading, error } = usePhotos({ page, limit, categoryId }) if (isLoading) { return } if (error) { return (
加载照片失败:{error.message}
) } return (
{data?.photos.map((photo) => ( handlePhotoView(photo)} /> ))}
) } function handlePhotoView(photo: Photo) { // 打开照片详情弹窗 } ``` ### UI组件模式 ```tsx // components/photo-card.tsx import Image from 'next/image' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' interface PhotoCardProps { photo: Photo onView?: () => void className?: string } export function PhotoCard({ photo, onView, className }: PhotoCardProps) { return (
{photo.title} {photo.category && ( {photo.category.name} )}

{photo.title}

{photo.description && (

{photo.description}

)}

{new Date(photo.created_at).toLocaleDateString('zh-CN')}

) } ``` ## 🌐 API 集成 ### API 客户端配置 ```typescript // lib/api.ts import axios, { AxiosError } from 'axios' export const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8888/api/v1', timeout: 10000, headers: { 'Content-Type': 'application/json', }, }) // 请求拦截器 api.interceptors.request.use((config) => { // 从localStorage获取token (仅在客户端) if (typeof window !== 'undefined') { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } } return config }) // 响应拦截器 api.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { // 清除过期token if (typeof window !== 'undefined') { localStorage.removeItem('token') window.location.href = '/login' } } return Promise.reject(error) } ) ``` ### TanStack Query 集成 ```typescript // lib/queries.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from './api' // 照片相关查询 export function usePhotos(params: PhotoListParams = {}) { return useQuery({ queryKey: ['photos', params], queryFn: async () => { const { data } = await api.get>('/photos', { params, }) return data.data }, staleTime: 5 * 60 * 1000, // 5分钟 }) } export function usePhoto(id: string) { return useQuery({ queryKey: ['photo', id], queryFn: async () => { const { data } = await api.get>(`/photos/${id}`) return data.data }, enabled: !!id, }) } // 分类查询 export function useCategories() { return useQuery({ queryKey: ['categories'], queryFn: async () => { const { data } = await api.get>('/categories') return data.data }, staleTime: 10 * 60 * 1000, // 10分钟 }) } ``` ### 自定义 Hooks ```typescript // hooks/use-photos.ts import { useState, useCallback } from 'react' import { usePhotos as usePhotosQuery } from '@/lib/queries' export function usePhotos(initialParams: PhotoListParams = {}) { const [params, setParams] = useState(initialParams) const query = usePhotosQuery(params) const updateParams = useCallback((newParams: Partial) => { setParams(prev => ({ ...prev, ...newParams })) }, []) const nextPage = useCallback(() => { setParams(prev => ({ ...prev, page: (prev.page || 1) + 1 })) }, []) const prevPage = useCallback(() => { setParams(prev => ({ ...prev, page: Math.max((prev.page || 1) - 1, 1) })) }, []) return { ...query, params, updateParams, nextPage, prevPage, } } ``` ## 🎨 样式和主题 ### Tailwind CSS 配置 ```typescript // tailwind.config.ts import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { border: 'hsl(var(--border))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', // ...其他 shadcn 颜色 }, animation: { 'fade-in': 'fadeIn 0.5s ease-in-out', 'slide-up': 'slideUp 0.3s ease-out', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideUp: { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' }, }, }, }, }, plugins: [require('tailwindcss-animate')], } ``` ### CSS变量配置 ```css /* app/globals.css */ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; /* ...其他CSS变量 */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ...暗色模式变量 */ } } @layer components { .photo-grid { @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6; } .photo-card-hover { @apply hover:shadow-lg hover:scale-105 transition-all duration-300; } } ``` ## 🚀 性能优化 ### Image 优化 ```tsx import Image from 'next/image' // 响应式图片 {photo.title} ``` ### 懒加载和分页 ```tsx // 无限滚动 function useInfinitePhotos() { return useInfiniteQuery({ queryKey: ['photos'], queryFn: ({ pageParam = 1 }) => fetchPhotos({ page: pageParam }), getNextPageParam: (lastPage) => lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined, }) } ``` 参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解前端开发进度。 description: globs: alwaysApply: false ---