---
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.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'
// 响应式图片
```
### 懒加载和分页
```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
---