Files
test1/components/nova-sdk/message-input/FilePreviewList.tsx
2026-03-20 07:33:46 +00:00

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]
}