"use client"; import Link from "next/link"; import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; import galleryData from "../data/image-host.json"; import styles from "./ImageHost.module.css"; import cardStyles from "./ImageHostCard.module.css"; type ImageItem = { id: string; title: string; src: string; category: string; ratio: string; position: string; width: number; height: number; size: string; format: string; uploadedAt: string; views: number; tags: string[]; local?: boolean; }; type AlbumItem = { id: string; title: string; description: string; category: string; coverId: string; imageIds: string[]; updatedAt: string; tags: string[]; }; type DataShape = { categories: string[]; images: ImageItem[]; albums: AlbumItem[]; feed: { kind: "image" | "album"; id: string }[]; }; type GalleryItem = | { key: string; kind: "image"; image: ImageItem; albumId?: string } | { key: string; kind: "album"; album: AlbumItem; cover: ImageItem }; type ViewState = | { type: "gallery" } | { type: "album"; id: string } | { type: "image"; id: string; albumId?: string }; const data = galleryData as DataShape; export default function ImageHostPage() { const uploadRef = useRef(null); const previewTimerRef = useRef | null>(null); const localUrlsRef = useRef([]); const [view, setView] = useState({ type: "gallery" }); const [category, setCategory] = useState("全部"); const [query, setQuery] = useState(""); const [uploadedImages, setUploadedImages] = useState([]); const [quickPreview, setQuickPreview] = useState(null); const [armingKey, setArmingKey] = useState(null); const [toast, setToast] = useState(""); const allImages = useMemo(() => [...uploadedImages, ...data.images], [uploadedImages]); const imageMap = useMemo(() => new Map(allImages.map((image) => [image.id, image])), [allImages]); const feedItems = useMemo(() => { const uploaded: GalleryItem[] = uploadedImages.map((image) => ({ key: `image-${image.id}`, kind: "image", image })); const bundled = data.feed.flatMap((entry) => { if (entry.kind === "image") { const image = imageMap.get(entry.id); return image ? [{ key: `image-${image.id}`, kind: "image", image }] : []; } const album = data.albums.find((item) => item.id === entry.id); const cover = album ? imageMap.get(album.coverId) : undefined; return album && cover ? [{ key: `album-${album.id}`, kind: "album", album, cover }] : []; }); return [...uploaded, ...bundled]; }, [imageMap, uploadedImages]); const filteredItems = useMemo(() => feedItems.filter((item) => { const itemCategory = item.kind === "image" ? item.image.category : item.album.category; const searchable = item.kind === "image" ? `${item.image.title} ${item.image.tags.join(" ")} ${item.image.category}` : `${item.album.title} ${item.album.description} ${item.album.tags.join(" ")} ${item.album.category}`; return (category === "全部" || itemCategory === category) && searchable.toLowerCase().includes(query.trim().toLowerCase()); }), [feedItems, category, query]); const currentAlbum = view.type === "album" ? data.albums.find((album) => album.id === view.id) ?? null : null; const albumImages = currentAlbum ? currentAlbum.imageIds.map((id) => imageMap.get(id)).filter((image): image is ImageItem => Boolean(image)) : []; const currentImage = view.type === "image" ? imageMap.get(view.id) ?? null : null; useEffect(() => () => { if (previewTimerRef.current) clearTimeout(previewTimerRef.current); localUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)); }, []); const notify = (message: string) => { setToast(message); window.setTimeout(() => setToast(""), 1900); }; const clearPreviewTimer = () => { if (previewTimerRef.current) clearTimeout(previewTimerRef.current); previewTimerRef.current = null; }; const armPreview = (item: GalleryItem) => { clearPreviewTimer(); setQuickPreview(null); setArmingKey(item.key); previewTimerRef.current = window.setTimeout(() => { setQuickPreview(item); setArmingKey(null); }, 3000); }; const disarmPreview = () => { clearPreviewTimer(); setArmingKey(null); setQuickPreview(null); }; const changeCategory = (value: string) => { setCategory(value); setView({ type: "gallery" }); window.scrollTo({ top: 0, behavior: "smooth" }); }; const openItem = (item: GalleryItem) => { disarmPreview(); setView(item.kind === "album" ? { type: "album", id: item.album.id } : { type: "image", id: item.image.id, albumId: item.albumId }); window.scrollTo({ top: 0, behavior: "smooth" }); }; const goBack = () => { if (view.type === "image" && view.albumId) setView({ type: "album", id: view.albumId }); else setView({ type: "gallery" }); window.scrollTo({ top: 0, behavior: "smooth" }); }; const uploadImages = (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith("image/")); if (!files.length) return; const today = new Date().toISOString().slice(0, 10); const additions = files.map((file, index) => { const url = URL.createObjectURL(file); localUrlsRef.current.push(url); return { id: `local-${Date.now()}-${index}`, title: file.name.replace(/\.[^.]+$/, ""), src: url, category: "动漫", ratio: index % 2 ? "4 / 5" : "3 / 2", position: "center", width: 0, height: 0, size: formatBytes(file.size), format: file.type.split("/")[1]?.toUpperCase() || "IMAGE", uploadedAt: today, views: 0, tags: ["本地预览", "刚刚上传"], local: true, }; }); setUploadedImages((current) => [...additions, ...current]); setView({ type: "gallery" }); setCategory("全部"); event.target.value = ""; notify(`已添加 ${additions.length} 张本地图片`); }; const copyImageUrl = async (image: ImageItem) => { const value = image.local ? image.src : `${window.location.origin}${image.src}`; try { await navigator.clipboard.writeText(value); notify("图片地址已复制"); } catch { notify("当前浏览器无法复制地址"); } }; return (
模板首页
{view.type === "gallery" &&

YOUR VISUAL LIBRARY

把喜欢的画面,收进一座安静的图床

响应式卡片流 · 本地上传演示 · 图集连续浏览
{feedItems.length}展示项目{data.albums.length}公开图集3s悬停预览
{category === "全部" ? "最近上传" : category}{filteredItems.length} 个结果
{(query || category !== "全部") && }
{filteredItems.length > 0 ? :

没有找到匹配内容

换一个关键词或查看全部分类。

}
} {view.type === "album" && currentAlbum &&
{albumImages.slice(0, 3).map((image, index) => )}

ALBUM COLLECTION

{currentAlbum.title}

{currentAlbum.description}
{currentAlbum.tags.map((tag) => #{tag})}
{albumImages.length} 张图片 · 更新于 {currentAlbum.updatedAt}
图集内容与图床相同的卡片浏览方式
({ key: `album-image-${currentAlbum.id}-${image.id}`, kind: "image", image, albumId: currentAlbum.id }))} armingKey={armingKey} onArm={armPreview} onDisarm={disarmPreview} onOpen={openItem} />
} {view.type === "image" && currentImage &&
{currentImage.title}
原图预览 · {currentImage.format}
} {quickPreview && } {toast &&
✓ {toast}
}
); } function GalleryGrid({ items, armingKey, onArm, onDisarm, onOpen }: { items: GalleryItem[]; armingKey: string | null; onArm: (item: GalleryItem) => void; onDisarm: () => void; onOpen: (item: GalleryItem) => void; }) { return
{items.map((item) => { const image = item.kind === "image" ? item.image : item.cover; const title = item.kind === "image" ? item.image.title : item.album.title; const tags = item.kind === "image" ? item.image.tags : item.album.tags; return ; })}
; } function formatBytes(bytes: number) { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; }