Files
photography/docs/原始 prd/前端开发文档.md
xujiang 9e381c783d
All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
feat: 重构项目为模块化结构,拆分 CLAUDE.md 文档
## 📁 模块化重构

### 新增模块 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 工作时只需关注单个模块的文档,大幅提升处理效率!
2025-07-09 10:54:08 +08:00

524 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 摄影作品集网站 - 前端开发文档
## 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"
}
}
```