115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import { X, FileIcon, Loader2 } from 'lucide-react'
|
|
import type { UploadFile } from '../types'
|
|
import { cn } from '@/utils/cn'
|
|
import { ImagePreview } from '@/components/ui/image-preview'
|
|
import { Image } from '@/components/ui/image'
|
|
|
|
interface FilePreviewListProps {
|
|
files: UploadFile[]
|
|
onRemove: (uid: string) => void
|
|
disabled?: boolean
|
|
}
|
|
|
|
export function FilePreviewList({ files, onRemove, disabled }: FilePreviewListProps) {
|
|
if (files.length === 0) return null
|
|
|
|
return (
|
|
<div className="flex flex-wrap gap-2 border-b border-border/80 px-0 pb-3">
|
|
{files.map(file => {
|
|
const isImage = /\.(jpg|jpeg|png|gif|webp|svg|ico|bmp)$/i.test(file.name)
|
|
const isUploading = file.uploadStatus === 'uploading' || file.uploadStatus === 'pending'
|
|
const hasError = file.uploadStatus === 'error'
|
|
|
|
const showImagePreview = isImage && file.url
|
|
|
|
return (
|
|
<div
|
|
key={file.uid}
|
|
className={cn(
|
|
'relative group flex max-w-[200px] items-center gap-2 rounded-md border bg-card/82 px-3 py-1.5 text-xs backdrop-blur-sm',
|
|
hasError ? 'border-red-200 dark:border-red-800' : 'border-border/80',
|
|
file.url && !isUploading ? 'cursor-pointer hover:border-primary/50' : ''
|
|
)}
|
|
onClick={() => {
|
|
if (file.url && !isUploading && !hasError) {
|
|
if (!showImagePreview) {
|
|
window.open(file.url, '_blank')
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{/* Icon / Image Preview */}
|
|
<div
|
|
className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded bg-accent/60 text-muted-foreground"
|
|
>
|
|
{isUploading ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : showImagePreview ? (
|
|
<ImagePreview
|
|
src={file.url!}
|
|
alt={file.name}
|
|
className="w-full h-full flex items-center justify-center"
|
|
>
|
|
<Image
|
|
src={file.url!}
|
|
alt={file.name}
|
|
className="w-full h-full object-cover"
|
|
onError={(e) => {
|
|
e.currentTarget.style.display = 'none'
|
|
e.currentTarget.parentElement?.classList.add('fallback-icon')
|
|
}}
|
|
/>
|
|
</ImagePreview>
|
|
) : (
|
|
<FileIcon className="w-4 h-4" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate font-medium text-foreground" title={file.name}>
|
|
{file.name}
|
|
</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{isUploading ? '上传中...' : hasError ? '上传失败' : formatFileSize(file.byte_size)}
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation() // 防止触发打开文件
|
|
onRemove(file.uid)
|
|
}}
|
|
disabled={disabled}
|
|
className="shrink-0 rounded-full p-0.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-accent"
|
|
>
|
|
<X className="h-3 w-3 text-muted-foreground" />
|
|
</button>
|
|
|
|
{/* Progress Bar */}
|
|
{isUploading && (
|
|
<div className="absolute bottom-0 left-0 h-0.5 w-full overflow-hidden rounded-b-md bg-accent">
|
|
<div
|
|
className="h-full bg-primary transition-all duration-300"
|
|
style={{ width: `${file.progress || 0}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatFileSize(bytes?: number) {
|
|
if (!bytes) return ''
|
|
if (bytes < 1024) return bytes + ' B'
|
|
const k = 1024
|
|
const sizes = ['KB', 'MB', 'GB']
|
|
const index = Math.min(
|
|
Math.floor(Math.log(bytes) / Math.log(k)) - 1,
|
|
sizes.length - 1,
|
|
)
|
|
return parseFloat((bytes / Math.pow(k, index + 1)).toFixed(1)) + ' ' + sizes[index]
|
|
}
|