Download apps/web/src/components/shared/FileListItem.tsx from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 3.23 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/components/shared/FileListItem.tsx
- Command line
-
hf download hf://spaces/hunian003/message/apps/web/src/components/shared/FileListItem.tsx
-
curl -L -o FileListItem.tsx https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/components/shared/FileListItem.tsx
3.23 kB
| 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> | |
| ); | |
| } |