特性
- 分片
- 秒传
- 断点续传
- 显示上传进度
接口
需要3个接口:
- GET /system/file/uploadChunk 请求:通过query参数传递文件信息内容,文件MD5、文件总大小、文件分片数、文件每片的大小等。
响应:是否秒传,待上传的分片。 - POST /system/file/uploadChunk 分片发送文件。
- POST /system/file/merge 文件分片传输完成,合并分片文件。
小文件上传(单个)
请求3个接口:

GET /system/file/uploadChunk请求参数:

接口响应数据:

POST /system/file/uploadChunk请求数据(返回数据为true或false):

POST /system/file/merge请求数据:

大文件上传(分片)
文件分片上传,所有分片的文件上传完成后,调用merge接口合并。





秒传
请求2个接口:

GET /system/file/uploadChunk请求参数:

接口响应数据:
exists为true时表示秒传。

POST /system/file/merge请求数据:

代码
FileUploader.vue
html
<template>
<div id="global-uploader">
<!-- 上传 -->
<uploader
ref="uploader"
:options="options"
:autoStart="false"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
class="uploader-app">
<uploader-unsupport></uploader-unsupport>
<uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
<uploader-list></uploader-list>
<!-- <uploader-list v-slot:default="props" v-show="panelShow">
<div class="file-panel" :class="{'collapse': collapse}">
<div class="file-title">
<h2>文件列表1</h2>
<div class="operate">
<n-button @click="fileListShow" type="text" :title="collapse ? '展开':'折叠' ">
<i class="el-icon-d-caret" style="color:black;font-size: 18px"
:class="collapse ? 'inuc-fullscreen': 'inuc-minus-round'"></i>
</n-button>
<n-button @click="close" type="text" title="关闭">
<i class="el-icon-close" style="color:black;font-size: 18px"></i>
</n-button>
</div>
</div>
<ul class="file-list">
<li v-for="file in props.fileList" :key="file.id">
<uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
</li>
<div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-empty-file"></i> 暂无待上传文件</div>
</ul>
</div>
</uploader-list> -->
</uploader>
</div>
</template>
<script>
/**
* 全局上传插件
* 调用方法:Bus.$emit('openUploader', {}) 打开文件选择框,参数为需要传递的额外参数
* 监听函数:Bus.$on('fileAdded', fn); 文件选择后的回调
* Bus.$on('fileSuccess', fn); 文件上传成功的回调
*/
import $ from 'jquery';
import SparkMD5 from 'spark-md5';
import uploader from 'vue-simple-uploader';
// import Bus from '../../../../assets/js/bus';
import { fileMerge } from '@/apis/fileuploader';
import { merge } from 'lodash';
import { ACCEPT_CONFIG } from './config';
let type = 1;
export default {
props: {
type: {
type: Number,
required: false
}
},
data() {
return {
options: {
// 目标上传 URL
target: '/system/file/upload',
//分块大小
chunkSize: 5 * 1024 * 1000,
//上传文件时文件的参数名,默认file
fileParameterName: 'file',
//并发上传数
//simultaneousUploads: 1,
//最大自动失败重试上传次数
maxChunkRetries: 2,
//重试间隔 单位毫秒
//chunkRetryInterval: 5000,
//是否开启服务器分片校验
testChunks: true,
// 服务器分片校验函数,秒传及断点续传基础
checkChunkUploadedByResponse: function (chunk, message) {
let objMessage = JSON.parse(message);
if (objMessage.exists) {
return true;
}
// if (objMessage.skipUpload) {
// return true;
// }
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
headers: {
'Authorization-Token': '',
},
// 额外的自定义查询参数
query: { upload_token: 'my_token' }
},
attrs: {
accept: ACCEPT_CONFIG.getAll()
},
panelShow: false, //选择文件后,展示上传panel
collapse: false
}
},
mounted() {
//接收子组件触发的事件
// Bus.$on('openUploader', query => {
// this.params = query || {};
// this.options.headers.Authorization = 'Bearer ' + query.token
// if (this.$refs.uploadBtn) {
// $("#global-uploader-btn").click();
// }
// });
},
computed: {
//Uploader实例
uploader() {
return this.$refs.uploader.uploader;
}
},
methods: {
getUrl() {
return `/lightning-web/upload/uploadChunk?type=${this.type}`
},
openUploader() {
if (this.$refs.uploadBtn) {
document.getElementById("global-uploader-btn").click();
}
},
test() {
alert();
},
onFileAdded(file) {
this.panelShow = true;
this.computeMD5(file);
// Bus.$emit('fileAdded');
},
//上传过程中,会不断触发file-progress上传进度的回调
onFileProgress(rootFile, file, chunk) {
console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},
onFileSuccess(rootFile, file, response, chunk) {
let res = JSON.parse(response);
// TODO 如有需要 解开注释 和后台协议如何处理这种情况:服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
// if (!res.result) {
// this.$message({message: res.message, type: 'error'});
// // 文件状态设为"失败"
// this.statusSet(file.id, 'failed');
// return
// }
// 如果服务端返回需要合并
if (res) {
// 文件状态设为"合并中"
this.statusSet(file.id, 'merging');
let param = {
'filename': rootFile.name,
'identifier': rootFile.uniqueIdentifier,
'totalSize': rootFile.size,
'type': type
}
fileMerge(param).then(res => {
// 文件合并成功
// Bus.$emit('fileSuccess');
this.statusRemove(file.id);
}).catch(e => {
console.log("合并异常,重新发起请求,文件名为:", file.name)
//由于网络或服务器原因,导致合并过程中断线,此时如果不重新发起请求,就会进入失败的状态,导致该文件无法重试
file.retry();
});
// 不需要合并
} else {
// Bus.$emit('fileSuccess');
console.log('上传成功');
}
},
onFileError(rootFile, file, response, chunk) {
// this.$message({
// message: response,
// type: 'error'
// })
},
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file) {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
// 文件状态设为"计算MD5"
this.statusSet(file.id, 'md5');
file.pause();
loadNext();
fileReader.onload = (e => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
this.$nextTick(() => {
$(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%')
})
} else {
let md5 = spark.end();
this.computeMD5Success(md5, file);
console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`)
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
computeMD5Success(md5, file) {
// 将自定义参数直接加载uploader实例的opts上
Object.assign(this.uploader.opts, {
query: {
...this.params,
}
})
file.uniqueIdentifier = md5;
file.resume();
this.statusRemove(file.id);
},
fileListShow() {
let $list = $('#global-uploader .file-list');
if ($list.is(':visible')) {
$list.slideUp();
this.collapse = true;
} else {
$list.slideDown();
this.collapse = false;
}
},
close() {
this.uploader.cancel();
this.panelShow = false;
},
/**
* 新增的自定义的状态: 'md5'、'transcoding'、'failed'
* @param id
* @param status
*/
statusSet(id, status) {
let statusMap = {
md5: {
text: '校验MD5',
bgc: '#fff'
},
merging: {
text: '合并中',
bgc: '#e2eeff'
},
transcoding: {
text: '转码中',
bgc: '#e2eeff'
},
failed: {
text: '上传失败',
bgc: '#e2eeff'
}
}
this.$nextTick(() => {
$(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
'position': 'absolute',
'top': '0',
'left': '0',
'right': '0',
'bottom': '0',
'zIndex': '1',
'line-height': 'initial',
'backgroundColor': statusMap[status].bgc
}).text(statusMap[status].text);
})
},
statusRemove(id) {
this.$nextTick(() => {
$(`.myStatus_${id}`).remove();
})
},
error(msg) {
this.$notify({
title: '错误',
message: msg,
type: 'error',
duration: 2000
})
}
},
watch: {
},
destroyed() {},
components: {}
}
</script>