"use client"; import { useEffect, useRef, useState } from "react"; import { ImageOff, Video, RotateCw } from "lucide-react"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { isRetiredStorageUrl } from "@/lib/storage/retired-storage"; import { queuePreview } from "@/lib/admin/preview-queue"; export function MediaThumbnail({ url, video = false, onOpen, }: { url: string; video?: boolean; onOpen?: () => void; }) { const container = useRef(null); const [state, setState] = useState<"waiting" | "loading" | "ready" | "error">("waiting"); const [attempt, setAttempt] = useState(0); const [source, setSource] = useState(""); const [failure, setFailure] = useState(""); const retired = isRetiredStorageUrl(url); useEffect(() => { setState("waiting"); setSource(""); setFailure(""); if (video || retired || !url || !container.current) return; let dispose: (() => void) | undefined; let abort = () => {}; let objectUrl = ""; let alive = true; const observer = new IntersectionObserver( ([entry]) => { if (!entry.isIntersecting || dispose) return; observer.disconnect(); dispose = queuePreview((release) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 48000); setState("loading"); // Fetch is abortable, so leaving a page also stops in-flight previews. const parsed = new URL(url, window.location.origin); const target = parsed.hostname === "scqnycdn.jiansun.vip" && !parsed.search ? `${url}?imageView2/2/w/192/q/72/format/webp` : buildImageProxyUrl(url, { width: 192, quality: 72, format: "webp", delivery: "proxy", }); fetch(target, { signal: controller.signal }) .then(async (response) => { if (!response.ok) { const detail = await response.json().catch(() => null); throw new Error(`${detail?.error || "预览请求失败"}(HTTP ${response.status})`); } const blob = await response.blob(); if (!blob.type.startsWith("image/")) throw new Error("Not an image"); if (alive) { objectUrl = URL.createObjectURL(blob); setSource(objectUrl); setState("ready"); } }) .catch((error) => { if (alive) { setFailure( error.name === "AbortError" ? "预览加载超时" : error.message || "预览加载失败", ); setState("error"); } }) .finally(() => { clearTimeout(timeout); release(); }); abort = () => { controller.abort(); clearTimeout(timeout); }; }); }, { rootMargin: "80px" }, ); observer.observe(container.current); return () => { alive = false; observer.disconnect(); abort(); dispose?.(); if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [url, video, retired, attempt]); return (
{video ? ( ) : retired ? ( 旧存储
已停用
) : source && state === "ready" ? ( ) : state === "error" ? ( ) : ( )}
); }