From 7d41f8f5a5719ce35156907b38aaf7e0b10b968e Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 27 Nov 2025 21:42:05 +0800 Subject: [PATCH] feat: add Gallery component for image display and navigation --- frontend/src/components/gallery.tsx | 367 +++++++++++++++++++ frontend/src/pages/resource_details_page.tsx | 345 +---------------- 2 files changed, 368 insertions(+), 344 deletions(-) create mode 100644 frontend/src/components/gallery.tsx diff --git a/frontend/src/components/gallery.tsx b/frontend/src/components/gallery.tsx new file mode 100644 index 0000000..fe2218b --- /dev/null +++ b/frontend/src/components/gallery.tsx @@ -0,0 +1,367 @@ +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { + MdOutlineChevronLeft, + MdOutlineChevronRight, + MdOutlineClose, +} from "react-icons/md"; +import { network } from "../network/network.ts"; +import Badge from "./badge.tsx"; + +export default function Gallery({ + images, + nsfw, +}: { + images: number[]; + nsfw: number[]; +}) { + const [currentIndex, setCurrentIndex] = useState(0); + const [direction, setDirection] = useState(0); // 方向:1=向右,-1=向左 + const [isHovered, setIsHovered] = useState(false); + const [width, setWidth] = useState(0); + const containerRef = useRef(null); + const dialogRef = useRef(null); + + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setWidth(containerRef.current.clientWidth); + } + }; + + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => { + window.removeEventListener("resize", updateWidth); + }; + }, []); + + // 预加载下一张图片 + useEffect(() => { + if (!images || images.length <= 1) return; + + const nextIndex = (currentIndex + 1) % images.length; + const nextImageUrl = network.getImageUrl(images[nextIndex]); + + const img = new Image(); + img.src = nextImageUrl; + }, [currentIndex, images]); + + if (!images || images.length === 0) { + return <>; + } + + const goToPrevious = () => { + setDirection(-1); + setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); + }; + + const goToNext = () => { + setDirection(1); + setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); + }; + + const goToIndex = (index: number) => { + setDirection(index > currentIndex ? 1 : -1); + setCurrentIndex(index); + }; + + if (nsfw == null) { + nsfw = []; + } + + // 如果图片数量超过8张,显示数字而不是圆点 + const showDots = images.length <= 8; + + return ( + <> + +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* 图片区域 */} +
{ + dialogRef.current?.showModal(); + }} + > + {width > 0 && ( + + ({ + x: dir > 0 ? width : -width, + }), + center: { + x: 0, + transition: { duration: 0.3, ease: "linear" }, + }, + exit: (dir: number) => ({ + x: dir > 0 ? -width : width, + transition: { duration: 0.3, ease: "linear" }, + }), + }} + initial="enter" + animate="center" + exit="exit" + custom={direction} + > + + + + )} +
+ + {/* 左右按钮 */} + {images.length > 1 && ( + <> + + + + )} + + {/* 底部指示器 */} + {images.length > 1 && ( +
+ {showDots ? ( + /* 圆点指示器 */ +
+ {images.map((_, index) => ( +
+ ) : ( + /* 数字指示器 */ +
+ {currentIndex + 1} / {images.length} +
+ )} +
+ )} +
+ + ); +} + +function GalleryFullscreen({ + dialogRef, + images, + currentIndex, + direction, + isHovered, + setIsHovered, + goToPrevious, + goToNext, +}: { + dialogRef: React.RefObject; + images: number[]; + currentIndex: number; + direction: number; + isHovered: boolean; + setIsHovered: (hovered: boolean) => void; + goToPrevious: () => void; + goToNext: () => void; +}) { + const [width, setWidth] = useState(0); + const containerRef = useRef(null); + + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + console.log(containerRef.current.clientWidth); + setWidth(containerRef.current.clientWidth); + } + }; + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => { + window.removeEventListener("resize", updateWidth); + }; + }, []); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (dialogRef.current?.open) { + if (e.key === "ArrowLeft") { + e.preventDefault(); + goToPrevious(); + } else if (e.key === "ArrowRight") { + e.preventDefault(); + goToNext(); + } else if (e.key === "Escape") { + dialogRef.current?.close(); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [dialogRef, goToPrevious, goToNext]); + + return ( + { + dialogRef.current?.close(); + }} + className="modal" + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ {width > 0 && ( + + ({ + x: dir > 0 ? width : -width, + }), + center: { + x: 0, + transition: { duration: 0.3, ease: "linear" }, + }, + exit: (dir: number) => ({ + x: dir > 0 ? -width : width, + transition: { duration: 0.3, ease: "linear" }, + }), + }} + initial="enter" + animate="center" + exit="exit" + custom={direction} + > + + + + )} + + {/* 全屏模式下的左右切换按钮 */} + {images.length > 1 && ( + <> + + + + {/* 全屏模式下的指示器 */} +
+
+ {currentIndex + 1} / {images.length} +
+
+ + {/* 关闭按钮 */} + + + )} +
+
+ ); +} + +function GalleryImage({ src, nfsw }: { src: string; nfsw: boolean }) { + const [show, setShow] = useState(!nfsw); + + return ( +
+ + {!show && ( + <> +
{ + setShow(true); + event.stopPropagation(); + }} + /> +
+ NSFW +
+ + )} +
+ ); +} diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index d28a8ff..002315f 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -32,9 +32,6 @@ import { MdOutlineAdd, MdOutlineArchive, MdOutlineArticle, - MdOutlineChevronLeft, - MdOutlineChevronRight, - MdOutlineClose, MdOutlineCloud, MdOutlineComment, MdOutlineContentCopy, @@ -71,7 +68,7 @@ import KunApi, { } from "../network/kun.ts"; import { Debounce } from "../utils/debounce.ts"; import remarkGfm from "remark-gfm"; -import { AnimatePresence, motion } from "framer-motion"; +import Gallery from "../components/gallery.tsx"; export default function ResourcePage() { const params = useParams(); @@ -2050,346 +2047,6 @@ function CollectionSelector({ ); } -function Gallery({ images, nsfw }: { images: number[], nsfw: number[] }) { - const [currentIndex, setCurrentIndex] = useState(0); - const [direction, setDirection] = useState(0); // 方向:1=向右,-1=向左 - const [isHovered, setIsHovered] = useState(false); - const [width, setWidth] = useState(0); - const containerRef = useRef(null); - const dialogRef = useRef(null); - - useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - setWidth(containerRef.current.clientWidth); - } - }; - - updateWidth(); - window.addEventListener("resize", updateWidth); - return () => { - window.removeEventListener("resize", updateWidth); - }; - }, []); - - // 预加载下一张图片 - useEffect(() => { - if (!images || images.length <= 1) return; - - const nextIndex = (currentIndex + 1) % images.length; - const nextImageUrl = network.getImageUrl(images[nextIndex]); - - const img = new Image(); - img.src = nextImageUrl; - }, [currentIndex, images]); - - if (!images || images.length === 0) { - return <>; - } - - const goToPrevious = () => { - setDirection(-1); - setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); - }; - - const goToNext = () => { - setDirection(1); - setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); - }; - - const goToIndex = (index: number) => { - setDirection(index > currentIndex ? 1 : -1); - setCurrentIndex(index); - }; - - if (nsfw == null) { - nsfw = []; - } - - // 如果图片数量超过8张,显示数字而不是圆点 - const showDots = images.length <= 8; - - return ( - <> - -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {/* 图片区域 */} -
{ - dialogRef.current?.showModal(); - }}> - {width > 0 && ( - - ({ - x: dir > 0 ? width : -width, - }), - center: { - x: 0, - transition: { duration: 0.3, ease: "linear" }, - }, - exit: (dir: number) => ({ - x: dir > 0 ? -width : width, - transition: { duration: 0.3, ease: "linear" }, - }), - }} - initial="enter" - animate="center" - exit="exit" - custom={direction} - > - - - - )} -
- - {/* 左右按钮 */} - {images.length > 1 && ( - <> - - - - )} - - {/* 底部指示器 */} - {images.length > 1 && ( -
- {showDots ? ( - /* 圆点指示器 */ -
- {images.map((_, index) => ( -
- ) : ( - /* 数字指示器 */ -
- {currentIndex + 1} / {images.length} -
- )} -
- )} -
- - ); -} - -function GalleryFullscreen({ - dialogRef, - images, - currentIndex, - direction, - isHovered, - setIsHovered, - goToPrevious, - goToNext, -}: { - dialogRef: React.RefObject; - images: number[]; - currentIndex: number; - direction: number; - isHovered: boolean; - setIsHovered: (hovered: boolean) => void; - goToPrevious: () => void; - goToNext: () => void; -}) { - const [width, setWidth] = useState(0); - const containerRef = useRef(null); - - useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - console.log(containerRef.current.clientWidth); - setWidth(containerRef.current.clientWidth); - } - }; - updateWidth(); - window.addEventListener("resize", updateWidth); - return () => { - window.removeEventListener("resize", updateWidth); - }; - }, []); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (dialogRef.current?.open) { - if (e.key === "ArrowLeft") { - e.preventDefault(); - goToPrevious(); - } else if (e.key === "ArrowRight") { - e.preventDefault(); - goToNext(); - } else if (e.key === "Escape") { - dialogRef.current?.close(); - } - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [dialogRef, goToPrevious, goToNext]); - - return ( - { - dialogRef.current?.close(); - }} - className="modal" - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
- {width > 0 && - ({ - x: dir > 0 ? width : -width, - }), - center: { - x: 0, - transition: { duration: 0.3, ease: "linear" }, - }, - exit: (dir: number) => ({ - x: dir > 0 ? -width : width, - transition: { duration: 0.3, ease: "linear" }, - }), - }} - initial="enter" - animate="center" - exit="exit" - custom={direction} - > - - - } - - {/* 全屏模式下的左右切换按钮 */} - {images.length > 1 && ( - <> - - - - {/* 全屏模式下的指示器 */} -
-
- {currentIndex + 1} / {images.length} -
-
- - {/* 关闭按钮 */} - - - )} -
-
- ); -} - -function GalleryImage({src, nfsw}: {src: string, nfsw: boolean}) { - const [show, setShow] = useState(!nfsw); - - return ( -
- - {!show && ( - <> -
{ - setShow(true) - event.stopPropagation(); - }} /> -
- - NSFW - -
- - )} -
- ); -} - function Characters({ characters }: { characters: CharacterParams[] }) { const { t } = useTranslation();