有小伙伴后台反馈说需要大文件上传组件封装版本
·
小编整理了一下,代码如下分为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-uploadaction="#":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-buttonv-if="!uploading && percent === 0"type="success":icon="UploadFilled"@click="startUpload">开始上传</el-button><el-buttonv-if="uploading"type="warning":icon="VideoPause"@click="pauseUpload">暂停</el-button><el-buttonv-if="!uploading && percent > 0 && percent < 100"type="success":icon="CaretRight"@click="resumeUpload">继续</el-button><el-buttonv-if="percent > 0 && percent < 100"type="danger":icon="Close"@click="cancelUpload">取消</el-button><el-buttonv-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 = 1024const 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.rawpercent.value = 0status.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 = truestatus.value = ''try {// 创建取消令牌cancelTokenSource.value = axios.CancelToken.source()// 计算总分片数const file = currentFile.valueconst 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 = 100status.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.valueconst start = chunkIndex * props.chunkSizeconst 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 = newPercentemit('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 = falseElMessage.info('上传已暂停')}}// 继续上传const resumeUpload = () => {startUpload()}// 取消上传const cancelUpload = () => {if (cancelTokenSource.value) {cancelTokenSource.value.cancel('用户取消上传')uploading.value = falsepercent.value = 0status.value = ''ElMessage.info('上传已取消')emit('cancel')}}// 重置上传const resetUpload = () => {if (cancelTokenSource.value && uploading.value) {cancelTokenSource.value.cancel('用户重置上传')}currentFile.value = nullpercent.value = 0status.value = ''uploading.value = falseuploadedChunks.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><BigFileUploaderaction="/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>
更多推荐
所有评论(0)