七牛大文件分片上传,前端展示上传进度条

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>

 

posted @ 2025-05-12 14:47  Fyy发大财  阅读(90)  评论(0)    收藏  举报