You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React文件上传组件:并行进度条改串行执行方案求助

文件上传进度串行执行实现方案

当前问题:文件上传组件中所有文件的进度计时器并行运行,需要改为串行执行——完成一个文件的上传进度后,再启动下一个文件的上传。

核心修改思路

现有实现中每个UploadFile子组件挂载时自动启动自身的上传计时器,导致并行执行。需要把上传的调度逻辑移到父组件,由父组件统一管理上传队列,控制串行执行的顺序。

具体修改步骤

  1. 改造UploadFile组件

    • 移除组件挂载时自动启动上传的useEffect
    • 暴露startUpload方法,由父组件触发上传动作
    • 新增上传完成/终止的回调,通知父组件切换到下一个文件
  2. 父组件添加串行调度逻辑

    • 维护上传状态,跟踪当前是否有文件正在上传
    • 监听文件列表变化,当无文件上传时,自动启动下一个待上传文件

修改后的代码

Upload.tsx(子组件)

import * as React from "react";
import { Stack, Button, ListItem } from "@material-ui/core";
import { useRef, forwardRef, useImperativeHandle } from "react";
import { Progress } from "./Progress";

export interface FileType {
  file: File;
  isDeleted: boolean;
  uploadStatus: number;
  uploadError: string;
  uploadAbort: boolean;
}

interface FileUploaderProps {
  selectedFiles: FileType[];
  setSelectedFiles: React.Dispatch<React.SetStateAction<FileType[]>>;
  file: FileType;
  onUploadComplete?: () => void;
}

const UploadFile = forwardRef(({ file, setSelectedFiles, onUploadComplete }: FileUploaderProps, ref) => {
  const timerRef = useRef<ReturnType<typeof setInterval> | undefined>();
  const pollRef = useRef<number>(file.uploadStatus);

  const updateProgress = () => {
    if (pollRef.current === 100) {
      clearInterval(timerRef.current);
      onUploadComplete?.();
      return;
    }
    const diff = Math.random() * 15;
    setSelectedFiles((prevState) => {
      return prevState.map((current) => {
        if (current.file.name === file.file.name) {
          const newProgress = Math.min(current.uploadStatus + diff, 100);
          pollRef.current = newProgress;
          return {
            ...current,
            uploadStatus: newProgress
          };
        }
        return current;
      });
    });
  };

  const startUpload = () => {
    if (file.uploadStatus === 100 || file.uploadAbort || file.isDeleted || timerRef.current) return;
    
    timerRef.current = setInterval(updateProgress, 100 * Math.floor(Math.random() * 6) + 250);
  };

  const cancelTimer = (e: React.MouseEvent<HTMLSpanElement>) => {
    e.stopPropagation();
    clearInterval(timerRef.current);
    timerRef.current = undefined;
    setSelectedFiles((prevState) => {
      return prevState.map((current) => {
        if (current.file.name === file.file.name) {
          return {
            ...current,
            uploadAbort: true,
            uploadStatus: 0,
            uploadError: "File Upload cancelled"
          };
        }
        return current;
      });
    });
    onUploadComplete?.();
  };

  useImperativeHandle(ref, () => ({
    startUpload
  }));

  return (
    <ListItem key={file.file.name} className="file-name">
      <span style={{ padding: "10px" }}>{file.file.name}</span>
      {file.uploadStatus !== 100 ? (
        file.uploadError || file.uploadAbort ? (
          <div className="upload-error">
            <span>{file.file.name}</span>
            <span style={{ padding: "10px" }}>{file.uploadError}</span>
          </div>
        ) : (
          <>
            <span>Uploading...</span>
            <Stack sx={{ width: "80px", padding: "20px" }} spacing={2}>
              <Progress progress={file.uploadStatus} />
            </Stack>
            <Button variant="contained" color="error" onClick={cancelTimer}>
              Cancel
            </Button>
          </>
        )
      ) : (
        <>
          <span className="primary">{file.file.name}</span>
        </>
      )}
      {file.uploadStatus === 100 && (
        <Button
          variant="contained"
          color="error"
          className="mgmt-Delete-icon"
          style={{ padding: "10px" }}
          onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
            e.stopPropagation();
            setSelectedFiles((prevState) => {
              return prevState.map((current) => {
                if (current.file.name === file.file.name) {
                  return {
                    ...current,
                    isDeleted: true
                  };
                }
                return current;
              });
            });
          }}
        >
          Delete
        </Button>
      )}
    </ListItem>
  );
});

export default UploadFile;

父组件示例(如App.tsx)

import { useState, useEffect, useRef } from "react";
import UploadFile, { FileType } from "./Upload";

export default function App() {
  const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]);
  const fileRefs = useRef<{[key: string]: { startUpload: () => void }}>({});

  // 监听文件列表变化,启动下一个上传
  useEffect(() => {
    // 找到第一个待上传的文件:未完成、未取消、未删除
    const nextFile = selectedFiles.find(
      f => f.uploadStatus < 100 && !f.uploadAbort && !f.isDeleted
    );

    if (nextFile && fileRefs.current[nextFile.file.name]) {
      fileRefs.current[nextFile.file.name].startUpload();
    }
  }, [selectedFiles]);

  // 处理文件选择
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (!files) return;
    const newFiles: FileType[] = Array.from(files).map(file => ({
      file,
      isDeleted: false,
      uploadStatus: 0,
      uploadError: "",
      uploadAbort: false
    }));
    setSelectedFiles(prev => [...prev, ...newFiles]);
  };

  return (
    <div className="App">
      <input type="file" multiple onChange={handleFileChange} />
      {selectedFiles.map(file => (
        <UploadFile
          key={file.file.name}
          file={file}
          selectedFiles={selectedFiles}
          setSelectedFiles={setSelectedFiles}
          onUploadComplete={() => {
            // 上传完成后触发父组件重新检查队列
            setSelectedFiles([...selectedFiles]);
          }}
          ref={el => {
            if (el) fileRefs.current[file.file.name] = el;
          }}
        />
      ))}
    </div>
  );
}

关键逻辑说明

  • 子组件通过forwardRef和useImperativeHandle暴露startUpload方法,让父组件控制上传时机
  • 父组件通过useEffect监听文件列表状态,每次状态变化时检查是否有可上传的文件,启动第一个符合条件的文件
  • 当文件上传完成或被取消时,通过onUploadComplete回调通知父组件,触发下一轮的队列检查

内容的提问来源于stack exchange,提问作者user16860065

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 09:49:15