177 lines
4.3 KiB
TypeScript
177 lines
4.3 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { clsx } from "clsx";
|
|
|
|
interface ImageProps {
|
|
src: string;
|
|
alt?: string;
|
|
className?: string;
|
|
width?: number | string;
|
|
height?: number | string;
|
|
objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
objectPosition?: string;
|
|
loading?: "lazy" | "eager";
|
|
placeholder?: string;
|
|
fallback?: string;
|
|
onLoad?: () => void;
|
|
onError?: () => void;
|
|
onClick?: () => void;
|
|
draggable?: boolean;
|
|
quality?: number;
|
|
blur?: boolean;
|
|
button?: boolean;
|
|
}
|
|
|
|
export default function Image({
|
|
src,
|
|
alt,
|
|
className,
|
|
width,
|
|
height,
|
|
objectFit = "cover",
|
|
objectPosition = "center",
|
|
loading = "lazy",
|
|
placeholder,
|
|
fallback,
|
|
onLoad,
|
|
onError,
|
|
onClick,
|
|
draggable = false,
|
|
quality = 80,
|
|
blur = false,
|
|
button = false,
|
|
}: ImageProps) {
|
|
const [imageSrc, setImageSrc] = useState(src);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [hasError, setHasError] = useState(false);
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const imgRef = useRef<HTMLImageElement>(null);
|
|
|
|
// 处理图片加载
|
|
const handleLoad = () => {
|
|
setIsLoading(false);
|
|
setIsLoaded(true);
|
|
setHasError(false);
|
|
onLoad?.();
|
|
};
|
|
|
|
// 处理图片错误
|
|
const handleError = () => {
|
|
setIsLoading(false);
|
|
setHasError(true);
|
|
if (fallback && imageSrc !== fallback) {
|
|
setImageSrc(fallback);
|
|
}
|
|
onError?.();
|
|
};
|
|
|
|
// 重置状态当src改变时
|
|
useEffect(() => {
|
|
if (src !== imageSrc) {
|
|
setImageSrc(src);
|
|
setIsLoading(true);
|
|
setHasError(false);
|
|
setIsLoaded(false);
|
|
}
|
|
}, [src, imageSrc]);
|
|
|
|
// 懒加载处理
|
|
useEffect(() => {
|
|
if (loading === "lazy" && imgRef.current) {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setImageSrc(src);
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.1 }
|
|
);
|
|
|
|
observer.observe(imgRef.current);
|
|
return () => observer.disconnect();
|
|
}
|
|
}, [loading, src]);
|
|
|
|
const imageStyle = {
|
|
width: width,
|
|
height: height,
|
|
objectFit,
|
|
objectPosition,
|
|
filter: blur ? "blur(4px)" : "none",
|
|
transition: "filter 0.3s ease",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"inline-block",
|
|
button &&
|
|
"cursor-pointer transition-all duration-300 hover:scale-105 hover:brightness-110",
|
|
className
|
|
)}
|
|
style={{ width, height }}
|
|
onClick={onClick}
|
|
>
|
|
{/* 加载占位符 */}
|
|
{isLoading && placeholder && (
|
|
<div
|
|
className="w-full h-full bg-gray-200 flex items-center justify-center"
|
|
style={{
|
|
backgroundImage: `url(${placeholder})`,
|
|
backgroundSize: "cover",
|
|
}}
|
|
>
|
|
<div className="animate-pulse bg-gray-300 rounded w-full h-full" />
|
|
</div>
|
|
)}
|
|
|
|
{/* 加载中状态 */}
|
|
{isLoading && !placeholder && (
|
|
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
|
|
<div className="animate-pulse bg-gray-300 rounded w-full h-full" />
|
|
</div>
|
|
)}
|
|
|
|
{/* 错误状态 */}
|
|
{hasError && (
|
|
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
|
|
<div className="text-gray-400 text-center">
|
|
<svg
|
|
className="w-8 h-8 mx-auto mb-2"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
<p className="text-sm">图片加载失败</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 实际图片 */}
|
|
<img
|
|
ref={imgRef}
|
|
src={imageSrc}
|
|
alt={alt}
|
|
style={imageStyle}
|
|
loading={loading}
|
|
draggable={draggable}
|
|
onLoad={handleLoad}
|
|
onError={handleError}
|
|
className={clsx(
|
|
"transition-opacity duration-300",
|
|
isLoaded ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|