feat(组件): 添加文件预览列表组件

- 实现 FilePreviewItem 展示单个文件信息
- 根据文件类型显示对应图标和颜色
- 图片文件显示缩略图预览
- 显示文件名、大小和上传状态
- 支持上传进度条展示
- 提供悬浮删除按钮
This commit is contained in:
gaoziman 2025-12-20 12:13:35 +08:00
parent cb01e2dffb
commit d98e540037

View File

@ -0,0 +1,147 @@
'use client';
import { X, File, FileImage, FileText, FileSpreadsheet, FileCode, FileArchive, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { UploadFile, FileType, formatFileSize } from '@/types/file-upload';
import Image from 'next/image';
interface FilePreviewListProps {
files: UploadFile[];
onRemove: (fileId: string) => void;
className?: string;
}
// 根据文件类型获取图标
function getFileIcon(fileType: FileType) {
switch (fileType) {
case 'image':
return FileImage;
case 'pdf':
case 'document':
return FileText;
case 'spreadsheet':
return FileSpreadsheet;
case 'code':
case 'markdown':
return FileCode;
case 'archive':
return FileArchive;
default:
return File;
}
}
// 根据文件类型获取颜色
function getFileColor(fileType: FileType): string {
switch (fileType) {
case 'image':
return 'text-blue-500';
case 'pdf':
return 'text-red-500';
case 'document':
return 'text-blue-600';
case 'spreadsheet':
return 'text-green-500';
case 'code':
case 'markdown':
return 'text-purple-500';
case 'archive':
return 'text-orange-500';
default:
return 'text-gray-500';
}
}
interface FilePreviewItemProps {
file: UploadFile;
onRemove: (fileId: string) => void;
}
function FilePreviewItem({ file, onRemove }: FilePreviewItemProps) {
const Icon = getFileIcon(file.type);
const colorClass = getFileColor(file.type);
return (
<div
className={cn(
'relative group flex items-center gap-2 p-2 rounded-lg border transition-all',
'bg-[var(--color-bg-secondary)] border-[var(--color-border)]',
'hover:border-[var(--color-border-hover)]',
file.status === 'error' && 'border-red-500 bg-red-50 dark:bg-red-950/20'
)}
>
{/* 文件预览/图标 */}
<div className="flex-shrink-0 w-10 h-10 rounded-lg overflow-hidden bg-[var(--color-bg-tertiary)] flex items-center justify-center">
{file.type === 'image' && file.previewUrl ? (
<Image
src={file.previewUrl}
alt={file.name}
width={40}
height={40}
className="w-full h-full object-cover"
/>
) : (
<Icon className={cn('w-5 h-5', colorClass)} />
)}
</div>
{/* 文件信息 */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate" title={file.name}>
{file.name}
</p>
<p className="text-xs text-[var(--color-text-tertiary)]">
{formatFileSize(file.size)}
{file.status === 'error' && file.error && (
<span className="text-red-500 ml-2">{file.error}</span>
)}
</p>
</div>
{/* 状态指示器 */}
{file.status === 'uploading' && (
<div className="flex-shrink-0">
<Loader2 className="w-4 h-4 animate-spin text-[var(--color-primary)]" />
</div>
)}
{/* 进度条 */}
{file.status === 'uploading' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-gray-200 dark:bg-gray-700 rounded-b-lg overflow-hidden">
<div
className="h-full bg-[var(--color-primary)] transition-all duration-300"
style={{ width: `${file.uploadProgress}%` }}
/>
</div>
)}
{/* 删除按钮 */}
<button
onClick={() => onRemove(file.id)}
className={cn(
'flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center',
'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]',
'hover:bg-[var(--color-bg-hover)] transition-colors',
'opacity-0 group-hover:opacity-100'
)}
title="移除文件"
>
<X className="w-4 h-4" />
</button>
</div>
);
}
export function FilePreviewList({ files, onRemove, className }: FilePreviewListProps) {
if (files.length === 0) {
return null;
}
return (
<div className={cn('flex flex-wrap gap-2', className)}>
{files.map((file) => (
<FilePreviewItem key={file.id} file={file} onRemove={onRemove} />
))}
</div>
);
}