当前位置: 首页 > news >正文

React-手写支持多文件、并行上传、串行上传、分片上传、单文件上传、失败自动重试、自动上传/手动按钮上传切换

import { useState } from "react";
import { useModel } from "@umijs/max";
import {getUploadFileId,uploadFileChunk,uploadFileFinish,
} from "@/services/ant-design-pro/material";
import { getVideoMD5 } from "@/utils/md5Utils";// 默认配置
const defaultConfig = {chunkSize: 5 * 1024 * 1024, // 每个分片大小,默认 5MBretryLimit: 3,               // 每个分片失败后的重试次数autoUpload: false,            // 是否自动上传multiple: true,               // 是否多选concurrent: false,           // 是否开启多文件并发上传maxConcurrent: 3,            // 最大并发数extraParams: {},             // 额外参数(可扩展传给后端)allowedTypes: [],             // 允许上传的文件类型,例如 ['video/mp4', 'video/mov']
};export default function MultiFileChunkUploader({ config = {} }) {const options = { ...defaultConfig, ...config };const [files, setFiles] = useState([]); // 保存多文件的状态
const { initialState } = useModel("@@initialState");const { currentUser } = initialState || {};const advertiser_id = currentUser?.advertiser_id;const accessToken = localStorage.getItem("accessToken");const headers = {"Access-Token": accessToken,"Content-Type": "application/json",}/*** 🔥 第一步:请求后端获取  upload_id* 每个文件上传前都要先拿到 upload_id*/const getFileId = async (file) => {const params = {advertiser_id,size: file.size,name: file.name,content_type: "video",};const res = await getUploadFileId(params, headers);if (res.code === 0) {return res.data; // { upload_id, ... }} else {console.error("获取文件ID失败", res.message);return null;}};/*** 🔥 第二步:上传单个分片* 支持重试机制,失败会重新请求,直到超过 retryLimit*/const uploadChunk = async (file, fileRef, chunk, index, retries = 0) => {try {console.log(index * chunk.size, "index * chunk.size");const signature = await getVideoMD5(chunk);const params = {advertiser_id,upload_id: fileRef.upload_id,signature,start_offset: index * options.chunkSize, // 当前分片在文件中的偏移量
        file: chunk,...options.extraParams,};let chunkHeaders = {}// ⚠️ 你需要替换这里:分片上传的接口调用Object.assign(chunkHeaders, headers, { "Content-Type": "multipart/form-data" })const res = await uploadFileChunk(params, chunkHeaders);console.log(res, "uploadFileChunk-res");// 🔥 后端返回 code 校验if (res.code !== 0) {// 如果重试次数未到上限,继续重试if (retries < options.retryLimit) {console.warn(`Chunk ${index} 返回 code=${res.code},重试中...(${retries + 1})`);return uploadChunk(file, fileRef, chunk, index, retries + 1);} else {// 超过重试次数,抛出错误throw new Error(`Chunk ${index} 上传失败,code=${res.code}, message=${res.message}`);}}return res.data; // ⚡ 后端需要返回 { start_offset, end_offset }} catch (err) {if (retries < options.retryLimit) {console.warn(`分片索引: ${index} 异常重试第几次: (${retries + 1})`);return uploadChunk(file, fileRef, chunk, index, retries + 1);} else {throw err;}}};/*** 🔥 第三步:通知后端文件已上传完成*/const notifyComplete = async (fileRef) => {const params = {advertiser_id,upload_id: fileRef.upload_id,};await uploadFileFinish(params, headers);};/*** 🔥 主流程:上传单个文件*/const handleFileUpload = async (fileIndex) => {const fileObj = files[fileIndex];const { file } = fileObj;// 修改文件状态为 uploadingsetFiles((prev) =>prev.map((f, i) =>i === fileIndex ? { ...f, status: "uploading", progress: 0 } : f));// 先请求 upload_idconst fileRef = await getFileId(file);if (!fileRef) {setFiles((prev) =>prev.map((f, i) =>i === fileIndex ? { ...f, status: "error" } : f));return;}// 分片上传const chunkSize = options.chunkSize;const chunks = Math.ceil(file.size / chunkSize);let uploaded = 0;for (let i = 0; i < chunks; i++) {const start = i * chunkSize;const end = Math.min(file.size, start + chunkSize);const chunk = file.slice(start, end);// 上传分片const res = await uploadChunk(file, fileRef, chunk, i);uploaded++;const progress = Math.round((uploaded / chunks) * 100);// 更新文件进度setFiles((prev) =>prev.map((f, j) =>j === fileIndex ? { ...f, progress, status: "uploading" } : f));// 判断是否所有分片已完成if (res?.start_offset === res?.end_offset) {await notifyComplete(fileRef);break;}}// 更新状态为 donesetFiles((prev) =>prev.map((f, i) =>i === fileIndex ? { ...f, progress: 100, status: "done" } : f));};/*** 🔥 上传所有文件* 根据 concurrent 决定是串行上传还是并发上传*/const handleAllUpload = async () => {if (!options.concurrent) {// 串行上传for (let i = 0; i < files.length; i++) {await handleFileUpload(i);}} else {// 并发上传,限制最大同时上传数const queue = [...files.keys()]; // 文件索引队列let activeCount = 0;return new Promise((resolve) => {const next = async () => {if (queue.length === 0 && activeCount === 0) {resolve();return;}while (activeCount < options.maxConcurrent && queue.length > 0) {const fileIndex = queue.shift();activeCount++;handleFileUpload(fileIndex).finally(() => {activeCount--;next();});}};next();});}};/*** 🔥 选择文件* 支持多选文件*/const handleFileChange = (e) => {// const selectedFiles = Array.from(e.target.files).map((f) => ({//   file: f,//   progress: 0,//   status: "waiting", // waiting | uploading | done | error// }));const selectedFiles = Array.from(e.target.files).filter(f => {// 如果 allowedTypes 配置了,过滤不允许的类型if (options.allowedTypes.length > 0 && !options.allowedTypes.includes(f.type)) {console.warn(`文件类型不允许: ${f.name}`);return false;}return true;}).map(f => ({file: f,progress: 0,status: "waiting",}));setFiles((prev) => [...prev, ...selectedFiles]);if (options.autoUpload) {setTimeout(() => handleAllUpload(), 0);}};// 限制文件选择对话框中可选文件类型const acceptAttr = options.allowedTypes.length > 0? options.allowedTypes.join(","): "*/*";return (<div className="p-4 border rounded-xl shadow-md w-[500px]"><input type="file" accept={acceptAttr} multiple={options.multiple} onChange={handleFileChange} />
{/* 手动上传按钮 */}{!options.autoUpload && files.length > 0 && (<buttononClick={handleAllUpload}className="mt-2 px-4 py-2 bg-blue-500 text-white rounded-lg">{options.concurrent ? `并发上传(最多 ${options.maxConcurrent})` : "串行上传"}</button>
      )}{/* 文件列表 + 进度条 */}<div className="mt-4 space-y-3">{files.map((f, index) => (<div key={index} className="border rounded p-2 shadow-sm"><div className="flex justify-between text-sm"><span>{f.file.name}</span><span>{f.progress}%</span></div><div className="w-full bg-gray-200 h-3 rounded mt-1"><divclassName="bg-green-500 h-3 rounded"style={{ width: `${f.progress}%` }}></div></div>{f.status === "done" && (<p className="text-xs text-green-600 mt-1">上传完成</p>
            )}{f.status === "error" && (<p className="text-xs text-red-600 mt-1">上传失败</p>
            )}</div>
        ))}</div></div>
  );
}

 

