Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-26 17:51:31 +07:00
co-authored by Claude Opus 5.5
commit 61d0a11dfa
371 changed files with 35836 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
"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);
}}
/>
);
}