gongxiangp2/components/detail/ThreeDOriginalDetail.tsx

302 lines
13 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.

/**
* 任务-3D原图详情组件
*/
'use client';
import { useState, useEffect, useRef } from 'react';
import { parseDownloadError } from '@/lib/download-errors';
interface Asset {
id: string;
assetType: string;
fileUrl: string;
fileName: string;
fileSize: number;
sortOrder: number;
}
interface ThreeDOriginalDetailProps {
material: {
id: string;
taskNo?: string;
dingtalkApprovalNo?: string | null;
productModel: string;
categoryName: string;
sceneName: string;
materialType?: string;
taskStatus?: string;
taskFetchedAt?: string;
assets: Asset[];
};
canDownloadSource: boolean;
onDownload: (type: string, assetId?: string) => Promise<void>;
onBack?: () => void;
showToast?: (message: string, options?: { variant?: 'success' | 'error' }) => void;
}
export default function ThreeDOriginalDetail({
material,
canDownloadSource,
onDownload,
onBack,
showToast,
}: ThreeDOriginalDetailProps) {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [downloading, setDownloading] = useState(false);
const [downloadType, setDownloadType] = useState<string>('');
const isTaskOther = material.materialType === '任务-其他';
// 轮播相关状态
const [autoPlayEnabled, setAutoPlayEnabled] = useState(true);
// 按类型分组资产
// ⚠️ 注意:preview是S3存储的压缩预览图(小文件),用于网页展示
// zip是本地存储的原图压缩包(大文件),用于下载
const mainImages = material.assets.filter((a) => a.assetType === 'preview').sort((a, b) => a.sortOrder - b.sortOrder);
const zipFile = material.assets.find((a) => a.assetType === 'zip');
// 轮播逻辑
const autoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// 清理之前的定时器
if (autoPlayTimerRef.current) {
clearInterval(autoPlayTimerRef.current);
}
// 只在有多张原图时启用轮播
if (autoPlayEnabled && mainImages.length > 1) {
autoPlayTimerRef.current = setInterval(() => {
setCurrentImageIndex((prev) => (prev + 1) % mainImages.length);
}, 5000); // 5秒轮播
}
return () => {
if (autoPlayTimerRef.current) {
clearInterval(autoPlayTimerRef.current);
}
};
}, [currentImageIndex, autoPlayEnabled, mainImages.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-1 gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(18rem,1fr)] lg:gap-6">
{/* 左侧:原图展示 */}
<div className="min-w-0 space-y-6">
{/* 原图预览区 */}
{mainImages.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">
{isTaskOther ? '预览图' : '原图'} ({currentImageIndex + 1}/{mainImages.length})
</h4>
{mainImages.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={mainImages[currentImageIndex]?.fileUrl}
alt={`原图 ${currentImageIndex + 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>
{isTaskOther && (
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">任务-其他只展示上传时提交的预览图</p>
)}
<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;
setCurrentImageIndex(index);
setAutoPlayEnabled(false); // 手动切换时暂停轮播
// 保持滚动位置
requestAnimationFrame(() => {
window.scrollTo(0, currentScrollY);
});
}}
onMouseDown={(e) => {
// 阻止默认的鼠标按下行为
e.preventDefault();
}}
className={`relative flex-shrink-0 border-2 rounded overflow-hidden ${
index === currentImageIndex
? '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"
/>
{index === currentImageIndex && (
<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>
)}
</div>
{/* 右侧:基本信息 + 下载按钮 */}
<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">钉钉审批编号</p>
<p className="text-base font-medium text-gray-900 dark:text-white break-all">
{material.dingtalkApprovalNo || '未填写'}
</p>
</div>
{!isTaskOther && (
<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>
)}
{!isTaskOther && (
<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>
)}
{!isTaskOther && 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>
</div>
{/* ZIP 文件信息和下载按钮 - 使用容器实现sticky,保持相对位置 */}
<div className="space-y-6 lg:sticky lg:top-24 lg:z-10">
{/* ZIP 源文件 */}
{zipFile && (
<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>
<div 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">{zipFile.fileName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatBytes(zipFile.fileSize)}</p>
</div>
{canDownloadSource ? (
<button
onClick={async () => {
setDownloading(true);
setDownloadType('zip');
try {
await onDownload('zip', zipFile.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"
>
{downloading && downloadType === 'zip' ? (
<div className="flex items-center space-x-2">
<svg className="animate-spin h-4 w-4 text-white" 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>下载中...</span>
</div>
) : (
'下载'
)}
</button>
) : (
<span className="text-xs text-gray-400">无权限</span>
)}
</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>
);
}