"use client"; import { useEffect, useRef, useState, type ImgHTMLAttributes } from "react"; import { useDecryptedSrc } from "@/hooks/use-decrypted-src"; import { IMG_PLACEHOLDER } from "@/lib/image/decrypt"; type DecryptedImageProps = Omit, "src"> & { src?: string | null; fallback?: string; /** 首屏/关键图:立即解密,不懒加载 */ priority?: boolean; /** * 视口外不发起解密请求。默认:非 priority 时开启。 * 解密图无法靠原生 loading=lazy 省流量,必须在进视口后再 fetch。 */ lazy?: boolean; /** IntersectionObserver rootMargin,提前预取(默认上下 320px) */ lazyRootMargin?: string; }; export function DecryptedImage({ src, fallback = IMG_PLACEHOLDER, priority = false, lazy, lazyRootMargin = "320px 0px", alt = "", onError, loading, decoding, fetchPriority, ...props }: DecryptedImageProps) { const enableLazy = lazy ?? !priority; const imgRef = useRef(null); const [inView, setInView] = useState(!enableLazy); useEffect(() => { if (!enableLazy || inView) return; const node = imgRef.current; if (!node) return; if (typeof IntersectionObserver === "undefined") { setInView(true); return; } const io = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0)) { setInView(true); io.disconnect(); } }, { root: null, rootMargin: lazyRootMargin, threshold: 0 }, ); io.observe(node); return () => io.disconnect(); }, [enableLazy, inView, lazyRootMargin]); const resolved = useDecryptedSrc(src, { priority, fallback, enabled: inView }); return ( {alt} { if (event.currentTarget.src !== fallback) { event.currentTarget.src = fallback; } onError?.(event); }} /> ); }