183 lines
7.3 KiB
TypeScript
183 lines
7.3 KiB
TypeScript
'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>
|
||
)
|
||
}
|