http://www.agseo.cn/news/162/

相关文章:

  • Linux服务器中代码仓库(gitea+drone)搭建
  • 二分查找
  • postcss-px-to-viewport-8-plugin无法转换tailwindcss样式问题
  • html中的latex数据公式展示
  • IK Multimedia TONEX MAX 1.10.2 逼真音色建模
  • 重塑云上 AI 应用“运行时”,函数计算进化之路
  • 82、SpringMVC 参数传递,浏览器和服务器之间的数据传输
  • 问卷调查数据库设计
  • Linux 系统调用详解与工作机制
  • 一客一策:Data Agent 如何重构大模型时代的智能营销?
  • MySQL函数
  • The 2025 Sichuan Provincial Collegiate Programming Contest
  • 详细介绍:Android 热点开发的相关api总结
  • 工业主板:工业自动化与智能设备的强大心脏
  • 十大经典排序算法 - lucky
  • 深度学习入门基于python
  • 2025网络赛1 C、D
  • 图像配准尝试
  • TypeScript索引访问类型详解
  • 【URP】Unity Shader Tags
  • 存储器的性能指标 计算机组成原理第三章
  • 基于Operator方式和二进制方式部署prometheus环境
  • 安全不是一个功能-而是一个地基
  • 你的错误处理一团糟-是时候修复它了-️
  • idea gitee 更新已取消 解决方案
  • 27家网省
  • 你的测试又慢又不可靠-因为你测错了东西
  • 国内人力资源信息管理软件排行:选红海云一体化人力HR系统
  • 历年 CSP-J/S 数学类真题知识点整理
  • Log4j2 CVE-2021-44228 漏洞复现