mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-16 15:51:14 +00:00
Add gallery
This commit is contained in:
@@ -47,6 +47,7 @@ export interface CreateResourceParams {
|
|||||||
tags: number[];
|
tags: number[];
|
||||||
article: string;
|
article: string;
|
||||||
images: number[];
|
images: number[];
|
||||||
|
gallery: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Image {
|
export interface Image {
|
||||||
@@ -84,6 +85,7 @@ export interface ResourceDetails {
|
|||||||
downloads: number;
|
downloads: number;
|
||||||
comments: number;
|
comments: number;
|
||||||
related: Resource[];
|
related: Resource[];
|
||||||
|
gallery: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default function EditResourcePage() {
|
|||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
|
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
@@ -55,6 +56,7 @@ export default function EditResourcePage() {
|
|||||||
setArticle(data.article);
|
setArticle(data.article);
|
||||||
setImages(data.images.map((i) => i.id));
|
setImages(data.images.map((i) => i.id));
|
||||||
setLinks(data.links ?? []);
|
setLinks(data.links ?? []);
|
||||||
|
setGalleryImages(data.gallery ?? []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
showToast({ message: t("Failed to load resource"), type: "error" });
|
showToast({ message: t("Failed to load resource"), type: "error" });
|
||||||
@@ -98,6 +100,7 @@ export default function EditResourcePage() {
|
|||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
links: links,
|
links: links,
|
||||||
|
gallery: galleryImages,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -318,6 +321,7 @@ export default function EditResourcePage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("Preview")}</td>
|
<td>{t("Preview")}</td>
|
||||||
<td>{"Markdown"}</td>
|
<td>{"Markdown"}</td>
|
||||||
|
<td>{t("Gallery")}</td>
|
||||||
<td>{t("Action")}</td>
|
<td>{t("Action")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -345,6 +349,22 @@ export default function EditResourcePage() {
|
|||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox checkbox-accent"
|
||||||
|
checked={galleryImages.includes(image)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setGalleryImages((prev) => [...prev, image]);
|
||||||
|
} else {
|
||||||
|
setGalleryImages((prev) =>
|
||||||
|
prev.filter((id) => id !== image),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className={"btn btn-square"}
|
className={"btn btn-square"}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function PublishPage() {
|
|||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
|
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ export default function PublishPage() {
|
|||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
links: links,
|
links: links,
|
||||||
|
gallery: galleryImages,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
localStorage.removeItem("publish_data");
|
localStorage.removeItem("publish_data");
|
||||||
@@ -329,6 +331,7 @@ export default function PublishPage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("Preview")}</td>
|
<td>{t("Preview")}</td>
|
||||||
<td>{"Markdown"}</td>
|
<td>{"Markdown"}</td>
|
||||||
|
<td>{"Gallery"}</td>
|
||||||
<td>{t("Action")}</td>
|
<td>{t("Action")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -356,6 +359,22 @@ export default function PublishPage() {
|
|||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox checkbox-accent"
|
||||||
|
checked={galleryImages.includes(image)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setGalleryImages((prev) => [...prev, image]);
|
||||||
|
} else {
|
||||||
|
setGalleryImages((prev) =>
|
||||||
|
prev.filter((id) => id !== image),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className={"btn btn-square"}
|
className={"btn btn-square"}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createRef,
|
createRef,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
use,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -29,6 +30,8 @@ import {
|
|||||||
MdOutlineAdd,
|
MdOutlineAdd,
|
||||||
MdOutlineArchive,
|
MdOutlineArchive,
|
||||||
MdOutlineArticle,
|
MdOutlineArticle,
|
||||||
|
MdOutlineChevronLeft,
|
||||||
|
MdOutlineChevronRight,
|
||||||
MdOutlineCloud,
|
MdOutlineCloud,
|
||||||
MdOutlineComment,
|
MdOutlineComment,
|
||||||
MdOutlineContentCopy,
|
MdOutlineContentCopy,
|
||||||
@@ -65,6 +68,7 @@ import KunApi, {
|
|||||||
} from "../network/kun.ts";
|
} from "../network/kun.ts";
|
||||||
import { Debounce } from "../utils/debounce.ts";
|
import { Debounce } from "../utils/debounce.ts";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
export default function ResourcePage() {
|
export default function ResourcePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -190,7 +194,9 @@ export default function ResourcePage() {
|
|||||||
return (
|
return (
|
||||||
<context.Provider value={reload}>
|
<context.Provider value={reload}>
|
||||||
<div className={"pt-2"}>
|
<div className={"pt-2"}>
|
||||||
<h1 className={"text-2xl font-bold px-4 py-2"}>{resource.title}</h1>
|
<div className="flex">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className={"text-2xl font-bold px-4 py-2"}>{resource.title}</h1>
|
||||||
{resource.alternativeTitles.map((e, i) => {
|
{resource.alternativeTitles.map((e, i) => {
|
||||||
return (
|
return (
|
||||||
<h2
|
<h2
|
||||||
@@ -220,7 +226,16 @@ export default function ResourcePage() {
|
|||||||
<div className="text-sm">{resource.author.username}</div>
|
<div className="text-sm">{resource.author.username}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<Tags tags={resource.tags} />
|
<Tags tags={resource.tags} />
|
||||||
|
</div>
|
||||||
|
<div className="w-md p-4 hidden sm:flex items-center justify-center">
|
||||||
|
<Gallery images={resource.gallery} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full p-4 flex sm:hidden items-center justify-center">
|
||||||
|
<Gallery images={resource.gallery} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={"px-3 mt-2 flex flex-wrap"}>
|
<div className={"px-3 mt-2 flex flex-wrap"}>
|
||||||
{resource.links &&
|
{resource.links &&
|
||||||
@@ -1860,3 +1875,124 @@ function CollectionSelector({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<div
|
||||||
|
className="relative w-full overflow-hidden rounded-xl bg-base-100-tr82 shadow-sm"
|
||||||
|
style={{ aspectRatio: "16/9" }}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{/* 图片区域 */}
|
||||||
|
<div ref={containerRef} className="w-full h-full relative">
|
||||||
|
{width > 0 && (
|
||||||
|
<AnimatePresence initial={false} custom={direction} mode="sync">
|
||||||
|
<motion.img
|
||||||
|
key={currentIndex}
|
||||||
|
src={network.getImageUrl(images[currentIndex])}
|
||||||
|
alt={`Gallery image ${currentIndex + 1}`}
|
||||||
|
className="absolute w-full h-full object-contain"
|
||||||
|
variants={{
|
||||||
|
enter: (dir: number) => ({
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 左右按钮 */}
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={`absolute left-2 top-1/2 -translate-y-1/2 transition-opacity hover:cursor-pointer ${
|
||||||
|
isHovered ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
onClick={goToPrevious}
|
||||||
|
>
|
||||||
|
<MdOutlineChevronLeft size={28} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`absolute right-2 top-1/2 -translate-y-1/2 transition-opacity hover:cursor-pointer ${
|
||||||
|
isHovered ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
onClick={goToNext}
|
||||||
|
>
|
||||||
|
<MdOutlineChevronRight size={28} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 底部圆点 */}
|
||||||
|
{images.length > 1 && (
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
|
{images.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
index === currentIndex
|
||||||
|
? "bg-primary w-4"
|
||||||
|
: "bg-base-content/30 hover:bg-base-content/50"
|
||||||
|
}`}
|
||||||
|
onClick={() => goToIndex(index)}
|
||||||
|
aria-label={`Go to image ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Resource struct {
|
|||||||
Downloads uint
|
Downloads uint
|
||||||
Comments uint
|
Comments uint
|
||||||
ModifiedTime time.Time
|
ModifiedTime time.Time
|
||||||
|
Gallery []uint `gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
@@ -52,6 +53,7 @@ type ResourceDetailView struct {
|
|||||||
Downloads uint `json:"downloads"`
|
Downloads uint `json:"downloads"`
|
||||||
Comments uint `json:"comments"`
|
Comments uint `json:"comments"`
|
||||||
Related []ResourceView `json:"related"`
|
Related []ResourceView `json:"related"`
|
||||||
|
Gallery []uint `json:"gallery"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) ToView() ResourceView {
|
func (r *Resource) ToView() ResourceView {
|
||||||
@@ -104,5 +106,6 @@ func (r *Resource) ToDetailView() ResourceDetailView {
|
|||||||
Views: r.Views,
|
Views: r.Views,
|
||||||
Downloads: r.Downloads,
|
Downloads: r.Downloads,
|
||||||
Comments: r.Comments,
|
Comments: r.Comments,
|
||||||
|
Gallery: r.Gallery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type ResourceParams struct {
|
|||||||
Tags []uint `json:"tags"`
|
Tags []uint `json:"tags"`
|
||||||
Article string `json:"article"`
|
Article string `json:"article"`
|
||||||
Images []uint `json:"images"`
|
Images []uint `json:"images"`
|
||||||
|
Gallery []uint `json:"gallery"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateResource(uid uint, params *ResourceParams) (uint, error) {
|
func CreateResource(uid uint, params *ResourceParams) (uint, error) {
|
||||||
@@ -62,6 +63,7 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) {
|
|||||||
Images: images,
|
Images: images,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
UserID: uid,
|
UserID: uid,
|
||||||
|
Gallery: params.Gallery,
|
||||||
}
|
}
|
||||||
if r, err = dao.CreateResource(r); err != nil {
|
if r, err = dao.CreateResource(r); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -452,6 +454,7 @@ func EditResource(uid, rid uint, params *ResourceParams) error {
|
|||||||
r.AlternativeTitles = params.AlternativeTitles
|
r.AlternativeTitles = params.AlternativeTitles
|
||||||
r.Article = params.Article
|
r.Article = params.Article
|
||||||
r.Links = params.Links
|
r.Links = params.Links
|
||||||
|
r.Gallery = params.Gallery
|
||||||
|
|
||||||
images := make([]model.Image, len(params.Images))
|
images := make([]model.Image, len(params.Images))
|
||||||
for i, id := range params.Images {
|
for i, id := range params.Images {
|
||||||
|
|||||||
Reference in New Issue
Block a user