HaoKun_web_stie/ruoyi-ui/src/components/FragmentUpload/index.vue
2024-08-15 11:14:31 +08:00

440 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 v-show="panelShow">
<div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
<div class="file-title">
<h2>文件列表</h2>
<div class="operate">
<el-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>
</el-button>
<el-button @click="close" type="text" title="关闭">
<i class="el-icon-close" style="color:black;font-size: 18px"></i>
</el-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 { getToken } from '@/utils/auth'
import {ACCEPT_CONFIG} from '@/assets/js/config'
import Bus from '@/assets/js/bus'
import SparkMD5 from 'spark-md5';
import { fileMerge } from '@/api/system/file/fileuploader/fileuploader';
export default {
data() {
return {
options: {
// 目标上传 URL
target: process.env.VUE_APP_BASE_API +'/official/chunkFilelist/upload',
//分块大小
chunkSize: 5 * 1024 * 1000,
//上传文件时文件的参数名默认file
fileParameterName: 'file',
//并发上传数
//simultaneousUploads: 1,
//最大自动失败重试上传次数
maxChunkRetries: 0,
//重试间隔 单位毫秒
//chunkRetryInterval: 5000,
//是否开启服务器分片校验
testChunks: true,
// 服务器分片校验函数,秒传及断点续传基础
checkChunkUploadedByResponse: function (chunk, message) {
let objMessage = JSON.parse(message).data;
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
headers: {
Authorization: 'Bearer ' + getToken()
},
// 额外的自定义查询参数
query() {
},
multiple: false,
},
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: {
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.data.skipUpload) {
// 不需要合并
Bus.$emit('fileSuccess');
this.panelShow = false
Bus.$emit('refreshFileList'); // 触发刷新文件列表事件
// res.data.videoUrl
this.$emit('input', res.data.videoUrl);
} else {
// 文件状态设为“合并中”
this.statusSet(file.id, 'merging');
let param = {
'filename': rootFile.name,
'identifier': rootFile.uniqueIdentifier,
'totalSize': rootFile.size
}
this.customMerge(param, file.id);
}
},
customMerge (param, fileId, count = 3) {
if (count === 0) return;
fileMerge(param).then(res => {
// 文件合并成功
Bus.$emit('fileSuccess');
this.statusRemove(fileId);
this.panelShow = false
Bus.$emit('refreshFileList'); // 触发刷新文件列表事件
// res.data
this.$emit('input', res.data);
}).catch(e => {
// console.log("合并异常,重新发起请求,文件名为:", file.name)
//由于网络或服务器原因,导致合并过程中断线,此时如果不重新发起请求,就会进入失败的状态,导致该文件无法重试
this.customMerge(params, fileId, count - 1)
});
},
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,
filename: file.name
}
})
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() {
Bus.$off('openUploader');
},
components: {}
}
</script>
<style scoped lang="scss">
#global-uploader {
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0px 4px;
}
li {
display: inline-block;
}
.uploader-app {
width: 660px;
}
.file-panel {
background-color: #fff;
border: 1px solid #e2e2e2;
border-radius: 7px 7px 0 0;
box-shadow: 0 0 10px rgba(0, 0, 0, .2);
.file-title {
display: flex;
height: 45px;
padding: 0 15px;
border-bottom: 1px solid #ddd;
font-size: 14px;
.operate {
flex: 1;
text-align: right;
}
}
.file-list {
position: relative;
height: 264px;
width: 654px;
overflow-x: hidden;
overflow-y: auto;
background-color: #fff;
> li {
background-color: #fff;
}
}
&.collapse {
.file-title {
background-color: #E7ECF2;
}
}
}
.no-file {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
}
.uploader-file {
width: 654px;
}
///deep/ .uploader-file-icon {
.uploader-file-icon {
&:before {
content: '' !important;
}
&[icon=image] {
background: url('~@/assets/images/image-icon.png');
}
&[icon=video] {
background: url('~@/assets/images/video-icon.png');
}
&[icon=document] {
background: url('~@/assets/images/text-icon.png');
}
}
///deep/ .uploader-file-actions > span {
.uploader-file-actions > span {
margin-right: 6px;
}
}
/* 隐藏上传按钮 */
#global-uploader-btn {
clip: rect(0, 0, 0, 0);
}
/*.uploader-list>ul>li{*/
/* width:100%;*/
/* color:red;*/
/* margin-bottom: 0;*/
/*}*/
/*a {*/
/* color: #42b983;*/
/*}*/
</style>