feat:init
This commit is contained in:
1 parent
53d0eeac6b
commit
ae0c050010
494 files changed
+17701
-2318
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ImageProps } from "./image.types";
|
||||
|
||||
export function Image({
|
||||
src,
|
||||
alt = "",
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
objectFit = "cover",
|
||||
objectPosition = "center",
|
||||
loading = "lazy",
|
||||
placeholder,
|
||||
fallback,
|
||||
onLoad,
|
||||
onError,
|
||||
draggable = false,
|
||||
blur = false,
|
||||
button = false,
|
||||
style,
|
||||
...restProps
|
||||
}: 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" &&
|
||||
typeof IntersectionObserver !== "undefined" &&
|
||||
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: React.CSSProperties = {
|
||||
width,
|
||||
height,
|
||||
objectFit,
|
||||
objectPosition,
|
||||
filter: blur ? "blur(4px)" : "none",
|
||||
transition: "filter 0.3s ease",
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
width === "100%" ? "block" : "inline-block",
|
||||
button &&
|
||||
"cursor-pointer transition-all duration-300 hover:scale-105 hover:brightness-110 hover:contrast-110 active:scale-95",
|
||||
className
|
||||
)}
|
||||
style={{ width, height }}
|
||||
{...restProps}
|
||||
>
|
||||
{/* 加载占位符 */}
|
||||
{isLoading && placeholder && (
|
||||
<div
|
||||
className="w-full h-full bg-gray-200 flex items-center justify-center"
|
||||
style={{
|
||||
backgroundImage: `url(${placeholder})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<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={cn(
|
||||
"transition-opacity duration-300",
|
||||
isLoaded ? "opacity-100" : "opacity-0",
|
||||
"w-full h-full"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
export interface ImageProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/** 图片地址 */
|
||||
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;
|
||||
/** 是否可拖拽 */
|
||||
draggable?: boolean;
|
||||
/** 模糊效果 */
|
||||
blur?: boolean;
|
||||
/** 图片质量(仅用于提示) */
|
||||
quality?: number;
|
||||
/** 是否启用按钮样式(hover 和 active 效果) */
|
||||
button?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./image";
|
||||
export * from "./image.types";
|
||||
Reference in new issue
Block a user