| import { ReactNode } from 'react'; |
| import { X, FileText, Image, Music, Video } from 'lucide-react'; |
| import { cn } from '@/lib/utils'; |
| import { Button } from '@/components/ui/button'; |
|
|
| interface FileListItemProps { |
| file: File; |
| index?: number; |
| onRemove: () => void; |
| iconColor?: 'yellow' | 'green' | 'blue' | 'gray'; |
| className?: string; |
| } |
|
|
| const iconColorMap = { |
| yellow: 'bg-yellow-100 text-yellow-600', |
| green: 'bg-green-100 text-green-600', |
| blue: 'bg-blue-100 text-blue-600', |
| gray: 'bg-gray-100 text-gray-600', |
| }; |
|
|
| const formatFileSize = (bytes: number): string => { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| }; |
|
|
| const getFileIcon = (file: File): ReactNode => { |
| const ext = file.name.split('.').pop()?.toLowerCase() || ''; |
| const type = file.type; |
|
|
| if (type.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) { |
| return <Image className="h-5 w-5" />; |
| } |
| if (type.startsWith('video/') || ['mp4', 'avi', 'mov', 'mkv', 'webm'].includes(ext)) { |
| return <Video className="h-5 w-5" />; |
| } |
| if (type.startsWith('audio/') || ['mp3', 'wav', 'aac', 'flac', 'ogg'].includes(ext)) { |
| return <Music className="h-5 w-5" />; |
| } |
| return <FileText className="h-5 w-5" />; |
| }; |
|
|
| const getIconColor = (file: File): 'yellow' | 'green' | 'blue' | 'gray' => { |
| const ext = file.name.split('.').pop()?.toLowerCase() || ''; |
| const type = file.type; |
|
|
| if (type.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) { |
| return 'blue'; |
| } |
| if (type.startsWith('audio/') || ['mp3', 'wav', 'aac', 'flac', 'ogg'].includes(ext)) { |
| return 'green'; |
| } |
| if (['md', 'txt', 'html', 'json', 'csv'].includes(ext)) { |
| return 'yellow'; |
| } |
| return 'gray'; |
| }; |
|
|
| export function FileListItem({ |
| file, |
| index, |
| onRemove, |
| iconColor, |
| className, |
| }: FileListItemProps) { |
| const color = iconColor || getIconColor(file); |
| const icon = getFileIcon(file); |
|
|
| return ( |
| <div |
| className={cn( |
| 'flex items-center gap-3 p-3 bg-muted/30 rounded-lg', |
| className |
| )} |
| > |
| {/* 序号 */} |
| {index !== undefined && ( |
| <span |
| className={cn( |
| 'bg-primary text-primary-foreground text-xs px-2 py-0.5 rounded', |
| 'font-medium min-w-[24px] text-center' |
| )} |
| > |
| {index + 1} |
| </span> |
| )} |
| |
| {/* 图标容器 - 40px */} |
| <div |
| className={cn( |
| 'h-10 w-10 rounded-lg flex items-center justify-center', |
| iconColorMap[color] |
| )} |
| > |
| {icon} |
| </div> |
| |
| {/* 文件信息 */} |
| <div className="flex-1 min-w-0"> |
| <p className="text-sm font-medium truncate">{file.name}</p> |
| <p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p> |
| </div> |
| |
| {/* 移除按钮 */} |
| <Button |
| variant="destructive" |
| size="icon" |
| className="h-7 w-7" |
| onClick={onRemove} |
| > |
| <X className="h-4 w-4" /> |
| </Button> |
| </div> |
| ); |
| } |