diff --git a/CLAUDE.md b/CLAUDE.md
index 195bebc..c6af08e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -52,10 +52,20 @@ cd frontend && make quick
- `make install` - 安装依赖
- `make dev` - 启动开发服务器
- `make build` - 构建生产版本
-- `make clean` - 清理构建文件
+- `make start` - 启动生产服务器
+- `make clean` - 清理构建文件和缓存
- `make status` - 检查项目状态
- `make add PACKAGE=name` - 添加依赖
+- `make add-dev PACKAGE=name` - 添加开发依赖
- `make remove PACKAGE=name` - 移除依赖
+- `make type-check` - 运行 TypeScript 类型检查
+- `make lint` - 运行代码检查
+- `make format` - 格式化代码
+- `make setup` - 设置开发环境
+- `make deploy-prep` - 准备生产部署
+- `make quick` - 快速启动(安装依赖并启动开发服务器)
+- `make update` - 更新所有依赖
+- `make reinstall` - 清理并重新安装依赖
## 项目结构
@@ -90,10 +100,19 @@ photography/
- `components/navigation.tsx` - 粘性导航,带移动菜单
- `components/photo-modal.tsx` - 照片详情模态框,带导航
- `components/timeline-view.tsx` - 基于时间轴的照片展示
+- `components/timeline-stats.tsx` - 时间轴统计信息
- `components/filter-bar.tsx` - 照片分类过滤
+- `components/about-view.tsx` - 关于页面组件
+- `components/contact-view.tsx` - 联系页面组件
+- `components/theme-provider.tsx` - 主题提供者(深色模式支持)
+- `components/providers/query-provider.tsx` - TanStack Query 提供者
+- `components/loading-spinner.tsx` - 加载动画组件
- `components/ui/` - 来自 shadcn/ui 的可重用 UI 组件
- `lib/api.ts` - 使用 axios 的 API 配置
- `lib/queries.ts` - 用于数据获取的 TanStack Query hooks
+- `lib/utils.ts` - 工具函数
+- `hooks/use-mobile.tsx` - 移动端检测钩子
+- `hooks/use-toast.ts` - 通知钩子
## 数据结构
@@ -121,7 +140,14 @@ interface Photo {
- `GET /api/photos` - 获取所有照片
- `GET /api/photos/:id` - 获取单个照片
- `GET /api/categories` - 获取分类列表
-- 标准 CRUD 操作用于照片管理
+- `POST /api/photos` - 添加新照片
+- `PUT /api/photos/:id` - 更新照片信息
+- `DELETE /api/photos/:id` - 删除照片
+
+启动模拟 API 服务器:
+```bash
+cd frontend && node mock-api.js
+```
## 环境变量
@@ -152,11 +178,19 @@ NEXT_PUBLIC_API_URL=http://localhost:3001/api
## 开发流程
1. 所有开发都在 `frontend/` 目录中进行
-2. 使用 `make dev` 或 `bun run dev` 启动开发服务器
-3. 使用 `make lint` 检查代码规范问题
-4. 使用 `make type-check` 运行 TypeScript 检查
-5. 部署前使用 `make build` 构建
-6. 使用 `make status` 检查项目健康状况
+2. 首次设置:`make setup` 创建环境变量文件
+3. 使用 `make dev` 或 `bun run dev` 启动开发服务器
+4. 使用 `make lint` 检查代码规范问题
+5. 使用 `make type-check` 运行 TypeScript 检查
+6. 使用 `make format` 格式化代码
+7. 部署前使用 `make deploy-prep` 进行完整检查和构建
+8. 使用 `make status` 检查项目健康状况
+
+### 快速开始工作流程
+```bash
+cd frontend
+make quick # 安装依赖并启动开发服务器
+```
## Bun 特定说明
@@ -181,4 +215,11 @@ NEXT_PUBLIC_API_URL=http://localhost:3001/api
### 开发环境配置
- 构建时忽略 TypeScript 和 ESLint 错误(适用于开发环境)
- 图像未优化设置,便于开发调试
-- 端口自动检测(如果 3000 被占用,会尝试 3001、3002 等)
\ No newline at end of file
+- 端口自动检测(如果 3000 被占用,会尝试 3001、3002 等)
+- 使用 Prettier 进行代码格式化
+- 支持通过 `make format` 统一代码风格
+
+## 文件锁定和包管理
+- 使用 `bun.lockb` 而非 `package-lock.json` 作为锁定文件
+- 项目名称:`my-v0-project`(可能需要更新为更合适的名称)
+- 所有包管理操作都应通过 Makefile 命令执行,确保使用 bun
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..375d014
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,46 @@
+# 产品文档
+
+本目录包含摄影作品集项目的完整产品文档。
+
+## 文档结构
+
+### 📐 Design(设计文档)
+- UI/UX 设计规范
+- 组件设计系统
+- 交互设计文档
+- 品牌指南和视觉规范
+
+### 🔌 API(API 文档)
+- API 接口文档
+- 数据结构定义
+- 接口调用示例
+- 错误码说明
+
+### 👥 User Guide(用户指南)
+- 用户操作手册
+- 功能使用说明
+- 常见问题解答
+- 最佳实践指南
+
+### 🛠️ Development(开发文档)
+- 开发环境搭建
+- 代码规范和约定
+- 开发工作流程
+- 测试指南
+
+### 🚀 Deployment(部署文档)
+- 部署配置说明
+- 环境变量配置
+- 服务器配置要求
+- 部署流程和脚本
+
+## 文档维护
+
+请在相应的子目录中维护各类文档,确保文档的及时更新和准确性。
+
+## 贡献指南
+
+1. 在相应目录下创建或更新文档
+2. 使用 Markdown 格式编写
+3. 确保文档结构清晰、内容准确
+4. 添加适当的图片和示例代码
\ No newline at end of file
diff --git a/docs/UI设计需求文档.md b/docs/UI设计需求文档.md
new file mode 100644
index 0000000..f777c93
--- /dev/null
+++ b/docs/UI设计需求文档.md
@@ -0,0 +1,175 @@
+# 摄影作品集网站 - UI设计需求文档
+
+## 1. 项目概述
+
+### 1.1 项目定位
+个人摄影作品展示网站,专注于高品质的视觉呈现和用户体验。
+
+### 1.2 设计目标
+- **极简美学**:突出作品本身,减少视觉干扰
+- **专业感**:体现摄影师的专业水准
+- **沉浸式体验**:让用户专注于作品欣赏
+- **响应式设计**:全设备完美适配
+
+## 2. 设计风格指南
+
+### 2.1 色彩方案
+- **主色调**:深色主题(#1a1a1a, #2d2d2d)
+- **辅助色**:纯白文字(#ffffff)
+- **强调色**:温暖金色(#d4af37)用于按钮和链接
+- **背景色**:渐变深灰(#0f0f0f 到 #1e1e1e)
+
+### 2.2 字体系统
+- **标题字体**:Modern Sans(如 Inter, SF Pro)
+- **正文字体**:清晰易读的无衬线字体
+- **字重层次**:Light(300), Regular(400), Medium(500), Bold(700)
+
+### 2.3 视觉层次
+- **一级标题**:32px-48px,Bold
+- **二级标题**:24px-32px,Medium
+- **正文**:16px-18px,Regular
+- **说明文字**:14px,Light
+
+## 3. 页面布局设计
+
+### 3.1 首页布局
+```
+[导航栏 - 固定在顶部]
+[英雄区域 - 全屏背景图/视频]
+ - 摄影师姓名
+ - 简短介绍
+ - CTA按钮
+[精选作品网格 - 3x2]
+[关于简介预览]
+[页脚]
+```
+
+### 3.2 作品集页面
+```
+[面包屑导航]
+[分类筛选器 - 水平排列]
+[作品网格展示]
+ - 瀑布流布局
+ - 悬停效果
+ - 快速预览
+[分页导航]
+```
+
+### 3.3 作品详情页
+```
+[返回按钮]
+[大图展示区域]
+ - 支持缩放
+ - 前后导航
+[作品信息侧栏]
+ - 标题、描述
+ - 拍摄参数
+ - 时间地点
+[相关推荐]
+```
+
+### 3.4 时间线页面
+```
+[年份导航]
+[时间线主体]
+ - 垂直时间轴
+ - 按月份分组
+ - 缩略图预览
+ - 作品数量统计
+```
+
+## 4. 交互设计规范
+
+### 4.1 悬停效果
+- **图片悬停**:轻微放大(1.05x) + 蒙层显示标题
+- **按钮悬停**:颜色渐变 + 轻微阴影
+- **导航悬停**:下划线动画
+
+### 4.2 加载动画
+- **页面加载**:优雅的骨架屏
+- **图片加载**:从模糊到清晰的渐进式加载
+- **切换动画**:平滑的页面过渡
+
+### 4.3 响应式交互
+- **移动端**:手势滑动支持
+- **桌面端**:键盘快捷键支持
+- **触摸优化**:合适的点击区域大小
+
+## 5. 组件设计规范
+
+### 5.1 导航组件
+```css
+.navigation {
+ position: fixed;
+ top: 0;
+ backdrop-filter: blur(10px);
+ background: rgba(26, 26, 26, 0.9);
+ padding: 20px;
+ z-index: 1000;
+}
+```
+
+### 5.2 图片网格组件
+- **网格间距**:16px-24px
+- **图片比例**:保持原始比例
+- **最小尺寸**:移动端 150px,桌面端 250px
+- **最大列数**:桌面4列,平板3列,手机2列
+
+### 5.3 模态框组件
+- **背景蒙层**:rgba(0, 0, 0, 0.8)
+- **关闭方式**:ESC键 + 点击蒙层 + 关闭按钮
+- **内容区域**:最大宽度90vw,最大高度90vh
+
+## 6. 移动端适配
+
+### 6.1 断点设置
+- **手机**:< 768px
+- **平板**:768px - 1024px
+- **桌面**:> 1024px
+- **大屏**:> 1440px
+
+### 6.2 移动端优化
+- **触摸友好**:按钮最小44px
+- **滑动操作**:支持左右滑动浏览
+- **下拉刷新**:原生滑动体验
+- **底部导航**:重要功能快速访问
+
+## 7. 性能优化设计
+
+### 7.1 图片优化
+- **懒加载**:视口外图片延迟加载
+- **渐进式**:先显示缩略图再加载高清
+- **格式优化**:WebP格式优先,降级JPG
+- **尺寸适配**:根据设备提供合适尺寸
+
+### 7.2 加载体验
+- **骨架屏**:内容加载前的占位
+- **进度指示**:长时间加载的进度反馈
+- **错误状态**:网络异常的友好提示
+
+## 8. 辅助功能设计
+
+### 8.1 可访问性
+- **键盘导航**:所有功能键盘可操作
+- **屏幕阅读器**:适当的ARIA标签
+- **对比度**:满足WCAG 2.1 AA标准
+- **焦点指示**:清晰的焦点样式
+
+### 8.2 多语言支持
+- **语言切换**:顶部导航显眼位置
+- **文字方向**:支持RTL语言
+- **日期格式**:本地化日期显示
+
+## 9. 设计交付物
+
+### 9.1 设计稿要求
+- **高保真原型**:Figma/Sketch源文件
+- **设计规范**:详细的设计系统文档
+- **交互说明**:动效和交互的详细说明
+- **响应式方案**:各断点的适配方案
+
+### 9.2 开发协作
+- **组件库**:可复用的UI组件
+- **图标资源**:SVG格式图标集
+- **字体文件**:web font资源
+- **样式指南**:CSS变量和类名规范
\ No newline at end of file
diff --git a/docs/api/README.md b/docs/api/README.md
new file mode 100644
index 0000000..9e93a0f
--- /dev/null
+++ b/docs/api/README.md
@@ -0,0 +1,25 @@
+# API 文档
+
+本目录包含摄影作品集项目的 API 接口文档。
+
+## 目录结构
+
+- `endpoints.md` - API 端点详细说明
+- `data-models.md` - 数据模型定义
+- `authentication.md` - 认证机制说明
+- `examples.md` - API 调用示例
+- `error-codes.md` - 错误码说明
+
+## API 基础信息
+
+- 基础 URL: `http://localhost:3001/api`
+- 数据格式: JSON
+- 认证方式: 待实现
+- 版本: v1
+
+## 主要端点
+
+- `/photos` - 照片相关操作
+- `/categories` - 分类管理
+- `/upload` - 文件上传(待实现)
+- `/user` - 用户管理(待实现)
\ No newline at end of file
diff --git a/docs/deployment/README.md b/docs/deployment/README.md
new file mode 100644
index 0000000..6b64304
--- /dev/null
+++ b/docs/deployment/README.md
@@ -0,0 +1,36 @@
+# 部署文档
+
+本目录包含摄影作品集项目的部署相关文档。
+
+## 目录结构
+
+- `environments.md` - 环境配置说明
+- `vercel-deployment.md` - Vercel 部署指南
+- `docker-deployment.md` - Docker 部署指南
+- `ci-cd.md` - CI/CD 配置说明
+- `monitoring.md` - 监控和日志配置
+- `backup.md` - 备份策略
+
+## 部署准备
+
+在部署前,请确保完成以下步骤:
+
+1. 代码检查: `make lint`
+2. 类型检查: `make type-check`
+3. 构建测试: `make build`
+4. 完整部署准备: `make deploy-prep`
+
+## 环境变量
+
+确保在部署环境中配置以下环境变量:
+
+```bash
+NEXT_PUBLIC_API_URL=your-api-url
+```
+
+## 支持的部署平台
+
+- Vercel (推荐)
+- Netlify
+- Docker
+- 传统服务器部署
\ No newline at end of file
diff --git a/docs/design/README.md b/docs/design/README.md
new file mode 100644
index 0000000..0777566
--- /dev/null
+++ b/docs/design/README.md
@@ -0,0 +1,18 @@
+# 设计文档
+
+本目录包含摄影作品集项目的设计相关文档。
+
+## 目录结构
+
+- `ui-design.md` - UI 设计规范
+- `component-system.md` - 组件设计系统
+- `interaction-design.md` - 交互设计文档
+- `brand-guide.md` - 品牌指南
+- `assets/` - 设计资源文件夹
+
+## 设计原则
+
+- 简洁优雅的视觉风格
+- 响应式设计,适配多种设备
+- 突出摄影作品的展示效果
+- 良好的用户体验和交互流程
\ No newline at end of file
diff --git a/docs/development/README.md b/docs/development/README.md
new file mode 100644
index 0000000..4b53039
--- /dev/null
+++ b/docs/development/README.md
@@ -0,0 +1,29 @@
+# 开发文档
+
+本目录包含摄影作品集项目的开发相关文档。
+
+## 目录结构
+
+- `setup.md` - 开发环境搭建
+- `coding-standards.md` - 代码规范
+- `architecture.md` - 架构设计说明
+- `components.md` - 组件开发指南
+- `testing.md` - 测试指南
+- `troubleshooting.md` - 常见问题解决
+
+## 技术栈
+
+- **前端**: Next.js 15 + React 19 + TypeScript
+- **样式**: Tailwind CSS + shadcn/ui
+- **状态管理**: TanStack Query + React Hooks
+- **包管理**: bun
+- **构建工具**: Next.js 内置构建系统
+
+## 开发流程
+
+1. 环境设置: `make setup`
+2. 安装依赖: `make install`
+3. 启动开发: `make dev`
+4. 代码检查: `make lint`
+5. 类型检查: `make type-check`
+6. 代码格式化: `make format`
\ No newline at end of file
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
new file mode 100644
index 0000000..7227db6
--- /dev/null
+++ b/docs/user-guide/README.md
@@ -0,0 +1,21 @@
+# 用户指南
+
+本目录包含摄影作品集项目的用户使用指南。
+
+## 目录结构
+
+- `getting-started.md` - 快速入门指南
+- `photo-management.md` - 照片管理功能
+- `gallery-features.md` - 画廊功能说明
+- `timeline-usage.md` - 时间轴使用方法
+- `theme-settings.md` - 主题设置说明
+- `faq.md` - 常见问题解答
+
+## 主要功能
+
+- 📸 照片画廊浏览
+- 📅 时间轴查看
+- 🏷️ 分类筛选
+- 🔍 照片详情查看
+- 🌙 深色模式切换
+- 📱 移动端适配
\ No newline at end of file
diff --git a/docs/前端开发文档.md b/docs/前端开发文档.md
new file mode 100644
index 0000000..00d26d6
--- /dev/null
+++ b/docs/前端开发文档.md
@@ -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 (
+
+
+
+ );
+}
+```
+
+### 2.2 作品集网格系统
+
+#### 自适应网格布局
+```typescript
+// components/gallery/PhotoGrid.tsx
+export function PhotoGrid({ photos }: { photos: PhotoData[] }) {
+ return (
+
+ {photos.map((photo, index) => (
+
+ openLightbox(photo)}
+ />
+
+ ))}
+
+ );
+}
+```
+
+#### 虚拟化长列表
+```typescript
+// hooks/useVirtualGrid.ts
+export function useVirtualGrid(items: PhotoData[], itemHeight: number) {
+ const [visibleItems, setVisibleItems] = useState([]);
+ 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(data.yearRange[1]);
+
+ return (
+
+ {/* 年份导航 */}
+
g.year)}
+ selectedYear={selectedYear}
+ onYearSelect={setSelectedYear}
+ />
+
+ {/* 时间线内容 */}
+
+ {data.groups
+ .filter(group => group.year === selectedYear)
+ .map(group => (
+
+ ))}
+
+
+ );
+}
+```
+
+### 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 (
+
+ {/* 图片区域 */}
+
+
+
+
+ {/* 导航控件 */}
+
+
+ {/* 信息面板 */}
+
+
+ );
+}
+```
+
+## 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((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: () => ,
+ 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 (
+ <>
+ {/* 汉堡菜单按钮 */}
+
+
+ {/* 侧边导航 */}
+
+ {isOpen && (
+
+
+
+ )}
+
+ >
+ );
+}
+```
+
+## 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 (
+
+ );
+}
+```
+
+## 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"
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/后端开发文档.md b/docs/后端开发文档.md
new file mode 100644
index 0000000..bb8ab89
--- /dev/null
+++ b/docs/后端开发文档.md
@@ -0,0 +1,543 @@
+# 摄影作品集网站 - 后端开发文档
+
+## 1. 技术架构
+
+### 1.1 技术栈选择
+- **后端框架**:Node.js + Express/Fastify
+- **数据库**:PostgreSQL + Redis
+- **图片处理**:Sharp + ImageMagick
+- **文件存储**:MinIO/AWS S3
+- **缓存策略**:Redis + CDN
+- **任务队列**:Bull Queue + Redis
+
+### 1.2 系统架构
+```
+[客户端] <-> [CDN] <-> [API网关] <-> [应用服务器]
+ ├── [图片处理服务]
+ ├── [文件管理服务]
+ └── [元数据服务]
+ ↓
+[缓存层 Redis] <-> [数据库 PostgreSQL]
+ ↓
+[对象存储 MinIO/S3]
+```
+
+## 2. 多格式图片处理系统
+
+### 2.1 图片格式管理
+```javascript
+// services/ImageProcessingService.js
+class ImageProcessingService {
+ async processRawPhoto(rawFile, metadata) {
+ const formats = {};
+
+ // 保存原始RAW文件
+ formats.raw = await this.saveRawFile(rawFile);
+
+ // 生成高质量JPG
+ formats.jpg = await this.generateJPG(rawFile, { quality: 95 });
+
+ // 生成WebP格式
+ formats.webp = await this.generateWebP(rawFile, { quality: 85 });
+
+ // 生成多种尺寸的缩略图
+ formats.thumbnails = await this.generateThumbnails(rawFile, [
+ { name: 'thumb', width: 300 },
+ { name: 'medium', width: 800 },
+ { name: 'large', width: 1600 }
+ ]);
+
+ return formats;
+ }
+
+ async generateWebP(sourceFile, options) {
+ return sharp(sourceFile)
+ .webp(options)
+ .toBuffer()
+ .then(buffer => this.uploadToStorage(buffer, 'webp'));
+ }
+}
+```
+
+### 2.2 智能压缩算法
+```javascript
+// utils/compressionUtils.js
+export class SmartCompression {
+ static async optimizeForDevice(imageBuffer, deviceType) {
+ const config = {
+ mobile: { width: 800, quality: 75 },
+ tablet: { width: 1200, quality: 80 },
+ desktop: { width: 1920, quality: 85 }
+ };
+
+ return sharp(imageBuffer)
+ .resize(config[deviceType].width, null, {
+ withoutEnlargement: true
+ })
+ .jpeg({ quality: config[deviceType].quality })
+ .toBuffer();
+ }
+}
+```
+
+## 3. 数据模型设计
+
+### 3.1 数据库Schema
+```sql
+-- 图片表
+CREATE TABLE photos (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ title VARCHAR(255) NOT NULL,
+ description TEXT,
+ original_filename VARCHAR(255),
+ file_size BIGINT,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW(),
+
+ -- 元数据
+ camera VARCHAR(100),
+ lens VARCHAR(100),
+ iso INTEGER,
+ aperture VARCHAR(10),
+ shutter_speed VARCHAR(20),
+ focal_length VARCHAR(20),
+
+ -- 位置信息
+ latitude DECIMAL(10, 8),
+ longitude DECIMAL(11, 8),
+ location_name VARCHAR(255),
+
+ -- 状态
+ status VARCHAR(20) DEFAULT 'processing',
+ visibility VARCHAR(20) DEFAULT 'public'
+);
+
+-- 文件格式表
+CREATE TABLE photo_formats (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ photo_id UUID REFERENCES photos(id) ON DELETE CASCADE,
+ format_type VARCHAR(10) NOT NULL, -- 'raw', 'jpg', 'webp', 'thumb'
+ file_path VARCHAR(500) NOT NULL,
+ file_size BIGINT,
+ width INTEGER,
+ height INTEGER,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+-- 收藏夹表
+CREATE TABLE collections (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ cover_photo_id UUID REFERENCES photos(id),
+ sort_order INTEGER DEFAULT 0,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+-- 图片收藏夹关联表
+CREATE TABLE photo_collections (
+ photo_id UUID REFERENCES photos(id) ON DELETE CASCADE,
+ collection_id UUID REFERENCES collections(id) ON DELETE CASCADE,
+ sort_order INTEGER DEFAULT 0,
+ PRIMARY KEY (photo_id, collection_id)
+);
+```
+
+### 3.2 数据访问层
+```javascript
+// models/PhotoModel.js
+class PhotoModel {
+ static async findWithFormats(photoId) {
+ const query = `
+ SELECT
+ p.*,
+ json_agg(
+ json_build_object(
+ 'format_type', pf.format_type,
+ 'file_path', pf.file_path,
+ 'width', pf.width,
+ 'height', pf.height
+ )
+ ) as formats
+ FROM photos p
+ LEFT JOIN photo_formats pf ON p.id = pf.photo_id
+ WHERE p.id = $1
+ GROUP BY p.id
+ `;
+
+ return await db.query(query, [photoId]);
+ }
+
+ static async findByTimeline(year, month = null) {
+ let query = `
+ SELECT * FROM photos
+ WHERE EXTRACT(YEAR FROM created_at) = $1
+ `;
+
+ const params = [year];
+
+ if (month) {
+ query += ` AND EXTRACT(MONTH FROM created_at) = $2`;
+ params.push(month);
+ }
+
+ query += ` ORDER BY created_at DESC`;
+
+ return await db.query(query, params);
+ }
+}
+```
+
+## 4. API设计
+
+### 4.1 RESTful API结构
+```javascript
+// routes/api/photos.js
+const router = express.Router();
+
+// 获取图片列表
+router.get('/', async (req, res) => {
+ const {
+ page = 1,
+ limit = 20,
+ collection,
+ year,
+ month
+ } = req.query;
+
+ try {
+ const photos = await PhotoService.getPhotos({
+ page: parseInt(page),
+ limit: parseInt(limit),
+ collection,
+ year: year ? parseInt(year) : null,
+ month: month ? parseInt(month) : null
+ });
+
+ res.json({
+ data: photos,
+ pagination: {
+ page,
+ limit,
+ total: photos.total
+ }
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 获取时间线数据
+router.get('/timeline', async (req, res) => {
+ try {
+ const timeline = await PhotoService.getTimeline();
+ res.json({ data: timeline });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 上传图片
+router.post('/', upload.single('photo'), async (req, res) => {
+ try {
+ const result = await PhotoService.uploadPhoto(req.file, req.body);
+ res.status(201).json({ data: result });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+```
+
+### 4.2 GraphQL Schema (可选)
+```graphql
+type Photo {
+ id: ID!
+ title: String!
+ description: String
+ createdAt: DateTime!
+
+ formats: [PhotoFormat!]!
+ metadata: PhotoMetadata
+ collections: [Collection!]!
+}
+
+type PhotoFormat {
+ type: FormatType!
+ url: String!
+ width: Int
+ height: Int
+ fileSize: Int
+}
+
+enum FormatType {
+ RAW
+ JPG
+ WEBP
+ THUMBNAIL
+}
+
+type Query {
+ photos(
+ first: Int
+ after: String
+ collection: ID
+ year: Int
+ month: Int
+ ): PhotoConnection!
+
+ timeline: [TimelineGroup!]!
+}
+
+type Mutation {
+ uploadPhoto(input: PhotoUploadInput!): Photo!
+ updatePhoto(id: ID!, input: PhotoUpdateInput!): Photo!
+}
+```
+
+## 5. 文件存储管理
+
+### 5.1 存储策略
+```javascript
+// services/StorageService.js
+class StorageService {
+ constructor() {
+ this.storage = process.env.NODE_ENV === 'production'
+ ? new S3Storage()
+ : new LocalStorage();
+ }
+
+ async uploadPhoto(buffer, metadata) {
+ const filename = this.generateFilename(metadata);
+ const path = this.getStoragePath(metadata.date, filename);
+
+ // 上传到存储服务
+ const url = await this.storage.upload(buffer, path);
+
+ // 更新CDN缓存
+ await this.invalidateCache(url);
+
+ return { url, path };
+ }
+
+ generateFilename(metadata) {
+ const timestamp = Date.now();
+ const random = Math.random().toString(36).substr(2, 9);
+ return `${timestamp}_${random}.${metadata.extension}`;
+ }
+
+ getStoragePath(date, filename) {
+ const year = new Date(date).getFullYear();
+ const month = String(new Date(date).getMonth() + 1).padStart(2, '0');
+ return `photos/${year}/${month}/${filename}`;
+ }
+}
+```
+
+### 5.2 CDN集成
+```javascript
+// services/CDNService.js
+class CDNService {
+ static getOptimizedUrl(originalUrl, options = {}) {
+ const {
+ width,
+ height,
+ quality = 85,
+ format = 'auto'
+ } = options;
+
+ // 使用ImageKit或Cloudinary等服务
+ const transformations = [];
+
+ if (width) transformations.push(`w_${width}`);
+ if (height) transformations.push(`h_${height}`);
+ if (quality) transformations.push(`q_${quality}`);
+ if (format !== 'auto') transformations.push(`f_${format}`);
+
+ return `${CDN_BASE_URL}/${transformations.join(',')}/auto/${originalUrl}`;
+ }
+}
+```
+
+## 6. 自动化处理流程
+
+### 6.1 图片上传队列
+```javascript
+// jobs/imageProcessingJob.js
+const Queue = require('bull');
+const imageQueue = new Queue('image processing');
+
+imageQueue.process(async (job) => {
+ const { photoId, rawFilePath } = job.data;
+
+ try {
+ // 1. 提取EXIF数据
+ const metadata = await extractMetadata(rawFilePath);
+
+ // 2. 生成多种格式
+ const formats = await generateFormats(rawFilePath);
+
+ // 3. 保存到数据库
+ await savePhotoFormats(photoId, formats);
+
+ // 4. 清理临时文件
+ await cleanupTempFiles(rawFilePath);
+
+ // 5. 发送完成通知
+ await notifyProcessingComplete(photoId);
+
+ } catch (error) {
+ logger.error('Image processing failed:', error);
+ throw error;
+ }
+});
+
+// 添加任务到队列
+const addImageProcessingJob = (photoId, rawFilePath) => {
+ return imageQueue.add({
+ photoId,
+ rawFilePath
+ }, {
+ attempts: 3,
+ backoff: 'exponential',
+ delay: 2000
+ });
+};
+```
+
+### 6.2 自动分类系统
+```javascript
+// services/AutoTaggingService.js
+class AutoTaggingService {
+ static async analyzePhoto(photoBuffer) {
+ // 使用AI服务进行图片分析
+ const analysis = await this.callVisionAPI(photoBuffer);
+
+ const tags = [];
+
+ // 提取颜色主题
+ const colors = analysis.colors.map(c => c.name);
+ tags.push(...colors);
+
+ // 识别物体和场景
+ const objects = analysis.objects.map(o => o.name);
+ tags.push(...objects);
+
+ // 检测拍摄风格
+ const style = this.detectPhotographyStyle(analysis);
+ if (style) tags.push(style);
+
+ return {
+ tags,
+ confidence: analysis.confidence,
+ description: analysis.description
+ };
+ }
+
+ static detectPhotographyStyle(analysis) {
+ // 基于构图和内容判断摄影风格
+ if (analysis.faces.length > 0) return 'portrait';
+ if (analysis.landscape) return 'landscape';
+ if (analysis.architecture) return 'architecture';
+ return null;
+ }
+}
+```
+
+## 7. 缓存策略
+
+### 7.1 多级缓存
+```javascript
+// services/CacheService.js
+class CacheService {
+ constructor() {
+ this.redis = new Redis(process.env.REDIS_URL);
+ this.memCache = new NodeCache({ stdTTL: 300 }); // 5分钟
+ }
+
+ async getPhotos(cacheKey, fetcher) {
+ // L1: 内存缓存
+ let data = this.memCache.get(cacheKey);
+ if (data) return data;
+
+ // L2: Redis缓存
+ data = await this.redis.get(cacheKey);
+ if (data) {
+ data = JSON.parse(data);
+ this.memCache.set(cacheKey, data);
+ return data;
+ }
+
+ // L3: 数据库查询
+ data = await fetcher();
+
+ // 缓存结果
+ await this.redis.setex(cacheKey, 3600, JSON.stringify(data)); // 1小时
+ this.memCache.set(cacheKey, data);
+
+ return data;
+ }
+
+ async invalidatePattern(pattern) {
+ const keys = await this.redis.keys(pattern);
+ if (keys.length > 0) {
+ await this.redis.del(...keys);
+ }
+
+ // 清理内存缓存
+ this.memCache.flushAll();
+ }
+}
+```
+
+## 8. 性能监控
+
+### 8.1 APM集成
+```javascript
+// middleware/monitoring.js
+const performanceMiddleware = (req, res, next) => {
+ const start = Date.now();
+
+ res.on('finish', () => {
+ const duration = Date.now() - start;
+
+ // 记录性能指标
+ metrics.timing('api.response_time', duration, {
+ route: req.route?.path,
+ method: req.method,
+ status: res.statusCode
+ });
+
+ // 慢查询告警
+ if (duration > 1000) {
+ logger.warn('Slow API response', {
+ url: req.url,
+ duration,
+ method: req.method
+ });
+ }
+ });
+
+ next();
+};
+```
+
+### 8.2 健康检查
+```javascript
+// routes/health.js
+router.get('/health', async (req, res) => {
+ const checks = {
+ database: await checkDatabase(),
+ redis: await checkRedis(),
+ storage: await checkStorage(),
+ imageProcessing: await checkImageService()
+ };
+
+ const healthy = Object.values(checks).every(check => check.status === 'ok');
+
+ res.status(healthy ? 200 : 503).json({
+ status: healthy ? 'healthy' : 'unhealthy',
+ checks,
+ timestamp: new Date().toISOString()
+ });
+});
+```
\ No newline at end of file
diff --git a/docs/测试需求文档.md b/docs/测试需求文档.md
new file mode 100644
index 0000000..c637d8a
--- /dev/null
+++ b/docs/测试需求文档.md
@@ -0,0 +1,562 @@
+# 摄影作品集网站 - 测试需求文档
+
+## 1. 测试策略概述
+
+### 1.1 测试目标
+- **功能完整性**:确保所有功能按需求正常工作
+- **性能稳定性**:验证系统在各种负载下的表现
+- **用户体验**:保证界面交互流畅自然
+- **兼容性**:确保跨设备、跨浏览器的一致性
+- **安全性**:验证数据安全和用户隐私保护
+
+### 1.2 测试类型分布
+- **单元测试**:40%(组件、工具函数、API端点)
+- **集成测试**:30%(模块间交互、数据流)
+- **端到端测试**:20%(用户完整流程)
+- **性能测试**:10%(加载速度、响应时间)
+
+## 2. 功能测试用例
+
+### 2.1 图片管理功能
+
+#### 2.1.1 图片上传测试
+```javascript
+// tests/upload.test.js
+describe('图片上传功能', () => {
+ test('应该能上传RAW格式图片', async () => {
+ const file = new File([rawImageBuffer], 'test.cr2', { type: 'image/x-canon-cr2' });
+ const result = await uploadPhoto(file);
+
+ expect(result.status).toBe('success');
+ expect(result.formats).toHaveProperty('raw');
+ expect(result.formats).toHaveProperty('jpg');
+ expect(result.formats).toHaveProperty('webp');
+ });
+
+ test('应该能上传JPEG格式图片', async () => {
+ const file = new File([jpegBuffer], 'test.jpg', { type: 'image/jpeg' });
+ const result = await uploadPhoto(file);
+
+ expect(result.status).toBe('success');
+ expect(result.formats.jpg).toBeDefined();
+ });
+
+ test('应该拒绝不支持的文件格式', async () => {
+ const file = new File([textBuffer], 'test.txt', { type: 'text/plain' });
+
+ await expect(uploadPhoto(file)).rejects.toThrow('不支持的文件格式');
+ });
+
+ test('应该拒绝过大的文件', async () => {
+ const largeFile = new File([new ArrayBuffer(100 * 1024 * 1024)], 'large.jpg');
+
+ await expect(uploadPhoto(largeFile)).rejects.toThrow('文件大小超出限制');
+ });
+});
+```
+
+#### 2.1.2 图片展示测试
+```javascript
+describe('图片展示功能', () => {
+ test('应该正确显示图片网格', async () => {
+ render();
+
+ // 验证网格布局
+ const grid = screen.getByTestId('photo-grid');
+ expect(grid).toBeInTheDocument();
+
+ // 验证图片数量
+ const images = screen.getAllByRole('img');
+ expect(images).toHaveLength(mockPhotos.length);
+ });
+
+ test('应该支持懒加载', async () => {
+ const { container } = render();
+
+ // 只有可视区域的图片应该被加载
+ const loadedImages = container.querySelectorAll('img[src]');
+ expect(loadedImages.length).toBeLessThan(longPhotoList.length);
+ });
+
+ test('应该响应式适配不同屏幕', () => {
+ // 桌面端
+ Object.defineProperty(window, 'innerWidth', { value: 1200 });
+ render();
+ expect(screen.getByTestId('photo-grid')).toHaveClass('lg:grid-cols-4');
+
+ // 移动端
+ Object.defineProperty(window, 'innerWidth', { value: 600 });
+ render();
+ expect(screen.getByTestId('photo-grid')).toHaveClass('grid-cols-2');
+ });
+});
+```
+
+### 2.2 时间线功能测试
+
+```javascript
+describe('时间线功能', () => {
+ test('应该按年份正确分组', async () => {
+ const timelineData = await getTimelineData();
+
+ expect(timelineData.groups).toBeDefined();
+ expect(timelineData.groups[0].year).toBe(2024);
+ expect(timelineData.groups[0].months).toHaveProperty('1');
+ });
+
+ test('应该支持年份切换', async () => {
+ render();
+
+ const yearButton = screen.getByText('2023');
+ fireEvent.click(yearButton);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('timeline-content')).toHaveTextContent('2023');
+ });
+ });
+
+ test('应该显示正确的照片数量统计', () => {
+ render();
+
+ const monthSummary = screen.getByTestId('month-summary-1');
+ expect(monthSummary).toHaveTextContent('15张照片');
+ });
+});
+```
+
+### 2.3 收藏夹功能测试
+
+```javascript
+describe('收藏夹功能', () => {
+ test('应该能创建新收藏夹', async () => {
+ render();
+
+ const createButton = screen.getByText('创建收藏夹');
+ fireEvent.click(createButton);
+
+ const nameInput = screen.getByLabelText('收藏夹名称');
+ fireEvent.change(nameInput, { target: { value: '风景摄影' } });
+
+ const saveButton = screen.getByText('保存');
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('风景摄影')).toBeInTheDocument();
+ });
+ });
+
+ test('应该能添加照片到收藏夹', async () => {
+ render();
+
+ const photo = screen.getAllByTestId('photo-item')[0];
+ fireEvent.contextMenu(photo);
+
+ const addToCollection = screen.getByText('添加到收藏夹');
+ fireEvent.click(addToCollection);
+
+ const collection = screen.getByText('风景摄影');
+ fireEvent.click(collection);
+
+ await waitFor(() => {
+ expect(screen.getByText('已添加到收藏夹')).toBeInTheDocument();
+ });
+ });
+});
+```
+
+## 3. 性能测试
+
+### 3.1 图片加载性能测试
+
+```javascript
+// tests/performance/imageLoading.test.js
+describe('图片加载性能', () => {
+ test('首屏图片应在2秒内加载完成', async () => {
+ const startTime = performance.now();
+
+ render();
+
+ await waitFor(() => {
+ const images = screen.getAllByRole('img');
+ images.forEach(img => {
+ expect(img).toHaveAttribute('src');
+ });
+ });
+
+ const loadTime = performance.now() - startTime;
+ expect(loadTime).toBeLessThan(2000);
+ });
+
+ test('大图片应该有渐进式加载', async () => {
+ render();
+
+ const img = screen.getByRole('img');
+
+ // 初始应显示占位图
+ expect(img).toHaveAttribute('src', expect.stringContaining('thumb.jpg'));
+
+ await waitFor(() => {
+ // 加载完成后应显示高清图
+ expect(img).toHaveAttribute('src', expect.stringContaining('large-image.jpg'));
+ });
+ });
+});
+```
+
+### 3.2 API响应时间测试
+
+```javascript
+describe('API性能测试', () => {
+ test('获取照片列表API应在500ms内响应', async () => {
+ const startTime = Date.now();
+
+ const response = await fetch('/api/photos?limit=20');
+ const data = await response.json();
+
+ const responseTime = Date.now() - startTime;
+
+ expect(response.status).toBe(200);
+ expect(responseTime).toBeLessThan(500);
+ expect(data.data).toHaveLength(20);
+ });
+
+ test('图片上传API应支持大文件', async () => {
+ const largeFile = new File([new ArrayBuffer(20 * 1024 * 1024)], 'large.jpg');
+ const formData = new FormData();
+ formData.append('photo', largeFile);
+
+ const startTime = Date.now();
+ const response = await fetch('/api/photos', {
+ method: 'POST',
+ body: formData
+ });
+
+ const uploadTime = Date.now() - startTime;
+
+ expect(response.status).toBe(201);
+ expect(uploadTime).toBeLessThan(30000); // 30秒内完成
+ });
+});
+```
+
+## 4. 兼容性测试
+
+### 4.1 浏览器兼容性测试
+
+```javascript
+// 使用Playwright进行跨浏览器测试
+const { test, devices } = require('@playwright/test');
+
+test.describe('浏览器兼容性', () => {
+ ['chromium', 'firefox', 'webkit'].forEach(browserName => {
+ test(`应该在${browserName}中正常显示`, async ({ browser }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ await page.goto('/');
+
+ // 验证关键元素存在
+ await expect(page.locator('[data-testid="photo-grid"]')).toBeVisible();
+ await expect(page.locator('nav')).toBeVisible();
+
+ // 验证图片加载
+ const firstImage = page.locator('img').first();
+ await expect(firstImage).toBeVisible();
+
+ await context.close();
+ });
+ });
+});
+```
+
+### 4.2 设备响应式测试
+
+```javascript
+describe('响应式设计测试', () => {
+ const devices = [
+ { name: 'iPhone 12', width: 390, height: 844 },
+ { name: 'iPad', width: 768, height: 1024 },
+ { name: 'Desktop', width: 1920, height: 1080 }
+ ];
+
+ devices.forEach(device => {
+ test(`应该在${device.name}上正确显示`, async () => {
+ await page.setViewportSize({
+ width: device.width,
+ height: device.height
+ });
+
+ await page.goto('/');
+
+ // 验证导航栏适配
+ if (device.width < 768) {
+ await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
+ } else {
+ await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
+ }
+
+ // 验证网格列数
+ const grid = page.locator('[data-testid="photo-grid"]');
+ const expectedCols = device.width < 768 ? 2 : device.width < 1024 ? 3 : 4;
+
+ await expect(grid).toHaveClass(new RegExp(`grid-cols-${expectedCols}`));
+ });
+ });
+});
+```
+
+## 5. 用户体验测试
+
+### 5.1 交互流程测试
+
+```javascript
+describe('用户交互流程', () => {
+ test('完整的照片浏览流程', async () => {
+ await page.goto('/');
+
+ // 1. 点击照片打开灯箱
+ const firstPhoto = page.locator('[data-testid="photo-item"]').first();
+ await firstPhoto.click();
+
+ await expect(page.locator('[data-testid="lightbox"]')).toBeVisible();
+
+ // 2. 使用键盘导航
+ await page.keyboard.press('ArrowRight');
+ await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
+
+ // 3. 缩放功能
+ await page.locator('[data-testid="lightbox-image"]').click();
+ await expect(page.locator('[data-testid="lightbox-image"]')).toHaveClass(/zoomed/);
+
+ // 4. 关闭灯箱
+ await page.keyboard.press('Escape');
+ await expect(page.locator('[data-testid="lightbox"]')).not.toBeVisible();
+ });
+
+ test('移动端手势操作', async () => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto('/');
+
+ const firstPhoto = page.locator('[data-testid="photo-item"]').first();
+ await firstPhoto.click();
+
+ // 模拟左滑手势
+ const lightbox = page.locator('[data-testid="lightbox-image"]');
+ await lightbox.touchAction([
+ { action: 'touchStart', x: 300, y: 400 },
+ { action: 'touchMove', x: 100, y: 400 },
+ { action: 'touchEnd' }
+ ]);
+
+ await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
+ });
+});
+```
+
+### 5.2 可访问性测试
+
+```javascript
+describe('可访问性测试', () => {
+ test('应该支持键盘导航', async () => {
+ await page.goto('/');
+
+ // Tab键导航
+ await page.keyboard.press('Tab');
+ await expect(page.locator(':focus')).toBeVisible();
+
+ // 回车键激活
+ await page.keyboard.press('Enter');
+ // 验证预期的交互结果
+ });
+
+ test('应该有正确的ARIA标签', async () => {
+ await page.goto('/');
+
+ const grid = page.locator('[data-testid="photo-grid"]');
+ await expect(grid).toHaveAttribute('role', 'grid');
+
+ const photos = page.locator('[data-testid="photo-item"]');
+ await expect(photos.first()).toHaveAttribute('role', 'gridcell');
+ });
+
+ test('应该支持屏幕阅读器', async () => {
+ await page.goto('/');
+
+ const firstPhoto = page.locator('[data-testid="photo-item"]').first();
+ await expect(firstPhoto).toHaveAttribute('aria-label');
+
+ const img = firstPhoto.locator('img');
+ await expect(img).toHaveAttribute('alt');
+ });
+});
+```
+
+## 6. 自动化测试配置
+
+### 6.1 Jest配置
+```javascript
+// jest.config.js
+module.exports = {
+ testEnvironment: 'jsdom',
+ setupFilesAfterEnv: ['/src/setupTests.js'],
+ moduleNameMapping: {
+ '^@/(.*)$': '/src/$1',
+ '^@/components/(.*)$': '/src/components/$1'
+ },
+ testMatch: [
+ '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
+ '/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
+ ],
+ collectCoverageFrom: [
+ 'src/**/*.{js,jsx,ts,tsx}',
+ '!src/**/*.d.ts',
+ '!src/index.js'
+ ],
+ coverageThreshold: {
+ global: {
+ branches: 80,
+ functions: 80,
+ lines: 80,
+ statements: 80
+ }
+ }
+};
+```
+
+### 6.2 Playwright配置
+```javascript
+// playwright.config.js
+module.exports = {
+ testDir: './e2e',
+ timeout: 30000,
+ use: {
+ baseURL: 'http://localhost:3000',
+ trace: 'on-first-retry',
+ video: 'retain-on-failure'
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] }
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] }
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] }
+ },
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] }
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] }
+ }
+ ]
+};
+```
+
+## 7. 性能监控和测试
+
+### 7.1 Lighthouse CI集成
+```javascript
+// lighthouse.config.js
+module.exports = {
+ ci: {
+ collect: {
+ url: ['http://localhost:3000', 'http://localhost:3000/gallery'],
+ numberOfRuns: 3
+ },
+ assert: {
+ assertions: {
+ 'categories:performance': ['error', { minScore: 0.9 }],
+ 'categories:accessibility': ['error', { minScore: 0.9 }],
+ 'categories:best-practices': ['error', { minScore: 0.9 }],
+ 'categories:seo': ['error', { minScore: 0.9 }]
+ }
+ }
+ }
+};
+```
+
+### 7.2 负载测试
+```javascript
+// load-test.js
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+
+export let options = {
+ stages: [
+ { duration: '2m', target: 100 }, // 2分钟内增加到100用户
+ { duration: '5m', target: 100 }, // 维持100用户5分钟
+ { duration: '2m', target: 200 }, // 增加到200用户
+ { duration: '5m', target: 200 }, // 维持200用户5分钟
+ { duration: '2m', target: 0 }, // 逐步减少到0
+ ],
+};
+
+export default function() {
+ let response = http.get('http://localhost:3000/api/photos');
+
+ check(response, {
+ 'status is 200': (r) => r.status === 200,
+ 'response time < 500ms': (r) => r.timings.duration < 500,
+ });
+
+ sleep(1);
+}
+```
+
+## 8. 测试报告和持续集成
+
+### 8.1 GitHub Actions配置
+```yaml
+# .github/workflows/test.yml
+name: Test
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '18'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run unit tests
+ run: npm run test:coverage
+
+ - name: Run E2E tests
+ run: npm run test:e2e
+
+ - name: Run Lighthouse CI
+ run: npm run lighthouse:ci
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v1
+```
+
+### 8.2 测试覆盖率报告
+```javascript
+// package.json scripts
+{
+ "scripts": {
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:coverage": "jest --coverage",
+ "test:e2e": "playwright test",
+ "lighthouse:ci": "lhci autorun"
+ }
+}
+```
\ No newline at end of file