'use client' import { useMemo, useRef, useState } from 'react' import { CERTIFICATE_REPORT_MATERIAL_TYPE, detectFileKindFromName } from '@/lib/certificate-report' interface CategoryItem { id: string name: string } interface CertificateReportUploaderProps { onSuccess: () => void onCancel: () => void productCategories: CategoryItem[] } interface ReportFileItem { file: File previewUrl?: string sortOrder: number fileKind: 'pdf' | 'image' } export default function CertificateReportUploader({ onSuccess, onCancel, productCategories, }: CertificateReportUploaderProps) { const [productModel, setProductModel] = useState('') const [reportTitle, setReportTitle] = useState('') const [productCategoryName, setProductCategoryName] = useState('') const [platformName, setPlatformName] = useState('') const [files, setFiles] = useState([]) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [processingStep, setProcessingStep] = useState('') const [error, setError] = useState('') const fileInputRef = useRef(null) const totalSizeText = useMemo(() => { const totalBytes = files.reduce((sum, item) => sum + item.file.size, 0) if (totalBytes <= 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] const unitIndex = Math.floor(Math.log(totalBytes) / Math.log(1024)) return `${(totalBytes / Math.pow(1024, unitIndex)).toFixed(2)} ${units[unitIndex]}` }, [files]) const handleFileSelect = (selectedFiles: FileList | null) => { if (!selectedFiles || selectedFiles.length === 0) { return } const currentLength = files.length const nextFiles: ReportFileItem[] = [] for (let index = 0; index < selectedFiles.length; index += 1) { const file = selectedFiles[index] const fileKind = detectFileKindFromName(file.name) const isPdf = fileKind === 'pdf' const isImage = file.type.startsWith('image/') if (!isPdf && !isImage) { continue } nextFiles.push({ file, previewUrl: isImage ? URL.createObjectURL(file) : undefined, sortOrder: currentLength + nextFiles.length, fileKind, }) } if (nextFiles.length === 0) { setError('请选择 PDF 或图片文件') if (fileInputRef.current) { fileInputRef.current.value = '' } return } setFiles((prev) => [...prev, ...nextFiles]) setError('') if (fileInputRef.current) { fileInputRef.current.value = '' } } const removeFile = (index: number) => { setFiles((prev) => { const target = prev[index] if (target?.previewUrl) { URL.revokeObjectURL(target.previewUrl) } return prev .filter((_, currentIndex) => currentIndex !== index) .map((item, currentIndex) => ({ ...item, sortOrder: currentIndex, })) }) } const handleSubmit = async () => { if (!productModel.trim() && !reportTitle.trim()) { setError('SPU 和 报告名称/检测对象 至少填写一个') return } if (!productCategoryName.trim()) { setError('请选择品类') return } if (files.length === 0) { setError('请至少上传一个 PDF 或图片文件') return } setUploading(true) setUploadProgress(0) setProcessingStep('初始化上传...') setError('') try { const initResponse = await fetch('/api/materials/upload/certificate-report/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productModel: productModel.trim(), reportTitle: reportTitle.trim(), productCategoryName: productCategoryName.trim(), platformName: platformName.trim(), }), }) if (!initResponse.ok) { const errorData = await initResponse.json().catch(() => ({})) throw new Error(errorData.error || '初始化失败') } const { materialId } = await initResponse.json() const { chunkUpload } = await import('@/lib/chunk-upload') const uploadedFiles: Array<{ uploadId: string fileName: string fileSize: number sortOrder: number fileKind: 'pdf' | 'image' }> = [] setProcessingStep('上传文件中...') for (let index = 0; index < files.length; index += 1) { const current = files[index] const result = await chunkUpload( current.file, { materialType: CERTIFICATE_REPORT_MATERIAL_TYPE, fileName: current.file.name, }, { skipComplete: true, onProgress: (progress) => { const fileProgress = (index + progress / 100) / files.length const totalProgress = 10 + fileProgress * 70 setUploadProgress(Math.round(totalProgress)) }, }, ) uploadedFiles.push({ uploadId: result.uploadId, fileName: current.file.name, fileSize: current.file.size, sortOrder: current.sortOrder, fileKind: current.fileKind, }) } setUploadProgress(82) setProcessingStep('生成分享内容...') const completeResponse = await fetch('/api/materials/upload/certificate-report/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ materialId, files: uploadedFiles, }), }) if (!completeResponse.ok) { const errorData = await completeResponse.json().catch(() => ({})) throw new Error(errorData.error || '完成上传失败') } setUploadProgress(100) setProcessingStep('完成') files.forEach((item) => { if (item.previewUrl) { URL.revokeObjectURL(item.previewUrl) } }) setTimeout(() => { onSuccess() }, 300) } catch (submitError) { console.error('[Certificate Report Upload]', submitError) setError(submitError instanceof Error ? submitError.message : '上传失败,请稍后再试') setProcessingStep('') setUploading(false) return } } return (
表格里只需要放分享链接。对方点击后,会在分享页里看到这组检测报告的 PDF 和图片内容。
{error && (
{error}
)}
setProductModel(event.target.value)} placeholder="没有 SPU 可以留空" className="input-apple" />
setReportTitle(event.target.value)} placeholder="例如:E0级颗粒板检测报告" className="input-apple" />

SPU 和这个字段至少填写一个

setPlatformName(event.target.value)} placeholder="可选" className="input-apple" />
handleFileSelect(event.target.files)} className="hidden" />

可以混合上传多份 PDF 和多张图片,分享时会按这里的顺序展示

{files.length > 0 && (
文件列表
{files.length} 个文件 · {totalSizeText}
{files.map((item, index) => (
{item.fileKind === 'image' && item.previewUrl ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( PDF )}
{item.file.name}
{item.fileKind === 'pdf' ? 'PDF 报告' : '图片附件'} · {(item.file.size / 1024 / 1024).toFixed(2)} MB
))}
)} {uploading && (
{processingStep || '上传中...'} {uploadProgress}%
)}
) }