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

react多文件分片上传——支持拖拽与进度展示

1.组件定义

import useMultipleChunkUploadHook from "@/hooks/upload/useMultipleChunkUploadHook";interface ChunkUploadProps {chunkSize?: number;enableDrag?: boolean;
}const MultipleChunkUpload: React.FC<ChunkUploadProps> = ({chunkSize = 5,enableDrag = false,
}) => {chunkSize = chunkSize * 1024 * 1024;const {files,fileInputRef,dropContainerRef,dragActive,handFileChange,initChunkUpload,uploadAll,cancelUpload,resetUpload,handleDrag,handleDrop,handleDropZoneClick,} = useMultipleChunkUploadHook(chunkSize, enableDrag);return (<div className="flex flex-col w-full max-w-6xl mx-auto p-4 rounded-xl shadow-xl">{enableDrag && (<divref={dropContainerRef}onClick={handleDropZoneClick}onDragEnter={handleDrag}onDragOver={handleDrag}onDragLeave={handleDrag}onDrop={handleDrop}className={`mb-4 p-6 border-2 border-dashed rounded-md text-center transition-all ${dragActive? "bg-blue-50 border-blue-500": "bg-gray-50 border-gray-300 hover:border-blue-600"}`}><h3 className="text-lg font-semibold text-gray-700">{dragActive ? "释放文件以上传" : "拖拽文件到此处"}</h3><p className="text-gray-500 text-sm mt-2">或点击此处选择多个文件</p></div>)}<inputtype="file"multipleref={fileInputRef}className="mb-4 w-full hidden"onChange={handFileChange}/><div className="flex items-center gap-3 mb-3"><buttononClick={() => fileInputRef.current?.click()}className="px-4 py-2 border-none text-white rounded-md font-medium bg-blue-400 hover:bg-blue-600 transition-colors duration-300">选择文件</button><buttononClick={() => uploadAll()}disabled={files.length === 0 || files.every(item => item.finished) }className={`px-4 py-2 border-none font-medium rounded-md text-white ${files.length === 0 || files.every(item => item.finished)? "bg-gray-300 cursor-not-allowed": "bg-green-400 hover:bg-green-600"}`}>上传所有</button><buttononClick={resetUpload}disabled={files.length === 0}className={`px-4 py-2 border-none font-medium rounded-md text-white ${files.length === 0? "bg-gray-300 cursor-not-allowed": "bg-gray-500 hover:bg-gray-600"}`}>重置所有</button></div><div className="overflow-x-auto"><table className="w-full text-sm text-left border rounded-lg"><thead className="bg-gray-100 text-gray-600"><tr><th className="py-2 px-3">名称</th><th className="py-2 px-3">大小</th><th className="py-2 px-3 w-56">进度</th><th className="py-2 px-3">状态</th><th className="py-2 px-3">操作</th></tr></thead><tbody>{files.length === 0 && (<tr><tdcolSpan={4}className="py-6 text-center text-gray-400"><h3>未选择文件</h3></td></tr>)}{files.map((item, index) => (<trkey={index}className="odd:bg-white even:bg-gray-50 hover:bg-blue-50 transition-colors"><td className="py-3 px-3 truncate max-w-[2/5]"><div className="text-sm">{item.file.name}</div></td><td><div>{item.file.size >= 1024 * 1024? `${(item.file.size /1024 /1024).toFixed(2)} MB`: `${(item.file.size / 1024).toFixed(2)} KB`}</div></td><td className="py-2 px-2 align-middle"><div className="flex items-center gap-2"><div className="relative flex-1 bg-gray-200 rounded-full h-4 overflow-hidden"><divclassName="absolute left-0 top-0 h-4 bg-blue-400 transition-all duration-300"style={{width: `${item.progress}%`,}}/></div><span className="text-xs text-gray-700 w-10 text-left">{item.progress}%</span></div></td><td className="py-3 px-3 align-middle"><div className="text-sm">{item.statusMessage}</div></td><td className="py-2 px-2 align-middle"><div className="flex gap-2">{/* 单文件上传(保留) */}<buttononClick={() =>initChunkUpload(index)}disabled={item.isUploading ||item.finished}className={`flex-1 py-1 px-1 text-xs border-none rounded-md text-white ${item.isUploading ||item.finished? "bg-gray-300 cursor-not-allowed": "bg-blue-500 hover:bg-blue-600"}`}>上传</button><buttononClick={() =>item.finished &&cancelUpload(index)}className={`flex-1 py-1 px-1 text-xs border-none rounded-md text-white ${item.finished? "bg-gray-300 cursor-not-allowed": "bg-red-400 hover:bg-red-600"}`}>取消</button></div></td></tr>))}</tbody></table></div></div>);
};export default MultipleChunkUpload;

2.组件hook

