fix:解决拖拽问题

This commit is contained in:
zhongyidian committed 2025-10-20 17:50:42 +08:00
1 parent ce06ad0214
commit 6367014e1f
34 files changed
+1712 -397

No files matched your search

+180
View File
@@ -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>
);
}
+6 -6
View File
@@ -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
+176
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
export { default as Image } from "./Image";
export { default as BackgroundImage } from "./BackgroundImage";
export { default as DraggableItem } from "./DraggableItem";