/**
* 详情图排序组件
* 左侧:实时预览(按顺序拼接显示)
* 右侧:可拖拽缩略图
*/
'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 (
);
}
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) => (
))}
{/* 右侧:可拖拽缩略图(单列,占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}
{/* 删除按钮 */}
{/* 拖拽提示 */}
拖拽排序
))}
);
}