/** * 详情图排序组件 * 左侧:实时预览(按顺序拼接显示) * 右侧:可拖拽缩略图 */ 'use client'; import { useState, useRef, useEffect } from 'react'; interface DetailImage { id: string; file: File; preview: string; previewStatus: 'loading' | 'ready' | 'unavailable'; previewMessage?: string; sortOrder: number; } interface DetailImageSorterProps { images: DetailImage[]; onChange: (images: DetailImage[]) => void; onRemove: (id: string) => void; disabled?: boolean; } function DetailImagePreview({ image, alt, compact = false, }: { image: DetailImage; alt: string; compact?: boolean; }) { const status = image.previewStatus; if (status === 'ready' && image.preview) { return ( {alt} ); } const statusLabel = status === 'loading' ? '正在生成缩略图' : '预览不可用'; return (
{image.file.name} {image.previewMessage || statusLabel}
); } export default function DetailImageSorter({ images, onChange, onRemove, disabled = false, }: DetailImageSorterProps) { const [draggedIndex, setDraggedIndex] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); const previewRef = useRef(null); // 按 sortOrder 排序 const sortedImages = [...images].sort((a, b) => a.sortOrder - b.sortOrder); // 拖拽开始 const handleDragStart = (e: React.DragEvent, index: number) => { if (disabled) return; setDraggedIndex(index); e.dataTransfer.effectAllowed = 'move'; e.stopPropagation(); // 阻止事件冒泡,避免触发外层文件拖拽 }; // 拖拽经过 const handleDragOver = (e: React.DragEvent, index: number) => { if (disabled) { return; } // 没有内部排序项时,说明是从资源管理器拖入的新文件。 // 此时不能拦截事件,交给外层详情图上传区域处理。 if (draggedIndex === null) { return; } e.preventDefault(); e.stopPropagation(); // 阻止事件冒泡 e.dataTransfer.dropEffect = 'move'; setHoveredIndex(index); }; // 拖拽离开 const handleDragLeave = (e: React.DragEvent) => { if (disabled) { return; } if (draggedIndex === null) { return; } e.stopPropagation(); // 阻止事件冒泡 setHoveredIndex(null); }; // 放置 const handleDrop = (e: React.DragEvent, dropIndex: number) => { if (disabled) { return; } // 外部文件放置必须继续冒泡到 ProductPackageUploader, // 否则已有详情图区域会变成无法追加图片的“死区”。 if (draggedIndex === null) { return; } e.preventDefault(); e.stopPropagation(); // 阻止事件冒泡,避免触发外层文件拖拽 if (draggedIndex === dropIndex) { setDraggedIndex(null); setHoveredIndex(null); return; } // 重新排序 const newImages = [...sortedImages]; const [draggedItem] = newImages.splice(draggedIndex, 1); newImages.splice(dropIndex, 0, draggedItem); // 更新 sortOrder const updatedImages = newImages.map((img, idx) => ({ ...img, sortOrder: idx, })); onChange(updatedImages); setDraggedIndex(null); setHoveredIndex(null); }; // 拖拽结束 const handleDragEnd = (e: React.DragEvent) => { e.stopPropagation(); // 阻止事件冒泡 setDraggedIndex(null); setHoveredIndex(null); }; // 点击缩略图滚动到对应位置 const scrollToImage = (index: number) => { if (!previewRef.current) return; const previewItem = previewRef.current.querySelector(`[data-detail-preview-index="${index}"]`); if (previewItem) { previewItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }; if (sortedImages.length === 0) { return null; } return (
{ if (draggedIndex !== null) { e.preventDefault(); e.stopPropagation(); } }} onDrop={(e) => { if (draggedIndex !== null) { e.preventDefault(); e.stopPropagation(); setDraggedIndex(null); setHoveredIndex(null); } }} > {/* 左侧:实时预览(占2/3宽度) */}

预览效果

{sortedImages.map((img, index) => (
{index + 1}
))}
{/* 右侧:可拖拽缩略图(单列,占1/3宽度) */}

拖拽排序

{sortedImages.map((img, index) => (
handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, index)} onDragEnd={handleDragEnd} onClick={() => scrollToImage(index)} className={` relative border-2 rounded-lg overflow-hidden ${disabled ? 'cursor-not-allowed opacity-60' : 'cursor-move'} transition-all duration-200 ${draggedIndex === index ? 'opacity-50 scale-95' : ''} ${hoveredIndex === index && draggedIndex !== null ? 'border-blue-500 scale-105' : 'border-gray-200 dark:border-gray-600'} hover:border-blue-300 dark:hover:border-blue-500 `} > {/* 序号 */}
{index + 1}
{/* 删除按钮 */} {/* 拖拽提示 */}
拖拽排序
))}
); }