feat: 重构项目为模块化结构,拆分 CLAUDE.md 文档
All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
## 📁 模块化重构 ### 新增模块 CLAUDE.md - `frontend/CLAUDE.md` - 前端开发指导 (Next.js, React, TypeScript) - `docs/deployment/CLAUDE.md` - 部署配置指导 (Caddy, 服务器配置) - `.gitea/workflows/CLAUDE.md` - CI/CD 流程指导 (Gitea Actions) ### 根目录 CLAUDE.md 优化 - 重构为项目概览和模块导航 - 提供模块选择指导 - 减少单个文件的上下文长度 ### 自动化机制 - 创建 `scripts/update-claude-docs.sh` 自动更新脚本 - 集成到 pre-commit hooks 中 - 文件变更时自动更新对应模块的 CLAUDE.md ## 🎯 优化效果 ### 上下文优化 - 每个模块独立的 CLAUDE.md 文件 - 大幅减少单次处理的上下文长度 - 提高 Claude 处理效率和准确性 ### 开发体验 - 根据工作内容选择对应模块 - 模块化的文档更聚焦和专业 - 自动维护文档时间戳 ### 项目结构 ``` photography/ ├── CLAUDE.md # 项目概览和模块导航 ├── frontend/CLAUDE.md # 前端开发指导 ├── docs/deployment/CLAUDE.md # 部署配置指导 ├── .gitea/workflows/CLAUDE.md # CI/CD 流程指导 └── scripts/update-claude-docs.sh # 自动更新脚本 ``` 现在 Claude 工作时只需关注单个模块的文档,大幅提升处理效率!
This commit is contained in:
524
docs/原始 prd/前端开发文档.md
Normal file
524
docs/原始 prd/前端开发文档.md
Normal file
@ -0,0 +1,524 @@
|
||||
# 摄影作品集网站 - 前端开发文档
|
||||
|
||||
## 1. 技术架构
|
||||
|
||||
### 1.1 技术栈选择
|
||||
- **框架**:Next.js 14 (App Router)
|
||||
- **样式**:Tailwind CSS + CSS Modules
|
||||
- **状态管理**:Zustand + React Query
|
||||
- **图片处理**:Sharp + Next.js Image
|
||||
- **动画**:Framer Motion
|
||||
- **UI组件**:Headless UI + 自定义组件
|
||||
|
||||
### 1.2 项目结构
|
||||
```
|
||||
src/
|
||||
├── app/ # App Router页面
|
||||
│ ├── layout.tsx # 根布局
|
||||
│ ├── page.tsx # 首页
|
||||
│ ├── gallery/ # 作品集页面
|
||||
│ ├── timeline/ # 时间线页面
|
||||
│ └── about/ # 关于页面
|
||||
├── components/ # 可复用组件
|
||||
│ ├── ui/ # 基础UI组件
|
||||
│ ├── gallery/ # 作品集相关组件
|
||||
│ ├── timeline/ # 时间线组件
|
||||
│ └── layout/ # 布局组件
|
||||
├── hooks/ # 自定义Hooks
|
||||
├── lib/ # 工具函数
|
||||
├── stores/ # 状态管理
|
||||
├── types/ # TypeScript类型
|
||||
└── styles/ # 全局样式
|
||||
```
|
||||
|
||||
## 2. 核心功能实现
|
||||
|
||||
### 2.1 图片管理系统
|
||||
|
||||
#### 多格式图片处理
|
||||
```typescript
|
||||
// lib/imageUtils.ts
|
||||
export interface ImageFormats {
|
||||
raw?: string; // RAW文件路径
|
||||
jpg: string; // JPG文件路径
|
||||
webp?: string; // WebP优化版本
|
||||
thumb: string; // 缩略图
|
||||
}
|
||||
|
||||
export interface PhotoData {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
formats: ImageFormats;
|
||||
metadata: {
|
||||
camera?: string;
|
||||
lens?: string;
|
||||
settings?: {
|
||||
iso: number;
|
||||
aperture: string;
|
||||
shutter: string;
|
||||
focal: string;
|
||||
};
|
||||
};
|
||||
collections: string[];
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应式图片组件
|
||||
```typescript
|
||||
// components/ui/ResponsiveImage.tsx
|
||||
interface ResponsiveImageProps {
|
||||
photo: PhotoData;
|
||||
sizes: string;
|
||||
priority?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function ResponsiveImage({ photo, sizes, priority, className, onClick }: ResponsiveImageProps) {
|
||||
return (
|
||||
<div className={`relative overflow-hidden ${className}`} onClick={onClick}>
|
||||
<Image
|
||||
src={photo.formats.webp || photo.formats.jpg}
|
||||
alt={photo.title}
|
||||
fill
|
||||
sizes={sizes}
|
||||
priority={priority}
|
||||
className="object-cover transition-transform hover:scale-105"
|
||||
placeholder="blur"
|
||||
blurDataURL={photo.formats.thumb}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 作品集网格系统
|
||||
|
||||
#### 自适应网格布局
|
||||
```typescript
|
||||
// components/gallery/PhotoGrid.tsx
|
||||
export function PhotoGrid({ photos }: { photos: PhotoData[] }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
|
||||
{photos.map((photo, index) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className={`aspect-square ${
|
||||
index % 7 === 0 ? 'md:col-span-2 md:row-span-2' : ''
|
||||
}`}
|
||||
>
|
||||
<ResponsiveImage
|
||||
photo={photo}
|
||||
sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
|
||||
priority={index < 8}
|
||||
className="w-full h-full cursor-pointer"
|
||||
onClick={() => openLightbox(photo)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 虚拟化长列表
|
||||
```typescript
|
||||
// hooks/useVirtualGrid.ts
|
||||
export function useVirtualGrid(items: PhotoData[], itemHeight: number) {
|
||||
const [visibleItems, setVisibleItems] = useState<PhotoData[]>([]);
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
|
||||
const handleScroll = useCallback((scrollTop: number, containerHeight: number) => {
|
||||
const newStartIndex = Math.floor(scrollTop / itemHeight);
|
||||
const endIndex = Math.min(
|
||||
newStartIndex + Math.ceil(containerHeight / itemHeight) + 2,
|
||||
items.length
|
||||
);
|
||||
|
||||
setStartIndex(newStartIndex);
|
||||
setVisibleItems(items.slice(newStartIndex, endIndex));
|
||||
}, [items, itemHeight]);
|
||||
|
||||
return { visibleItems, startIndex, handleScroll };
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 时间线功能
|
||||
|
||||
#### 时间线数据结构
|
||||
```typescript
|
||||
// types/timeline.ts
|
||||
export interface TimelineGroup {
|
||||
year: number;
|
||||
months: {
|
||||
[month: number]: {
|
||||
photos: PhotoData[];
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimelineData {
|
||||
groups: TimelineGroup[];
|
||||
totalPhotos: number;
|
||||
yearRange: [number, number];
|
||||
}
|
||||
```
|
||||
|
||||
#### 时间线组件
|
||||
```typescript
|
||||
// components/timeline/Timeline.tsx
|
||||
export function Timeline({ data }: { data: TimelineData }) {
|
||||
const [selectedYear, setSelectedYear] = useState<number>(data.yearRange[1]);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{/* 年份导航 */}
|
||||
<YearNavigation
|
||||
years={data.groups.map(g => g.year)}
|
||||
selectedYear={selectedYear}
|
||||
onYearSelect={setSelectedYear}
|
||||
/>
|
||||
|
||||
{/* 时间线内容 */}
|
||||
<div className="flex-1 ml-8">
|
||||
{data.groups
|
||||
.filter(group => group.year === selectedYear)
|
||||
.map(group => (
|
||||
<YearSection key={group.year} group={group} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 灯箱组件
|
||||
|
||||
#### 全功能图片查看器
|
||||
```typescript
|
||||
// components/ui/Lightbox.tsx
|
||||
export function Lightbox() {
|
||||
const { isOpen, currentPhoto, photos, currentIndex } = useLightboxStore();
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
if (!isOpen || !currentPhoto) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
{/* 图片区域 */}
|
||||
<div className="relative max-w-full max-h-full">
|
||||
<motion.img
|
||||
src={currentPhoto.formats.jpg}
|
||||
alt={currentPhoto.title}
|
||||
className={`max-w-full max-h-full ${isZoomed ? 'cursor-zoom-out' : 'cursor-zoom-in'}`}
|
||||
style={isZoomed ? { transform: `scale(2) translate(${position.x}px, ${position.y}px)` } : {}}
|
||||
onClick={handleImageClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 导航控件 */}
|
||||
<NavigationControls
|
||||
currentIndex={currentIndex}
|
||||
total={photos.length}
|
||||
onPrevious={goToPrevious}
|
||||
onNext={goToNext}
|
||||
/>
|
||||
|
||||
{/* 信息面板 */}
|
||||
<PhotoInfoPanel photo={currentPhoto} />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 性能优化策略
|
||||
|
||||
### 3.1 图片优化
|
||||
|
||||
#### 渐进式加载
|
||||
```typescript
|
||||
// hooks/useProgressiveImage.ts
|
||||
export function useProgressiveImage(src: string, placeholderSrc: string) {
|
||||
const [currentSrc, setCurrentSrc] = useState(placeholderSrc);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
setCurrentSrc(src);
|
||||
setLoading(false);
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
return { src: currentSrc, loading };
|
||||
}
|
||||
```
|
||||
|
||||
#### 智能预加载
|
||||
```typescript
|
||||
// hooks/useImagePreloader.ts
|
||||
export function useImagePreloader(photos: PhotoData[], currentIndex: number) {
|
||||
useEffect(() => {
|
||||
// 预加载当前图片的前后各2张
|
||||
const preloadRange = 2;
|
||||
const start = Math.max(0, currentIndex - preloadRange);
|
||||
const end = Math.min(photos.length, currentIndex + preloadRange + 1);
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
if (i !== currentIndex) {
|
||||
const img = new Image();
|
||||
img.src = photos[i].formats.jpg;
|
||||
}
|
||||
}
|
||||
}, [photos, currentIndex]);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 状态管理优化
|
||||
|
||||
#### Zustand Store设计
|
||||
```typescript
|
||||
// stores/galleryStore.ts
|
||||
interface GalleryState {
|
||||
photos: PhotoData[];
|
||||
collections: Collection[];
|
||||
currentCollection: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
setPhotos: (photos: PhotoData[]) => void;
|
||||
setCurrentCollection: (id: string | null) => void;
|
||||
addPhotos: (photos: PhotoData[]) => void;
|
||||
}
|
||||
|
||||
export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
photos: [],
|
||||
collections: [],
|
||||
currentCollection: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
setPhotos: (photos) => set({ photos }),
|
||||
setCurrentCollection: (id) => set({ currentCollection: id }),
|
||||
addPhotos: (newPhotos) => set((state) => ({
|
||||
photos: [...state.photos, ...newPhotos]
|
||||
})),
|
||||
}));
|
||||
```
|
||||
|
||||
### 3.3 代码分割
|
||||
|
||||
#### 动态导入
|
||||
```typescript
|
||||
// app/gallery/page.tsx
|
||||
const LazyPhotoGrid = dynamic(() => import('@/components/gallery/PhotoGrid'), {
|
||||
loading: () => <PhotoGridSkeleton />,
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const LazyLightbox = dynamic(() => import('@/components/ui/Lightbox'), {
|
||||
ssr: false
|
||||
});
|
||||
```
|
||||
|
||||
## 4. 移动端优化
|
||||
|
||||
### 4.1 触摸手势
|
||||
```typescript
|
||||
// hooks/useSwipeGesture.ts
|
||||
export function useSwipeGesture(onSwipeLeft: () => void, onSwipeRight: () => void) {
|
||||
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
||||
const touchEnd = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
touchEnd.current = null;
|
||||
touchStart.current = {
|
||||
x: e.targetTouches[0].clientX,
|
||||
y: e.targetTouches[0].clientY,
|
||||
};
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!touchStart.current || !touchEnd.current) return;
|
||||
|
||||
const distance = touchStart.current.x - touchEnd.current.x;
|
||||
const isLeftSwipe = distance > 50;
|
||||
const isRightSwipe = distance < -50;
|
||||
|
||||
if (isLeftSwipe) onSwipeLeft();
|
||||
if (isRightSwipe) onSwipeRight();
|
||||
};
|
||||
|
||||
return { handleTouchStart, handleTouchEnd };
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 移动端导航
|
||||
```typescript
|
||||
// components/layout/MobileNavigation.tsx
|
||||
export function MobileNavigation() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 汉堡菜单按钮 */}
|
||||
<button
|
||||
className="md:hidden fixed top-4 right-4 z-50"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<HamburgerIcon />
|
||||
</button>
|
||||
|
||||
{/* 侧边导航 */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.nav
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '100%' }}
|
||||
className="fixed inset-y-0 right-0 w-64 bg-black/95 backdrop-blur z-40"
|
||||
>
|
||||
<NavigationItems />
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. SEO优化
|
||||
|
||||
### 5.1 元数据管理
|
||||
```typescript
|
||||
// app/gallery/[collection]/page.tsx
|
||||
export async function generateMetadata({ params }: { params: { collection: string } }) {
|
||||
const collection = await getCollection(params.collection);
|
||||
|
||||
return {
|
||||
title: `${collection.name} | 摄影作品集`,
|
||||
description: collection.description,
|
||||
openGraph: {
|
||||
title: collection.name,
|
||||
description: collection.description,
|
||||
images: [collection.coverImage],
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 结构化数据
|
||||
```typescript
|
||||
// components/seo/StructuredData.tsx
|
||||
export function PhotoStructuredData({ photo }: { photo: PhotoData }) {
|
||||
const structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Photograph',
|
||||
name: photo.title,
|
||||
description: photo.description,
|
||||
contentUrl: photo.formats.jpg,
|
||||
thumbnail: photo.formats.thumb,
|
||||
dateCreated: photo.date,
|
||||
creator: {
|
||||
'@type': 'Person',
|
||||
name: '摄影师姓名',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 部署和监控
|
||||
|
||||
### 6.1 构建优化
|
||||
```javascript
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
images: {
|
||||
domains: ['your-domain.com'],
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256],
|
||||
},
|
||||
|
||||
experimental: {
|
||||
optimizeCss: true,
|
||||
optimizePackageImports: ['framer-motion'],
|
||||
},
|
||||
|
||||
compiler: {
|
||||
removeConsole: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 性能监控
|
||||
```typescript
|
||||
// lib/analytics.ts
|
||||
export function trackPageView(url: string) {
|
||||
if (typeof window !== 'undefined') {
|
||||
gtag('config', 'GA_TRACKING_ID', {
|
||||
page_location: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function trackPhotoView(photoId: string) {
|
||||
gtag('event', 'photo_view', {
|
||||
event_category: 'engagement',
|
||||
event_label: photoId,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 开发工具配置
|
||||
|
||||
### 7.1 TypeScript配置
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/lib/*": ["./src/lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 代码质量工具
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user