fix:解决拖拽问题
This commit is contained in:
1 parent
ce06ad0214
commit
6367014e1f
34 files changed
+1712
-397
No files matched your search
@@ -0,0 +1,180 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
interface BackgroundImageProps {
|
||||
src: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
backgroundSize?: "cover" | "contain" | "auto" | string;
|
||||
backgroundPosition?: string;
|
||||
backgroundRepeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y";
|
||||
placeholder?: string;
|
||||
fallback?: string;
|
||||
onLoad?: () => void;
|
||||
onError?: () => void;
|
||||
onClick?: () => void;
|
||||
children?: React.ReactNode;
|
||||
overlay?: boolean;
|
||||
overlayColor?: string;
|
||||
overlayOpacity?: number;
|
||||
}
|
||||
|
||||
export default function BackgroundImage({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
backgroundSize = "cover",
|
||||
backgroundPosition = "center",
|
||||
backgroundRepeat = "no-repeat",
|
||||
placeholder,
|
||||
fallback,
|
||||
onLoad,
|
||||
onError,
|
||||
onClick,
|
||||
children,
|
||||
overlay = false,
|
||||
overlayColor = "rgba(0, 0, 0, 0.5)",
|
||||
overlayOpacity = 0.5,
|
||||
}: BackgroundImageProps) {
|
||||
const [imageSrc, setImageSrc] = useState(src);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const loadedImagesRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// 预加载图片
|
||||
const preloadImage = (url: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject();
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
// 统一的加载逻辑
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadImage = async () => {
|
||||
if (!imageSrc || !isMounted) return;
|
||||
|
||||
// 检查是否已经加载过这个图片
|
||||
if (loadedImagesRef.current.has(imageSrc)) {
|
||||
setIsLoading(false);
|
||||
setIsLoaded(true);
|
||||
setHasError(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await preloadImage(imageSrc);
|
||||
if (isMounted) {
|
||||
loadedImagesRef.current.add(imageSrc);
|
||||
setIsLoading(false);
|
||||
setIsLoaded(true);
|
||||
setHasError(false);
|
||||
onLoad?.();
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
if (fallback && imageSrc !== fallback) {
|
||||
setImageSrc(fallback);
|
||||
} else {
|
||||
onError?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadImage();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [imageSrc]); // 只在imageSrc改变时执行
|
||||
|
||||
// 当src改变时更新imageSrc
|
||||
useEffect(() => {
|
||||
if (src !== imageSrc) {
|
||||
setImageSrc(src);
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
setIsLoaded(false);
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
const backgroundStyle = {
|
||||
width,
|
||||
height,
|
||||
backgroundImage: isLoaded
|
||||
? `url(${imageSrc})`
|
||||
: placeholder
|
||||
? `url(${placeholder})`
|
||||
: undefined,
|
||||
backgroundSize,
|
||||
backgroundPosition,
|
||||
backgroundRepeat,
|
||||
position: "relative" as const,
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
backgroundColor: overlayColor,
|
||||
opacity: overlayOpacity,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("relative overflow-hidden", className)}
|
||||
style={backgroundStyle}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 加载占位符 */}
|
||||
{isLoading && !placeholder && (
|
||||
<div className="absolute inset-0 bg-gray-200 flex items-center justify-center">
|
||||
<div className="animate-pulse bg-gray-300 rounded w-full h-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{hasError && (
|
||||
<div className="absolute inset-0 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>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
{overlay && isLoaded && (
|
||||
<div className="absolute inset-0" style={overlayStyle} />
|
||||
)}
|
||||
|
||||
{/* 内容 */}
|
||||
{children && (
|
||||
<div className="relative z-10 w-full h-full">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import { ReactNode } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { GripVertical } from "lucide-react";
|
||||
|
||||
interface DraggableItemProps {
|
||||
id: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function DraggableItem({ id, children }: DraggableItemProps) {
|
||||
export default function DraggableItem({ id, children }: DraggableItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -29,7 +29,7 @@ export function DraggableItem({ id, children }: DraggableItemProps) {
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`bg-white border border-gray-200 rounded-lg p-4 mb-3 flex items-center gap-3 shadow-sm hover:shadow-md transition-shadow ${
|
||||
isDragging ? 'cursor-grabbing' : 'cursor-grab'
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Image } from "./Image";
|
||||
export { default as BackgroundImage } from "./BackgroundImage";
|
||||
export { default as DraggableItem } from "./DraggableItem";
|
||||
Reference in new issue
Block a user