'use client' import { useState, useEffect } from 'react' import Image from 'next/image' import { useToast } from '@/hooks/useToast' import ToastBubble from './ToastBubble' import { parseDownloadError } from '@/lib/download-errors' import { validateAndTriggerDownload } from '@/lib/client-download' import type { StandardModelWhiteBackgroundSourceType } from '@/lib/standard-model-white-background-metadata' interface Sku { id: string skuName: string skuCode?: string previewUrl: string zipFileName: string zipFilePath: string zipFileSize: string sortOrder: number uploaderId: string uploader?: { id: string username: string realName?: string } whiteBackgroundUploaded?: boolean whiteBackgroundComplete?: boolean whiteBackgroundFixedCount?: number whiteBackgroundOtherCount?: number whiteBackgroundCount?: number whiteBackgroundSourceType?: StandardModelWhiteBackgroundSourceType | null whiteBackgroundUploadedBy?: string | null whiteBackgroundUploadedAt?: string | Date | null whiteBackgroundUploader?: { id: string username: string realName?: string | null } | null } interface SkuListProps { materialId: string canEdit: boolean onEditClick: () => void onSelectionChange?: (selectedIds: Set) => void refreshKey?: number } export default function SkuList({ materialId, canEdit, onEditClick, onSelectionChange, refreshKey = 0 }: SkuListProps) { const [skus, setSkus] = useState([]) const [loading, setLoading] = useState(true) const { toast, showToast } = useToast() const [selectedSkuIds, setSelectedSkuIds] = useState>(new Set()) const [downloadingSkuId, setDownloadingSkuId] = useState(null) const [downloadingWhiteBgSkuId, setDownloadingWhiteBgSkuId] = useState(null) const [previewImage, setPreviewImage] = useState(null) useEffect(() => { fetchSkus() }, [materialId, refreshKey]) const fetchSkus = async () => { try { setLoading(true) const response = await fetch(`/api/materials/${materialId}/skus`) if (response.ok) { const data = await response.json() setSkus(data.skus || []) } } catch (error) { console.error('加载 SKU 列表失败:', error) } finally { setLoading(false) } } const handleSelectAll = () => { const newSelected = selectedSkuIds.size === skus.length ? new Set() : new Set(skus.map((sku) => sku.id)) setSelectedSkuIds(newSelected) onSelectionChange?.(newSelected) } const handleSelectSku = (skuId: string) => { const newSelected = new Set(selectedSkuIds) if (newSelected.has(skuId)) { newSelected.delete(skuId) } else { newSelected.add(skuId) } setSelectedSkuIds(newSelected) onSelectionChange?.(newSelected) } const handleDownloadSku = async (sku: Sku) => { if (!sku.zipFilePath || !sku.zipFilePath.trim()) { showToast('该 SKU 暂无模型压缩包', { variant: 'error' }) return } try { setDownloadingSkuId(sku.id) const directDownloadUrl = `/api/materials/${materialId}/skus/${sku.id}/download` await validateAndTriggerDownload(directDownloadUrl) } catch (error) { console.error('下载 SKU 模型失败:', error) const { message } = parseDownloadError(error) showToast(message, { variant: 'error' }) } finally { setDownloadingSkuId(null) } } const handleDownloadWhiteBackground = async (sku: Sku) => { if (!sku.whiteBackgroundUploaded) { showToast('该 SKU 暂无白底图', { variant: 'error' }) return } try { setDownloadingWhiteBgSkuId(sku.id) showToast('正在打包白底图,请稍候', { variant: 'success', duration: 1800 }) const directDownloadUrl = `/api/materials/${materialId}/skus/${sku.id}/download?downloadType=white-background` const response = await fetch(directDownloadUrl, { method: 'GET', cache: 'no-store', credentials: 'include', }) if (!response.ok) { let errorData: unknown = null try { errorData = await response.json() } catch {} throw errorData || { error: '下载 SKU 白底图失败' } } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url const contentDisposition = response.headers.get('Content-Disposition') let fileName = `${sku.skuName}_白底图.zip` if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) if (filenameMatch && filenameMatch[1]) { fileName = decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')) } } link.download = fileName document.body.appendChild(link) link.click() setTimeout(() => { link.remove() window.URL.revokeObjectURL(url) }, 100) } catch (error) { console.error('下载 SKU 白底图失败:', error) const { message } = parseDownloadError(error) showToast(message, { variant: 'error' }) } finally { setDownloadingWhiteBgSkuId(null) } } if (loading) { return
加载中...
} if (skus.length === 0) { return (
暂无 SKU {canEdit && ( )}
) } return (
SKU 列表 ({skus.length}个) {canEdit && ( )}
{selectedSkuIds.size > 0 && ( 已选 {selectedSkuIds.size} 个 )}
{skus.map((sku) => (
handleSelectSku(sku.id)} className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 flex-shrink-0" />
setPreviewImage(sku.previewUrl)} > {sku.skuName}

{sku.skuName}

模型:{sku.zipFilePath && sku.zipFilePath.trim() ? '已上传' : '未上传'} · 白底图:{sku.whiteBackgroundUploaded ? `${sku.whiteBackgroundCount || 0}张` : '未上传'} {sku.whiteBackgroundUploaded && sku.whiteBackgroundComplete === false ? '(未补齐)' : ''}

{sku.whiteBackgroundUploaded && (

{[ sku.whiteBackgroundSourceType ? `出图:${sku.whiteBackgroundSourceType}` : null, sku.whiteBackgroundUploadedAt ? `时间:${new Date(sku.whiteBackgroundUploadedAt).toLocaleDateString('zh-CN')}` : null, ].filter(Boolean).join(' · ')}

)}
))}
{previewImage && (
setPreviewImage(null)} >
预览图
)} {toast && toast.show && }
) }