gongxiangp2/components/detail/ProductPackageDetail.tsx
2026-05-07 16:37:22 +08:00

508 lines
23 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, 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>
);
}