import axios, { ResultData } from "@/utils/axios";
import {calculateFileHash,createChunks,formatDuration,useClearInput,
} from "@/utils/toolsUtil";
import { ChangeEvent, useEffect, useRef, useState } from "react";export interface FileUploadItem {file: File;fileId: string;progress: number;isUploading: boolean;statusMessage: string;abortController?: AbortController | null;finished?: boolean;
}/*** 多文件分片上传* * @param chunkSize 分片大小* @param enableDrag 是否拖拽* @returns */
const useMultipleChunkUploadHook = (chunkSize: number, enableDrag: boolean) => {const [files, setFiles] = useState<FileUploadItem[]>([]);const fileInputRef = useRef<HTMLInputElement | null>(null);const dropContainerRef = useRef<HTMLDivElement | null>(null);const [dragActive, setDragActive] = useState(false);const handFileChange = (e: ChangeEvent<HTMLInputElement>) => {const selectedFiles = e.target.files;if (!selectedFiles || selectedFiles.length === 0) return;const fileArray = Array.from(selectedFiles).map((f) => ({file: f,fileId: "",progress: 0,isUploading: false,statusMessage: "待上传",abortController: null,finished: false,}));setFiles(fileArray);};const clearFileInput = () => {useClearInput(fileInputRef);};const updateFile = (index: number, patch: Partial<FileUploadItem>) => {setFiles((prev) =>prev.map((it, i) => (i === index ? { ...it, ...patch } : it)));};/*** 合并分片*/const chunkMerge = async (fileName: string,uploadId: string,fileMD5: string,uploadStartTime: number,index: number) => {try {const response: ResultData<any> = await axios.post("/file/upload/merge",{ fileName, uploadId, fileMD5 });if (response.code === 200) {const uploadEndTime = performance.now();updateFile(index, {statusMessage: `上传完成(耗时 ${formatDuration(uploadEndTime - uploadStartTime)})`,progress: 100,isUploading: false,finished: true,});} else {updateFile(index, {statusMessage: "分片合并失败",isUploading: false,});}} catch (err) {updateFile(index, {statusMessage: "合并接口异常",isUploading: false,});}};const uploadSingle = async (index: number): Promise<void> => {const item = files[index];if (!item) return Promise.resolve();// 如果已经在上传或已完成,直接返回if (item.isUploading || item.finished) return;updateFile(index, {isUploading: true,statusMessage: "计算文件 MD5...",progress: 0,});const uploadStartTime = performance.now();const abortController = new AbortController();updateFile(index, { abortController });try {// 计算文件 MD5const entireFileMD5 = await calculateFileHash(item.file);updateFile(index, { statusMessage: "初始化上传任务..." });const formData = new FormData();formData.append("fileName", item.file.name);formData.append("fileSize", item.file.size.toString());formData.append("fileMD5", entireFileMD5);const response: ResultData<any> = await axios.post("/file/upload/init",formData);if (response.code !== 200) {updateFile(index, {statusMessage: "初始化失败",isUploading: false,abortController: null,});return;}const uploadId = response.data.uploadId;const serverUploadMD5 = response.data.uploadMD5;updateFile(index, {fileId: uploadId,statusMessage: "开始上传分片...",});// 分片上传const chunks = createChunks(item.file, chunkSize);const totalChunks = chunks.length;for (let i = 0; i < totalChunks; i++) {if (abortController.signal.aborted) {updateFile(index, {statusMessage: "已取消",isUploading: false,abortController: null,});return;}const chunk = chunks[i];const fd = new FormData();fd.append("file", chunk.chunk, item.file.name);fd.append("uploadId", uploadId);fd.append("fileMD5", serverUploadMD5 || entireFileMD5);fd.append("index", i.toString());await axios.post("/file/upload/chunk", fd, {headers: {"Content-Type": "multipart/form-data",},signal: abortController.signal,});const progress = Math.round(((i + 1) / totalChunks) * 100);updateFile(index, {progress,statusMessage: `分片上传 ${i + 1}/${totalChunks}`,});}// 所有分片上传完,通知合并updateFile(index, {statusMessage: "所有分片上传成功,正在合并...",});await chunkMerge(item.file.name,uploadId,serverUploadMD5 || entireFileMD5,uploadStartTime,index);updateFile(index, { abortController: null });} catch (err: any) {if (abortController.signal.aborted) {updateFile(index, {statusMessage: "已取消",isUploading: false,abortController: null,});} else {updateFile(index, {statusMessage: `上传失败: ${err?.message || "未知错误"}`,isUploading: false,abortController: null,});}}};const initChunkUpload = async (index: number) => {return uploadSingle(index);};const uploadAll = async (limit = 5) => {const total = files.length;if (total === 0) return;let idx = 0;let active = 0;return new Promise<void>((resolve) => {const next = () => {// 所有任务完成if (idx >= total && active === 0) {resolve();return;}while (active < limit && idx < total) {const curIndex = idx++;const item = files[curIndex];// 跳过已完成if (item.finished) {next(); continue;}active++;// 启动上传uploadSingle(curIndex).catch(() => {// 单文件错误已在uploadSingle内部处理}).finally(() => {active--;next();});}};next();});};const cancelUpload = (index: number) => {const item = files[index];if (!item) return;if (item.abortController) {item.abortController.abort();updateFile(index, {isUploading: false,statusMessage: "取消中...",});} else {// 如果还没开启上传,直接标记为已取消updateFile(index, { isUploading: false, statusMessage: "已取消" });}};const resetUpload = () => {files.forEach((it, i) => {if (it.abortController) it.abortController.abort();updateFile(i, { abortController: null, isUploading: false });});setFiles([]);clearFileInput();};const handleDrag = (e: React.DragEvent) => {e.preventDefault();e.stopPropagation();setDragActive(e.type === "dragenter" || e.type === "dragover");};const handleDrop = (e: React.DragEvent) => {e.preventDefault();e.stopPropagation();setDragActive(false);if (!enableDrag) return;const dropped = Array.from(e.dataTransfer.files || []);if (dropped.length === 0) return;const fileArray = dropped.map((f) => ({file: f,fileId: "",progress: 0,isUploading: false,statusMessage: "待上传",abortController: null,finished: false,}));setFiles(fileArray);if (fileInputRef.current) {const dt = new DataTransfer();dropped.forEach((f) => dt.items.add(f));fileInputRef.current.files = dt.files;// 手动触发 changeconst ev = new Event("change", { bubbles: true });fileInputRef.current.dispatchEvent(ev);}};const handleDropZoneClick = () => {if (fileInputRef.current) fileInputRef.current.click();};useEffect(() => {const dropContainer = dropContainerRef.current;if (!dropContainer || !enableDrag) return;const handleDragOver = (e: DragEvent) => {e.preventDefault();e.stopPropagation();setDragActive(true);};const handleDragLeave = (e: DragEvent) => {e.preventDefault();e.stopPropagation();setDragActive(false);};dropContainer.addEventListener("dragover", handleDragOver);dropContainer.addEventListener("dragenter", handleDragOver);dropContainer.addEventListener("dragleave", handleDragLeave);dropContainer.addEventListener("drop", handleDrop as any);return () => {dropContainer.removeEventListener("dragover", handleDragOver);dropContainer.removeEventListener("dragenter", handleDragOver);dropContainer.removeEventListener("dragleave", handleDragLeave);dropContainer.removeEventListener("drop", handleDrop as any);};}, [enableDrag, files]);return {files,fileInputRef,dropContainerRef,dragActive,handFileChange,initChunkUpload,uploadAll,cancelUpload,resetUpload,handleDrag,handleDrop,handleDropZoneClick,};
};export default useMultipleChunkUploadHook;

3.组件使用

import MultipleChunkUpload from "@/components/multipleUpload";/*** 多文件分片上传*/
const MultipleChunkUploadPage: React.FC = () => {return (<><div className="flex justify-center"><MultipleChunkUpload chunkSize={6} enableDrag={true} /></div></>);
}export default MultipleChunkUploadPage;

4.上传测试

5.后端代码

后端使用了.Net Core请参见笔者的另一篇文章分片上传https://blog.csdn.net/l244112311/article/details/151226362

http://www.dtcms.com/a/473495.html

相关文章:

  • Excel如何合并单元格?【图文详解】Excel合并单元格技巧?单元格合并高阶操作?
  • Fabric.js 完全指南:从入门到实战的Canvas绘图引擎详解
  • 学网站建设要多少钱遵义网站建设网站
  • 数据分析:Python懂车帝汽车数据分析可视化系统 爬虫(Django+Vue+销量分析 源码+文档)✅
  • 从Java集合到云原生现代数据管理的演进之路
  • 03_pod详解
  • 线性代数 | excellent algebraic space
  • 计算机网络篇之TCP滑动窗口
  • java项目使用宝塔面板部署服务器nginx不能反向代理找到图片资源
  • 180课时吃透Go语言游戏后端开发11:Go语言中的并发编程
  • 江苏建设部官方网站纯 flash 网站
  • Oracle OMF 配置文档
  • 帮别人做网站怎么赚钱wordpress 静态设置
  • SpringBoot Jar包冲突在线检测
  • 基于OpenCV的通过人脸对年龄、性别、表情与疲劳进行检测
  • vue3 类似 Word 修订模式,变更(插入、删除、修改)可以实时查看标记 如何实现
  • LLM 笔记 —— 07 Tokenizers(BPE、WordPeice、SentencePiece、Unigram)
  • Serverless数据库架构:FaunaDB+Vercel无缝集成方案
  • 【自然语言处理】“bert-base-chinese”的基本用法及实战案例
  • LLM 笔记 —— 08 Embeddings(One-hot、Word、Word2Vec、Glove、FastText)
  • 广告公司网站设计策划phpcmsv9手机网站
  • 【Qt】乌班图安装Qt环境
  • 边缘计算中的前后端数据同步:Serverless函数与Web Worker的异构处理
  • Windows Pad平板对 Qt 的支持
  • 基于JETSON ORIN/RK3588+AI相机:机器人-多路视觉边缘计算方案
  • 没有网怎么安装wordpress沈阳企业网站优化排名方案
  • 【C++STL :list类 (二) 】list vs vector:终极对决与迭代器深度解析 揭秘list迭代器的陷阱与精髓
  • 虚幻引擎入门教程:虚幻引擎的安装
  • FastbuildAI后端服务启动流程分析
  • AI×Cursor 零基础前端学习路径:避误区学HTML/CSS/JS