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

13 KiB
Raw Blame History

摄影作品集网站 - 前端开发文档

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 图片管理系统

多格式图片处理

// 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[];
}

响应式图片组件

// 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 作品集网格系统

自适应网格布局

// 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>
  );
}

虚拟化长列表

// 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 时间线功能

时间线数据结构

// 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];
}

时间线组件

// 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 灯箱组件

全功能图片查看器

// 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 图片优化

渐进式加载

// 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 };
}

智能预加载

// 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设计

// 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 代码分割

动态导入

// 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 触摸手势

// 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 移动端导航

// 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 元数据管理

// 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 结构化数据

// 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 构建优化

// 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 性能监控

// 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配置

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"]
    }
  }
}

7.2 代码质量工具

// package.json
{
  "scripts": {
    "lint": "next lint",
    "lint:fix": "next lint --fix",
    "type-check": "tsc --noEmit",
    "test": "jest",
    "test:watch": "jest --watch"
  }
}