gongxiangp2/components/detail/ProductPackageDetail.tsx

781 lines
33 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';
import {
detailInkButtonClass,
detailOutlineButtonClass,
detailMetaLabelClass,
detailMetaValueClass,
detailPanelClass,
detailSectionTitleClass,
detailThumbActiveClass,
detailThumbIdleClass,
} from '@/components/detail/detail-styles';
interface Asset {
id: string;
assetType: string;
fileUrl: string;
fileName: string;
fileSize: number;
sortOrder: number;
}
type AppendableGroup = 'main' | 'sku';
type DetailInsertPosition = {
group: 'detail';
afterAssetId: string;
beforeAssetId: string;
};
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;
/** 单图编辑权限(上传者 / 托管部门成员 / 系统管理员),由服务端判定 */
canManageAssets?: boolean;
/** 正在编辑处理的资产 id,用于禁用与遮罩 */
editingAssetId?: string | null;
onRequestReplace?: (asset: Asset) => void;
onRequestRename?: (asset: Asset) => void;
onRequestDelete?: (asset: Asset) => void;
onRequestAppend?: (group: AppendableGroup) => void;
onRequestInsert?: (position: DetailInsertPosition) => void;
/** 已加入待提交清单的资产 id,缩略图上显示待提交标记 */
pendingAssetIds?: string[];
}
function AddImageButton({
label,
hint,
onClick,
disabled,
className,
}: {
label: string;
hint: string;
onClick: () => void;
disabled: boolean;
className: string;
}) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
aria-label={`${label},${hint}`}
className={`${className} flex items-center justify-center rounded-[8px] border border-dashed border-[#cfd6dd] bg-[#fafafa] text-center text-[#4f6077] transition-colors hover:border-[#1a1c1d] hover:bg-white hover:text-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`}
>
<span className="flex flex-col items-center gap-1 px-2">
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" stroke="currentColor" aria-hidden="true">
<path d="M10 4v12M4 10h12" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
</svg>
<span className="text-[12px] font-medium tracking-[-0.02em]">{label}</span>
<span className="text-[10px] leading-4 text-[#86898e]">{hint}</span>
</span>
</button>
);
}
function DetailInsertButton({
disabled,
onClick,
afterIndex,
beforeIndex,
}: {
disabled: boolean;
onClick: () => void;
afterIndex: number;
beforeIndex: number;
}) {
return (
<div className="pointer-events-none relative z-20 h-0">
<button
type="button"
disabled={disabled}
onClick={onClick}
title="插入图片"
aria-label={`在详情图 ${afterIndex} 和详情图 ${beforeIndex} 之间插入图片`}
className="pointer-events-auto absolute left-1/2 top-0 flex h-10 w-10 -translate-x-1/2 -translate-y-1/2 items-center justify-center text-[#4f6077] transition-colors hover:text-[#1a1c1d] focus-visible:text-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[#aeb4ba] bg-white/95 shadow-[0_1px_3px_rgba(15,23,42,0.18)] transition-colors hover:border-[#1a1c1d]">
<svg viewBox="0 0 20 20" className="h-3.5 w-3.5" fill="none" stroke="currentColor" aria-hidden="true">
<path d="M10 5v10M5 10h10" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
</svg>
</span>
</button>
</div>
);
}
function AssetEditMenu({
asset,
busy,
disabled,
onRequestReplace,
onRequestRename,
onRequestDelete,
}: {
asset: Asset;
busy: boolean;
disabled: boolean;
onRequestReplace: (asset: Asset) => void;
onRequestRename: (asset: Asset) => void;
onRequestDelete: (asset: Asset) => void;
}) {
const [open, setOpen] = useState(false);
if (disabled) {
return busy ? (
<span className="absolute right-1 top-1 z-10 rounded-[6px] bg-white/90 px-1.5 py-0.5 text-[11px] text-[#6b7075]">
处理中…
</span>
) : null;
}
return (
<span className="absolute right-1 top-1 z-10">
<button
type="button"
aria-label={`编辑图片 ${asset.fileName}`}
className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[#e7e8ea] bg-white/95 text-[#1a1c1d] opacity-100 transition-opacity hover:border-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-1 md:opacity-0 md:group-hover:opacity-100 md:focus-visible:opacity-100"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setOpen((value) => !value);
}}
>
<svg viewBox="0 0 16 16" className="h-3.5 w-3.5" fill="currentColor" aria-hidden="true">
<circle cx="8" cy="3.5" r="1.4" />
<circle cx="8" cy="8" r="1.4" />
<circle cx="8" cy="12.5" r="1.4" />
</svg>
</button>
{open && (
<>
<span
className="fixed inset-0 z-20 cursor-default"
onClick={(event) => {
event.stopPropagation();
setOpen(false);
}}
onMouseDown={(event) => {
event.preventDefault();
}}
/>
<span className="absolute right-0 top-7 z-30 block w-28 overflow-hidden rounded-[8px] border border-[#e7e8ea] bg-white py-1">
{[
{ label: '替换图片', action: () => { setOpen(false); onRequestReplace(asset); } },
{ label: '重命名', action: () => { setOpen(false); onRequestRename(asset); } },
{ label: '删除图片', action: () => { setOpen(false); onRequestDelete(asset); }, danger: true },
].map((item) => (
<button
key={item.label}
type="button"
className={`block w-full px-3 py-1.5 text-left text-[12px] tracking-[-0.01em] hover:bg-[#fafafa] ${
item.danger ? 'text-[#b42318]' : 'text-[#1a1c1d]'
}`}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
item.action();
}}
>
{item.label}
</button>
))}
</span>
</>
)}
</span>
);
}
export default function ProductPackageDetail({
material,
canDownloadSource,
onDownload,
onBack,
showToast,
canManageAssets = false,
editingAssetId = null,
onRequestReplace,
onRequestRename,
onRequestDelete,
onRequestAppend,
onRequestInsert,
pendingAssetIds = [],
}: 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={`${detailPanelClass} p-4`}>
<div className="flex items-start">
<div>
<h4 className="text-[13px] font-medium text-[#1a1c1d]">此素材已被外部系统获取</h4>
<p className="mt-1 text-[13px] text-[#6b7075]">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-[13px] text-[#6b7075]">编辑或删除可能影响外部系统使用</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">
{/* 图片预览区(可显示主图或SKU图) */}
{((previewType === 'main' && mainImages.length > 0) || (previewType === 'sku' && skuImages.length > 0)) && (
<div className={`${detailPanelClass} p-4`}>
<div className="flex items-center justify-between mb-3">
<h4 className={detailSectionTitleClass}>
{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-[12px] text-[#86898e] hover:text-[#1a1c1d]"
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"
/>
</div>
</div>
)}
{/* 主图小卡片展示区 */}
{(mainImages.length > 0 || (canManageAssets && onRequestAppend)) && (
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>主图 ({mainImages.length})</h4>
<div className="flex flex-wrap gap-2">
{mainImages.map((img, index) => (
<div
key={img.id}
className="group relative flex-shrink-0"
style={{ width: '84px', height: '84px' }}
>
<button
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={`absolute inset-0 overflow-hidden rounded-[8px] ${
previewType === 'main' && index === currentMainIndex
? detailThumbActiveClass
: detailThumbIdleClass
}`}
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 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" 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>
{canManageAssets && onRequestReplace && onRequestRename && onRequestDelete && (
<>
<AssetEditMenu
asset={img}
busy={editingAssetId === img.id}
disabled={Boolean(editingAssetId)}
onRequestReplace={onRequestReplace}
onRequestRename={onRequestRename}
onRequestDelete={onRequestDelete}
/>
{editingAssetId === img.id && (
<span className="pointer-events-none absolute inset-0 z-0 animate-pulse rounded-[8px] border border-[#1a1c1d]/30 bg-white/60" />
)}
{pendingAssetIds.includes(img.id) && (
<span className="pointer-events-none absolute left-1 top-1 z-10 rounded bg-[#1a1c1d]/80 px-1.5 py-0.5 text-[10px] font-medium text-white">待提交</span>
)}
</>
)}
</div>
))}
{canManageAssets && onRequestAppend && (
<AddImageButton
label="追加图片"
hint="加入主图末尾"
disabled={Boolean(editingAssetId)}
onClick={() => onRequestAppend('main')}
className="h-[84px] w-[84px] flex-shrink-0"
/>
)}
</div>
</div>
)}
{/* 详情图区域(使用页面整体滚动,图片无缝拼接) */}
{detailImages.length > 0 && (
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>详情图 ({detailImages.length})</h4>
<div className="space-y-0">
{detailImages.map((img, index) => (
<div key={img.id}>
<div className="group relative">
<img
src={img.fileUrl}
alt={`详情图 ${index + 1}`}
className="w-full block"
style={{ display: 'block' }}
/>
<div className="absolute left-2 top-2 bg-[#1a1c1d] px-2 py-1 text-[11px] text-white">
{index + 1}
</div>
{canManageAssets && onRequestReplace && onRequestRename && onRequestDelete && (
<>
<AssetEditMenu
asset={img}
busy={editingAssetId === img.id}
disabled={Boolean(editingAssetId)}
onRequestReplace={onRequestReplace}
onRequestRename={onRequestRename}
onRequestDelete={onRequestDelete}
/>
{editingAssetId === img.id && (
<span className="pointer-events-none absolute inset-x-0 top-0 z-0 flex items-center justify-center bg-white/60 py-2 text-[12px] text-[#6b7075]">
正在上传临时图片,请稍候…
</span>
)}
{pendingAssetIds.includes(img.id) && (
<span className="pointer-events-none absolute left-1 top-1 z-10 rounded bg-[#1a1c1d]/80 px-1.5 py-0.5 text-[10px] font-medium text-white">待提交</span>
)}
</>
)}
</div>
{canManageAssets && onRequestInsert && detailImages[index + 1] && (
<DetailInsertButton
disabled={Boolean(editingAssetId)}
afterIndex={index + 1}
beforeIndex={index + 2}
onClick={() => onRequestInsert({
group: 'detail',
afterAssetId: img.id,
beforeAssetId: detailImages[index + 1].id,
})}
/>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* 右侧:基本信息 + SKU图 + 下载按钮 */}
<div className="space-y-6">
{/* 基本信息卡片 */}
<div data-detail-meta className={`${detailPanelClass} p-6`}>
<h3 className="mb-4 text-[15px] font-semibold tracking-[-0.03em] text-[#1a1c1d]">素材详情</h3>
<div className="space-y-4">
<div>
<p className={detailMetaLabelClass}>任务号</p>
<p className={detailMetaValueClass}>{material.taskNo}</p>
</div>
<div>
<p className={detailMetaLabelClass}>SPU型号</p>
<p className={detailMetaValueClass}>{material.productModel}</p>
</div>
<div>
<p className={detailMetaLabelClass}>店铺</p>
<p className={detailMetaValueClass}>{material.categoryName}</p>
</div>
{material.sceneName && (
<div>
<p className={detailMetaLabelClass}>场景</p>
<p className={detailMetaValueClass}>{material.sceneName}</p>
</div>
)}
<div>
<p className={detailMetaLabelClass}>出图方式</p>
<p className={detailMetaValueClass}>
{getTaskDetailSourceTypeLabel(material.taskDetailSourceType)}
</p>
</div>
<div>
<p className={detailMetaLabelClass}>是否整套</p>
<p className={detailMetaValueClass}>
{material.isFullSet ? '是' : '否'}
</p>
</div>
</div>
</div>
{/* SKU图小卡片展示区 */}
{(skuImages.length > 0 || (canManageAssets && onRequestAppend)) && (
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>SKU图 ({skuImages.length})</h4>
<div className="grid grid-cols-4 gap-2">
{skuImages.map((img, index) => (
<div key={img.id} className="group relative aspect-square w-full">
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setPreviewType('sku');
setCurrentSkuIndex(index);
setAutoPlayEnabled(false); // 手动切换时暂停轮播
}}
className={`absolute inset-0 overflow-hidden rounded-[8px] ${
previewType === 'sku' && index === currentSkuIndex
? detailThumbActiveClass
: detailThumbIdleClass
}`}
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 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" 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>
{canManageAssets && onRequestReplace && onRequestRename && onRequestDelete && (
<>
<AssetEditMenu
asset={img}
busy={editingAssetId === img.id}
disabled={Boolean(editingAssetId)}
onRequestReplace={onRequestReplace}
onRequestRename={onRequestRename}
onRequestDelete={onRequestDelete}
/>
{editingAssetId === img.id && (
<span className="pointer-events-none absolute inset-0 z-0 animate-pulse rounded-[8px] border border-[#1a1c1d]/30 bg-white/60" />
)}
{pendingAssetIds.includes(img.id) && (
<span className="pointer-events-none absolute left-1 top-1 z-10 rounded bg-[#1a1c1d]/80 px-1.5 py-0.5 text-[10px] font-medium text-white">待提交</span>
)}
</>
)}
</div>
))}
{canManageAssets && onRequestAppend && (
<AddImageButton
label="追加图片"
hint="加入 SKU 末尾"
disabled={Boolean(editingAssetId)}
onClick={() => onRequestAppend('sku')}
className="aspect-square w-full"
/>
)}
</div>
</div>
)}
{/* PSD 文件信息、下载按钮组和返回按钮 - 使用容器实现sticky,保持相对位置 */}
<div className="space-y-6 lg:sticky lg:top-24 lg:z-10">
{/* PSD 文件信息 */}
{psdFiles.length > 0 && (
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>PSD 源文件 ({psdFiles.length})</h4>
<div className="space-y-2">
{psdFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex-1">
<p className="text-[13px] font-medium text-[#1a1c1d]">{file.fileName}</p>
<p className="text-[12px] text-[#86898e]">{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={detailInkButtonClass}
type="button"
>
下载
</button>
) : (
<span className="text-[12px] text-[#86898e]">无权限</span>
)}
</div>
))}
</div>
</div>
)}
{/* 下载按钮组 */}
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>批量下载</h4>
{downloading && (
<div className="mb-3 rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex items-center space-x-2">
<svg className="h-4 w-4 animate-spin text-[#1a1c1d]" 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-[13px] text-[#6b7075]">
{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={detailInkButtonClass}
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={detailInkButtonClass}
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={detailInkButtonClass}
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={detailInkButtonClass}
type="button"
>
下载全部
</button>
</div>
</div>
{/* 返回上一页按钮 */}
{onBack && (
<button
onClick={onBack}
className={`w-full ${detailOutlineButtonClass}`}
>
返回上一页
</button>
)}
</div>
</div>
</div>
</div>
);
}