fix
This commit is contained in:
429
.cursor/rules/frontend/nextjs-structure.mdc
Normal file
429
.cursor/rules/frontend/nextjs-structure.mdc
Normal file
@ -0,0 +1,429 @@
|
||||
---
|
||||
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 (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">照片画廊</h1>
|
||||
<FilterBar />
|
||||
<PhotoGallery />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 布局组件
|
||||
```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 (
|
||||
<html lang="zh-CN">
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider>
|
||||
<QueryProvider>
|
||||
<Navigation />
|
||||
{children}
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧩 组件开发规范
|
||||
|
||||
### 功能组件模式
|
||||
```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 <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-8 text-red-600">
|
||||
加载照片失败:{error.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{data?.photos.map((photo) => (
|
||||
<PhotoCard
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onView={() => handlePhotoView(photo)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card className={cn("group cursor-pointer hover:shadow-lg transition-shadow", className)}>
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-square overflow-hidden rounded-t-lg">
|
||||
<Image
|
||||
src={photo.thumbnail || '/placeholder.jpg'}
|
||||
alt={photo.title}
|
||||
fill
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
onClick={onView}
|
||||
/>
|
||||
{photo.category && (
|
||||
<Badge className="absolute top-2 left-2">
|
||||
{photo.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold truncate">{photo.title}</h3>
|
||||
{photo.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
||||
{photo.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{new Date(photo.created_at).toLocaleDateString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🌐 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<ApiResponse<PhotoListData>>('/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<ApiResponse<Photo>>(`/photos/${id}`)
|
||||
return data.data
|
||||
},
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
// 分类查询
|
||||
export function useCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<ApiResponse<Category[]>>('/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<PhotoListParams>) => {
|
||||
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'
|
||||
|
||||
// 响应式图片
|
||||
<Image
|
||||
src={photo.thumbnail}
|
||||
alt={photo.title}
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto"
|
||||
priority={index < 4} // 首屏图片优先加载
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
|
||||
/>
|
||||
```
|
||||
|
||||
### 懒加载和分页
|
||||
```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
|
||||
---
|
||||
Reference in New Issue
Block a user