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 工作时只需关注单个模块的文档,大幅提升处理效率!
13 KiB
13 KiB
摄影作品集网站 - 前端开发文档
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"
}
}