最近工作中遇到一个实际问题:在将 BPF 数据转换为 Entier 格式时,生成的文件体积异常庞大,部分文件甚至达到 10G 级别。这类大文件上传至服务器时,不仅会占用大量带宽,还需耗费极长时间,一旦中途因网络波动或设备故障中断,就得重新上传,效率极低。
为此,我基于 Golang 开发了一套网页版断点续传工具 —— 通过前端页面交互配合后端逻辑,实现了文件分片传输与断点记录功能。即便夜间上传过程中出现中断,次日也能从断点处继续传输,极大提升了大文件上传的便捷性与稳定性。代码本人亲测(我设置了最大可上传单文件大小为20G),可以直接用。废话不多说了,咱们就言归正传,你需要创建main.go和index.html两个文件。
代码如下:main.go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
)
// 文件上传状态记录
type UploadStatus struct {
TotalChunks int `json:"total_chunks"`
Uploaded []bool `json:"uploaded"`
Filename string `json:"filename"`
UUID string `json:"uuid"`
Path string `json:"path"`
Completed bool `json:"completed"`
Size int64 `json:"size"`
UploadedAt time.Time `json:"uploaded_at"`
}
// 全局上传状态记录
var uploadsMutex sync.Mutex
var uploads = make(map[string]*UploadStatus)
func main() {
// 创建上传目录
os.MkdirAll("/home/datawork/uploads", 0755)
os.MkdirAll("/home/datawork/chunks", 0755)
// 注册路由
http.HandleFunc("/api/upload", handleUpload)
http.HandleFunc("/api/status", handleStatus)
http.HandleFunc("/api/merge", handleMerge)
http.HandleFunc("/", serveIndex)
// 启动服务器
fmt.Println("服务器启动在 http://10.25.77.4:8999")
log.Fatal(http.ListenAndServe(":8999", nil))
}
// 处理文件块上传
func handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
return
}
// 解析表单数据
err := r.ParseMultipartForm(32 << 20) // 32MB 缓冲区
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 获取文件信息
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// 获取上传参数
uuid := r.FormValue("uuid")
chunkIndex := r.FormValue("chunkIndex")
totalChunks := r.FormValue("totalChunks")
filename := r.FormValue("filename")
fileSize := r.FormValue("fileSize")
// 验证参数
if uuid == "" || chunkIndex == "" || totalChunks == "" || filename == "" || fileSize == "" {
http.Error(w, "缺少必要参数", http.StatusBadRequest)
return
}
// 转换参数类型
index, err := strconv.Atoi(chunkIndex)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
total, err := strconv.Atoi(totalChunks)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
size, err := strconv.ParseInt(fileSize, 10, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 创建文件块目录
chunkDir := filepath.Join("/home/datawork/chunks", uuid)
os.MkdirAll(chunkDir, 0755)
// 保存文件块
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("%d", index))
out, err := os.Create(chunkPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 更新上传状态
uploadsMutex.Lock()
defer uploadsMutex.Unlock()
// 初始化上传状态(如果不存在)
if _, exists := uploads[uuid]; !exists {
uploads[uuid] = &UploadStatus{
TotalChunks: total,
Uploaded: make([]bool, total),
Filename: filename,
UUID: uuid,
Path: filepath.Join("/home/datawork/uploads", filename),
Completed: false,
Size: size,
UploadedAt: time.Now(),
}
}
// 标记当前块已上传
if index < len(uploads[uuid].Uploaded) {
uploads[uuid].Uploaded[index] = true
}
// 检查是否所有块都已上传
allUploaded := true
for _, uploaded := range uploads[uuid].Uploaded {
if !uploaded {
allUploaded = false
break
}
}
if allUploaded {
uploads[uuid].Completed = true
}
// 返回当前上传状态
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(uploads[uuid])
}
// 获取上传状态
func handleStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
return
}
uuid := r.URL.Query().Get("uuid")
if uuid == "" {
http.Error(w, "缺少uuid参数", http.StatusBadRequest)
return
}
uploadsMutex.Lock()
defer uploadsMutex.Unlock()
if status, exists := uploads[uuid]; exists {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
} else {
http.Error(w, "找不到上传记录", http.StatusNotFound)
}
}
// 合并文件块
func handleMerge(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
return
}
uuid := r.FormValue("uuid")
if uuid == "" {
http.Error(w, "缺少uuid参数", http.StatusBadRequest)
return
}
uploadsMutex.Lock()
defer uploadsMutex.Unlock()
if status, exists := uploads[uuid]; exists {
if !status.Completed {
http.Error(w, "文件尚未上传完成", http.StatusBadRequest)
return
}
// 创建目标文件
out, err := os.Create(status.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer out.Close()
// 按顺序合并文件块
chunkDir := filepath.Join("/home/datawork/chunks", uuid)
for i := 0; i < status.TotalChunks; i++ {
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("%d", i))
chunk, err := os.Open(chunkPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = io.Copy(out, chunk)
chunk.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 删除已合并的块
os.Remove(chunkPath)
}
// 删除临时目录
os.RemoveAll(chunkDir)
// 返回成功信息
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"path": status.Path,
})
} else {
http.Error(w, "找不到上传记录", http.StatusNotFound)
}
}
// 提供前端页面
func serveIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./index.html")
}
网页代码如下:index.html
<!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://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
danger: '#EF4444',
warning: '#F59E0B',
dark: '#1F2937',
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.bg-gradient-blue {
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
}
.shadow-blue {
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.2);
}
.progress-animation {
transition: width 0.3s ease-in-out;
}
}
</style>
</head>
<body class="font-inter bg-gray-50 min-h-screen flex flex-col">
<!-- 导航栏 -->
<header class="bg-gradient-blue text-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold flex items-center">
<i class="fa fa-cloud-upload mr-2"></i> 大文件上传系统
</h1>
<nav>
<ul class="flex space-x-6">
<li><a href="#" class="hover:text-gray-200 transition-colors duration-300"><i class="fa fa-home mr-1"></i> 首页</a></li>
<li><a href="#" class="hover:text-gray-200 transition-colors duration-300"><i class="fa fa-question-circle mr-1"></i> 帮助</a></li>
</ul>
</nav>
</div>
</header>
<!-- 主要内容区 -->
<main class="flex-grow container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-lg p-6 md:p-8 transform transition-all duration-300 hover:shadow-blue">
<div class="text-center mb-8">
<h2 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-dark mb-2">上传大文件</h2>
<p class="text-gray-600">支持超大文件上传和断点续传,最大支持 20GB 文件</p>
</div>
<!-- 文件上传区域 -->
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-8 hover:border-primary transition-colors duration-300" id="dropZone">
<i class="fa fa-cloud-upload text-5xl text-gray-400 mb-4"></i>
<h3 class="text-xl font-semibold text-gray-700 mb-2">拖放文件到此处上传</h3>
<p class="text-gray-500 mb-4">或点击选择文件</p>
<label for="fileInput" class="inline-block bg-primary hover:bg-primary/90 text-white font-medium py-3 px-6 rounded-lg shadow-lg transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5">
<i class="fa fa-folder-open mr-2"></i> 选择文件
</label>
<input type="file" id="fileInput" class="hidden" accept="*">
</div>
<!-- 上传进度区域 -->
<div id="uploadProgressContainer" class="hidden">
<div class="flex justify-between items-center mb-2">
<h3 class="font-semibold text-dark" id="fileName">文件名</h3>
<span class="text-sm text-gray-500" id="fileSize">0 MB</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3 mb-2">
<div id="uploadProgressBar" class="bg-primary h-3 rounded-full progress-animation" style="width: 0%"></div>
</div>
<div class="flex justify-between text-sm mb-4">
<span id="uploadPercentage">0%</span>
<span id="uploadSpeed">0 KB/s</span>
<span id="uploadTimeLeft">计算中...</span>
</div>
<div class="flex space-x-3">
<button id="pauseUploadBtn" class="flex-1 bg-warning hover:bg-warning/90 text-white py-2 px-4 rounded-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa fa-pause mr-2"></i> 暂停
</button>
<button id="resumeUploadBtn" class="flex-1 bg-secondary hover:bg-secondary/90 text-white py-2 px-4 rounded-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa fa-play mr-2"></i> 继续
</button>
<button id="cancelUploadBtn" class="flex-1 bg-danger hover:bg-danger/90 text-white py-2 px-4 rounded-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa fa-times mr-2"></i> 取消
</button>
</div>
</div>
<!-- 历史上传记录 -->
<div class="mt-10">
<h3 class="text-xl font-semibold text-dark mb-4 flex items-center">
<i class="fa fa-history mr-2 text-primary"></i> 上传历史
</h3>
<div id="uploadHistory" class="space-y-4">
<!-- 上传历史将通过JavaScript动态添加 -->
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-dark text-white py-6 mt-10">
<div class="container mx-auto px-4 text-center">
<p>© 2023 大文件上传系统 | 支持断点续传和超大文件上传</p>
<div class="mt-2 text-sm text-gray-400">
<p>系统支持最大 20GB 文件上传,适用于各种大文件传输场景</p>
</div>
</div>
</footer>
<script>
// 全局变量
let file = null;
let uploadStatus = null;
let isUploading = false;
let isPaused = false;
let chunks = [];
let currentChunkIndex = 0;
let totalChunks = 0;
let uploadStartTime = 0;
let uploadedBytes = 0;
let chunkSize = 10 * 1024 * 1024; // 10MB 分块大小
// DOM 元素
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const uploadProgressContainer = document.getElementById('uploadProgressContainer');
const uploadProgressBar = document.getElementById('uploadProgressBar');
const uploadPercentage = document.getElementById('uploadPercentage');
const uploadSpeed = document.getElementById('uploadSpeed');
const uploadTimeLeft = document.getElementById('uploadTimeLeft');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const pauseUploadBtn = document.getElementById('pauseUploadBtn');
const resumeUploadBtn = document.getElementById('resumeUploadBtn');
const cancelUploadBtn = document.getElementById('cancelUploadBtn');
const uploadHistory = document.getElementById('uploadHistory');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化拖拽上传
initDragDrop();
// 初始化文件选择
fileInput.addEventListener('change', handleFileSelect);
// 初始化按钮事件
pauseUploadBtn.addEventListener('click', pauseUpload);
resumeUploadBtn.addEventListener('click', resumeUpload);
cancelUploadBtn.addEventListener('click', cancelUpload);
// 加载上传历史
loadUploadHistory();
});
// 初始化拖拽上传
function initDragDrop() {
// 阻止默认行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
// 添加拖拽样式
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
// 处理文件拖放
dropZone.addEventListener('drop', handleDrop, false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight() {
dropZone.classList.add('border-primary');
dropZone.classList.add('bg-primary/5');
}
function unhighlight() {
dropZone.classList.remove('border-primary');
dropZone.classList.remove('bg-primary/5');
}
function handleDrop(e) {
const dt = e.dataTransfer;
const droppedFile = dt.files[0];
if (droppedFile) {
handleFile(droppedFile);
}
}
// 处理文件选择
function handleFileSelect(e) {
const selectedFile = e.target.files[0];
if (selectedFile) {
handleFile(selectedFile);
}
}
// 处理文件
function handleFile(selectedFile) {
file = selectedFile;
// 显示文件信息
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
// 显示进度条
uploadProgressContainer.classList.remove('hidden');
// 检查是否有之前的上传记录
checkUploadStatus(file);
}
// 检查上传状态
function checkUploadStatus(file) {
// 生成文件唯一标识符
const fileUUID = generateUUID(file);
// 从本地存储获取上传状态
const savedStatus = localStorage.getItem(`uploadStatus_${fileUUID}`);
if (savedStatus) {
uploadStatus = JSON.parse(savedStatus);
// 检查服务器上的状态
fetch(`/api/status?uuid=${uploadStatus.UUID}`)
.then(response => response.json())
.then(data => {
// 更新上传状态
uploadStatus = data;
// 更新UI
updateProgressUI();
// 显示上传历史
addToUploadHistory(uploadStatus);
// 询问用户是否继续上传
if (!uploadStatus.Completed) {
if (confirm(`检测到未完成的上传任务,是否继续上传?\n已上传: ${calculateProgress(uploadStatus)}%`)) {
startUpload();
}
}
})
.catch(error => {
console.error('获取上传状态失败:', error);
// 状态可能已过期,重新开始上传
uploadStatus = null;
startUpload();
});
} else {
// 开始新的上传
uploadStatus = {
UUID: fileUUID,
Filename: file.name,
TotalChunks: Math.ceil(file.size / chunkSize),
Uploaded: Array(Math.ceil(file.size / chunkSize)).fill(false),
Completed: false,
Size: file.size,
UploadedAt: new Date().toISOString()
};
// 更新UI
updateProgressUI();
// 开始上传
startUpload();
}
}
// 开始上传
function startUpload() {
if (!file) return;
isUploading = true;
isPaused = false;
// 更新按钮状态
pauseUploadBtn.disabled = false;
resumeUploadBtn.disabled = true;
cancelUploadBtn.disabled = false;
// 记录开始时间
uploadStartTime = Date.now();
uploadedBytes = 0;
// 准备分块
prepareChunks();
// 开始上传第一个块
uploadNextChunk();
}
// 准备文件分块
function prepareChunks() {
chunks = [];
totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push({
index: i,
blob: chunk,
size: chunk.size,
uploaded: uploadStatus ? uploadStatus.Uploaded[i] : false
});
}
// 设置当前块索引为第一个未上传的块
currentChunkIndex = 0;
while (currentChunkIndex < chunks.length && chunks[currentChunkIndex].uploaded) {
currentChunkIndex++;
uploadedBytes += chunks[currentChunkIndex - 1].size;
}
}
// 上传下一个块
function uploadNextChunk() {
if (currentChunkIndex >= chunks.length || isPaused || !isUploading) {
if (currentChunkIndex >= chunks.length && isUploading) {
// 所有块都已上传,检查是否所有块都成功
checkAllChunksUploaded();
}
return;
}
const chunk = chunks[currentChunkIndex];
// 创建表单数据
const formData = new FormData();
formData.append('file', chunk.blob);
formData.append('uuid', uploadStatus.UUID);
formData.append('chunkIndex', chunk.index);
formData.append('totalChunks', totalChunks);
formData.append('filename', file.name);
formData.append('fileSize', file.size);
// 上传块
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
// 监听进度
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
// 计算当前块的上传进度
const chunkProgress = e.loaded / e.total;
// 更新总进度
const totalProgress = (uploadedBytes + e.loaded) / file.size;
// 更新UI
updateProgressUI(totalProgress);
// 计算上传速度和剩余时间
const elapsedTime = (Date.now() - uploadStartTime) / 1000; // 秒
const uploadSpeedValue = (uploadedBytes + e.loaded) / elapsedTime; // 字节/秒
// 更新速度和剩余时间
uploadSpeed.textContent = formatFileSize(uploadSpeedValue) + '/s';
if (uploadSpeedValue > 0) {
const remainingBytes = file.size - (uploadedBytes + e.loaded);
const remainingSeconds = remainingBytes / uploadSpeedValue;
uploadTimeLeft.textContent = formatTime(remainingSeconds);
}
}
});
// 上传完成
xhr.onload = () => {
if (xhr.status === 200) {
// 更新上传状态
uploadedBytes += chunk.size;
chunks[currentChunkIndex].uploaded = true;
// 更新服务器状态
uploadStatus.Uploaded[currentChunkIndex] = true;
// 保存状态到本地存储
localStorage.setItem(`uploadStatus_${uploadStatus.UUID}`, JSON.stringify(uploadStatus));
// 更新历史记录
addToUploadHistory(uploadStatus);
// 上传下一个块
currentChunkIndex++;
uploadNextChunk();
} else {
console.error('上传失败:', xhr.status, xhr.statusText);
// 重试当前块
setTimeout(() => uploadNextChunk(), 1000);
}
};
// 上传错误
xhr.onerror = () => {
console.error('上传错误:', xhr.status, xhr.statusText);
// 重试当前块
setTimeout(() => uploadNextChunk(), 1000);
};
// 发送请求
xhr.send(formData);
}
// 检查是否所有块都已上传
function checkAllChunksUploaded() {
let allUploaded = true;
for (let i = 0; i < chunks.length; i++) {
if (!chunks[i].uploaded) {
allUploaded = false;
break;
}
}
if (allUploaded) {
// 所有块都已上传,合并文件
mergeChunks();
} else {
// 有块上传失败,重新上传
uploadNextChunk();
}
}
// 合并文件块
function mergeChunks() {
// 显示正在合并的消息
uploadSpeed.textContent = '正在合并文件...';
uploadTimeLeft.textContent = '请稍候...';
// 发送合并请求
fetch('/api/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `uuid=${uploadStatus.UUID}`,
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// 更新上传状态
uploadStatus.Completed = true;
localStorage.setItem(`uploadStatus_${uploadStatus.UUID}`, JSON.stringify(uploadStatus));
// 更新UI
updateProgressUI(1.0);
uploadSpeed.textContent = '上传完成';
uploadTimeLeft.textContent = '用时: ' + formatTime((Date.now() - uploadStartTime) / 1000);
// 更新历史记录
addToUploadHistory(uploadStatus);
// 禁用控制按钮
pauseUploadBtn.disabled = true;
resumeUploadBtn.disabled = true;
cancelUploadBtn.disabled = true;
// 显示成功消息
alert('文件上传完成!');
} else {
console.error('合并文件失败:', data);
alert('合并文件失败,请重试');
}
})
.catch(error => {
console.error('合并文件错误:', error);
alert('合并文件时发生错误,请重试');
});
}
// 暂停上传
function pauseUpload() {
isPaused = true;
isUploading = false;
// 更新按钮状态
pauseUploadBtn.disabled = true;
resumeUploadBtn.disabled = false;
cancelUploadBtn.disabled = false;
// 更新UI
uploadSpeed.textContent = '已暂停';
uploadTimeLeft.textContent = '点击继续';
}
// 继续上传
function resumeUpload() {
if (!file) return;
isUploading = true;
isPaused = false;
// 更新按钮状态
pauseUploadBtn.disabled = false;
resumeUploadBtn.disabled = true;
cancelUploadBtn.disabled = false;
// 记录开始时间
uploadStartTime = Date.now();
// 开始上传下一个块
uploadNextChunk();
}
// 取消上传
function cancelUpload() {
if (!confirm('确定要取消上传吗?')) return;
isUploading = false;
isPaused = false;
// 重置UI
uploadProgressBar.style.width = '0%';
uploadPercentage.textContent = '0%';
uploadSpeed.textContent = '已取消';
uploadTimeLeft.textContent = '';
// 更新按钮状态
pauseUploadBtn.disabled = true;
resumeUploadBtn.disabled = true;
cancelUploadBtn.disabled = true;
// 隐藏进度条
setTimeout(() => {
uploadProgressContainer.classList.add('hidden');
}, 1000);
// 清空文件
file = null;
fileInput.value = '';
}
// 更新进度UI
function updateProgressUI(progress = null) {
if (progress === null && uploadStatus) {
progress = calculateProgress(uploadStatus);
}
if (progress !== null) {
const percentage = Math.round(progress * 100);
uploadProgressBar.style.width = `${percentage}%`;
uploadPercentage.textContent = `${percentage}%`;
}
}
// 计算上传进度
function calculateProgress(status) {
if (!status) return 0;
let uploadedChunks = 0;
for (let i = 0; i < status.Uploaded.length; i++) {
if (status.Uploaded[i]) {
uploadedChunks++;
}
}
return uploadedChunks / status.TotalChunks;
}
// 生成文件唯一标识符
function generateUUID(file) {
// 使用文件名、大小和修改日期生成唯一标识符
return `${file.name}-${file.size}-${file.lastModified}`;
}
// 格式化文件大小
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 formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
let timeStr = '';
if (hours > 0) timeStr += `${hours}h `;
if (minutes > 0) timeStr += `${minutes}m `;
timeStr += `${secs}s`;
return timeStr;
}
// 添加到上传历史
function addToUploadHistory(status) {
// 从本地存储获取所有上传历史
let history = JSON.parse(localStorage.getItem('uploadHistory') || '[]');
// 查找是否已存在相同的上传记录
const index = history.findIndex(item => item.UUID === status.UUID);
if (index === -1) {
// 添加新记录
history.push(status);
} else {
// 更新现有记录
history[index] = status;
}
// 保存到本地存储
localStorage.setItem('uploadHistory', JSON.stringify(history));
// 更新UI
renderUploadHistory(history);
}
// 加载上传历史
function loadUploadHistory() {
const history = JSON.parse(localStorage.getItem('uploadHistory') || '[]');
renderUploadHistory(history);
}
// 渲染上传历史
function renderUploadHistory(history) {
// 清空历史记录
uploadHistory.innerHTML = '';
// 按上传时间排序(最新的在前)
history.sort((a, b) => new Date(b.UploadedAt) - new Date(a.UploadedAt));
// 显示最多10条记录
const displayHistory = history.slice(0, 10);
if (displayHistory.length === 0) {
uploadHistory.innerHTML = `
<div class="bg-gray-50 p-4 rounded-lg text-center text-gray-500">
<i class="fa fa-history text-2xl mb-2"></i>
<p>暂无上传记录</p>
</div>
`;
return;
}
// 渲染每条记录
displayHistory.forEach(status => {
const progress = calculateProgress(status);
const percentage = Math.round(progress * 100);
const date = new Date(status.UploadedAt).toLocaleString();
let statusClass = 'bg-warning';
let statusText = '上传中';
if (status.Completed) {
statusClass = 'bg-secondary';
statusText = '已完成';
} else if (percentage === 0) {
statusClass = 'bg-gray-400';
statusText = '未开始';
}
const historyItem = document.createElement('div');
historyItem.className = 'bg-white rounded-lg shadow p-4 border border-gray-100 transition-all duration-300 hover:shadow-md';
historyItem.innerHTML = `
<div class="flex justify-between items-start mb-2">
<div class="flex-1">
<h4 class="font-medium text-dark truncate">${status.Filename}</h4>
<p class="text-sm text-gray-500">${formatFileSize(status.Size)}</p>
</div>
<span class="px-2 py-1 rounded-full text-xs font-medium ${statusClass.replace('bg-', 'text-')} ${statusClass}">
${statusText}
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-1">
<div class="h-2 rounded-full ${statusClass} progress-animation" style="width: ${percentage}%"></div>
</div>
<div class="flex justify-between text-xs text-gray-500">
<span>${percentage}%</span>
<span>${date}</span>
</div>
<div class="mt-3 flex space-x-2">
${status.Completed ? '' : `
<button class="text-primary hover:text-primary/80 text-sm flex items-center transition-colors duration-300"
onclick="resumeUploadFromHistory('${status.UUID}')">
<i class="fa fa-play-circle mr-1"></i> 继续
</button>
`}
<button class="text-danger hover:text-danger/80 text-sm flex items-center transition-colors duration-300"
onclick="deleteUploadHistory('${status.UUID}')">
<i class="fa fa-trash mr-1"></i> 删除
</button>
</div>
`;
uploadHistory.appendChild(historyItem);
});
}
// 从历史记录继续上传
window.resumeUploadFromHistory = function(uuid) {
const history = JSON.parse(localStorage.getItem('uploadHistory') || '[]');
const status = history.find(item => item.UUID === uuid);
if (status && !status.Completed) {
// 这里需要用户重新选择文件
alert('请重新选择文件以继续上传');
fileInput.click();
// 存储要恢复的上传UUID
localStorage.setItem('resumeUploadUUID', uuid);
}
};
// 删除上传历史
window.deleteUploadHistory = function(uuid) {
if (!confirm('确定要删除此上传记录吗?')) return;
let history = JSON.parse(localStorage.getItem('uploadHistory') || '[]');
history = history.filter(item => item.UUID !== uuid);
localStorage.setItem('uploadHistory', JSON.stringify(history));
// 重新渲染历史记录
renderUploadHistory(history);
};
</script>
</body>
</html>
到此就结束了,剩下的就是你放飞自我的去上传吧,还可以看上传历史呦!
效果图:
转载请注明转自:运达's blog 原文地址:https://www.yunda51.com/?p=2027