小编整理了一下,代码如下分为v2和v3版本:

Vue2:

<template>
  <div class="big-file-uploader">
    <el-upload
      action="#"
      :auto-upload="false"
      :on-change="handleFileChange"
      :show-file-list="false"
      :multiple="false"
      :accept="accept"
      :disabled="uploading"
    >
      <el-button :icon="uploadIcon" type="primary" :loading="uploading">
        {{ uploading ? '上传中...' : '选择文件' }}
      </el-button>
      <div slot="tip" class="el-upload__tip" v-if="tip">
        {{ tip }}
      </div>
    </el-upload>
    <div v-if="currentFile" class="file-card">
      <div class="file-info">
        <div class="file-name">
          <i class="el-icon-document"></i>
          <span>{{ currentFile.name }}</span>
        </div>
        <div class="file-size">{{ formatFileSize(currentFile.size) }}</div>
        <div class="file-chunks" v-if="totalChunks > 0">
          分片: {{ uploadedChunks.length }}/{{ totalChunks }}
        </div>
      </div>
      <div class="upload-progress">
        <el-progress 
          :percentage="percent" 
          :status="progressStatus" 
          :stroke-width="16"
          :text-inside="true"
        />
        <div class="progress-text">
          {{ progressText }}
        </div>
      </div>
      <div class="action-buttons">
        <el-button 
          v-if="!uploading && percent === 0" 
          type="success" 
          icon="el-icon-upload2"
          @click="startUpload"
        >
          开始上传
        </el-button>
        <el-button 
          v-if="uploading" 
          type="warning" 
          icon="el-icon-video-pause"
          @click="pauseUpload"
        >
          暂停
        </el-button>
        <el-button 
          v-if="!uploading && percent > 0 && percent < 100" 
          type="success" 
          icon="el-icon-caret-right"
          @click="resumeUpload"
        >
          继续
        </el-button>
        <el-button 
          v-if="percent > 0 && percent < 100" 
          type="danger" 
          icon="el-icon-close"
          @click="cancelUpload"
        >
          取消
        </el-button>
        <el-button 
          v-if="percent === 100" 
          type="primary" 
          icon="el-icon-refresh"
          @click="resetUpload"
        >
          上传新文件
        </el-button>
      </div>
      <div v-if="uploading" class="upload-details">
        <el-divider></el-divider>
        <div class="detail-item">
          <span>上传速度:</span>
          <span>{{ uploadSpeed }}</span>
        </div>
        <div class="detail-item">
          <span>剩余时间:</span>
          <span>{{ remainingTime }}</span>
        </div>
        <div class="detail-item">
          <span>当前分片:</span>
          <span>{{ currentChunk + 1 }}/{{ totalChunks }}</span>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import axios from 'axios';
