gongxiangp2/components/upload/DetailImageSorter.tsx
chenpei ea9076df45 feat: 实现统一的下载错误处理系统并修复产品全案数据库Schema
- 创建统一的下载错误处理模块 (lib/download-errors.ts)
- 实现13种错误类型分类和中文错误消息
- 后端统一使用 createDownloadError 创建标准化错误响应
- 前端统一使用 parseDownloadError 解析错误并显示
- 修复正式系统数据库Schema,添加产品全案相关字段和表
- 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段
- 创建 material_assets, api_keys, api_call_logs 表
2026-01-05 17:52:42 +08:00

185 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 详情图排序组件
* 左侧:实时预览(按顺序拼接显示)
* 右侧:可拖拽缩略图
*/
'use client';
import { useState, useRef, useEffect } from 'react';
interface DetailImage {
id: string;
file: File;
preview: string;
sortOrder: number;
}
interface DetailImageSorterProps {
images: DetailImage[];
onChange: (images: DetailImage[]) => void;
onRemove: (id: string) => void;
}
export default function DetailImageSorter({ images, onChange, onRemove }: DetailImageSorterProps) {
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const previewRef = useRef<HTMLDivElement>(null);
// 按 sortOrder 排序
const sortedImages = [...images].sort((a, b) => a.sortOrder - b.sortOrder);
// 拖拽开始
const handleDragStart = (e: React.DragEvent, index: number) => {
setDraggedIndex(index);
e.dataTransfer.effectAllowed = 'move';
e.stopPropagation(); // 阻止事件冒泡,避免触发外层文件拖拽
};
// 拖拽经过
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
e.stopPropagation(); // 阻止事件冒泡
e.dataTransfer.dropEffect = 'move';
setHoveredIndex(index);
};
// 拖拽离开
const handleDragLeave = (e: React.DragEvent) => {
e.stopPropagation(); // 阻止事件冒泡
setHoveredIndex(null);
};
// 放置
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
e.stopPropagation(); // 阻止事件冒泡,避免触发外层文件拖拽
if (draggedIndex === null || 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 previewImages = previewRef.current.querySelectorAll('img');
if (previewImages[index]) {
previewImages[index].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
if (sortedImages.length === 0) {
return null;
}
return (
<div
className="grid grid-cols-3 gap-4"
onDragOver={(e) => e.stopPropagation()}
onDrop={(e) => e.stopPropagation()}
>
{/* 左侧:实时预览(占2/3宽度) */}
<div className="col-span-2 border rounded-lg p-4 bg-gray-50 dark:bg-gray-800">
<h4 className="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">预览效果</h4>
<div
ref={previewRef}
className="max-h-[600px] overflow-y-auto"
>
{sortedImages.map((img, index) => (
<div key={img.id} className="relative">
<img
src={img.preview}
alt={`详情图 ${index + 1}`}
className="w-full block"
style={{ display: 'block' }}
/>
<div className="absolute top-2 left-2 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded">
{index + 1}
</div>
</div>
))}
</div>
</div>
{/* 右侧:可拖拽缩略图(单列,占1/3宽度) */}
<div className="col-span-1 border rounded-lg p-4 dark:border-gray-700">
<h4 className="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">拖拽排序</h4>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{sortedImages.map((img, index) => (
<div
key={img.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
onClick={() => scrollToImage(index)}
className={`
relative cursor-move border-2 rounded-lg overflow-hidden
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
`}
>
<img
src={img.preview}
alt={`缩略图 ${index + 1}`}
className="w-full h-32 object-cover"
/>
{/* 序号 */}
<div className="absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded">
{index + 1}
</div>
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
onRemove(img.id);
}}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 text-sm hover:bg-red-600 flex items-center justify-center"
>
×
</button>
{/* 拖拽提示 */}
<div className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center py-1">
拖拽排序
</div>
</div>
))}
</div>
</div>
</div>
);
}