diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index 3a82de4..83618c4 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -47,6 +47,7 @@ export interface CreateResourceParams { tags: number[]; article: string; images: number[]; + gallery: number[]; } export interface Image { @@ -84,6 +85,7 @@ export interface ResourceDetails { downloads: number; comments: number; related: Resource[]; + gallery: number[]; } export interface Storage { diff --git a/frontend/src/pages/edit_resource_page.tsx b/frontend/src/pages/edit_resource_page.tsx index cf90622..9563ae7 100644 --- a/frontend/src/pages/edit_resource_page.tsx +++ b/frontend/src/pages/edit_resource_page.tsx @@ -28,6 +28,7 @@ export default function EditResourcePage() { const [article, setArticle] = useState(""); const [images, setImages] = useState([]); const [links, setLinks] = useState<{ label: string; url: string }[]>([]); + const [galleryImages, setGalleryImages] = useState([]); const [error, setError] = useState(null); const [isSubmitting, setSubmitting] = useState(false); const [isLoading, setLoading] = useState(true); @@ -55,6 +56,7 @@ export default function EditResourcePage() { setArticle(data.article); setImages(data.images.map((i) => i.id)); setLinks(data.links ?? []); + setGalleryImages(data.gallery ?? []); setLoading(false); } else { showToast({ message: t("Failed to load resource"), type: "error" }); @@ -98,6 +100,7 @@ export default function EditResourcePage() { article: article, images: images, links: links, + gallery: galleryImages, }); if (res.success) { setSubmitting(false); @@ -318,6 +321,7 @@ export default function EditResourcePage() { {t("Preview")} {"Markdown"} + {t("Gallery")} {t("Action")} @@ -345,6 +349,22 @@ export default function EditResourcePage() { + + { + if (e.target.checked) { + setGalleryImages((prev) => [...prev, image]); + } else { + setGalleryImages((prev) => + prev.filter((id) => id !== image), + ); + } + }} + /> + + + { + if (e.target.checked) { + setGalleryImages((prev) => [...prev, image]); + } else { + setGalleryImages((prev) => + prev.filter((id) => id !== image), + ); + } + }} + /> + - + + +
+ +
+ + +
+ +
{resource.links && @@ -1860,3 +1875,124 @@ function CollectionSelector({
); } + +function Gallery({ images }: { images: 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); + + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setWidth(containerRef.current.clientWidth); + } + }; + + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => { + window.removeEventListener("resize", updateWidth); + }; + }, []); + + 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); + }; + + return ( +
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 && ( + <> + + + + )} + + {/* 底部圆点 */} + {images.length > 1 && ( +
+ {images.map((_, index) => ( +
+ )} +
+ ); +} diff --git a/server/model/resource.go b/server/model/resource.go index 2aaa6cd..0f453bd 100644 --- a/server/model/resource.go +++ b/server/model/resource.go @@ -21,6 +21,7 @@ type Resource struct { Downloads uint Comments uint ModifiedTime time.Time + Gallery []uint `gorm:"serializer:json"` } type Link struct { @@ -52,6 +53,7 @@ type ResourceDetailView struct { Downloads uint `json:"downloads"` Comments uint `json:"comments"` Related []ResourceView `json:"related"` + Gallery []uint `json:"gallery"` } func (r *Resource) ToView() ResourceView { @@ -104,5 +106,6 @@ func (r *Resource) ToDetailView() ResourceDetailView { Views: r.Views, Downloads: r.Downloads, Comments: r.Comments, + Gallery: r.Gallery, } } diff --git a/server/service/resource.go b/server/service/resource.go index ea13c93..5010c25 100644 --- a/server/service/resource.go +++ b/server/service/resource.go @@ -27,6 +27,7 @@ type ResourceParams struct { Tags []uint `json:"tags"` Article string `json:"article"` Images []uint `json:"images"` + Gallery []uint `json:"gallery"` } func CreateResource(uid uint, params *ResourceParams) (uint, error) { @@ -62,6 +63,7 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) { Images: images, Tags: tags, UserID: uid, + Gallery: params.Gallery, } if r, err = dao.CreateResource(r); err != nil { return 0, err @@ -452,6 +454,7 @@ func EditResource(uid, rid uint, params *ResourceParams) error { r.AlternativeTitles = params.AlternativeTitles r.Article = params.Article r.Links = params.Links + r.Gallery = params.Gallery images := make([]model.Image, len(params.Images)) for i, id := range params.Images {