import SparkMD5 from 'spark-md5'; // 需要安装: npm install spark-md5
export default {
  name: 'BigFileUploader',
  props: {
    // 上传接口地址
    action: {
      type: String,
      required: true
    },
    // 分片大小(字节)
    chunkSize: {
      type: Number,
      default: 2 * 1024 * 1024 // 默认2MB
    },
    // 同时上传的分片数量
    concurrent: {
      type: Number,
      default: 3
    },
    // 接受的文件类型
    accept: {
      type: String,
      default: '*'
    },
    // 提示文本
    tip: {
      type: String,
      default: '支持大文件分片上传,请选择文件'
    },
    // 自定义请求头
    headers: {
      type: Object,
      default: () => ({})
    },
    // 其他额外数据
    data: {
      type: Object,
      default: () => ({})
    },
    // 是否计算文件MD5(用于秒传和断点续传)
    enableHash: {
      type: Boolean,
      default: true
    },
    // 最大重试次数
    maxRetries: {
      type: Number,
      default: 3
    }
  },
  data() {
    return {
      currentFile: null,
      uploading: false,
      percent: 0,
      uploadedChunks: [],
      cancelToken: null,
      fileIdentifier: '',
      totalChunks: 0,
      currentChunk: 0,
      uploadSpeed: '0 KB/s',
      remainingTime: '计算中...',
      uploadStartTime: 0,
      uploadSize: 0,
      uploadIcon: 'el-icon-upload2',
      status: ''
    };
  },
  computed: {
    progressText() {
      if (this.percent === 0) return '等待上传';
      if (this.percent === 100) return '上传完成';
      return `上传中 ${this.percent}%`;
    },
    progressStatus() {
      if (this.percent === 100) return 'success';
      if (this.status === 'error') return 'exception';
      return '';
    }
  },
  methods: {
    // 格式化文件大小显示
    formatFileSize(bytes) {
      if (bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', '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];
    },
    // 生成文件唯一标识(使用MD5或文件信息)
    async generateFileIdentifier(file) {
      if (!this.enableHash) {
        return `${file.name}-${file.size}-${file.lastModified}`;
      }
      return new Promise((resolve) => {
        const spark = new SparkMD5.ArrayBuffer();
        const fileReader = new FileReader();
        const chunkSize = 2 * 1024 * 1024; // 2MB chunks for hashing
        const chunks = Math.ceil(file.size / chunkSize);
        let currentChunk = 0;
        fileReader.onload = (e) => {
          spark.append(e.target.result);
          currentChunk++;
          if (currentChunk < chunks) {
            loadNext();
          } else {
            resolve(spark.end());
          }
        };
        fileReader.onerror = () => {
          // 如果哈希计算失败,使用备用方案
          resolve(`${file.name}-${file.size}-${file.lastModified}`);
        };
        const loadNext = () => {
          const start = currentChunk * chunkSize;
          const end = Math.min(start + chunkSize, file.size);
          fileReader.readAsArrayBuffer(file.slice(start, end));
        };
        loadNext();
      });
    },
    // 文件选择回调
    async handleFileChange(file) {
      if (this.uploading) {
        this.$message.warning('请先完成当前上传任务');
        return;
      }
      try {
        this.$emit('file-selected', file.raw);
        this.currentFile = file.raw;
        this.percent = 0;
        this.status = '';
        this.uploadedChunks = [];
        this.uploadSize = 0;
        this.uploadStartTime = 0;
        // 生成文件唯一标识
        this.fileIdentifier = await this.generateFileIdentifier(this.currentFile);
        this.totalChunks = Math.ceil(this.currentFile.size / this.chunkSize);
        // 检查已上传的分片
        await this.checkUploadedChunks();
      } catch (error) {
        console.error('文件选择错误:', error);
        this.$message.error('文件选择失败');
      }
    },
    // 检查已上传的分片
    async checkUploadedChunks() {
      try {
        const response = await axios.get(`${this.action}/check`, {
          params: {
            identifier: this.fileIdentifier,
            fileName: this.currentFile.name,
            totalChunks: this.totalChunks
          },
          headers: this.headers
        });
        if (response.data.success) {
          this.uploadedChunks = response.data.uploadedChunks || [];
          this.percent = Math.round((this.uploadedChunks.length / this.totalChunks) * 100);
          if (this.uploadedChunks.length > 0) {
            this.$notify({
              title: '发现未完成的上传',
              message: `检测到已有 ${this.uploadedChunks.length} 个分片上传成功,可以继续上传`,
              type: 'info',
              duration: 3000
            });
          }
        }
      } catch (error) {
        console.warn('检查已上传分片失败:', error);
        // 不影响正常上传流程
      }
    },
    // 开始上传
    async startUpload() {
      if (!this.currentFile) {
        this.$message.warning('请先选择文件');
        return;
      }
      this.uploading = true;
      this.status = '';
      this.uploadStartTime = Date.now();
      this.uploadSize = this.uploadedChunks.length * this.chunkSize;
      try {
        this.cancelToken = axios.CancelToken.source();
        const totalChunks = this.totalChunks;
        // 创建上传任务队列
        const uploadTasks = [];
        for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
          if (this.uploadedChunks.includes(chunkIndex)) {
            continue;
          }
          uploadTasks.push({
            chunkIndex,
            retryCount: 0
          });
        }
        // 并发上传控制
        const concurrent = Math.min(this.concurrent, uploadTasks.length);
        const workers = [];
        for (let i = 0; i < concurrent; i++) {
          workers.push(this.processUploadQueue(uploadTasks));
        }
        await Promise.all(workers);
        // 所有分片上传完成,请求合并
        await this.mergeChunks();
        // 上传成功
        this.percent = 100;
        this.status = 'success';
        this.$message.success('文件上传成功');
        this.$emit('success', {
          file: this.currentFile,
          identifier: this.fileIdentifier,
          totalSize: this.currentFile.size
        });
      } catch (error) {
        if (axios.isCancel(error)) {
          this.$message.info('上传已取消');
          this.$emit('cancel');
        } else {
          console.error('上传失败:', error);
          this.status = 'error';
          this.$message.error('文件上传失败');
          this.$emit('error', error);
        }
      } finally {
        this.uploading = false;
      }
    },
    // 处理上传队列
    async processUploadQueue(queue) {
      while (queue.length > 0) {
        if (this.cancelToken && this.cancelToken.reason) {
          break; // 上传被取消
        }
        const task = queue.shift();
        if (!task) continue;
        try {
          await this.uploadChunk(task.chunkIndex);
          // 上传成功后从队列中移除
          this.uploadedChunks.push(task.chunkIndex);
          this.currentChunk = task.chunkIndex;
          // 更新进度
          this.updateProgress();
        } catch (error) {
          if (axios.isCancel(error)) {
            throw error;
          }
          // 重试逻辑
          if (task.retryCount < this.maxRetries) {
            task.retryCount++;
            queue.unshift(task); // 重新加入队列
            console.warn(`分片 ${task.chunkIndex} 上传失败,第 ${task.retryCount} 次重试`);
          } else {
            console.error(`分片 ${task.chunkIndex} 上传失败,已达最大重试次数`);
            throw new Error(`分片 ${task.chunkIndex} 上传失败`);
          }
        }
      }
    },
    // 上传单个分片
    async uploadChunk(chunkIndex) {
      const file = this.currentFile;
      const start = chunkIndex * this.chunkSize;
      const end = Math.min(start + this.chunkSize, file.size);
      const chunk = file.slice(start, end);
      const formData = new FormData();
      formData.append('file', chunk);
      formData.append('chunkIndex', chunkIndex);
      formData.append('totalChunks', this.totalChunks);
      formData.append('identifier', this.fileIdentifier);
      formData.append('filename', file.name);
      formData.append('chunkSize', this.chunkSize);
      // 添加额外数据
      Object.keys(this.data).forEach(key => {
        formData.append(key, this.data[key]);
      });
      await axios.post(this.action, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          ...this.headers
        },
        cancelToken: this.cancelToken ? this.cancelToken.token : undefined,
        onUploadProgress: (progressEvent) => {
          // 更新上传速度和剩余时间
          this.calculateSpeedAndTime(progressEvent.loaded);
        }
      });
    },
    // 更新上传进度
    updateProgress() {
      const newPercent = Math.round((this.uploadedChunks.length / this.totalChunks) * 100);
      if (newPercent !== this.percent) {
        this.percent = newPercent;
        this.$emit('progress', {
          percent: this.percent,
          uploadedChunks: this.uploadedChunks.length,
          totalChunks: this.totalChunks,
          uploadedSize: this.uploadedChunks.length * this.chunkSize,
          totalSize: this.currentFile.size
        });
      }
    },
    // 计算上传速度和剩余时间
    calculateSpeedAndTime(loaded) {
      const currentTime = Date.now();
      const elapsedTime = (currentTime - this.uploadStartTime) / 1000; // 秒
      if (elapsedTime > 0) {
        const speed = loaded / elapsedTime; // bytes per second
        this.uploadSpeed = `${this.formatFileSize(speed)}/s`;
        const remainingBytes = this.currentFile.size - this.uploadSize - loaded;
        const remainingSeconds = remainingBytes / speed;
        if (remainingSeconds > 3600) {
          this.remainingTime = `${Math.ceil(remainingSeconds / 3600)}小时`;
        } else if (remainingSeconds > 60) {
          this.remainingTime = `${Math.ceil(remainingSeconds / 60)}分钟`;
        } else {
          this.remainingTime = `${Math.ceil(remainingSeconds)}秒`;
        }
      }
    },
    // 合并分片
    async mergeChunks() {
      try {
        await axios.post(`${this.action}/merge`, {
          identifier: this.fileIdentifier,
          filename: this.currentFile.name,
          totalChunks: this.totalChunks,
          chunkSize: this.chunkSize,
          totalSize: this.currentFile.size
        }, {
          headers: this.headers
        });
      } catch (error) {
        console.error('分片合并失败:', error);
        throw new Error('分片合并失败');
      }
    },
    // 暂停上传
    pauseUpload() {
      if (this.cancelToken) {
        this.cancelToken.cancel('用户暂停上传');
        this.uploading = false;
        this.$message.info('上传已暂停');
        this.$emit('paused', {
          uploadedChunks: this.uploadedChunks.length,
          totalChunks: this.totalChunks,
          percent: this.percent
        });
      }
    },
    // 继续上传
    resumeUpload() {
      this.startUpload();
    },
    // 取消上传
    cancelUpload() {
      if (this.cancelToken) {
        this.cancelToken.cancel('用户取消上传');
        this.uploading = false;
        this.percent = 0;
        this.status = '';
        this.$message.info('上传已取消');
        this.$emit('cancel');
      }
    },
    // 重置上传
    resetUpload() {
      if (this.cancelToken && this.uploading) {
        this.cancelToken.cancel('用户重置上传');
      }
      this.currentFile = null;
      this.percent = 0;
      this.status = '';
      this.uploading = false;
      this.uploadedChunks = [];
      this.fileIdentifier = '';
      this.totalChunks = 0;
      this.currentChunk = 0;
      this.uploadSpeed = '0 KB/s';
      this.remainingTime = '计算中...';
      this.$emit('reset');
    }
  },
  beforeDestroy() {
    if (this.cancelToken && this.uploading) {
      this.cancelToken.cancel('组件销毁');
    }
  }
};
</script>
<style scoped>
.big-file-uploader {
  padding: 20px;
}
.file-card {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background-color: #f5f7fa;
}
.file-info {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  gap: 10px;
}
.file-name {
  display: flex;
  align-items: center;
  font-weight: 500;
  flex: 1;
  min-width: 200px;
}
.file-name i {
  margin-right: 8px;
  color: #409eff;
}
.file-size, .file-chunks {
  color: #909399;
  font-size: 0.9em;
  white-space: nowrap;
}
.upload-progress {
  margin-bottom: 15px;
}
.progress-text {
  text-align: center;
  margin-top: 5px;
  color: #606266;
  font-size: 0.9em;
}
.action-buttons {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.upload-details {
  margin-top: 15px;
}
.detail-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 5px;
  font-size: 0.9em;
  color: #606266;
}
.detail-item span:first-child {
  font-weight: 500;
}
.el-upload__tip {
  margin-top: 10px;
  color: #909399;
  font-size: 0.9em;
}
@media (max-width: 768px) {
  .file-info {
    flex-direction: column;
    align-items: flex-start;
  }
  .action-buttons {
    justify-content: center;
  }
}
    使用示例 
    father.vue
    <template>
      <div class="upload-example">
        <el-card shadow="hover" class="upload-card">
          <div slot="header">
            <span>大文件上传示例</span>
            <el-tag type="success" style="margin-left: 10px;">Vue2 + Element UI</el-tag>
          </div>
    
          <big-file-uploader
            :action="uploadAction"
            :chunk-size="5 * 1024 * 1024"
            :concurrent="3"
            :max-retries="3"
            :enable-hash="true"
            accept="*/*"
            :tip="uploadTip"
            :headers="uploadHeaders"
            :data="uploadData"
            @success="handleSuccess"
            @error="handleError"
            @progress="handleProgress"
            @paused="handlePaused"
            @cancel="handleCancel"
            @reset="handleReset"
            @file-selected="handleFileSelected"
          />
    
          <el-divider></el-divider>
    
          <div class="upload-status">
            <h3>上传状态监控</h3>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="当前进度">
                <el-tag :type="getStatusType">{{ progressInfo.percent }}%</el-tag>
              </el-descriptions-item>
              <el-descriptions-item label="已上传分片">
                {{ progressInfo.uploadedChunks }}/{{ progressInfo.totalChunks }}
              </el-descriptions-item>
              <el-descriptions-item label="已上传大小">
                {{ formatSize(progressInfo.uploadedSize) }}
              </el-descriptions-item>
              <el-descriptions-item label="总文件大小">
                {{ formatSize(progressInfo.totalSize) }}
              </el-descriptions-item>
              <el-descriptions-item label="上传速度" v-if="uploadStats.speed">
                {{ uploadStats.speed }}
              </el-descriptions-item>
              <el-descriptions-item label="剩余时间" v-if="uploadStats.remaining">
                {{ uploadStats.remaining }}
              </el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
    
        <el-alert
          title="后端接口要求"
          type="info"
          description="组件需要后端提供三个接口:分片上传接口、检查已上传分片接口、合并分片接口"
          show-icon
          :closable="false"
          style="margin-top: 20px;"
        />
      </div>
    </template>
    
    <script>
    import BigFileUploader from '@/components/BigFileUploader.vue';
    
    export default {
      name: 'UploadExample',
    
      components: {
        BigFileUploader
      },
    
      data() {
        return {
          uploadAction: '/api/upload', // 你的上传接口地址
          uploadTip: '请选择要上传的文件,支持大文件分片上传和断点续传',
          uploadHeaders: {
            'Authorization': 'Bearer your-token-here'
          },
          uploadData: {
            userId: 123,
            category: 'uploads'
          },
          progressInfo: {
            percent: 0,
            uploadedChunks: 0,
            totalChunks: 0,
            uploadedSize: 0,
            totalSize: 0
          },
          uploadStats: {
            speed: '',
            remaining: ''
          }
        };
      },
    
      computed: {
        getStatusType() {
          if (this.progressInfo.percent === 100) return 'success';
          if (this.progressInfo.percent > 0) return 'primary';
          return 'info';
        }
      },
    
      methods: {
        handleSuccess(data) {
          console.log('上传成功:', data);
          this.$notify({
            title: '上传成功',
            message: `文件 ${data.file.name} 上传完成`,
            type: 'success',
            duration: 3000
          });
        },
    
        handleError(error) {
          console.error('上传失败:', error);
          this.$notify.error({
            title: '上传失败',
            message: error.message || '文件上传失败,请重试',
            duration: 3000
          });
        },
    
        handleProgress(progress) {
          this.progressInfo = { ...progress };
    
          // 模拟计算上传速度和剩余时间(实际组件中会计算)
          if (progress.percent > 0 && progress.percent < 100) {
            this.uploadStats.speed = '2.5 MB/s';
            const remainingTime = Math.round((100 - progress.percent) / 2);
            this.uploadStats.remaining = `${remainingTime}秒`;
          } else {
            this.uploadStats.speed = '';
            this.uploadStats.remaining = '';
          }
        },
    
        handlePaused(data) {
          console.log('上传暂停:', data);
          this.$message.info(`上传已暂停,已完成 ${data.percent}%`);
        },
    
        handleCancel() {
          console.log('上传取消');
          this.resetProgress();
        },
    
        handleReset() {
          console.log('上传重置');
          this.resetProgress();
        },
    
        handleFileSelected(file) {
          console.log('文件选择:', file);
          this.$message.info(`已选择文件: ${file.name}`);
        },
    
        resetProgress() {
          this.progressInfo = {
            percent: 0,
            uploadedChunks: 0,
            totalChunks: 0,
            uploadedSize: 0,
            totalSize: 0
          };
          this.uploadStats = {
            speed: '',
            remaining: ''
          };
        },
    
        formatSize(bytes) {
          if (!bytes) return '0 B';
          const k = 1024;
          const sizes = ['B', '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];
        }
      }
    };
    </script>
    
    <style scoped>
    .upload-example {
      padding: 20px;
      max-width: 800px;
      margin: 0 auto;
    }
    
    .upload-card {
      margin-bottom: 20px;
    }
    
    .upload-status {
      margin-top: 20px;
    }
    
    .upload-status h3 {
      margin-bottom: 15px;
      color: #409EFF;
    }
    
    ::v-deep .el-descriptions__body {
      background-color: #f9fafc;
    }
    </style>

    # 安装必要的依赖

    npm install axios spark-md5

    # 如果还没有安装 Element UI

    npm install element-ui

    Vue3:

    <template>  <div class="big-file-uploader">    <!-- 上传按钮 -->    <el-upload      action="#"      :auto-upload="false"      :on-change="handleFileChange"      :show-file-list="false"      :multiple="false"      :accept="accept"      :disabled="uploading"    >      <el-button :icon="Upload" type="primary">选择文件</el-button>      <template #tip>        <div class="el-upload__tip" v-if="tip">          {{ tip }}        </div>      </template>    </el-upload>
        <!-- 文件信息和上传控制 -->    <div v-if="currentFile" class="file-card">      <div class="file-info">        <div class="file-name">          <el-icon><Document /></el-icon>          <span>{{ currentFile.name }}</span>        </div>        <div class="file-size">{{ formatFileSize(currentFile.size) }}</div>      </div>
          <div class="upload-progress">        <el-progress           :percentage="percent"           :status="status"           :stroke-width="16"          :text-inside="true"        />        <div class="progress-text">          {{ progressText }}        </div>      </div>
          <div class="action-buttons">        <el-button           v-if="!uploading && percent === 0"           type="success"           :icon="UploadFilled"          @click="startUpload"        >          开始上传        </el-button>
            <el-button           v-if="uploading"           type="warning"           :icon="VideoPause"          @click="pauseUpload"        >          暂停        </el-button>
            <el-button           v-if="!uploading && percent > 0 && percent < 100"           type="success"           :icon="CaretRight"          @click="resumeUpload"        >          继续        </el-button>
            <el-button           v-if="percent > 0 && percent < 100"           type="danger"           :icon="Close"          @click="cancelUpload"        >          取消        </el-button>
            <el-button           v-if="percent === 100"           type="primary"           :icon="RefreshLeft"          @click="resetUpload"        >          上传新文件        </el-button>      </div>    </div>  </div></template>
    <script setup>import { ref, computed } from 'vue'import { ElMessage, ElNotification } from 'element-plus'import {  Upload,  Document,  UploadFilled,  VideoPause,  CaretRight,  Close,  RefreshLeft} from '@element-plus/icons-vue'import axios from 'axios'
    // 定义组件属性const props = defineProps({  // 上传接口地址  action: {    type: String,    required: true  },  // 分片大小(字节)  chunkSize: {    type: Number,    default: 2 * 1024 * 1024 // 默认2MB  },  // 同时上传的分片数量  concurrent: {    type: Number,    default: 3  },  // 接受的文件类型  accept: {    type: String,    default: '*'  },  // 提示文本  tip: {    type: String,    default: '支持大文件分片上传,请选择文件'  },  // 自定义请求头  headers: {    type: Object,    default: () => ({})  },  // 其他额外数据  data: {    type: Object,    default: () => ({})  }})
    // 定义组件事件const emit = defineEmits(['success', 'error', 'progress', 'cancel'])
    // 响应式数据const currentFile = ref(null)const uploading = ref(false)const percent = ref(0)const status = ref('') // ''|success|exception|warningconst uploadedChunks = ref([])const cancelTokenSource = ref(null)const fileIdentifier = ref('') // 文件唯一标识
    // 计算属性const progressText = computed(() => {  if (percent.value === 0) return '等待上传'  if (percent.value === 100) return '上传完成'  return `已上传 ${percent.value}%`})
    // 处理方法// 格式化文件大小const formatFileSize = (bytes) => {  if (bytes === 0) return '0 B'  const k = 1024  const sizes = ['B', '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]}
    // 生成文件标识(简易版,生产环境应使用文件哈希)const generateFileIdentifier = (file) => {  return `${file.name}-${file.size}-${file.lastModified}`}
    // 文件选择回调const handleFileChange = (uploadFile) => {  if (uploading.value) {    ElMessage.warning('请先完成当前上传任务')    return  }
      currentFile.value = uploadFile.raw  percent.value = 0  status.value = ''  uploadedChunks.value = []  fileIdentifier.value = generateFileIdentifier(currentFile.value)
      // 检查之前是否上传过部分分片  checkUploadedChunks()}
    // 检查已上传的分片const checkUploadedChunks = async () => {  try {    const response = await axios.get(`${props.action}/check`, {      params: {        identifier: fileIdentifier.value,        fileName: currentFile.value.name      },      headers: props.headers    })
        if (response.data.success) {      uploadedChunks.value = response.data.uploadedChunks || []      const totalChunks = Math.ceil(currentFile.value.size / props.chunkSize)      percent.value = Math.round((uploadedChunks.value.length / totalChunks) * 100)
          if (uploadedChunks.value.length > 0) {        ElNotification({          title: '发现未完成的上传',          message: `检测到已有 ${uploadedChunks.value.length} 个分片上传成功,可以继续上传`,          type: 'info'        })      }    }  } catch (error) {    console.error('检查已上传分片失败:', error)    // 不影响正常上传流程  }}
    // 开始上传const startUpload = async () => {  if (!currentFile.value) {    ElMessage.warning('请先选择文件')    return  }
      uploading.value = true  status.value = ''
      try {    // 创建取消令牌    cancelTokenSource.value = axios.CancelToken.source()
        // 计算总分片数    const file = currentFile.value    const totalChunks = Math.ceil(file.size / props.chunkSize)
        // 创建分片上传任务    const uploadPromises = []    const activeUploads = new Set()
        for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {      // 如果分片已上传,跳过      if (uploadedChunks.value.includes(chunkIndex)) {        continue      }
          // 控制并发数      if (activeUploads.size >= props.concurrent) {        await Promise.race(activeUploads)      }
          const promise = uploadChunk(chunkIndex, totalChunks)        .then(() => {          activeUploads.delete(promise)        })        .catch(err => {          activeUploads.delete(promise)          throw err        })
          activeUploads.add(promise)      uploadPromises.push(promise)    }
        // 等待所有分片上传完成    await Promise.all(uploadPromises)
        // 所有分片上传完成,请求合并    await mergeChunks(totalChunks)
        // 上传成功    percent.value = 100    status.value = 'success'    ElMessage.success('文件上传成功')    emit('success', {      file: currentFile.value,      identifier: fileIdentifier.value    })  } catch (error) {    if (axios.isCancel(error)) {      console.log('上传被取消:', error.message)      ElMessage.info('上传已取消')      emit('cancel')    } else {      console.error('上传失败:', error)      status.value = 'exception'      ElMessage.error('文件上传失败')      emit('error', error)    }  } finally {    uploading.value = false  }}
    // 上传单个分片const uploadChunk = async (chunkIndex, totalChunks) => {  const file = currentFile.value  const start = chunkIndex * props.chunkSize  const end = Math.min(start + props.chunkSize, file.size)  const chunk = file.slice(start, end)
      const formData = new FormData()  formData.append('file', chunk)  formData.append('chunkIndex', chunkIndex)  formData.append('totalChunks', totalChunks)  formData.append('identifier', fileIdentifier.value)  formData.append('filename', file.name)  formData.append('chunkSize', props.chunkSize)
      // 添加额外数据  Object.keys(props.data).forEach(key => {    formData.append(key, props.data[key])  })
      try {    await axios.post(props.action, formData, {      headers: {        'Content-Type': 'multipart/form-data',        ...props.headers      },      cancelToken: cancelTokenSource.value.token,      onUploadProgress: (progressEvent) => {        // 分片上传进度,如果需要可以在这里处理      }    })
        // 记录已上传的分片    uploadedChunks.value.push(chunkIndex)
        // 更新总进度    const newPercent = Math.round((uploadedChunks.value.length / totalChunks) * 100)    if (newPercent !== percent.value) {      percent.value = newPercent      emit('progress', {        percent: percent.value,        uploadedChunks: uploadedChunks.value.length,        totalChunks: totalChunks      })    }  } catch (error) {    if (axios.isCancel(error)) {      throw error    }    console.error(`分片 ${chunkIndex} 上传失败:`, error)    throw new Error(`分片 ${chunkIndex} 上传失败`)  }}
    // 合并分片const mergeChunks = async (totalChunks) => {  try {    await axios.post(`${props.action}/merge`, {      identifier: fileIdentifier.value,      filename: currentFile.value.name,      totalChunks: totalChunks    }, {      headers: props.headers    })  } catch (error) {    console.error('分片合并失败:', error)    throw new Error('分片合并失败')  }}
    // 暂停上传const pauseUpload = () => {  if (cancelTokenSource.value) {    cancelTokenSource.value.cancel('用户暂停上传')    uploading.value = false    ElMessage.info('上传已暂停')  }}
    // 继续上传const resumeUpload = () => {  startUpload()}
    // 取消上传const cancelUpload = () => {  if (cancelTokenSource.value) {    cancelTokenSource.value.cancel('用户取消上传')    uploading.value = false    percent.value = 0    status.value = ''    ElMessage.info('上传已取消')    emit('cancel')  }}
    // 重置上传const resetUpload = () => {  if (cancelTokenSource.value && uploading.value) {    cancelTokenSource.value.cancel('用户重置上传')  }
      currentFile.value = null  percent.value = 0  status.value = ''  uploading.value = false  uploadedChunks.value = []  fileIdentifier.value = ''}</script>
    <style scoped>.big-file-uploader {  padding: 20px;  border: 1px dashed #dcdfe6;  border-radius: 6px;}
    .file-card {  margin-top: 20px;  padding: 15px;  border: 1px solid #ebeef5;  border-radius: 4px;  background-color: #f5f7fa;}
    .file-info {  display: flex;  justify-content: space-between;  align-items: center;  margin-bottom: 15px;}
    .file-name {  display: flex;  align-items: center;  font-weight: 500;}
    .file-name .el-icon {  margin-right: 8px;  color: #409eff;}
    .file-size {  color: #909399;  font-size: 0.9em;}
    .upload-progress {  margin-bottom: 15px;}
    .progress-text {  text-align: center;  margin-top: 5px;  color: #606266;  font-size: 0.9em;}
    .action-buttons {  display: flex;  gap: 10px;  flex-wrap: wrap;}</style>  
    
    

    在父组件中使用这个上传组件:

    <template>  <div id="app">    <h1>大文件上传演示</h1>    <BigFileUploader      action="/api/upload"      :chunk-size="5 * 1024 * 1024" <!-- 5MB分片 -->      :concurrent="3"      accept=".pdf,.doc,.docx,.zip,.rar"      tip="请选择PDF、Word文档或压缩文件,最大支持10GB"      :data="{ userId: 123, category: 'documents' }"      @success="handleSuccess"      @error="handleError"      @progress="handleProgress"    />  </div></template>
    <script>import BigFileUploader from './components/BigFileUploader.vue'
    export default {  name: 'App',  components: {    BigFileUploader  },  methods: {    handleSuccess(data) {      console.log('上传成功', data)      this.$message.success(`文件 ${data.file.name} 上传成功`)    },    handleError(error) {      console.error('上传失败', error)      this.$message.error('文件上传失败')    },    handleProgress(progress) {      console.log('上传进度', progress)    }  }}</script>
    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