最近工作中遇到一个实际问题:在将 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>

到此就结束了,剩下的就是你放飞自我的去上传吧,还可以看上传历史呦!
效果图:upload
转载请注明转自:运达's blog 原文地址:https://www.yunda51.com/?p=2027