Skip to content

演示demo源码

演示demo地址:http://jiang-12-13.com:8988/

前端代码(原生js实现)

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>大文件上传演示</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/@fortawesome/fontawesome-free@6.5.0/css/all.min.css"
    />
    <link rel="stylesheet" href="./style.css" />
    <script src="https://unpkg.com/tsparticles@2/tsparticles.bundle.min.js"></script>
    <script src="https://unpkg.com/axios@1.8.4/dist/axios.min.js"></script>
    <script src="https://unpkg.com/enlarge-file-upload@latest/dist/upload.js"></script>
  </head>
  <body>
    <div id="tsparticles"></div>

    <div class="container">
      <div id="message" class="message hidden">
        <i class="icon fas"></i>
        <span class="text"></span>
      </div>

      <div class="title">大文件上传演示</div>

      <div class="progress-ring">
        <svg>
          <defs>
            <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stop-color="#00e0ff" />
              <stop offset="100%" stop-color="#ffffff" />
            </linearGradient>
          </defs>
          <circle class="progress-ring-bg" cx="120" cy="120" r="110" />
          <circle class="progress-ring-fill" cx="120" cy="120" r="110" />
        </svg>
        <div class="progress-ring-text">请选择文件</div>
      </div>
      <div style="margin-bottom: 20px; font-size: 1rem" class="fileName"></div>
      <div class="button-group">
        <input
          type="file"
          class="btn"
          title="选择文件"
          style="display: none"
          onchange="handleFileSelect(event)"
        />
        <button type="file" onclick="optFile()" class="btn" title="上传">
          <i class="fas fa-upload"></i>
        </button>
        <button class="btn" id="pauseBtn" title="暂停" onclick="handlePause()">
          <i class="fas fa-pause"></i>
        </button>
        <button class="btn" title="继续" id="resumeBtn" style="display: none" onclick="handleResume()">
          <i class="fas fa-play"></i>
        </button>
        <button
          id="downloadBtn"
          class="btn disabled"
          title="下载"
          onclick="download()"
        >
          <i class="fas fa-download"></i>
        </button>
      </div>
      <div class="notice">
        <span style="color: #00e0ff"
          >本库借助<a
            target="_blank"
            href="https://www.npmjs.com/package/enlarge-file-upload"
            >enlarge-file-upload</a
          >库实现大文件上传</span
        ><br />
        <span style="color: #00e0ff">欢迎加入大文件上传技术交流QQ群: 324710217</span><br />
        <span style="color: #00e0ff">注意:</span>
        本演示仅用于演示大文件上传的效果,实际使用中请根据实际需求进行调整。
        <br />
      </div>
    </div>
    <script>
      // 粒子效果配置
      tsParticles.load("tsparticles", {
        background: { color: "#0d1117" },
        fpsLimit: 60,
        interactivity: {
          events: { onHover: { enable: true, mode: "repulse" }, resize: true },
          modes: { repulse: { distance: 100, duration: 0.4 } },
        },
        particles: {
          color: { value: "#00e0ff" },
          links: {
            enable: true,
            distance: 150,
            color: "#00e0ff",
            opacity: 0.4,
            width: 1,
          },
          move: { enable: true, speed: 2, outModes: "out" },
          number: { value: 80, density: { enable: true, area: 800 } },
          opacity: { value: 0.5 },
          shape: { type: "circle" },
          size: { value: { min: 1, max: 5 } },
        },
        detectRetina: true,
      });
      // 环形进度条控制变量声明
      const circle = document.querySelector(".progress-ring-fill");
      const progressText = document.querySelector(".progress-ring-text");
      const downloadBtn = document.getElementById("downloadBtn");
      const pauseBtn = document.getElementById("pauseBtn");
      const resumeBtn = document.getElementById("resumeBtn");
      const radius = 110;
      const circumference = 2 * Math.PI * radius;
      circle.style.strokeDasharray = `${circumference}`;
      circle.style.strokeDashoffset = `${circumference}`;

      // 环形进度条控制函数
      function setProgress(percent, text, val = "-30", tra = "0.3s") {
        const offset = circumference - (percent / 100) * circumference;
        circle.style.transition = "stroke-dashoffset 0.6s ease";
        circle.style.strokeDashoffset = offset;
        if (text) {
          progressText.textContent = text;
        } else {
          progressText.textContent = `${percent.toFixed(2)}%`;
        }
        progressText.style.transform = `translate(${val}%, -50%)`;
        progressText.style.transition = `transform 0.3s ease`;
      }

      function showMessage(type, text, duration = 3000) {
        const messageEl = document.getElementById("message");
        const iconEl = messageEl.querySelector(".icon");
        const textEl = messageEl.querySelector(".text");

        // 清除旧状态
        messageEl.className = "message";
        iconEl.className = "icon fas";
        textEl.textContent = text;

        // 设置状态样式和图标
        switch (type) {
          case "success":
            messageEl.classList.add("success");
            iconEl.classList.add("fa-check-circle");
            break;
          case "warning":
            messageEl.classList.add("warning");
            iconEl.classList.add("fa-exclamation-triangle");
            break;
          case "error":
            messageEl.classList.add("error");
            iconEl.classList.add("fa-times-circle");
            break;
        }

        // 显示消息
        messageEl.classList.add("show");

        // 隐藏消息
        setTimeout(() => {
          messageEl.classList.remove("show");
        }, duration);
      }

      // 启用下载按钮函数
      function enableDownloadButton() {
        downloadBtn.classList.remove("disabled");
        downloadBtn.disabled = false;
      }

      // 禁用下载按钮函数
      function disableDownloadButton() {
        downloadBtn.classList.add("disabled");
        downloadBtn.disabled = true;
      }

         const pre = "http://jiang-12-13.com:8989";
    //   const pre = "http://localhost:3000";
      const fileName = document.querySelector(".fileName");

      const globalOBj = {};
      function optFile() {
        // showMessage("error", "上传失败,请重试!");
        const fileInput = document.querySelector(".btn[type='file']");
        fileInput.click();
      }

      async function uploadFunction({ chunk, index, hash, cancelToken }) {
        const formData = new FormData();
        formData.append("chunk", chunk);
        formData.append("hash", hash || globalOBj.hash);
        formData.append("index", index.toString());
        formData.append("fileName", globalOBj.fileName);
        formData.append("totalChunks", globalOBj.uploader.state.totalChunks);

        try {
          const res = await axios.post(`${pre}/api/upload`, formData, {
            cancelToken,
          });
          if (res.data) {
            globalOBj.loadLink = res.data.filePath;
            setProgress(100, undefined, "-15");
            asd;
          }
        } catch (error) {
          // 处理请求被取消的情况
          if (axios.isCancel(error)) {
            console.log("请求被取消", index); // 调试信息
            throw error; // 重新抛出错误,让外层捕获
          }

          // 处理其他错误(如 422)
          if (
            error.response &&
            error.response.data.code === 422 &&
            globalOBj.error
          ) {
            showMessage("error", error.response.data.message);
            globalOBj.error = false;
            globalOBj.uploader.reset();
          } else {
            // 其他未知错误也重新抛出
            throw error;
          }
        }
      }

      // 文件选择处理函数
      async function handleFileSelect(event) {
        const file = event.target.files[0];
        if (!file) return;
        // globalOBj.uploader?.reset();
        fileName.textContent = `已选择: ${file.name}`;
        fileName.classList.remove("show"); // 先移除旧类以触发重播
        void fileName.offsetWidth; // 触发重绘
        fileName.classList.add("show");
        const MAX_FILE_SIZE = 500 * 1024 * 1024; // 30MB
        if (file.size > MAX_FILE_SIZE) {
          showMessage(
            "warning",
            "测试服务器,仅用于大文件上传演示,文件大小不能超过 500MB!"
          );
          return;
        }
        globalOBj.uploader?.reset();
        const flag = await check(file);
        if (flag) {
          // 秒传
          showMessage("success", "文件秒传");
          setProgress(100, "秒传", "-12");
          globalOBj.fileName = file.name;
          enableDownloadButton();
          globalOBj.loadLink = flag.data;
          globalOBj.speed = "秒传";
          return;
        }
        if (file && globalOBj.uploader) {
          disableDownloadButton();
          globalOBj.loadLink = "";
          globalOBj.fileName = file.name;
          globalOBj.uploader.upload(file);
          globalOBj.error = true;
        }
      }

      // 暂停上传
      function handlePause() {
        if (globalOBj.uploader) {
          globalOBj.uploader.pause();
          // 隐藏暂停按钮,显示继续按钮
          pauseBtn.style.display = "none";
          resumeBtn.style.display = "inline-block";
        }
      }

      // 继续上传
      function handleResume() {
        if (globalOBj.uploader) {
          globalOBj.uploader.resume();
          resumeBtn.style.display = "none";
          pauseBtn.style.display = "inline-block";
        }
      }
      async function download() {
        if (!globalOBj.loadLink) return;
        // 创建下载链接
        const url = `${pre}/api/upload/download?path=${encodeURIComponent(
          globalOBj.loadLink
        )}`;
        // 创建隐藏的a标签并触发点击
        const a = document.createElement("a");
        a.href = url;
        a.download = globalOBj.fileName;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }
      async function check(file) {
        disableDownloadButton();
        const currentTime = Date.now();
        setProgress("100", "计算hash中...", "-35");
        const { chunkHashMap, totalChunks } = await fileUploader.checker(file, {
          chunkMap: { indices: [0] },
          hash: false,
        });
        const hash = chunkHashMap.get(0);
        const intervalTime = Date.now() - currentTime;
        const outTime = intervalTime > 2000 ? 0 : 2000 - intervalTime;
        // 使用定时保证动画效果
        await new Promise((resolve) => {
          setTimeout(() => {
            setProgress(0, undefined, "-15");
            resolve();
          }, outTime);
        });
        globalOBj.hash = hash;
        const response = await axios.post(`${pre}/api/upload/check`, {
          hash,
        });
        const obj = response.data;
        const uploaderConfig = {
          chunkSize: 5 * 1024 * 1024, // 100KB
          concurrency: 5,
          maxRetries: 3,
          hash: false,
          uploadFunction,
          onProgress: (progressValue) => {
            setProgress(progressValue, undefined, "-20");
          },
          onSuccess: () => {
            showMessage("success", "文件上传成功");
            enableDownloadButton();
          },
          onSpeed: (speedValue) => {
            globalOBj.speed = speedValue;
          },
        };
        if (obj.flag === "0") {
          return obj;
        } else if (obj.flag === "1") {
          const arr = obj.data.map(Number);
          uploaderConfig.hash = false;
          // 创建一个包含0到total的完整数组
          const fullSet = Array.from({ length: totalChunks }, (_, i) => i);
          console.log(fullSet);
          // 找出缺少的数字
          const missingNumbers = fullSet.filter((num) => !arr.includes(num));
          showMessage("success", "开始断点续传");
          console.log(missingNumbers, "missingNumbers");
          uploaderConfig.includeChunks = missingNumbers;
          globalOBj.uploader = fileUploader.createUploader(uploaderConfig);
          return false;
        } else {
          uploaderConfig.includeChunks = undefined;
          uploaderConfig.hash = false;
          globalOBj.uploader = fileUploader.createUploader(uploaderConfig);
          return false;
        }
      }
    </script>
  </body>
</html>

后端代码(nodejs实现)

......待补充