feat:init

This commit is contained in:
zhongyidian committed 2025-12-29 19:19:10 +08:00
1 parent 53d0eeac6b
commit ae0c050010
494 files changed
+17701 -2318

No files matched your search

+165
View File
@@ -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>
);
}
+36
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./image";
export * from "./image.types";