gongxiangp2/components/upload/StandardModelSkuZipUpload.tsx
2026-07-24 18:15:27 +08:00

183 lines
7.3 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 } from 'react'
import {
hasAllowedFileExtension,
hasDragLeftDropZone,
prepareFileDrag,
} from './drag-drop-utils'
interface StandardModelSkuZipUploadProps {
selectedFile: File | null
existingFileName?: string
existingFileSize?: string | number
uploading?: boolean
error?: string
onSelectFile: (file: File) => void
onRemoveFile: () => void
}
function formatFileSize(bytes: string | number | undefined): string {
const size = typeof bytes === 'string' ? parseInt(bytes, 10) : Number(bytes || 0)
if (!size || Number.isNaN(size)) {
return '未知'
}
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(Math.floor(Math.log(size) / Math.log(1024)), units.length - 1)
return `${(size / Math.pow(1024, index)).toFixed(2)} ${units[index]}`
}
function isSupportedZipFile(file: File): boolean {
return hasAllowedFileExtension(file, ['zip', 'rar', '7z'])
}
export default function StandardModelSkuZipUpload({
selectedFile,
existingFileName = '',
existingFileSize,
uploading = false,
error,
onSelectFile,
onRemoveFile,
}: StandardModelSkuZipUploadProps) {
const [isDragging, setIsDragging] = useState(false)
const [validationError, setValidationError] = useState('')
const hasExistingFile = !!existingFileName.trim()
const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
prepareFileDrag(event, uploading)
setIsDragging(false)
if (uploading) {
setValidationError('文件正在上传,请等待完成后再更换')
return
}
const file = event.dataTransfer.files?.[0]
if (!file || !isSupportedZipFile(file)) {
setValidationError('压缩包仅支持 ZIP、RAR、7Z 格式')
return
}
setValidationError('')
onSelectFile(file)
}
return (
<div className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
模型压缩包 {selectedFile ? '已选择' : hasExistingFile ? '可重传' : '待上传'}
</span>
{uploading && (
<span className="inline-flex items-center rounded-full bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
上传处理中
</span>
)}
</div>
<div className="mt-3 rounded-xl border border-dashed border-gray-200 bg-gray-50/80 px-3 py-2 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-900/30 dark:text-gray-300">
{selectedFile
? `待上传:${selectedFile.name}(${formatFileSize(selectedFile.size)})`
: hasExistingFile
? `当前模型:${existingFileName}(${formatFileSize(existingFileSize)})`
: '当前还没有模型压缩包,请先选择要上传的文件。'}
</div>
{(error || validationError) && (
<div className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300">
{error || validationError}
</div>
)}
<div className="mt-5">
<input
id="standard-model-sku-zip-upload"
type="file"
accept=".zip,.rar,.7z"
className="hidden"
disabled={uploading}
onChange={(event) => {
const file = event.target.files?.[0]
if (file && isSupportedZipFile(file)) {
setValidationError('')
onSelectFile(file)
} else if (file) {
setValidationError('压缩包仅支持 ZIP、RAR、7Z 格式')
}
event.currentTarget.value = ''
}}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: 上传槽位需要拖拽和点击交互 */}
<label
htmlFor="standard-model-sku-zip-upload"
aria-disabled={uploading}
className={`group block min-h-40 rounded-2xl border-2 border-dashed p-4 transition-colors ${
uploading ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
} ${
isDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800/40 hover:border-gray-400 dark:hover:border-gray-500'
}`}
onDragEnter={(event) => {
prepareFileDrag(event, uploading)
if (uploading) return
setIsDragging(true)
}}
onDragLeave={(event) => {
event.preventDefault()
event.stopPropagation()
if (hasDragLeftDropZone(event)) {
setIsDragging(false)
}
}}
onDragOver={(event) => prepareFileDrag(event, uploading)}
onDrop={handleDrop}
>
{selectedFile ? (
<div className="flex min-h-32 flex-col items-center justify-center text-center">
<div className="relative flex w-full max-w-sm flex-col items-center rounded-2xl border border-gray-200 bg-white/90 px-4 py-6 dark:border-gray-700 dark:bg-gray-900/40">
<button
type="button"
aria-label="移除已选择的模型压缩包"
disabled={uploading}
className="absolute right-3 top-3 inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/55 text-sm font-semibold text-white transition hover:bg-black/70"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onRemoveFile()
}}
>
×
</button>
<svg className="h-8 w-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="mt-3 max-w-full truncate text-sm font-medium text-gray-900 dark:text-white">
{selectedFile.name}
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(selectedFile.size)}
</p>
<p className="mt-3 text-[11px] text-gray-500 dark:text-gray-400">
点击或拖拽可重新选择模型压缩包
</p>
</div>
</div>
) : (
<div className="flex min-h-32 flex-col items-center justify-center text-center">
<svg className="h-8 w-8 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span className="mt-3 text-sm font-medium text-gray-700 dark:text-gray-200">拖拽压缩包到这里,或点击上传</span>
<span className="mt-1 text-xs text-gray-500 dark:text-gray-400">支持 ZIP / RAR / 7Z</span>
</div>
)}
</label>
</div>
</div>
)
}