508 lines
23 KiB
TypeScript
508 lines
23 KiB
TypeScript
/**
|
||
* 任务-详情详情组件
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { parseDownloadError } from '@/lib/download-errors';
|
||
import { getTaskDetailSourceTypeLabel } from '@/lib/task-detail-source-type';
|
||
|
||
interface Asset {
|
||
id: string;
|
||
assetType: string;
|
||
fileUrl: string;
|
||
fileName: string;
|
||
fileSize: number;
|
||
sortOrder: number;
|
||
}
|
||
|
||
interface ProductPackageDetailProps {
|
||
material: {
|
||
id: string;
|
||
taskNo: string;
|
||
productModel: string;
|
||
categoryName: string;
|
||
sceneName?: string;
|
||
taskDetailSourceType?: string | null;
|
||
isFullSet?: boolean;
|
||
taskStatus: string;
|
||
taskFetchedAt?: string;
|
||
assets: Asset[];
|
||
};
|
||
canDownloadSource: boolean;
|
||
onDownload: (type: 'all' | 'main' | 'sku' | 'detail' | 'psd', assetId?: string) => Promise<void>;
|
||
onBack?: () => void;
|
||
showToast?: (message: string, options?: { variant?: 'success' | 'error' }) => void;
|
||
}
|
||
|
||
export default function ProductPackageDetail({
|
||
material,
|
||
canDownloadSource,
|
||
onDownload,
|
||
onBack,
|
||
showToast,
|
||
}: ProductPackageDetailProps) {
|
||
// 当前预览的图片类型和索引
|
||
const [previewType, setPreviewType] = useState<'main' | 'sku'>('main');
|
||
const [currentMainIndex, setCurrentMainIndex] = useState(0);
|
||
const [currentSkuIndex, setCurrentSkuIndex] = useState(0);
|
||
const [downloading, setDownloading] = useState(false);
|
||
const [downloadType, setDownloadType] = useState<string>('');
|
||
|
||
// 轮播相关状态
|
||
const [autoPlayEnabled, setAutoPlayEnabled] = useState(true);
|
||
|
||
// 按类型分组资产(使用新的资产类型格式)
|
||
const mainImages = material.assets.filter((a) => a.assetType === 'main-preview').sort((a, b) => a.sortOrder - b.sortOrder);
|
||
const skuImages = material.assets.filter((a) => a.assetType === 'sku-preview').sort((a, b) => a.sortOrder - b.sortOrder);
|
||
const detailImages = material.assets.filter((a) => a.assetType === 'detail-preview').sort((a, b) => a.sortOrder - b.sortOrder);
|
||
const psdFiles = material.assets.filter((a) => a.assetType === 'psd');
|
||
|
||
// 轮播逻辑
|
||
const autoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
useEffect(() => {
|
||
// 清理之前的定时器
|
||
if (autoPlayTimerRef.current) {
|
||
clearInterval(autoPlayTimerRef.current);
|
||
}
|
||
|
||
// 只在预览主图或SKU图时启用轮播
|
||
if (autoPlayEnabled) {
|
||
const currentImages = previewType === 'main' ? mainImages : skuImages;
|
||
if (currentImages.length > 1) {
|
||
autoPlayTimerRef.current = setInterval(() => {
|
||
if (previewType === 'main') {
|
||
setCurrentMainIndex((prev) => (prev + 1) % mainImages.length);
|
||
} else {
|
||
setCurrentSkuIndex((prev) => (prev + 1) % skuImages.length);
|
||
}
|
||
}, 5000); // 5秒轮播
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
if (autoPlayTimerRef.current) {
|
||
clearInterval(autoPlayTimerRef.current);
|
||
}
|
||
};
|
||
}, [previewType, currentMainIndex, currentSkuIndex, autoPlayEnabled, mainImages.length, skuImages.length]);
|
||
|
||
// 格式化文件大小
|
||
const formatBytes = (bytes: number) => {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 警告:已被外部系统获取 */}
|
||
{material.taskStatus === 'fetched' && material.taskFetchedAt && (
|
||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||
<div className="flex items-start">
|
||
<svg className="w-5 h-5 text-yellow-600 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||
</svg>
|
||
<div>
|
||
<h4 className="text-sm font-medium text-yellow-800">注意:此素材已被外部系统获取</h4>
|
||
<p className="text-sm text-yellow-700 mt-1">
|
||
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
|
||
</p>
|
||
<p className="text-sm text-yellow-700">编辑或删除此素材可能影响外部系统的使用</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-[2fr_1fr] gap-6">
|
||
{/* 左侧:主图预览区 + 主图小卡片 + 详情图滚动 */}
|
||
<div className="space-y-6">
|
||
{/* 图片预览区(可显示主图或SKU图) */}
|
||
{((previewType === 'main' && mainImages.length > 0) || (previewType === 'sku' && skuImages.length > 0)) && (
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h4 className="text-sm font-medium">
|
||
{previewType === 'main' ? `主图 (${currentMainIndex + 1}/${mainImages.length})` : `SKU图 (${currentSkuIndex + 1}/${skuImages.length})`}
|
||
</h4>
|
||
{((previewType === 'main' && mainImages.length > 1) || (previewType === 'sku' && skuImages.length > 1)) && (
|
||
<button
|
||
onClick={() => setAutoPlayEnabled(!autoPlayEnabled)}
|
||
className="text-xs text-gray-500 hover:text-gray-700"
|
||
type="button"
|
||
>
|
||
{autoPlayEnabled ? '暂停轮播' : '开始轮播'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="relative">
|
||
<img
|
||
src={
|
||
previewType === 'main'
|
||
? mainImages[currentMainIndex]?.fileUrl
|
||
: skuImages[currentSkuIndex]?.fileUrl
|
||
}
|
||
alt={previewType === 'main' ? `主图 ${currentMainIndex + 1}` : `SKU图 ${currentSkuIndex + 1}`}
|
||
className="w-full rounded-lg"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 主图小卡片展示区 */}
|
||
{mainImages.length > 0 && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">主图 ({mainImages.length})</h4>
|
||
<div className="flex flex-wrap gap-2">
|
||
{mainImages.map((img, index) => (
|
||
<button
|
||
key={img.id}
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
// 阻止滚动行为
|
||
const currentScrollY = window.scrollY;
|
||
setPreviewType('main');
|
||
setCurrentMainIndex(index);
|
||
setAutoPlayEnabled(false); // 手动切换时暂停轮播
|
||
// 保持滚动位置
|
||
requestAnimationFrame(() => {
|
||
window.scrollTo(0, currentScrollY);
|
||
});
|
||
}}
|
||
onMouseDown={(e) => {
|
||
// 阻止默认的鼠标按下行为
|
||
e.preventDefault();
|
||
}}
|
||
className={`relative flex-shrink-0 border-2 rounded overflow-hidden ${
|
||
previewType === 'main' && index === currentMainIndex
|
||
? 'border-blue-500 ring-2 ring-blue-200'
|
||
: 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
style={{ width: '84px', height: '84px' }}
|
||
type="button"
|
||
>
|
||
<img
|
||
src={img.fileUrl}
|
||
alt={`主图 ${index + 1}`}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
{previewType === 'main' && index === currentMainIndex && (
|
||
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 详情图区域(使用页面整体滚动,图片无缝拼接) */}
|
||
{detailImages.length > 0 && (
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<h4 className="text-sm font-medium mb-3">详情图 ({detailImages.length})</h4>
|
||
<div className="space-y-0">
|
||
{detailImages.map((img, index) => (
|
||
<div key={img.id} className="relative">
|
||
<img
|
||
src={img.fileUrl}
|
||
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>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧:基本信息 + SKU图 + 下载按钮 */}
|
||
<div className="space-y-6">
|
||
{/* 基本信息卡片 */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||
<h3 className="text-lg font-medium mb-4 text-gray-900 dark:text-white">素材详情</h3>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">任务号</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">{material.taskNo}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">SPU型号</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">{material.productModel}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">店铺</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">{material.categoryName}</p>
|
||
</div>
|
||
{material.sceneName && (
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">场景</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">{material.sceneName}</p>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">出图方式</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">
|
||
{getTaskDetailSourceTypeLabel(material.taskDetailSourceType)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">是否整套</p>
|
||
<p className="text-base font-medium text-gray-900 dark:text-white">
|
||
{material.isFullSet ? '是' : '否'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* SKU图小卡片展示区 */}
|
||
{skuImages.length > 0 && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">SKU图 ({skuImages.length})</h4>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{skuImages.map((img, index) => (
|
||
<button
|
||
key={img.id}
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setPreviewType('sku');
|
||
setCurrentSkuIndex(index);
|
||
setAutoPlayEnabled(false); // 手动切换时暂停轮播
|
||
}}
|
||
className={`relative w-full aspect-square border-2 rounded overflow-hidden ${
|
||
previewType === 'sku' && index === currentSkuIndex
|
||
? 'border-blue-500 ring-2 ring-blue-200'
|
||
: 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
type="button"
|
||
>
|
||
<img
|
||
src={img.fileUrl}
|
||
alt={`SKU图 ${index + 1}`}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
{previewType === 'sku' && index === currentSkuIndex && (
|
||
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* PSD 文件信息、下载按钮组和返回按钮 - 使用容器实现sticky,保持相对位置 */}
|
||
<div className="sticky top-24 z-10 space-y-6">
|
||
{/* PSD 文件信息 */}
|
||
{psdFiles.length > 0 && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">PSD 源文件 ({psdFiles.length})</h4>
|
||
<div className="space-y-2">
|
||
{psdFiles.map((file) => (
|
||
<div key={file.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||
<div className="flex-1">
|
||
<p className="text-sm font-medium text-gray-900 dark:text-white">{file.fileName}</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">{formatBytes(file.fileSize)}</p>
|
||
</div>
|
||
{canDownloadSource ? (
|
||
<button
|
||
onClick={async () => {
|
||
setDownloading(true);
|
||
setDownloadType('psd');
|
||
try {
|
||
await onDownload('psd', file.id);
|
||
setTimeout(() => {
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
const { message } = parseDownloadError(error);
|
||
if (showToast) {
|
||
showToast(message, { variant: 'error' });
|
||
}
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}
|
||
}}
|
||
disabled={downloading}
|
||
className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
type="button"
|
||
>
|
||
下载
|
||
</button>
|
||
) : (
|
||
<span className="text-xs text-gray-400">无权限</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 下载按钮组 */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">批量下载</h4>
|
||
{downloading && (
|
||
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||
<div className="flex items-center space-x-2">
|
||
<svg className="animate-spin h-4 w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
<span className="text-sm text-blue-600 dark:text-blue-400">
|
||
{downloadType === 'all' ? '正在准备全部文件,请稍候...' :
|
||
downloadType === 'main' ? '正在下载主图,请稍候...' :
|
||
downloadType === 'sku' ? '正在下载SKU图,请稍候...' :
|
||
downloadType === 'detail' ? '正在下载详情图,请稍候...' :
|
||
downloadType === 'psd' ? '正在下载PSD文件,请稍候...' :
|
||
'正在准备文件,请稍候...'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{mainImages.length > 0 && (
|
||
<button
|
||
onClick={async () => {
|
||
setDownloading(true);
|
||
setDownloadType('main');
|
||
try {
|
||
await onDownload('main');
|
||
// 下载完成后重置状态
|
||
setTimeout(() => {
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
const { message } = parseDownloadError(error);
|
||
if (showToast) {
|
||
showToast(message, { variant: 'error' });
|
||
}
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}
|
||
}}
|
||
disabled={downloading}
|
||
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
type="button"
|
||
>
|
||
下载主图
|
||
</button>
|
||
)}
|
||
{skuImages.length > 0 && (
|
||
<button
|
||
onClick={async () => {
|
||
setDownloading(true);
|
||
setDownloadType('sku');
|
||
try {
|
||
await onDownload('sku');
|
||
setTimeout(() => {
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
const { message } = parseDownloadError(error);
|
||
if (showToast) {
|
||
showToast(message, { variant: 'error' });
|
||
}
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}
|
||
}}
|
||
disabled={downloading}
|
||
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
type="button"
|
||
>
|
||
下载SKU图
|
||
</button>
|
||
)}
|
||
{detailImages.length > 0 && (
|
||
<button
|
||
onClick={async () => {
|
||
setDownloading(true);
|
||
setDownloadType('detail');
|
||
try {
|
||
await onDownload('detail');
|
||
setTimeout(() => {
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
const { message } = parseDownloadError(error);
|
||
if (showToast) {
|
||
showToast(message, { variant: 'error' });
|
||
}
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}
|
||
}}
|
||
disabled={downloading}
|
||
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
type="button"
|
||
>
|
||
下载详情图
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={async () => {
|
||
setDownloading(true);
|
||
setDownloadType('all');
|
||
try {
|
||
await onDownload('all');
|
||
setTimeout(() => {
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('下载失败:', error);
|
||
const { message } = parseDownloadError(error);
|
||
if (showToast) {
|
||
showToast(message, { variant: 'error' });
|
||
}
|
||
setDownloading(false);
|
||
setDownloadType('');
|
||
}
|
||
}}
|
||
disabled={downloading}
|
||
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
type="button"
|
||
>
|
||
下载全部
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 返回上一页按钮 */}
|
||
{onBack && (
|
||
<button
|
||
onClick={onBack}
|
||
className="w-full py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-base font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||
>
|
||
返回上一页
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|