演示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实现)
......待补充