diff --git a/app/components/ImageModal.tsx b/app/components/ImageModal.tsx
new file mode 100644
index 0000000..5b1437d
--- /dev/null
+++ b/app/components/ImageModal.tsx
@@ -0,0 +1,202 @@
+'use client';
+
+import { X, Info, ZoomIn, ZoomOut, RotateCw, Download, Maximize2, Minimize2 } from 'lucide-react';
+import { Photo } from '@/app/types';
+import { useState, useEffect } from 'react';
+
+interface ImageModalProps {
+ photo: Photo | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onInfoClick: () => void;
+}
+
+export default function ImageModal({ photo, isOpen, onClose, onInfoClick }: ImageModalProps) {
+ const [scale, setScale] = useState(1);
+ const [rotation, setRotation] = useState(0);
+ const [isMobile, setIsMobile] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ // 检测移动端
+ useEffect(() => {
+ const checkMobile = () => {
+ setIsMobile(window.innerWidth < 768);
+ };
+
+ checkMobile();
+ window.addEventListener('resize', checkMobile);
+
+ return () => window.removeEventListener('resize', checkMobile);
+ }, []);
+
+ if (!photo || !isOpen) return null;
+
+ const handleZoomIn = () => {
+ setScale(prev => Math.min(prev + 0.25, 3));
+ };
+
+ const handleZoomOut = () => {
+ setScale(prev => Math.max(prev - 0.25, 0.5));
+ };
+
+ const handleRotate = () => {
+ setRotation(prev => (prev + 90) % 360);
+ };
+
+ const handleReset = () => {
+ setScale(1);
+ setRotation(0);
+ };
+
+ const handleDownload = () => {
+ const link = document.createElement('a');
+ link.href = photo.imageUrl;
+ link.download = photo.originalFilename || photo.title;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ const toggleFullscreen = () => {
+ setIsFullscreen(!isFullscreen);
+ };
+
+ // 移动端全屏样式
+ const modalStyle = isMobile && isFullscreen ? {
+ position: 'fixed' as const,
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ zIndex: 9999,
+ backgroundColor: '#000',
+ } : {};
+
+ return (
+ <>
+ {/* 遮罩层 */}
+
+
+ {/* 图片模态框 */}
+
+
+ {/* 顶部工具栏 - 移动端全屏时隐藏 */}
+ {(!isMobile || !isFullscreen) && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* 右上角按钮 */}
+
+ {/* 移动端全屏切换按钮 */}
+ {isMobile && (
+
+ )}
+
+
+
+
+
+
+ {/* 图片容器 */}
+
+
+

{
+ e.stopPropagation();
+ // 移动端点击图片切换全屏
+ if (isMobile) {
+ toggleFullscreen();
+ }
+ }}
+ />
+
+
+
+ {/* 底部信息栏 - 移动端全屏时隐藏 */}
+ {(!isMobile || !isFullscreen) && (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx
new file mode 100644
index 0000000..3f641da
--- /dev/null
+++ b/app/components/Navbar.tsx
@@ -0,0 +1,202 @@
+'use client';
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { Menu, X, Moon, Sun, Home, Images } from 'lucide-react';
+
+interface NavbarProps {
+ darkMode: boolean;
+ onToggleDarkMode: () => void;
+}
+
+export default function Navbar({ darkMode, onToggleDarkMode }: NavbarProps) {
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const pathname = usePathname();
+
+ const navigation = [
+ { name: '每月课题', href: '/', icon: Home },
+ { name: '群相册', href: '/photos', icon: Images },
+ ];
+
+ const toggleMobileMenu = () => {
+ setMobileMenuOpen(!mobileMenuOpen);
+ };
+
+ const closeMobileMenu = () => {
+ setMobileMenuOpen(false);
+ };
+
+ return (
+ <>
+ {/* 桌面端导航栏 */}
+
+
+ {/* 移动端顶部栏 */}
+
+
+
+
+
+
+
+
+
+
+ 漳州太鼓
+
+
+
+
+
+
+
+
+ {/* 移动端菜单遮罩 */}
+ {mobileMenuOpen && (
+
+ )}
+
+ {/* 移动端侧边栏菜单 */}
+
+
+
+
+ 每月课题
+
+
+
+
+ {/* 移动端导航链接 */}
+
+
+ {/* 移动端主题切换 */}
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/components/PhotoDetailModal.tsx b/app/components/PhotoDetailModal.tsx
new file mode 100644
index 0000000..e85a166
--- /dev/null
+++ b/app/components/PhotoDetailModal.tsx
@@ -0,0 +1,165 @@
+'use client';
+
+import { X, Info, Calendar, File, Link as LinkIcon, Hash } from 'lucide-react';
+import { Photo } from '@/app/types';
+import { formatDate, formatFileSize } from '@/app/lib/utils';
+
+interface PhotoDetailModalProps {
+ photo: Photo | null;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export default function PhotoDetailModal({ photo, isOpen, onClose }: PhotoDetailModalProps) {
+ if (!photo || !isOpen) return null;
+
+ return (
+ <>
+ {/* 遮罩层 */}
+
+
+ {/* 右侧抽屉 */}
+
+
+ {/* 抽屉头部 */}
+
+
+ {/* 抽屉内容 */}
+
+ {/* 照片标题 */}
+
+
+ {photo.title}
+
+
+ 点击照片可查看大图
+
+
+
+ {/* 照片预览 */}
+
+
+ {/* 详细信息 */}
+
+ {/* 上传时间 */}
+
+
+
+
+ 上传时间
+
+
+
+ {formatDate(photo.uploadedAt)}
+
+
+
+ {/* 文件信息 */}
+
+
+
+
+ 文件信息
+
+
+
+
+ 文件名:{' '}
+ {photo.originalFilename || '未命名文件'}
+
+
+ 文件大小:{' '}
+ {formatFileSize(photo.size)}
+
+
+ 格式:{' '}
+ {photo.contentType || 'image/jpeg'}
+
+
+
+
+ {/* 图片链接 */}
+
+
+ {/* 唯一标识 */}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/components/PhotoGallery.tsx b/app/components/PhotoGallery.tsx
new file mode 100644
index 0000000..39602da
--- /dev/null
+++ b/app/components/PhotoGallery.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import Image from 'next/image';
+import { Loader2, Calendar, Image as ImageIcon, Info } from 'lucide-react';
+import { Photo, Pagination } from '@/app/types';
+import { formatDate, distributeToColumns, getColumnCount } from '@/app/lib/utils';
+
+interface PhotoGalleryProps {
+ photos: Photo[];
+ loading: boolean;
+ loadingMore: boolean;
+ pagination: Pagination;
+ selectedPhoto: Photo | null;
+ onPhotoClick: (photo: Photo) => void;
+ loadMoreRef: React.RefObject;
+}
+
+export default function PhotoGallery({
+ photos,
+ loading,
+ loadingMore,
+ pagination,
+ selectedPhoto,
+ onPhotoClick,
+ loadMoreRef,
+}: PhotoGalleryProps) {
+ // 计算瀑布流列数
+ const columnCount = getColumnCount();
+ const columns = distributeToColumns(photos, columnCount);
+
+ return (
+
+ {/* 加载状态 */}
+ {loading && (
+
+
+ 加载照片中...
+
+ )}
+
+ {/* 照片网格 */}
+ {!loading && photos.length > 0 && (
+
+
+ {columns.map((column, columnIndex) => (
+
+ {column.map((photo) => (
+
+ {/* 图片容器 */}
+
onPhotoClick(photo)}
+ >
+
+
+
+ {/* 移除右上角信息按钮,现在在大图模态框中显示 */}
+
+
+ {/* 简化的图片信息 - 只在hover时显示 */}
+
+
+ {photo.title}
+
+
+
+ {formatDate(photo.uploadedAt)}
+
+
+
+ ))}
+
+ ))}
+
+
+ {/* 加载更多指示器 */}
+ {pagination.hasNextPage && (
+
+ {loadingMore ? (
+
+
+ 加载更多照片...
+
+ ) : (
+
滚动加载更多
+ )}
+
+ )}
+
+ )}
+
+ {/* 空状态 */}
+ {!loading && photos.length === 0 && (
+
+
+
+
+
+ 暂无照片
+
+
+ 还没有上传任何照片。
+
+
+ )}
+
+ );
+}
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
new file mode 100644
index 0000000..1a97584
--- /dev/null
+++ b/app/components/Sidebar.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { Search, Moon, Sun, Calendar, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-react';
+import { Pagination } from '@/app/types';
+
+interface SidebarProps {
+ darkMode: boolean;
+ sidebarCollapsed: boolean;
+ searchQuery: string;
+ sortBy: string;
+ pagination: Pagination;
+ loading: boolean;
+ onToggleDarkMode: () => void;
+ onToggleSidebar: () => void;
+ onSearchChange: (query: string) => void;
+ onSortChange: (sortBy: string) => void;
+ onSearchSubmit: (e: React.FormEvent) => void;
+}
+
+export default function Sidebar({
+ darkMode,
+ sidebarCollapsed,
+ searchQuery,
+ sortBy,
+ pagination,
+ loading,
+ onToggleDarkMode,
+ onToggleSidebar,
+ onSearchChange,
+ onSortChange,
+ onSearchSubmit,
+}: SidebarProps) {
+ const sidebarContent = (
+ <>
+ {/* 搜索表单 */}
+
+
+ {/* 排序选项 */}
+
+ {!sidebarCollapsed && (
+
+ )}
+
+
+
+ >
+ );
+
+ return (
+ <>
+ {/* 桌面端侧边栏 */}
+
+ >
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index f7fa87e..30241f3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "漳州太鼓",
+ description: "课题与群相册",
};
export default function RootLayout({
@@ -23,7 +23,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
diff --git a/app/lib/api.ts b/app/lib/api.ts
new file mode 100644
index 0000000..b450662
--- /dev/null
+++ b/app/lib/api.ts
@@ -0,0 +1,62 @@
+import { Photo, Pagination, PhotoFetchParams } from '@/app/types';
+
+/**
+ * 获取照片数据
+ * @param params 获取参数
+ * @returns 照片数据和分页信息
+ */
+export async function fetchPhotos(params: PhotoFetchParams = {}): Promise<{
+ photos: Photo[];
+ pagination: Pagination;
+}> {
+ const {
+ page = 1,
+ limit = 20,
+ search = '',
+ sort = 'newest'
+ } = params;
+
+ try {
+ const queryParams = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ sort,
+ ...(search && { search }),
+ });
+
+ const response = await fetch(`/api/photos?${queryParams}`);
+ const data = await response.json();
+
+ if (data.success) {
+ return {
+ photos: data.data.photos,
+ pagination: data.data.pagination,
+ };
+ } else {
+ throw new Error(data.error || '获取照片失败');
+ }
+ } catch (error) {
+ console.error('Error fetching photos:', error);
+ throw error;
+ }
+}
+
+/**
+ * 获取所有照片(不分页)
+ * @returns 所有照片数组
+ */
+export async function fetchAllPhotos(): Promise {
+ try {
+ const response = await fetch('/api/photos?limit=1000');
+ const data = await response.json();
+
+ if (data.success) {
+ return data.data.photos;
+ } else {
+ throw new Error(data.error || '获取照片失败');
+ }
+ } catch (error) {
+ console.error('Error fetching all photos:', error);
+ throw error;
+ }
+}
diff --git a/app/lib/utils.ts b/app/lib/utils.ts
new file mode 100644
index 0000000..6df02f1
--- /dev/null
+++ b/app/lib/utils.ts
@@ -0,0 +1,53 @@
+/**
+ * 格式化日期
+ * @param dateString ISO日期字符串
+ * @returns 格式化的日期字符串
+ */
+export function formatDate(dateString: string): string {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+}
+
+/**
+ * 将照片分配到瀑布流列中
+ * @param photos 照片数组
+ * @param columnCount 列数
+ * @returns 分配到各列的照片数组
+ */
+export function distributeToColumns(items: T[], columnCount: number): T[][] {
+ const columns: T[][] = Array.from({ length: columnCount }, () => []);
+ items.forEach((item, index) => {
+ columns[index % columnCount].push(item);
+ });
+ return columns;
+}
+
+/**
+ * 根据窗口宽度计算瀑布流列数
+ * @returns 列数
+ */
+export function getColumnCount(): number {
+ if (typeof window === 'undefined') return 4;
+ if (window.innerWidth < 640) return 2;
+ if (window.innerWidth < 1024) return 3;
+ return 4;
+}
+
+/**
+ * 格式化文件大小
+ * @param bytes 字节数
+ * @returns 格式化的文件大小字符串
+ */
+export function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+}
diff --git a/app/page.tsx b/app/page.tsx
index c73c919..0fe9b03 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,137 +1,11 @@
'use client';
-import { useState, useEffect, useCallback, useRef } from 'react';
-import Image from 'next/image';
-import { Search, Loader2, Moon, Sun, Calendar, Image as ImageIcon } from 'lucide-react';
+import { useState } from 'react';
+import Navbar from '@/app/components/Navbar';
+import { BookOpen, Users, Calendar, BarChart, FileText, Settings, ChevronRight, Plus, CheckCircle, Clock, AlertCircle } from 'lucide-react';
-// 照片类型定义
-interface Photo {
- id: string;
- url: string;
- title: string;
- uploadedAt: string;
- size: number;
- contentType: string;
- originalFilename?: string;
- imageUrl: string;
-}
-
-// 分页信息类型
-interface Pagination {
- page: number;
- limit: number;
- total: number;
- totalPages: number;
- hasNextPage: boolean;
- hasPrevPage: boolean;
-}
-
-export default function Home() {
- const [photos, setPhotos] = useState([]);
- const [loading, setLoading] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
- const [searchQuery, setSearchQuery] = useState('');
- const [sortBy, setSortBy] = useState('newest');
+export default function HomePage() {
const [darkMode, setDarkMode] = useState(false);
- const [pagination, setPagination] = useState({
- page: 1,
- limit: 20,
- total: 0,
- totalPages: 0,
- hasNextPage: false,
- hasPrevPage: false,
- });
- const [selectedPhoto, setSelectedPhoto] = useState(null);
-
- const observerRef = useRef(null);
- const loadMoreRef = useRef(null);
-
- // 获取照片数据
- const fetchPhotos = useCallback(async (page = 1, isLoadMore = false) => {
- if (isLoadMore) {
- setLoadingMore(true);
- } else {
- setLoading(true);
- }
-
- try {
- const params = new URLSearchParams({
- page: page.toString(),
- limit: '20',
- sort: sortBy,
- ...(searchQuery && { search: searchQuery }),
- });
-
- const response = await fetch(`/api/photos?${params}`);
- const data = await response.json();
-
- if (data.success) {
- if (isLoadMore) {
- setPhotos(prev => [...prev, ...data.data.photos]);
- } else {
- setPhotos(data.data.photos);
- }
- setPagination(data.data.pagination);
- }
- } catch (error) {
- console.error('Error fetching photos:', error);
- } finally {
- setLoading(false);
- setLoadingMore(false);
- }
- }, [searchQuery, sortBy]);
-
- // 初始加载和搜索/排序变化时重新加载
- useEffect(() => {
- fetchPhotos(1, false);
- }, [fetchPhotos]);
-
- // 无限滚动加载
- useEffect(() => {
- if (!pagination.hasNextPage || loadingMore) return;
-
- observerRef.current = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting) {
- fetchPhotos(pagination.page + 1, true);
- }
- },
- { threshold: 0.5 }
- );
-
- if (loadMoreRef.current) {
- observerRef.current.observe(loadMoreRef.current);
- }
-
- return () => {
- if (observerRef.current) {
- observerRef.current.disconnect();
- }
- };
- }, [pagination, loadingMore, fetchPhotos]);
-
- // 处理搜索
- const handleSearch = useCallback((e: React.FormEvent) => {
- e.preventDefault();
- fetchPhotos(1, false);
- }, [fetchPhotos]);
-
- // 处理排序变化
- const handleSortChange = useCallback((e: React.ChangeEvent) => {
- setSortBy(e.target.value);
- }, []);
-
- // 格式化日期
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('zh-CN', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- });
- };
// 切换暗色模式
const toggleDarkMode = () => {
@@ -143,253 +17,16 @@ export default function Home() {
}
};
- // 计算瀑布流列数
- const getColumnCount = () => {
- if (typeof window === 'undefined') return 4;
- if (window.innerWidth < 640) return 2;
- if (window.innerWidth < 1024) return 3;
- return 4;
- };
-
- // 将照片分配到瀑布流列中
- const distributeToColumns = (photos: Photo[], columnCount: number): Photo[][] => {
- const columns: Photo[][] = Array.from({ length: columnCount }, () => []);
- photos.forEach((photo, index) => {
- columns[index % columnCount].push(photo);
- });
- return columns;
- };
-
- const columnCount = getColumnCount();
- const columns = distributeToColumns(photos, columnCount);
return (
- {/* 顶部导航栏 */}
-
-
-
-
-
- 照片展示系统
-
-
-
+ {/* 导航栏 */}
+
-
-
+ {/* 主内容 */}
+
-
-
-
-
-
-
-
- {/* 加载状态 */}
- {loading && (
-
-
- 加载照片中...
-
- )}
-
- {/* 照片网格 */}
- {!loading && photos.length > 0 && (
- <>
-
-
- 共 {pagination.total} 张照片 • 第 {pagination.page}/{pagination.totalPages} 页
-
-
-
-
- {columns.map((column, columnIndex) => (
-
- {column.map((photo) => (
-
setSelectedPhoto(photo)}
- >
- {/* 图片容器 */}
-
-
- {/* 图片信息 */}
-
-
- {photo.title}
-
-
-
-
- {formatDate(photo.uploadedAt)}
-
-
-
- {(photo.size / 1024 / 1024).toFixed(2)} MB
-
-
-
-
- ))}
-
- ))}
-
-
- {/* 加载更多指示器 */}
- {pagination.hasNextPage && (
-
- {loadingMore ? (
-
-
- 加载更多照片...
-
- ) : (
-
滚动加载更多
- )}
-
- )}
- >
- )}
-
- {/* 空状态 */}
- {!loading && photos.length === 0 && (
-
-
-
-
-
- 暂无照片
-
-
- {searchQuery ? '没有找到匹配的照片,请尝试其他搜索词。' : '还没有上传任何照片。'}
-
-
- )}
-
- {/* 照片详情模态框 */}
- {selectedPhoto && (
-
-
-
-
-
- {/* 图片区域 */}
-
-
-
-
- {/* 信息区域 */}
-
-
- {selectedPhoto.title}
-
-
-
-
-
- 上传时间
-
-
- {formatDate(selectedPhoto.uploadedAt)}
-
-
-
-
-
- 文件信息
-
-
- {selectedPhoto.originalFilename || '未命名文件'} • {(selectedPhoto.size / 1024 / 1024).toFixed(2)} MB
-
-
-
-
-
-
-
- 唯一标识
-
-
- {selectedPhoto.id}
-
-
-
-
-
-
-
- )}
);
}
diff --git a/app/photos/page.tsx b/app/photos/page.tsx
new file mode 100644
index 0000000..17198fc
--- /dev/null
+++ b/app/photos/page.tsx
@@ -0,0 +1,260 @@
+'use client';
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import Navbar from '@/app/components/Navbar';
+import Sidebar from '@/app/components/Sidebar';
+import PhotoGallery from '@/app/components/PhotoGallery';
+import PhotoDetailModal from '@/app/components/PhotoDetailModal';
+import ImageModal from '@/app/components/ImageModal';
+import { Photo, Pagination } from '@/app/types';
+import { fetchPhotos } from '@/app/lib/api';
+
+export default function PhotosPage() {
+ const [photos, setPhotos] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortBy, setSortBy] = useState('newest');
+ const [darkMode, setDarkMode] = useState(false);
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [pagination, setPagination] = useState({
+ page: 1,
+ limit: 20,
+ total: 0,
+ totalPages: 0,
+ hasNextPage: false,
+ hasPrevPage: false,
+ });
+ const [selectedPhoto, setSelectedPhoto] = useState(null);
+ const [detailDrawerOpen, setDetailDrawerOpen] = useState(false);
+ const [imageModalOpen, setImageModalOpen] = useState(false);
+
+ const observerRef = useRef(null);
+ const loadMoreRef = useRef(null);
+
+ // 获取照片数据
+ const fetchPhotosData = useCallback(async (page = 1, isLoadMore = false) => {
+ if (isLoadMore) {
+ setLoadingMore(true);
+ } else {
+ setLoading(true);
+ }
+
+ try {
+ const data = await fetchPhotos({
+ page,
+ limit: 20,
+ search: searchQuery,
+ sort: sortBy as 'newest' | 'oldest' | 'title',
+ });
+
+ if (isLoadMore) {
+ setPhotos(prev => [...prev, ...data.photos]);
+ } else {
+ setPhotos(data.photos);
+ }
+ setPagination(data.pagination);
+ } catch (error) {
+ console.error('Error fetching photos:', error);
+ } finally {
+ setLoading(false);
+ setLoadingMore(false);
+ }
+ }, [searchQuery, sortBy]);
+
+ // 初始加载和搜索/排序变化时重新加载
+ useEffect(() => {
+ fetchPhotosData(1, false);
+ }, [fetchPhotosData]);
+
+ // 无限滚动加载
+ useEffect(() => {
+ if (!pagination.hasNextPage || loadingMore) return;
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting) {
+ fetchPhotosData(pagination.page + 1, true);
+ }
+ },
+ { threshold: 0.5 }
+ );
+
+ if (loadMoreRef.current) {
+ observerRef.current.observe(loadMoreRef.current);
+ }
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ }
+ };
+ }, [pagination, loadingMore, fetchPhotosData]);
+
+ // 处理搜索
+ const handleSearch = useCallback((e: React.FormEvent) => {
+ e.preventDefault();
+ fetchPhotosData(1, false);
+ }, [fetchPhotosData]);
+
+ // 处理排序变化
+ const handleSortChange = useCallback((value: string) => {
+ setSortBy(value);
+ }, []);
+
+ // 切换暗色模式
+ const toggleDarkMode = () => {
+ setDarkMode(!darkMode);
+ if (!darkMode) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ };
+
+ // 切换侧边栏收缩状态
+ const toggleSidebar = () => {
+ setSidebarCollapsed(!sidebarCollapsed);
+ };
+
+ // 切换移动端菜单
+ const toggleMobileMenu = () => {
+ setMobileMenuOpen(!mobileMenuOpen);
+ };
+
+ // 关闭移动端菜单
+ const closeMobileMenu = () => {
+ setMobileMenuOpen(false);
+ };
+
+ // 打开图片模态框(点击卡片时)
+ const openImageModal = (photo: Photo) => {
+ setSelectedPhoto(photo);
+ setImageModalOpen(true);
+ };
+
+ // 关闭图片模态框
+ const closeImageModal = () => {
+ setImageModalOpen(false);
+ };
+
+ // 打开照片详情抽屉(从图片模态框的info按钮)
+ const openDetailDrawer = () => {
+ setDetailDrawerOpen(true);
+ // 保持图片模态框打开状态
+ };
+
+ // 关闭照片详情抽屉
+ const closePhotoDetail = () => {
+ setDetailDrawerOpen(false);
+ // 不清除选中的照片,以便图片模态框保持打开状态
+ };
+
+ // 完全关闭所有模态框和抽屉
+ const closeAllModals = () => {
+ setImageModalOpen(false);
+ setDetailDrawerOpen(false);
+ // 延迟清除选中的照片
+ setTimeout(() => setSelectedPhoto(null), 300);
+ };
+
+ return (
+
+ {/* 导航栏 */}
+
+
+ {/* 移动端菜单遮罩 */}
+ {mobileMenuOpen && (
+
+ )}
+
+ {/* 移动端侧边栏菜单 */}
+
+
+
+
+ 照片展示系统
+
+
+
+ {/* 移动端侧边栏内容 */}
+
+
+
+
+ {/* 桌面端布局 */}
+
+ {/* 桌面端侧边栏 */}
+
+
+ {/* 主内容区域 */}
+
+
+
+
+ {/* 图片模态框 */}
+
+
+ {/* 照片详情抽屉 */}
+
+
+
+ );
+}
diff --git a/app/types/index.ts b/app/types/index.ts
new file mode 100644
index 0000000..cec799f
--- /dev/null
+++ b/app/types/index.ts
@@ -0,0 +1,29 @@
+// 照片类型定义
+export interface Photo {
+ id: string;
+ url: string;
+ title: string;
+ uploadedAt: string;
+ size: number;
+ contentType: string;
+ originalFilename?: string;
+ imageUrl: string;
+}
+
+// 分页信息类型
+export interface Pagination {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ hasNextPage: boolean;
+ hasPrevPage: boolean;
+}
+
+// 照片获取参数类型
+export interface PhotoFetchParams {
+ page?: number;
+ limit?: number;
+ search?: string;
+ sort?: 'newest' | 'oldest' | 'title';
+}