初始化模版工程
This commit is contained in:
114
components/nova-sdk/message-input/FilePreviewList.tsx
Normal file
114
components/nova-sdk/message-input/FilePreviewList.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user