首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >react 中 使用上传附件 批量上传多个附件,并实现加载功能

react 中 使用上传附件 批量上传多个附件,并实现加载功能

原创
作者头像
小焱
发布2025-08-14 10:04:18
发布2025-08-14 10:04:18
9800
代码可运行
举报
文章被收录于专栏:前端开发前端开发
运行总次数:0
代码可运行

在 React 中实现批量上传附件并显示加载状态,我们可以结合 HTML5 的 File API 和状态管理来实现。下面是一个完整的实现方案,包含文件选择、批量上传、进度显示和上传状态管理等功能。

代码语言:javascript
代码运行次数:0
运行
复制
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;

组件功能说明

这个批量文件上传组件包含以下核心功能:

  1. 多文件选择:通过隐藏的 <input type="file" multiple> 实现,支持一次性选择多个文件
  2. 文件管理:显示已选择的文件列表,包含文件名、大小,并可移除不需要的文件
  3. 批量上传:一键上传所有选中的文件,支持上传状态跟踪
  4. 进度显示:为每个文件显示上传进度条和百分比
  5. 状态反馈:清晰展示每个文件的状态(等待上传、上传中、成功、失败)
  6. 用户体验优化
    • 上传过程中禁用重复上传和删除操作
    • 提供直观的视觉反馈(颜色变化、状态文本)
    • 格式化文件大小,便于用户理解

使用方法

  1. 将组件导入到你的页面中并使用:import FileUploader from './FileUploader'; function App() { return ( <div className="container mx-auto p-4"> <h1 className="text-2xl font-bold mb-6">批量文件上传</h1> <FileUploader /> </div> ); }
  2. 注意事项:
    • 代码中的上传接口 /api/upload 需要替换为你的实际后端接口
    • 可以通过修改 accept 属性限制可上传的文件类型,例如 accept=".jpg,.jpeg,.png,.pdf"
    • 当前实现为顺序上传,若需要并行上传,可修改 uploadAllFiles 方法使用 Promise.all

这个组件使用了基本的 CSS 类名,你可以根据项目的 UI 框架(如 Tailwind、Ant Design 等)进行样式调整,使其与你的应用风格保持一致。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 组件功能说明
  • 使用方法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档