在 React 中实现批量上传附件并显示加载状态,我们可以结合 HTML5 的 File API 和状态管理来实现。下面是一个完整的实现方案,包含文件选择、批量上传、进度显示和上传状态管理等功能。
import React, { useState, useRef } from 'react';
const FileUploader = () => {
// 存储选中的文件列表
const [files, setFiles] = useState([]);
// 存储每个文件的上传进度
const [uploadProgress, setUploadProgress] = useState({});
// 存储每个文件的上传状态 (idle, uploading, success, error)
const [uploadStatus, setUploadStatus] = useState({});
// 文件输入框的引用
const fileInputRef = useRef(null);
// 处理文件选择
const handleFileSelect = (e) => {
const selectedFiles = Array.from(e.target.files);
if (selectedFiles.length > 0) {
// 为每个文件添加唯一标识和初始状态
const newFiles = selectedFiles.map(file => ({
id: Date.now() + Math.random().toString(36).substring(2, 9),
file,
name: file.name,
size: file.size,
type: file.type
}));
// 将新选择的文件添加到现有列表
setFiles(prev => [...prev, ...newFiles]);
// 初始化上传进度和状态
newFiles.forEach(file => {
setUploadProgress(prev => ({ ...prev, [file.id]: 0 }));
setUploadStatus(prev => ({ ...prev, [file.id]: 'idle' }));
});
// 清空输入框,允许重复选择相同文件
e.target.value = '';
}
};
// 处理单个文件上传
const uploadFile = async (fileItem) => {
const formData = new FormData();
formData.append('file', fileItem.file);
// 更新文件状态为上传中
setUploadStatus(prev => ({ ...prev, [fileItem.id]: 'uploading' }));
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// 监听上传进度
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
setUploadProgress(prev => ({ ...prev, [fileItem.id]: percent }));
}
});
if (!response.ok) {
throw new Error('上传失败');
}
// 上传成功
setUploadStatus(prev => ({ ...prev, [fileItem.id]: 'success' }));
return true;
} catch (error) {
console.error('上传错误:', error);
setUploadStatus(prev => ({ ...prev, [fileItem.id]: 'error' }));
return false;
}
};
// 批量上传所有未上传的文件
const uploadAllFiles = async () => {
const filesToUpload = files.filter(file => uploadStatus[file.id] !== 'uploading');
for (const file of filesToUpload) {
// 按顺序上传,可改为Promise.all并行上传
await uploadFile(file);
}
};
// 移除单个文件
const removeFile = (id) => {
setFiles(prev => prev.filter(file => file.id !== id));
// 清理进度和状态
const newProgress = { ...uploadProgress };
const newStatus = { ...uploadStatus };
delete newProgress[id];
delete newStatus[id];
setUploadProgress(newProgress);
setUploadStatus(newStatus);
};
// 触发文件选择对话框
const triggerFileSelect = () => {
fileInputRef.current.click();
};
// 格式化文件大小
const formatFileSize = (bytes) => {
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];
};
return (
<div className="file-uploader">
<div className="upload-controls mb-4">
<button
onClick={triggerFileSelect}
className="select-btn bg-blue-500 text-white px-4 py-2 rounded mr-2 hover:bg-blue-600 transition"
>
选择文件
</button>
<button
onClick={uploadAllFiles}
disabled={files.length === 0 || Object.values(uploadStatus).some(status => status === 'uploading')}
className="upload-btn bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition disabled:opacity-50"
>
批量上传
</button>
{/* 隐藏的文件输入框 */}
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
className="hidden"
accept="*" // 可根据需要限制文件类型,如 ".jpg,.pdf,.doc"
/>
</div>
{/* 文件列表和上传进度 */}
<div className="file-list">
{files.length === 0 ? (
<div className="empty-state text-gray-500 italic">
未选择任何文件
</div>
) : (
<ul className="space-y-3">
{files.map(file => (
<li
key={file.id}
className="file-item p-3 border rounded-lg flex flex-col"
>
<div className="file-info flex justify-between items-center mb-2">
<div>
<span className="file-name font-medium">{file.name}</span>
<span className="file-size text-sm text-gray-500 ml-2">
({formatFileSize(file.size)})
</span>
</div>
<button
onClick={() => removeFile(file.id)}
disabled={uploadStatus[file.id] === 'uploading'}
className="remove-btn text-red-500 hover:text-red-700 disabled:opacity-50"
>
×
</button>
</div>
{/* 上传进度条 */}
<div className="progress-container h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`progress-bar h-full transition-all duration-300 ${
uploadStatus[file.id] === 'success'
? 'bg-green-500'
: uploadStatus[file.id] === 'error'
? 'bg-red-500'
: 'bg-blue-500'
}`}
style={{ width: `${uploadProgress[file.id]}%` }}
/>
</div>
{/* 上传状态文本 */}
<div className="status-text text-sm mt-1">
{uploadStatus[file.id] === 'idle' && '等待上传'}
{uploadStatus[file.id] === 'uploading' && `上传中: ${uploadProgress[file.id]}%`}
{uploadStatus[file.id] === 'success' && (
<span className="text-green-600">上传成功</span>
)}
{uploadStatus[file.id] === 'error' && (
<span className="text-red-600">上传失败,请重试</span>
)}
</div>
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default FileUploader;
这个批量文件上传组件包含以下核心功能:
<input type="file" multiple>
实现,支持一次性选择多个文件/api/upload
需要替换为你的实际后端接口accept
属性限制可上传的文件类型,例如 accept=".jpg,.jpeg,.png,.pdf"
uploadAllFiles
方法使用 Promise.all
这个组件使用了基本的 CSS 类名,你可以根据项目的 UI 框架(如 Tailwind、Ant Design 等)进行样式调整,使其与你的应用风格保持一致。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。