82 lines
2.2 KiB
TypeScript
82 lines
2.2 KiB
TypeScript
"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<ImgHTMLAttributes<HTMLImageElement>, "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<HTMLImageElement>(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 (
|
|
<img
|
|
{...props}
|
|
ref={imgRef}
|
|
alt={alt}
|
|
src={resolved}
|
|
loading={loading ?? (priority ? "eager" : "lazy")}
|
|
decoding={decoding ?? (priority ? "sync" : "async")}
|
|
fetchPriority={fetchPriority ?? (priority ? "high" : "auto")}
|
|
onError={(event) => {
|
|
if (event.currentTarget.src !== fallback) {
|
|
event.currentTarget.src = fallback;
|
|
}
|
|
onError?.(event);
|
|
}}
|
|
/>
|
|
);
|
|
}
|