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 && ( + + )} + + + + +
+ + {/* 图片容器 */} +
+
+ {photo.title} { + e.stopPropagation(); + // 移动端点击图片切换全屏 + if (isMobile) { + toggleFullscreen(); + } + }} + /> +
+
+ + {/* 底部信息栏 - 移动端全屏时隐藏 */} + {(!isMobile || !isFullscreen) && ( +
+
+

{photo.title}

+
+
+ )} +
+
+ + ); +} 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'} +

+
+
+ + {/* 图片链接 */} +
+
+ +

+ 图片链接 +

+
+ +
+ + {/* 唯一标识 */} +
+
+ +

+ 唯一标识 +

+
+
+

+ {photo.id} +

+
+
+
+
+ +
+
+ + ); +} 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)} + > + {photo.title} +
+ + {/* 移除右上角信息按钮,现在在大图模态框中显示 */} +
+ + {/* 简化的图片信息 - 只在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 = ( + <> + {/* 搜索表单 */} +
+
+ + onSearchChange(e.target.value)} + placeholder={sidebarCollapsed ? "搜索..." : "搜索标题或日期..."} + className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ {!sidebarCollapsed && ( + + )} +
+ + {/* 排序选项 */} +
+ {!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 (
- {/* 顶部导航栏 */} -
-
-
-
-

- 照片展示系统 -

- -
+ {/* 导航栏 */} + -
-
-
- - setSearchQuery(e.target.value)} - placeholder="搜索标题或日期..." - className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
- -
+ {/* 主内容 */} +
- -
-
-
-
- -
- {/* 加载状态 */} - {loading && ( -
- - 加载照片中... -
- )} - - {/* 照片网格 */} - {!loading && photos.length > 0 && ( - <> -
-

- 共 {pagination.total} 张照片 • 第 {pagination.page}/{pagination.totalPages} 页 -

-
- -
- {columns.map((column, columnIndex) => ( -
- {column.map((photo) => ( -
setSelectedPhoto(photo)} - > - {/* 图片容器 */} -
- {photo.title} -
-
- - {/* 图片信息 */} -
-

- {photo.title} -

-
-
- - {formatDate(photo.uploadedAt)} -
-
- - {(photo.size / 1024 / 1024).toFixed(2)} MB -
-
-
-
- ))} -
- ))} -
- - {/* 加载更多指示器 */} - {pagination.hasNextPage && ( -
- {loadingMore ? ( -
- - 加载更多照片... -
- ) : ( -

滚动加载更多

- )} -
- )} - - )} - - {/* 空状态 */} - {!loading && photos.length === 0 && ( -
-
- -
-

- 暂无照片 -

-

- {searchQuery ? '没有找到匹配的照片,请尝试其他搜索词。' : '还没有上传任何照片。'} -

-
- )}
- - {/* 照片详情模态框 */} - {selectedPhoto && ( -
-
- - -
- {/* 图片区域 */} -
- {selectedPhoto.title} -
- - {/* 信息区域 */} -
-

- {selectedPhoto.title} -

- -
-
-

- 上传时间 -

-

- {formatDate(selectedPhoto.uploadedAt)} -

-
- -
-

- 文件信息 -

-

- {selectedPhoto.originalFilename || '未命名文件'} • {(selectedPhoto.size / 1024 / 1024).toFixed(2)} MB -

-
- -
-

- 图片链接 -

- - {selectedPhoto.imageUrl} - -
- -
-

- 唯一标识 -

-

- {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'; +}