七牛大文件分片上传,前端展示上传进度条
1.引入pom文件
<dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>[7.16.0, 7.16.99]</version> </dependency>
2.service
package com.ruoyi.design.service.impl;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Client;
import com.qiniu.storage.*;
import com.qiniu.util.Auth;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.nacos.QiNiuProperties;
import com.ruoyi.design.service.QiniuUploadService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@Service
public class QiniuUploadServiceImpl implements QiniuUploadService {
private static final Logger log = LoggerFactory.getLogger(QiniuUploadServiceImpl.class);
@Resource
private QiNiuProperties qiNiuProperties;
// private String urlPrefix;
// private String bucketName;
private int partSizeInMB = 10;
// 密钥配置
private Auth auth;
private Consumer<Double> progressListener;
@Override
public void setProgressListener(Consumer<Double> progressListener) {
this.progressListener = progressListener;
}
private Auth getAuth() {
if (auth == null) {
auth = Auth.create(qiNiuProperties.getAccessKey(), qiNiuProperties.getAccessSecretKey());
}
return auth;
}
@Override
public CompletableFuture<Void> uploadFileAsync(String localFilePath, String targetFileName) {
CompletableFuture<Void> future = new CompletableFuture<>();
// 在线程池中执行上传任务,避免阻塞调用线程
CompletableFuture.runAsync(() -> {
try {
uploadFile(localFilePath, targetFileName);
future.complete(null);
} catch (Exception e) {
log.error("异步上传文件失败: {}", e.getMessage(), e);
future.completeExceptionally(e);
}
});
return future;
}
@Override
public void uploadFile(String localFilePath, String targetFileName) throws IOException {
long startTime = System.currentTimeMillis();
log.info("开始上传文件: {}, 目标文件名: {}", localFilePath, targetFileName);
RandomAccessFile file = null;
File tempFile = null;
try {
tempFile = new File(localFilePath);
long fileSize = tempFile.length();
log.info("文件实际大小: {} 字节 ({} MB)", fileSize, fileSize / (1024 * 1024));
if (!tempFile.exists()) {
throw new FileNotFoundException("文件不存在: " + localFilePath);
}
file = new RandomAccessFile(tempFile, "r");
String token = getUpToken();
Configuration configuration = new Configuration();
Client client = new Client(configuration);
// 1. 初始化上传
String uploadId = initUpload(client, token, targetFileName);
if (uploadId == null) {
throw new ServiceException("初始化上传失败");
}
// 2. 上传所有分片
List<Map<String, Object>> partsInfo = uploadParts(file, fileSize, client, token, uploadId, targetFileName);
// 3. 获取已上传的分片信息(可选)
List<Map<String, Object>> listPartInfo = listUploadedParts(client, token, uploadId, targetFileName);
log.info("已上传分片信息: {}", listPartInfo);
// 4. 完成上传
completeUpload(client, token, uploadId, partsInfo, localFilePath, targetFileName);
log.info("文件上传成功: {}", localFilePath);
log.info("uploadFile方法执行完成,耗时: {} 毫秒", System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("上传过程发生异常: {}", e.getMessage(), e);
throw e;
} finally {
// 确保资源被正确关闭和清理
closeResource(file);
}
}
// 获取上传凭证
private String getUpToken() {
return getAuth().uploadToken(qiNiuProperties.getPublicBucket());
}
// 初始化上传,返回uploadId
private String initUpload(Client client, String token, String targetFileName) throws QiniuException {
ApiUploadV2InitUpload initUploadApi = new ApiUploadV2InitUpload(client);
ApiUploadV2InitUpload.Request initUploadRequest = new ApiUploadV2InitUpload.Request(qiNiuProperties.getEndpoint(), token)
.setKey(targetFileName);
ApiUploadV2InitUpload.Response initUploadResponse = initUploadApi.request(initUploadRequest);
String uploadId = initUploadResponse.getUploadId();
log.info("初始化上传成功, uploadId: {}", uploadId);
return uploadId;
}
// 上传所有分片,返回分片信息列表
private List<Map<String, Object>> uploadParts(RandomAccessFile file, long fileSize,
Client client, String token, String uploadId, String targetFileName)
throws IOException, QiniuException {
List<Map<String, Object>> partsInfo = new ArrayList<>();
int defaultPartSize = partSizeInMB * 1024 * 1024; // 转换为字节
long partOffset = 0;
int partNumber = 1;
long totalUploaded = 0; // 记录已上传的总字节数
log.info("开始分片上传,文件大小: {} 字节,默认分片大小: {} 字节", fileSize, defaultPartSize);
while (partOffset < fileSize) {
long currentPartSize = Math.min(fileSize - partOffset, defaultPartSize);
log.info("准备上传分片 #{},偏移量: {},大小: {} 字节", partNumber, partOffset, currentPartSize);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
long bytesToRead = currentPartSize;
file.seek(partOffset);
while (bytesToRead > 0 && (bytesRead = file.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead))) != -1) {
baos.write(buffer, 0, bytesRead);
bytesToRead -= bytesRead;
}
byte[] partData = baos.toByteArray();
log.info("分片 #{} 读取完成,实际大小: {} 字节", partNumber, partData.length);
// 检查读取的字节数是否符合预期
if (partData.length != currentPartSize) {
log.warn("警告: 读取的分片大小({})与预期({})不符", partData.length, currentPartSize);
}
String etag = uploadSinglePart(client, token, uploadId, partNumber, partData, targetFileName);
Map<String, Object> partInfo = new HashMap<>();
partInfo.put(ApiUploadV2CompleteUpload.Request.PART_NUMBER, partNumber);
partInfo.put(ApiUploadV2CompleteUpload.Request.PART_ETG, etag);
partsInfo.add(partInfo);
double progress = Double.parseDouble(String.format("%.2f", (double)totalUploaded/fileSize * 100));
// 通知进度监听器
if (progressListener != null) {
progressListener.accept(progress);
}
totalUploaded += partData.length;
log.info("分片 #" + partNumber + " 上传成功,已上传: " + totalUploaded + " / " + fileSize + " 字节 (" +
String.format("%.2f", progress) + "%)");
}
// 确保最后发送100%进度
if (progressListener != null && totalUploaded >= fileSize) {
progressListener.accept(100.0);
}
partNumber++;
partOffset += currentPartSize; // 使用实际分片大小更新偏移量
}
log.info("所有分片上传完成,共上传 {} 个分片", (partNumber-1));
return partsInfo;
}
// 上传单个分片
private String uploadSinglePart(Client client, String token, String uploadId,
int partNumber, byte[] partData, String targetFileName) throws QiniuException {
ApiUploadV2UploadPart uploadPartApi = new ApiUploadV2UploadPart(client);
ApiUploadV2UploadPart.Request uploadPartRequest = new ApiUploadV2UploadPart.Request(qiNiuProperties.getEndpoint(), token, uploadId, partNumber)
.setKey(targetFileName)
.setUploadData(partData, 0, partData.length, null);
ApiUploadV2UploadPart.Response uploadPartResponse = uploadPartApi.request(uploadPartRequest);
return uploadPartResponse.getEtag();
}
// 获取已上传的分片信息
private List<Map<String, Object>> listUploadedParts(Client client, String token, String uploadId, String targetFileName)
throws QiniuException {
List<Map<String, Object>> listPartInfo = new ArrayList<>();
Integer partNumberMarker = null;
while (true) {
ApiUploadV2ListParts listPartsApi = new ApiUploadV2ListParts(client);
ApiUploadV2ListParts.Request listPartsRequest = new ApiUploadV2ListParts.Request(qiNiuProperties.getEndpoint(), token, uploadId)
.setKey(targetFileName)
.setPartNumberMarker(partNumberMarker);
ApiUploadV2ListParts.Response listPartsResponse = listPartsApi.request(listPartsRequest);
partNumberMarker = listPartsResponse.getPartNumberMarker();
listPartInfo.addAll(listPartsResponse.getParts());
log.info("获取分片列表, 已获取: {} 个分片", listPartInfo.size());
// 列举结束
if (partNumberMarker == 0) {
break;
}
}
return listPartInfo;
}
// 完成上传
private void completeUpload(Client client, String token, String uploadId,
List<Map<String, Object>> partsInfo, String localFileName, String targetFileName)
throws QiniuException {
String customKey = "upload_from";
String customValue = "ruoyi_system";
Map<String, Object> customParam = new HashMap<>();
customParam.put("x:" + customKey, customValue);
ApiUploadV2CompleteUpload completeUploadApi = new ApiUploadV2CompleteUpload(client);
ApiUploadV2CompleteUpload.Request completeUploadRequest = new ApiUploadV2CompleteUpload.Request(qiNiuProperties.getEndpoint(), token, uploadId, partsInfo)
.setKey(targetFileName)
.setFileName(localFileName)
.setCustomParam(customParam);
ApiUploadV2CompleteUpload.Response completeUploadResponse = completeUploadApi.request(completeUploadRequest);
log.info("完成上传, 响应: {}", completeUploadResponse.getResponse());
}
// 关闭资源
private void closeResource(Closeable resource) {
if (resource != null) {
try {
resource.close();
log.info("资源已关闭");
} catch (IOException e) {
log.error("关闭资源失败: {}", e.getMessage(), e);
}
}
}
}
2.
package com.ruoyi.design.service; import com.ruoyi.design.service.impl.QiniuUploadServiceImpl; import java.io.IOException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public interface QiniuUploadService { /** * 异步上传文件到七牛云,并支持进度监听 * @param localFilePath 本地文件路径 * @param targetFileName 上传到七牛云后的文件名 * @return CompletableFuture 可用于监听上传完成状态 */ CompletableFuture<Void> uploadFileAsync(String localFilePath, String targetFileName) ; /** * 同步上传文件到七牛云,并支持进度监听 * @param localFilePath 本地文件路径 * @param targetFileName 上传到七牛云后的文件名 * @throws Exception 上传过程中发生的异常 */ void uploadFile(String localFilePath, String targetFileName) throws IOException; /** * 设置上传进度监听器 * @param progressListener 进度监听回调函数 */ void setProgressListener(Consumer<Double> progressListener); }
3.
package com.ruoyi.design.controller.test; import com.ruoyi.common.core.core.domain.AjaxResult; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.design.service.QiniuUploadService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; /** * 七牛云文件上传Controller */ @RestController @RequestMapping("/system/qiniu") public class QiniuSdkController { private static final Logger log = LoggerFactory.getLogger(QiniuSdkController.class); @Autowired private QiniuUploadService qiniuUploadService; // 存储任务ID与SseEmitter的映射关系,使用线程安全的ConcurrentHashMap private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>(); // 存储任务ID与上传进度的映射关系 private final ConcurrentHashMap<String, AtomicLong> progressMap = new ConcurrentHashMap<>(); /** * 临时文件存储目录 */ private static final String TEMP_DIR = System.getProperty("java.io.tmpdir") + File.separator + "ruoyi_qiniu_temp" + File.separator; private void sendProgressUpdate(String taskId, long progress) { SseEmitter emitter = emitters.get(taskId); if (emitter != null) { try { // 如果上传完成,发送完成事件并完成发射器 Map<String, Object> data = new HashMap<>(); data.put("progress", progress); data.put("taskId", taskId); emitter.send(SseEmitter.event() .name("progress") .data(data)); if (progress>= 100) { Map<String, Object> completeData = new HashMap<>(); completeData.put("status", "completed"); completeData.put("taskId", taskId); emitter.send(SseEmitter.event() .name("complete") .data(completeData)); // emitter.complete(); } } catch (IOException e) { log.error("发送进度更新失败: taskId={}, error={}", taskId, e.getMessage()); emitter.completeWithError(e); emitters.remove(taskId); } } } @GetMapping(value = "/progress/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter trackProgress(@PathVariable String taskId) throws IOException { // 创建一个永不超时的SseEmitter SseEmitter emitter = new SseEmitter(0L); // 将taskId与emitter关联 emitters.put(taskId, emitter); // 如果已经有进度数据,立即发送给客户端 AtomicLong progress = progressMap.get(taskId); if (progress != null) { sendProgressUpdate(taskId, progress.get()); } // 设置SseEmitter的回调函数 emitter.onCompletion(() -> { log.info("SSE连接完成: taskId={}", taskId); emitters.remove(taskId); }); emitter.onTimeout(() -> { log.info("SSE连接超时: taskId={}", taskId); emitters.remove(taskId); emitter.complete(); }); emitter.onError(throwable -> { log.error("SSE连接错误: taskId={}, error={}", taskId, throwable.getMessage()); emitters.remove(taskId); emitter.completeWithError(throwable); }); return emitter; } /** * 上传单个文件到七牛云 * * @param file 上传的文件 * @return 上传结果,包含文件URL等信息 */ @PostMapping("/upload") public AjaxResult uploadFile(@RequestParam("file") MultipartFile file, @RequestParam(value = "targetFileName") String targetFileName) { long startTime = System.currentTimeMillis(); if (file == null || file.isEmpty()) { return AjaxResult.error("上传文件不能为空"); } File tempFile = null; try { // 创建临时文件 tempFile = createTempFile(file); // 生成目标文件名(如果未指定) if (StringUtils.isEmpty(targetFileName)) { targetFileName = generateUniqueFileName(file.getOriginalFilename()); } // 生成任务ID String taskId = UUID.randomUUID().toString(); // 初始化进度跟踪 progressMap.put(taskId, new AtomicLong(0)); // 设置进度监听器 qiniuUploadService.setProgressListener(progress -> { AtomicLong progressValue = progressMap.get(taskId); if (progressValue != null) { progressValue.set(Math.round(progress)); sendProgressUpdate(taskId, progressValue.get()); } }); // 异步上传文件 CompletableFuture<Void> future = qiniuUploadService.uploadFileAsync(tempFile.getAbsolutePath(), targetFileName); // 构建返回结果 String fileUrl = buildFileUrl(targetFileName); Map<String, Object> result = new HashMap<>(); result.put("taskId", taskId); result.put("fileUrl", fileUrl); // 添加文件删除的回调 final File finalTempFile = tempFile; future.whenComplete((v, throwable) -> { if (throwable != null) { log.error("文件上传失败: {}", throwable.getMessage()); } // 无论上传成功还是失败,都删除临时文件 deleteTempFile(finalTempFile); }); log.info("文件上传接口执行结束,耗时: {} ms", System.currentTimeMillis() - startTime); return AjaxResult.success("文件上传开始", result); } catch (Exception e) { // 发生异常时删除临时文件 if (tempFile != null) { deleteTempFile(tempFile); } log.error("上传文件异常: {}", e.getMessage(), e); return AjaxResult.error("上传文件异常: " + e.getMessage()); } } /** * 创建临时文件 */ private File createTempFile(MultipartFile file) throws IOException { // 创建临时目录(如果不存在) File tempDir = new File(TEMP_DIR); if (!tempDir.exists()) { if (!tempDir.mkdirs()) { throw new IOException("无法创建临时目录: " + TEMP_DIR); } } // 创建临时文件 String originalFilename = file.getOriginalFilename(); String fileExtension = getFileExtension(originalFilename); File tempFile = File.createTempFile("qiniu_upload_", fileExtension, tempDir); // 保存文件内容到临时文件 file.transferTo(tempFile); return tempFile; } /** * 删除临时文件 */ private void deleteTempFile(File file) { if (file != null && file.exists()) { try { if (file.delete()) { log.info("临时文件已删除: {}", file.getAbsolutePath()); } else { log.warn("无法删除临时文件: {}", file.getAbsolutePath()); } } catch (Exception e) { log.error("删除临时文件失败: {}", e.getMessage(), e); } } } /** * 生成唯一文件名 */ private String generateUniqueFileName(String originalFilename) { String fileExtension = getFileExtension(originalFilename); return UUID.randomUUID().toString() + fileExtension; } /** * 获取文件扩展名 */ private String getFileExtension(String fileName) { if (fileName == null || !fileName.contains(".")) { return ""; } return fileName.substring(fileName.lastIndexOf(".")); } /** * 构建文件URL */ private String buildFileUrl(String targetFileName) { // 这里假设Service中已经配置了URL前缀,可以通过注入配置或调用Service方法获取 // 简单起见,这里直接返回文件名,实际应用中应根据七牛云配置构建完整URL return targetFileName; } }
4.
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件上传到七牛云</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script> <script> tailwind.config = { theme: { extend: { colors: { primary: '#3B82F6', secondary: '#10B981', neutral: '#64748B', }, fontFamily: { inter: ['Inter', 'system-ui', 'sans-serif'], }, } } } </script> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .file-drop-area { @apply relative border-2 border-dashed border-neutral/30 rounded-lg p-8 text-center transition-all duration-300; } .file-drop-area.active { @apply border-primary bg-primary/5; } .upload-progress { @apply h-2 bg-gray-200 rounded-full overflow-hidden; } .upload-progress-bar { @apply h-full bg-primary transition-all duration-300; } .file-item { @apply flex items-center justify-between p-3 bg-white rounded-lg shadow-sm mb-2 border border-gray-100 transition-all hover:shadow; } .btn { @apply px-4 py-2 rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2; } .btn-primary { @apply bg-primary text-white hover:bg-primary/90 focus:ring-primary/50; } .btn-secondary { @apply bg-secondary text-white hover:bg-secondary/90 focus:ring-secondary/50; } .btn-outline { @apply border border-neutral text-neutral hover:bg-neutral/10 focus:ring-neutral/50; } } </style> </head> <body class="bg-gray-50 font-inter min-h-screen"> <div class="container mx-auto px-4 py-8 max-w-4xl"> <div class="bg-white rounded-xl shadow-lg p-6 mb-8"> <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-800 mb-6 flex items-center"> <i class="fa fa-cloud-upload text-primary mr-3"></i> 文件上传到七牛云 </h1> <div id="fileDropArea" class="file-drop-area mb-8"> <input type="file" id="fileInput" class="hidden" multiple> <div class="flex flex-col items-center justify-center"> <i class="fa fa-file-archive text-4xl text-neutral/50 mb-4"></i> <p class="text-lg text-neutral mb-2">拖放文件到这里,或</p> <button id="selectFilesBtn" class="btn btn-primary"> <i class="fa fa-plus mr-2"></i>选择文件 </button> <p class="text-sm text-neutral/70 mt-4">支持上传多个文件,系统会自动压缩为ZIP</p> </div> </div> <div id="fileList" class="mb-8 hidden"> <h2 class="text-xl font-semibold text-gray-700 mb-4 flex items-center"> <i class="fa fa-files-o text-primary mr-2"></i>已选择的文件 </h2> <div id="selectedFiles" class="space-y-2 max-h-60 overflow-y-auto pr-2"></div> </div> <div id="uploadControls" class="flex flex-col sm:flex-row justify-between items-center gap-4 hidden"> <div id="uploadStatus" class="w-full sm:w-2/3"> <div class="flex justify-between text-sm mb-1"> <span id="progressText" class="text-neutral">准备上传...</span> <span id="progressPercent" class="text-neutral font-medium">0%</span> </div> <div class="upload-progress"> <div id="progressBar" class="upload-progress-bar" style="width: 0%"></div> </div> </div> <button id="uploadBtn" class="btn btn-secondary w-full sm:w-auto"> <i class="fa fa-upload mr-2"></i>开始上传 </button> </div> <div id="uploadResult" class="mt-8 p-4 rounded-lg hidden"> <div id="successMessage" class="bg-green-50 border border-green-200 text-green-700 p-4 rounded-lg hidden"> <div class="flex items-start"> <div class="flex-shrink-0"> <i class="fa fa-check-circle text-green-500 text-xl"></i> </div> <div class="ml-3"> <h3 class="text-sm font-medium">上传成功</h3> <div class="mt-2 text-sm"> <p>文件已成功上传到七牛云</p> <p class="mt-2"> <span class="font-medium">文件名:</span> <span id="uploadedFileName" class="ml-2 break-all"></span> </p> <p class="mt-1"> <span class="font-medium">文件大小:</span> <span id="uploadedFileSize" class="ml-2"></span> </p> <p class="mt-1"> <span class="font-medium">下载链接:</span> <a id="downloadLink" href="#" target="_blank" class="ml-2 text-primary hover:underline"></a> </p> </div> </div> </div> </div> <div id="errorMessage" class="bg-red-50 border border-red-200 text-red-700 p-4 rounded-lg hidden"> <div class="flex items-start"> <div class="flex-shrink-0"> <i class="fa fa-exclamation-circle text-red-500 text-xl"></i> </div> <div class="ml-3"> <h3 class="text-sm font-medium">上传失败</h3> <div class="mt-2 text-sm"> <p id="errorDetails"></p> </div> </div> </div> </div> </div> </div> <div class="text-center text-neutral/60 text-sm"> <p>© 2025 文件上传系统 | 支持多文件压缩上传到七牛云</p> </div> </div> <script> document.addEventListener('DOMContentLoaded', function () { const fileDropArea = document.getElementById('fileDropArea'); const fileInput = document.getElementById('fileInput'); const selectFilesBtn = document.getElementById('selectFilesBtn'); const fileList = document.getElementById('fileList'); const selectedFilesArray = []; const uploadControls = document.getElementById('uploadControls'); const uploadBtn = document.getElementById('uploadBtn'); const progressBar = document.getElementById('progressBar'); const progressPercent = document.getElementById('progressPercent'); const progressText = document.getElementById('progressText'); const uploadResult = document.getElementById('uploadResult'); const successMessage = document.getElementById('successMessage'); const errorMessage = document.getElementById('errorMessage'); const uploadedFileName = document.getElementById('uploadedFileName'); const uploadedFileSize = document.getElementById('uploadedFileSize'); const downloadLink = document.getElementById('downloadLink'); const errorDetails = document.getElementById('errorDetails'); // 处理文件选择 selectFilesBtn.addEventListener('click', () => { fileInput.click(); }); // 处理文件拖放 ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { fileDropArea.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } ['dragenter', 'dragover'].forEach(eventName => { fileDropArea.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { fileDropArea.addEventListener(eventName, unhighlight, false); }); function highlight() { fileDropArea.classList.add('active'); } function unhighlight() { fileDropArea.classList.remove('active'); } fileDropArea.addEventListener('drop', handleDrop, false); function handleDrop(e) { const dt = e.dataTransfer; const files = dt.files; handleFiles(files); } fileInput.addEventListener('change', function () { handleFiles(this.files); }); function handleFiles(files) { if (files.length === 0) return; selectedFilesArray.length = 0; for (let i = 0; i < files.length; i++) { selectedFilesArray.push(files[i]); } displaySelectedFiles(); showUploadControls(); } function displaySelectedFiles() { const selectedFiles = document.getElementById('selectedFiles'); selectedFiles.innerHTML = ''; selectedFilesArray.forEach(file => { const fileSize = formatFileSize(file.size); const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` <div class="flex items-center"> <i class="fa fa-file-o text-primary mr-3"></i> <div class="overflow-hidden"> <p class="text-sm font-medium truncate">${file.name}</p> <p class="text-xs text-neutral">${fileSize}</p> </div> </div> <button class="remove-file-btn text-neutral hover:text-red-500 transition-colors" data-index="${selectedFilesArray.indexOf(file)}"> <i class="fa fa-times"></i> </button> `; selectedFiles.appendChild(fileItem); }); // 添加删除文件事件监听 const removeFileBtns = document.querySelectorAll('.remove-file-btn'); removeFileBtns.forEach(btn => { btn.addEventListener('click', function () { const index = parseInt(this.getAttribute('data-index')); selectedFilesArray.splice(index, 1); displaySelectedFiles(); if (selectedFilesArray.length === 0) { hideUploadControls(); } }); }); fileList.classList.remove('hidden'); } function showUploadControls() { uploadControls.classList.remove('hidden'); } function hideUploadControls() { uploadControls.classList.add('hidden'); fileList.classList.add('hidden'); } // 上传按钮点击事件 uploadBtn.addEventListener('click', async function () { try { resetProgress(); uploadBtn.disabled = true; uploadBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>上传中...'; // 检查是否需要压缩 let fileToUpload; if (selectedFilesArray.length > 1) { progressText.textContent = '正在压缩文件...'; fileToUpload = await createZipFile(selectedFilesArray); } else if (selectedFilesArray.length === 1) { fileToUpload = selectedFilesArray[0]; } else { throw new Error('没有选择文件'); } // 直接上传文件到七牛云 progressText.textContent = '正在上传到七牛云...'; const uploadResult = await uploadToQiniu(fileToUpload); // 显示上传成功信息 showSuccessMessage(fileToUpload, uploadResult); } catch (error) { console.error('上传失败:', error); showErrorMessage(error.message || '上传过程中发生错误'); } finally { uploadBtn.disabled = false; uploadBtn.innerHTML = '<i class="fa fa-upload mr-2"></i>开始上传'; } }); // 创建ZIP文件 async function createZipFile(files) { return new Promise((resolve, reject) => { const zip = new JSZip(); // 添加文件到ZIP files.forEach(file => { zip.file(file.name, file); }); // 生成ZIP文件名 const date = new Date(); const timestamp = `${date.getFullYear()}${padZero(date.getMonth() + 1)}${padZero(date.getDate())}_${padZero(date.getHours())}${padZero(date.getMinutes())}${padZero(date.getSeconds())}`; const zipFileName = `files_${timestamp}.zip`; // 生成ZIP文件 zip.generateAsync({ type: 'blob' }, (metadata) => { const progress = Math.round(metadata.percent); updateProgress(progress, `正在压缩文件: ${progress}%`); }) .then(content => { const zipFile = new File([content], zipFileName, { type: 'application/zip' }); updateProgress(100, '文件压缩完成'); resolve(zipFile); }) .catch(error => { reject(new Error(`文件压缩失败: ${error.message}`)); }); }); } // 上传到七牛云 async function uploadToQiniu(file) { try { // 直接上传文件到后端接口 const formData = new FormData(); formData.append('file', file); formData.append('targetFileName', file.name); // 发送上传请求,不再使用onUploadProgress const response = await axios.post('http://xxxxx:8120/system/qiniu/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); // 获取任务ID并开始监听进度 const taskId = response.data.data.taskId; await startProgressTracking(taskId); return response.data; } catch (error) { throw new Error(`上传到七牛云失败: ${error.message}`); } } // 添加进度跟踪函数 function startProgressTracking(taskId) { return new Promise((resolve, reject) => { const eventSource = new EventSource(`http://xxxx:8120/system/qiniu/progress/${taskId}`); eventSource.addEventListener('progress', (event) => { const data = JSON.parse(event.data); const percent = Math.min(data.progress, 99.99); // 保持在99.99%直到完成 updateProgress(percent, `正在上传: ${percent}%`); }); eventSource.addEventListener('complete', (event) => { const data = JSON.parse(event.data); if (data.status === 'completed') { updateProgress(100, '上传完成'); eventSource.close(); resolve(); // 解析Promise } }); eventSource.onerror = (error) => { console.error('SSE连接错误:', error); eventSource.close(); reject(new Error('上传进度监控失败')); }; }); } // 更新进度条 function updateProgress(percent, text) { progressBar.style.width = `${percent}%`; progressPercent.textContent = `${percent}%`; progressText.textContent = text; } // 重置进度 function resetProgress() { progressBar.style.width = '0%'; progressPercent.textContent = '0%'; progressText.textContent = '准备上传...'; uploadResult.classList.add('hidden'); successMessage.classList.add('hidden'); errorMessage.classList.add('hidden'); } // 显示成功消息 function showSuccessMessage(file, result) { uploadResult.classList.remove('hidden'); successMessage.classList.remove('hidden'); uploadedFileName.textContent = file.name; uploadedFileSize.textContent = formatFileSize(file.size); // 使用后端返回的文件URL const fileUrl = result.data.fileUrl; downloadLink.href = fileUrl; downloadLink.textContent = fileUrl; } // 显示错误消息 function showErrorMessage(message) { uploadResult.classList.remove('hidden'); errorMessage.classList.remove('hidden'); errorDetails.textContent = message; } // 格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 补零函数 function padZero(num) { return num < 10 ? '0' + num : num; } }); </script> </body> </html>
浙公网安备 33010602011771号