11 changed files with 1628 additions and 4311 deletions
-
28src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload-old.vue
-
1286src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload.vue
-
243src/views/components/category/PreviewForm.vue
-
692src/views/components/category/preUpload-time.vue
-
1176src/views/components/category/preUpload.vue
-
654src/views/components/category/preUpload2.vue
-
838src/views/components/category/preUpload3.vue
-
670src/views/components/category/preUpload4.vue
-
279src/views/components/category/preUpload5.vue
-
50src/views/prearchiveLibrary/file/index.vue
-
23src/views/prearchiveLibrary/index.vue
1286
src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload.vue
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,692 +0,0 @@ |
|||
<template> |
|||
<div class="uploader-big"> |
|||
<uploader |
|||
ref="uploader" |
|||
:options="initOptions" |
|||
:file-status-text="fileStatusText" |
|||
:auto-start="false" |
|||
class="uploader-app" |
|||
@file-added="onFileAdded" |
|||
@file-success="onUploadSuccess" |
|||
@file-progress="onFileProgress" |
|||
@file-error="onFileError" |
|||
> |
|||
<uploader-unsupport /> |
|||
<!-- @click="clickUploader" --> |
|||
<uploader-drop class="custom_uploader_drop" element-loading-text="正在读取中"> |
|||
<uploader-btn ref="uploadBtn" :attrs="attrs"> |
|||
<slot> |
|||
<i class="iconfont icon-shangchuan" /> |
|||
上传 |
|||
</slot> |
|||
</uploader-btn> |
|||
</uploader-drop> |
|||
<div v-if="isUpload" class="upload_process_box"> |
|||
<div>文件名:{{ fileName }}</div> |
|||
<el-progress :percentage="uploadProcessNum" /> |
|||
<div v-if="isMd5Upload"> |
|||
正在读取文件中 - {{ md5ProgressText }} |
|||
</div> |
|||
<div v-if="!isSyncUpload&&!isMd5Upload"> |
|||
上传至服务器 - <span>{{ uploadSpeed }} M/s</span> |
|||
</div> |
|||
<div v-if="isSyncUpload"> |
|||
上传中,请稍后 |
|||
</div> |
|||
</div> |
|||
</uploader> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import SparkMD5 from 'spark-md5' |
|||
import { mapGetters } from 'vuex' |
|||
import axios from 'axios' |
|||
import { getToken } from '@/utils/auth' |
|||
import { getCurrentTime } from '@/utils/index' |
|||
|
|||
export default { |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: function() { |
|||
return {} |
|||
} |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: function() { |
|||
return '' |
|||
} |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: function() { |
|||
return {} |
|||
} |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: function() { |
|||
return '' |
|||
} |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
initOptions: { |
|||
target: null, |
|||
headers: { |
|||
Authorization: getToken() |
|||
}, |
|||
singleFile: false, // 启用单个文件上传 |
|||
uploadMethod: 'post', // 真正上传的时候使用的 HTTP 方法,默认 POST |
|||
maxChunkRetries: 3, // 最大自动失败重试上传次数 |
|||
testChunks: true, // 是否开启服务器分片校验 |
|||
parseTimeRemaining: function(timeRemaining, parsedTimeRemaining) { |
|||
return parsedTimeRemaining |
|||
.replace(/\syears?/, '年') |
|||
.replace(/\days?/, '天') |
|||
.replace(/\shours?/, '小时') |
|||
.replace(/\sminutes?/, '分钟') |
|||
.replace(/\sseconds?/, '秒') |
|||
}, |
|||
checkChunkUploadedByResponse: (chunk, message) => { |
|||
const result = JSON.parse(message) |
|||
if (result.data.skipUpload) { |
|||
this.skip = true |
|||
return true |
|||
} |
|||
return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0 |
|||
} |
|||
}, |
|||
attrs: { |
|||
accept: '' |
|||
}, |
|||
fileStatusText: { |
|||
success: '上传成功', |
|||
error: '上传失败', |
|||
uploading: '上传中', |
|||
paused: '已暂停', |
|||
waiting: '等待上传' |
|||
}, |
|||
isStartUpload: false, // 开始上传 |
|||
md5ProgressText: 0, |
|||
isMd5Upload: false, // 计算md5状态 |
|||
isUpload: false, // 正在上传 |
|||
uploadProcessNum: 0, // 上传进度 |
|||
uploadSpeed: 0, // 上传速度 |
|||
fileName: '', // 文件名 |
|||
isSyncUpload: false, // 是否在同步远程数据 |
|||
syncUploadProcessNum: 0, // 同步远程数据 |
|||
response: null, // 上传成功 |
|||
queryTimer: null, // 轮询计时器 |
|||
socket: null |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters([ |
|||
'baseApi' |
|||
]), |
|||
// Uploader实例 |
|||
uploader() { |
|||
return this.$refs.uploader.uploader |
|||
} |
|||
}, |
|||
created() { |
|||
this.initOptions.target = this.baseApi + '/api/collect/upload' |
|||
if (this.isBatchMount === 'true') { |
|||
this.attrs.accept = '.zip' |
|||
} else { |
|||
this.attrs.accept = '' |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
clearInterval(this.queryTimer) |
|||
}, |
|||
methods: { |
|||
|
|||
clickUploader(e) { |
|||
this.$refs.uploadBtn.$el.click() |
|||
}, |
|||
// 上传前 |
|||
onFileAdded(file) { |
|||
this.uploadProcessNum = 0 |
|||
// 计算MD5 |
|||
this.computeMD5(file).then((result) => this.startUpload(result)) |
|||
}, |
|||
/** |
|||
* 计算md5值,以实现断点续传及秒传 |
|||
*/ |
|||
computeMD5(file) { |
|||
const maxMessage = '上传文件大小不能超过 10GB!' |
|||
const maxSize = 10 * 1024 * 1024 * 1024 |
|||
if (file && file.size > maxSize) { |
|||
this.$message.warning(maxMessage) |
|||
return false |
|||
} |
|||
const fileReader = new FileReader() |
|||
const time = new Date().getTime() |
|||
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice |
|||
|
|||
let currentChunk = 0 |
|||
// const chunkSize = this.initOptions.chunkSize |
|||
const chunkSize = 10 * 1024 * 1024 |
|||
|
|||
// 使用 Math.ceil 实现向上取整 |
|||
const chunksWithCeil = Math.ceil(file.size / chunkSize) |
|||
console.log('使用 Math.ceil 得到的 chunks 数量:', chunksWithCeil) |
|||
|
|||
// 手动实现向上取整 |
|||
let chunks = Math.floor(file.size / chunkSize) |
|||
if (file.size % chunkSize !== 0) { |
|||
chunks++ |
|||
} |
|||
console.log('chunksManual', chunks) |
|||
|
|||
const spark = new SparkMD5.ArrayBuffer() |
|||
this.fileName = file.name |
|||
|
|||
// 文件状态设为"计算MD5" |
|||
this.isMd5Upload = true |
|||
this.isUpload = true |
|||
file.pause() |
|||
loadNext() |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
fileReader.onload = (e) => { |
|||
spark.append(e.target.result) |
|||
if (currentChunk < chunks) { |
|||
currentChunk++ |
|||
loadNext() |
|||
|
|||
// 实时展示MD5的计算进度 |
|||
this.$nextTick(() => { |
|||
this.md5ProgressText = ((currentChunk / chunks) * 100).toFixed(0) + '%' |
|||
}) |
|||
} else { |
|||
console.log('spark', spark) |
|||
const md5 = spark.end() |
|||
console.log('md5', md5) |
|||
// md5计算完毕 |
|||
resolve({ md5, file }) |
|||
console.log(file) |
|||
console.log( |
|||
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${ |
|||
new Date().getTime() - time |
|||
} ms` |
|||
) |
|||
} |
|||
} |
|||
|
|||
fileReader.onerror = function() { |
|||
this.$message({ message: `文件${file.name}读取出错,请检查该文件`, type: 'error', offset: 8 }) |
|||
file.cancel() |
|||
reject() |
|||
} |
|||
}) |
|||
|
|||
function loadNext() { |
|||
const start = currentChunk * chunkSize |
|||
const end = start + chunkSize >= file.size ? file.size : start + chunkSize |
|||
|
|||
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)) |
|||
} |
|||
}, |
|||
|
|||
// md5计算完毕,开始上传 |
|||
startUpload({ md5, file }) { |
|||
file.uniqueIdentifier = md5 |
|||
file.resume() |
|||
this.isMd5Upload = false |
|||
this.isStartUpload = true |
|||
}, |
|||
// 上传中 |
|||
onFileProgress(rootFile, file, chunk) { |
|||
const uploader = this.$refs.uploader.uploader |
|||
this.uploadProcessNum = Math.floor(uploader.progress() * 100) |
|||
// this.emit('onUploadProcess', uploader.progress()) |
|||
const averageSpeed = uploader.averageSpeed |
|||
const speed = averageSpeed / 1000 / 10 |
|||
this.uploadSpeed = speed.toFixed(2) |
|||
}, |
|||
// 上传中转站成功 |
|||
onUploadSuccess(rootFile, file, response, chunk) { |
|||
const res1 = JSON.parse(response) |
|||
if (res1.code === 200) { |
|||
// 开始merge |
|||
console.log('rootFile.uniqueIdentifier', rootFile.uniqueIdentifier) |
|||
const body = { |
|||
totalChunks: rootFile.chunks.length, |
|||
md5File: rootFile.uniqueIdentifier, |
|||
fileName: rootFile.name |
|||
} |
|||
console.log('body', body) |
|||
console.log(file) |
|||
this.handleUploadConfirm() |
|||
} else { |
|||
this.$message({ message: '上传失败!', type: 'error', offset: 8 }) |
|||
this.$emit('onUploadError', res1) |
|||
} |
|||
if (this.skip) { |
|||
this.skip = false |
|||
} |
|||
}, |
|||
// handleUploadConfirm() { |
|||
// if (this.$refs.uploader.fileList.length === 0) { |
|||
// this.$message({ message: '请选择要上传的文件!', type: 'error', offset: 8 }) |
|||
// return false |
|||
// } |
|||
// this.nowDate = getCurrentTime() |
|||
// const jsonArrayToSend = [] |
|||
// this.isSyncUpload = true |
|||
// // 使用 Promise.all 确保所有异步操作完成 |
|||
// Promise.all(this.$refs.uploader.fileList.map(async(item) => { |
|||
// console.log('item', item) |
|||
// const json = {} |
|||
// const jsonArray = [] |
|||
// const jsonString = {} |
|||
|
|||
// if (item.file.type.substring(0, item.file.type.indexOf('/')) === 'image') { |
|||
// const fileBase64 = await this.getBase64(item.file) |
|||
// const imgRes = await this.getImgPx(fileBase64) |
|||
// item.file.px = imgRes.width + 'px*' + imgRes.height + 'px' |
|||
// } else { |
|||
// item.file.px = '' |
|||
// } |
|||
|
|||
// jsonString.file_name = item.file.name |
|||
// jsonString.file_size = item.file.size |
|||
// jsonString.file_type = item.file.name.substring(item.name.lastIndexOf('.') + 1, item.file.name.length) |
|||
// json.last_modified = item.file.lastModified |
|||
// jsonString.file_path = '' |
|||
// jsonString.sequence = null |
|||
// jsonString.archive_id = this.arcId |
|||
// jsonString.file_dpi = item.file.px |
|||
// jsonString.file_thumbnail = '' |
|||
// jsonString.create_time = this.nowDate |
|||
// jsonString.id = null |
|||
// jsonArray.push(jsonString) |
|||
|
|||
// if (this.isBatchMount === 'true') { |
|||
// json.categoryId = this.selectedCategory.id |
|||
// } else { |
|||
// json.documentId = this.selectedDocument.id |
|||
// } |
|||
|
|||
// json.archivesId = this.arcId |
|||
// json.identifier = item.uniqueIdentifier |
|||
// json.filename = item.name |
|||
// json.totalChunks = item.chunks.length - 1 |
|||
// json.totalSize = item.size |
|||
// json.fileJsonString = JSON.stringify(jsonArray) |
|||
|
|||
// jsonArrayToSend.push(json) |
|||
// })).then(() => { |
|||
// console.log('jsonArrayToSend', jsonArrayToSend) |
|||
|
|||
// if (this.$refs.uploader.fileList.every(item => item.completed) && this.isUpload) { |
|||
// if (this.isBatchMount === 'true') { |
|||
// // 本地挂接 |
|||
// axios.post(this.baseApi + '/api/collect/merge', jsonArrayToSend, { |
|||
// headers: { |
|||
// 'Authorization': getToken() |
|||
// } |
|||
// }).then((res) => { |
|||
// console.log(res) |
|||
// if (res.data.code === 200 && res.data.data.length === 1 && res.data.data[0] !== '') { |
|||
// this.$message({ message: '文件上传成功', type: 'success', offset: 8 }) |
|||
// this.$emit('onUploadSuccess', res.data.data[0], this.fileName, jsonArrayToSend) |
|||
// } else { |
|||
// this.$message({ message: '文件上传失败', type: 'error', offset: 8 }) |
|||
// } |
|||
// this.isSyncUpload = false |
|||
// setTimeout(() => { |
|||
// this.isUpload = false |
|||
// }, 2000) |
|||
// }).catch(err => { |
|||
// this.isSyncUpload = false |
|||
// setTimeout(() => { |
|||
// this.isUpload = false |
|||
// }, 2000) |
|||
// this.$emit('onUploadError', err) |
|||
// this.$message({ message: '上传服务器失败', type: 'error', offset: 8 }) |
|||
// clearInterval(this.queryTimer) |
|||
// }) |
|||
// } else { |
|||
// // 预归档库 |
|||
// axios.post(this.baseApi + '/api/re-document/merge', jsonArrayToSend, { |
|||
// headers: { |
|||
// 'Authorization': getToken() |
|||
// } |
|||
// }).then((res) => { |
|||
// console.log(res) |
|||
// if (res.data.code === 200 && res.data.data.length === 1 && res.data.data[0] !== '') { |
|||
// this.$message({ message: '文件上传成功', type: 'success', offset: 8 }) |
|||
// this.$emit('onUploadSuccess', res.data.data[0], this.fileName, jsonArrayToSend) |
|||
// } else { |
|||
// this.$message({ message: '文件上传失败', type: 'error', offset: 8 }) |
|||
// } |
|||
// this.isSyncUpload = false |
|||
// }).catch(err => { |
|||
// this.isSyncUpload = false |
|||
// setTimeout(() => { |
|||
// this.isUpload = false |
|||
// }, 2000) |
|||
// this.$emit('onUploadError', err) |
|||
// this.$message({ message: '上传服务器失败', type: 'error', offset: 8 }) |
|||
// clearInterval(this.queryTimer) |
|||
// }) |
|||
// } |
|||
// } else { |
|||
// this.isSyncUpload = false |
|||
// setTimeout(() => { |
|||
// this.isUpload = false |
|||
// }, 2000) |
|||
// this.$message({ message: '请耐心等待文件上传完成后再保存!', type: 'error', offset: 8 }) |
|||
// } |
|||
// }) |
|||
// }, |
|||
handleUploadConfirm() { |
|||
// 检查是否选择了文件 |
|||
if (this.$refs.uploader.fileList.length === 0) { |
|||
this.showMessage('请选择要上传的文件!', 'error') |
|||
return false |
|||
} |
|||
|
|||
this.nowDate = getCurrentTime() |
|||
const jsonArrayToSend = [] |
|||
this.isSyncUpload = true |
|||
|
|||
// 处理文件列表 |
|||
const processFile = async(item) => { |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
|
|||
// 处理图片文件 |
|||
if (item.file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(item.file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
item.file.px = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
item.file.px = '' |
|||
} |
|||
|
|||
// 填充 jsonString 对象 |
|||
jsonString.file_name = item.file.name |
|||
jsonString.file_size = item.file.size |
|||
jsonString.file_type = item.file.name.split('.').pop() |
|||
json.last_modified = item.file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.file_dpi = item.file.px |
|||
jsonString.file_thumbnail = '' |
|||
jsonString.create_time = this.nowDate |
|||
jsonString.id = null |
|||
jsonArray.push(jsonString) |
|||
|
|||
// 根据挂载类型设置不同的 id |
|||
if (this.isBatchMount === 'true') { |
|||
json.categoryId = this.selectedCategory.id |
|||
} else { |
|||
json.documentId = this.selectedDocument.id |
|||
} |
|||
|
|||
// 填充 json 对象 |
|||
json.archivesId = this.arcId |
|||
json.identifier = item.uniqueIdentifier |
|||
json.filename = item.name |
|||
json.totalChunks = item.chunks.length - 1 |
|||
json.totalSize = item.size |
|||
json.fileJsonString = JSON.stringify(jsonArray) |
|||
|
|||
jsonArrayToSend.push(json) |
|||
} |
|||
|
|||
// 使用 Promise.all 确保所有异步操作完成 |
|||
Promise.all(this.$refs.uploader.fileList.map(processFile)) |
|||
.then(() => { |
|||
console.log('jsonArrayToSend', jsonArrayToSend) |
|||
|
|||
// 检查文件是否全部上传完成 |
|||
if (this.$refs.uploader.fileList.every(item => item.completed) && this.isUpload) { |
|||
const apiUrl = this.isBatchMount === 'true' |
|||
? `${this.baseApi}/api/collect/merge` |
|||
: `${this.baseApi}/api/re-document/merge` |
|||
|
|||
// 发送请求 |
|||
this.sendRequest(apiUrl, jsonArrayToSend) |
|||
.then((res) => { |
|||
if (res.data.code === 200 && res.data.data.length === 1 && res.data.data[0] !== '') { |
|||
this.showMessage('文件上传成功', 'success') |
|||
this.$emit('onUploadSuccess', res.data.data[0], this.fileName, jsonArrayToSend) |
|||
} else { |
|||
this.showMessage('文件上传失败', 'error') |
|||
} |
|||
this.isSyncUpload = false |
|||
if (this.isBatchMount === 'true') { |
|||
setTimeout(() => { |
|||
this.isUpload = false |
|||
}, 2000) |
|||
} |
|||
}) |
|||
.catch((err) => { |
|||
this.handleRequestError(err) |
|||
}) |
|||
} else { |
|||
this.isSyncUpload = false |
|||
setTimeout(() => { |
|||
this.isUpload = false |
|||
}, 2000) |
|||
this.showMessage('请耐心等待文件上传完成后再保存!', 'error') |
|||
} |
|||
}) |
|||
.catch((err) => { |
|||
this.handleRequestError(err) |
|||
}) |
|||
}, |
|||
|
|||
// 显示消息的方法 |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 8 }) |
|||
}, |
|||
|
|||
// 发送请求的方法 |
|||
sendRequest(url, data) { |
|||
return axios.post(url, data, { |
|||
headers: { |
|||
'Authorization': getToken() |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 处理请求错误的方法 |
|||
handleRequestError(err) { |
|||
this.isSyncUpload = false |
|||
setTimeout(() => { |
|||
this.isUpload = false |
|||
}, 2000) |
|||
this.$emit('onUploadError', err) |
|||
this.showMessage('上传服务器失败', 'error') |
|||
clearInterval(this.queryTimer) |
|||
}, |
|||
// 上传失败 |
|||
onFileError(rootFile, file, response, chunk) { |
|||
this.$message({ message: '上传失败', type: 'error', offset: 8 }) |
|||
this.$emit('onUploadError', response) |
|||
this.isUpload = false |
|||
}, |
|||
// 将上传的图片转为base64 |
|||
getBase64(file) { |
|||
const reader = new FileReader() |
|||
reader.readAsDataURL(file) |
|||
return new Promise((resolve) => { |
|||
reader.onload = () => { |
|||
resolve(reader.result) |
|||
} |
|||
}) |
|||
}, |
|||
// 获取图片的分辨率 |
|||
getImgPx(img) { |
|||
const image = new Image() |
|||
image.src = img |
|||
return new Promise((resolve) => { |
|||
image.onload = () => { |
|||
const width = image.width |
|||
const height = image.height |
|||
resolve({ width, height }) |
|||
} |
|||
}) |
|||
}, |
|||
handleClearData() { |
|||
this.isStartUpload = false |
|||
this.md5ProgressText = 0 |
|||
this.isMd5Upload = false |
|||
this.isUpload = false |
|||
this.uploadProcessNum = 0 |
|||
this.uploadSpeed = 0 |
|||
this.fileName = '' |
|||
this.isSyncUpload = false |
|||
const uploaderInstance = this.$refs.uploader.uploader |
|||
console.log('uploaderInstance.fileList222', uploaderInstance.fileList) |
|||
uploaderInstance.fileList.forEach(file => file.cancel()) |
|||
uploaderInstance.fileList = [] |
|||
uploaderInstance.files = [] |
|||
this.$refs.uploader.files = [] |
|||
this.$refs.uploader.fileList = [] |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style lang="scss" scoped> |
|||
.uploader-big{ |
|||
position: relative; |
|||
// width: 100%; |
|||
margin-left: 10px; |
|||
.uploader{ |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
width: 100%; |
|||
text-align: center; |
|||
margin-bottom: 8px; |
|||
.uploader-drop{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
text-align: center; |
|||
width: 100px; |
|||
height: 34px; |
|||
padding: 0; |
|||
border: 1px solid #0348f3; |
|||
background-color: transparent; |
|||
border-radius: 4px; |
|||
.uploader-btn{ |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
border: none; |
|||
color: #0348f3; |
|||
line-height: 36px; |
|||
i{ |
|||
font-size: 20px; |
|||
color: #1F55EB; |
|||
} |
|||
&:hover{ |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
.el-upload__tip{ |
|||
font-size: 12px; |
|||
color: #A6ADB6; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.upload_process_box{ |
|||
position: absolute; |
|||
right: 0; |
|||
top: 44px; |
|||
width: 540px; |
|||
border-radius: 5px; |
|||
border: 1px dashed #409eff; |
|||
padding: 10px 0; |
|||
} |
|||
|
|||
.uploader-file[status="success"] .uploader-file-remove { |
|||
display:block |
|||
} |
|||
::v-deep .uploader-list{ |
|||
max-height: 344px; |
|||
overflow-y: scroll; |
|||
.uploader-file-size{ |
|||
text-align: right !important; |
|||
} |
|||
.uploader-file-icon:before{ |
|||
content: ""; |
|||
} |
|||
.uploader-file-name{ |
|||
text-align: left !important; |
|||
} |
|||
.uploader-file{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/attachment.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
&.icon-image{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/image.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-excel,&.icon-xlsx,&.icon-xls{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/excel.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-pdf{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/pdf.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-ppt, &.icon-pptx{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/ppt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-word,&.icon-docx,&.icon-doc{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/word.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-zip,&.icon-rar{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/zip.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-txt{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/txt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
&.icon-ofd{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/OFD.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
</style> |
|||
1176
src/views/components/category/preUpload.vue
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,654 +0,0 @@ |
|||
<template> |
|||
<div class="uploader-big"> |
|||
<uploader |
|||
ref="uploader" |
|||
:options="initOptions" |
|||
:file-status-text="fileStatusText" |
|||
:auto-start="false" |
|||
class="uploader-app" |
|||
@file-added="onFileAdded" |
|||
@file-success="onUploadSuccess" |
|||
@file-progress="onFileProgress" |
|||
@file-error="onFileError" |
|||
> |
|||
<uploader-unsupport /> |
|||
<uploader-drop class="custom_uploader_drop" element-loading-text="正在读取中"> |
|||
<uploader-btn ref="uploadBtn" :attrs="attrs" :multiple="true"> |
|||
<slot> |
|||
<i class="iconfont icon-shangchuan" /> |
|||
上传 |
|||
</slot> |
|||
</uploader-btn> |
|||
</uploader-drop> |
|||
|
|||
<!-- 多文件上传进度展示 --> |
|||
<div v-if="isUpload" class="upload_process_box"> |
|||
<div v-for="(file, index) in uploadFiles" :key="index" class="file-progress-item"> |
|||
<div>文件名:{{ file.name }}</div> |
|||
<el-progress :percentage="file.progress" /> |
|||
<div v-if="file.isMd5Processing"> |
|||
正在读取文件中 - {{ file.md5Progress }} |
|||
</div> |
|||
<div v-if="!file.isMd5Processing && !file.isSyncing"> |
|||
上传至服务器 - <span>{{ file.speed }} M/s</span> |
|||
</div> |
|||
<div v-if="file.isSyncing"> |
|||
上传中,请稍后 |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 整体进度 --> |
|||
<div v-if="uploadFiles.length > 1" class="total-progress"> |
|||
<div>整体进度</div> |
|||
<el-progress :percentage="totalProgress" /> |
|||
</div> |
|||
</div> |
|||
</uploader> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import SparkMD5 from 'spark-md5' |
|||
import { mapGetters } from 'vuex' |
|||
import axios from 'axios' |
|||
import { getToken } from '@/utils/auth' |
|||
import { getCurrentTime } from '@/utils/index' |
|||
|
|||
export default { |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
initOptions: { |
|||
target: null, |
|||
headers: { |
|||
Authorization: getToken() |
|||
}, |
|||
singleFile: false, // 关闭单文件上传限制 |
|||
uploadMethod: 'post', |
|||
maxChunkRetries: 3, |
|||
testChunks: true, |
|||
parseTimeRemaining: function(timeRemaining, parsedTimeRemaining) { |
|||
return parsedTimeRemaining |
|||
.replace(/\syears?/, '年') |
|||
.replace(/\days?/, '天') |
|||
.replace(/\shours?/, '小时') |
|||
.replace(/\sminutes?/, '分钟') |
|||
.replace(/\sseconds?/, '秒') |
|||
}, |
|||
checkChunkUploadedByResponse: (chunk, message) => { |
|||
const result = JSON.parse(message) |
|||
if (result.data.skipUpload) { |
|||
this.skip = true |
|||
return true |
|||
} |
|||
return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0 |
|||
} |
|||
}, |
|||
attrs: { |
|||
accept: '' |
|||
}, |
|||
fileStatusText: { |
|||
success: '上传成功', |
|||
error: '上传失败', |
|||
uploading: '上传中', |
|||
paused: '已暂停', |
|||
waiting: '等待上传' |
|||
}, |
|||
uploadFiles: [], // 多文件上传信息 |
|||
totalProgress: 0, // 整体进度 |
|||
isUpload: false, // 是否有文件正在上传 |
|||
queryTimer: null, |
|||
socket: null, |
|||
completedFiles: 0, // 已完成上传的文件数 |
|||
totalFiles: 0, // 总文件数 |
|||
totalMergeStartTime: null, // 整体合并开始时间 |
|||
totalMergeEndTime: null // 整体合并结束时间 |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(['baseApi']), |
|||
uploader() { |
|||
return this.$refs.uploader?.uploader |
|||
} |
|||
}, |
|||
created() { |
|||
this.initOptions.target = this.baseApi + '/api/collect/upload' |
|||
this.attrs.accept = this.isBatchMount === 'true' ? '.zip' : '' |
|||
}, |
|||
beforeDestroy() { |
|||
clearInterval(this.queryTimer) |
|||
}, |
|||
methods: { |
|||
clickUploader() { |
|||
this.$refs.uploadBtn.$el.click() |
|||
}, |
|||
|
|||
// 格式化时间为 YYYY-MM-DD HH:mm:ss |
|||
formatTime(time) { |
|||
if (!time) return '' |
|||
const date = new Date(time) |
|||
return `${date.getFullYear()}-${this.padZero(date.getMonth() + 1)}-${this.padZero(date.getDate())} ${this.padZero(date.getHours())}:${this.padZero(date.getMinutes())}:${this.padZero(date.getSeconds())}` |
|||
}, |
|||
|
|||
// 补零函数 |
|||
padZero(num) { |
|||
return num.toString().padStart(2, '0') |
|||
}, |
|||
|
|||
// 计算时间差,返回 xx分xx秒 格式 |
|||
calculateDuration(startTime, endTime) { |
|||
if (!startTime || !endTime) return '' |
|||
const duration = (endTime - startTime) / 1000 |
|||
const minutes = Math.floor(duration / 60) |
|||
const seconds = Math.floor(duration % 60) |
|||
const milliseconds = Math.floor((duration - Math.floor(duration)) * 1000) |
|||
return `${minutes > 0 ? minutes + '分' : ''}${seconds}秒${milliseconds > 0 ? milliseconds + '毫秒' : ''}` |
|||
}, |
|||
|
|||
// 文件添加时触发 |
|||
async onFileAdded(file) { |
|||
// 检查文件大小 |
|||
const maxSize = 10 * 1024 * 1024 * 1024 // 10GB |
|||
if (file.size > maxSize) { |
|||
this.$message.warning(`文件 ${file.name} 大小不能超过 10GB!`) |
|||
return false |
|||
} |
|||
|
|||
// 初始化文件上传信息,添加时间相关字段 |
|||
this.uploadFiles.push({ |
|||
id: file.id, |
|||
name: file.name, |
|||
progress: 0, |
|||
speed: 0, |
|||
isMd5Processing: true, |
|||
md5Progress: '0%', |
|||
isSyncing: false, |
|||
md5: null, |
|||
file: file, |
|||
completed: false, |
|||
chunkUploadStartTime: null, // 分片上传开始时间 |
|||
chunkUploadEndTime: null // 分片上传结束时间 |
|||
}) |
|||
|
|||
this.isUpload = true |
|||
this.totalFiles = this.uploadFiles.length |
|||
|
|||
// 计算MD5 |
|||
try { |
|||
const md5 = await this.computeMD5(file) |
|||
const fileInfo = this.uploadFiles.find(f => f.id === file.id) |
|||
if (fileInfo) { |
|||
fileInfo.md5 = md5 |
|||
fileInfo.isMd5Processing = false |
|||
file.uniqueIdentifier = md5 |
|||
// 记录分片上传开始时间 |
|||
fileInfo.chunkUploadStartTime = new Date().getTime() |
|||
console.log(`【文件${file.name}】分片上传开始时间:${this.formatTime(fileInfo.chunkUploadStartTime)}`) |
|||
file.resume() // 开始上传 |
|||
} |
|||
} catch (error) { |
|||
this.$message.error(`文件 ${file.name} 处理失败: ${error.message}`) |
|||
this.removeFile(file.id) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 计算文件MD5 |
|||
*/ |
|||
computeMD5(file) { |
|||
return new Promise((resolve, reject) => { |
|||
const fileReader = new FileReader() |
|||
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice |
|||
const chunkSize = 10 * 1024 * 1024 // 10MB |
|||
const chunks = Math.ceil(file.size / chunkSize) |
|||
let currentChunk = 0 |
|||
const spark = new SparkMD5.ArrayBuffer() |
|||
|
|||
file.pause() |
|||
|
|||
const loadNext = () => { |
|||
const start = currentChunk * chunkSize |
|||
const end = Math.min(start + chunkSize, file.size) |
|||
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)) |
|||
} |
|||
|
|||
fileReader.onload = (e) => { |
|||
spark.append(e.target.result) |
|||
currentChunk++ |
|||
|
|||
// 更新MD5计算进度 |
|||
const progress = Math.floor((currentChunk / chunks) * 100) |
|||
const fileInfo = this.uploadFiles.find(f => f.id === file.id) |
|||
if (fileInfo) { |
|||
fileInfo.md5Progress = `${progress}%` |
|||
} |
|||
|
|||
if (currentChunk < chunks) { |
|||
loadNext() |
|||
} else { |
|||
const md5 = spark.end() |
|||
resolve(md5) |
|||
} |
|||
} |
|||
|
|||
fileReader.onerror = () => { |
|||
reject(new Error(`文件读取出错: ${fileReader.error.message}`)) |
|||
} |
|||
|
|||
loadNext() |
|||
}) |
|||
}, |
|||
|
|||
// 上传进度更新 |
|||
onFileProgress(rootFile, file, chunk) { |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo) { |
|||
const progress = Math.floor(this.uploader.progress() * 100) |
|||
fileInfo.progress = progress |
|||
|
|||
// 计算上传速度 |
|||
const averageSpeed = this.uploader.averageSpeed |
|||
const speed = averageSpeed / 1000 / 1024 // 转换为MB/s |
|||
fileInfo.speed = speed.toFixed(2) |
|||
} |
|||
|
|||
// 更新整体进度 |
|||
this.calculateTotalProgress() |
|||
}, |
|||
|
|||
// 单个文件上传成功 |
|||
onUploadSuccess(rootFile, file, response) { |
|||
const res1 = JSON.parse(response) |
|||
if (res1.code === 200) { |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo) { |
|||
// 记录分片上传结束时间并计算耗时 |
|||
fileInfo.chunkUploadEndTime = new Date().getTime() |
|||
const chunkDuration = this.calculateDuration(fileInfo.chunkUploadStartTime, fileInfo.chunkUploadEndTime) |
|||
// 控制台打印分片上传时间信息 |
|||
console.log(`【文件${rootFile.name}】分片上传结束时间:${this.formatTime(fileInfo.chunkUploadEndTime)},分片上传耗时:${chunkDuration}`) |
|||
fileInfo.completed = true |
|||
fileInfo.progress = 100 |
|||
this.completedFiles++ |
|||
} |
|||
|
|||
// 检查是否所有文件都已上传完成 |
|||
if (this.completedFiles === this.totalFiles) { |
|||
this.handleUploadConfirm() |
|||
} |
|||
} else { |
|||
this.$message.error(`文件 ${rootFile.name} 上传失败: ${res1.msg || '未知错误'}`) |
|||
this.removeFile(rootFile.id) |
|||
} |
|||
}, |
|||
|
|||
// 所有文件上传完成后统一执行merge |
|||
handleUploadConfirm() { |
|||
if (this.uploadFiles.length === 0) { |
|||
this.showMessage('没有可处理的文件!', 'error') |
|||
return |
|||
} |
|||
|
|||
// 记录整体合并开始时间 |
|||
this.totalMergeStartTime = new Date().getTime() |
|||
console.log(`【整体合并】所有文件分片上传完成,开始合并,合并开始时间:${this.formatTime(this.totalMergeStartTime)}`) |
|||
this.nowDate = getCurrentTime() |
|||
const jsonArrayToSend = [] |
|||
|
|||
// 标记所有文件为同步中,并记录单个文件合并开始时间 |
|||
this.uploadFiles.forEach(file => { |
|||
file.isSyncing = true |
|||
file.mergeUploadStartTime = new Date().getTime() |
|||
console.log(`【文件${file.name}】合并上传开始时间:${this.formatTime(file.mergeUploadStartTime)}`) |
|||
}) |
|||
|
|||
// 处理所有文件信息 |
|||
const processFiles = this.uploadFiles.map(async(fileInfo) => { |
|||
const item = fileInfo.file |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
|
|||
// 处理图片文件分辨率 |
|||
if (item.file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(item.file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
item.file.px = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
item.file.px = '' |
|||
} |
|||
|
|||
// 构建文件信息 |
|||
jsonString.file_name = item.file.name |
|||
jsonString.file_size = item.file.size |
|||
jsonString.file_type = item.file.name.split('.').pop() || '' |
|||
json.last_modified = item.file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.file_dpi = item.file.px |
|||
jsonString.file_thumbnail = '' |
|||
jsonString.create_time = this.nowDate |
|||
jsonString.id = null |
|||
jsonArray.push(jsonString) |
|||
|
|||
// 根据挂载类型设置不同参数 |
|||
if (this.isBatchMount === 'true') { |
|||
json.categoryId = this.selectedCategory.id |
|||
} else { |
|||
json.documentId = this.selectedDocument.id |
|||
} |
|||
|
|||
// 构建请求参数 |
|||
json.archivesId = this.arcId |
|||
json.identifier = fileInfo.md5 |
|||
json.filename = item.name |
|||
json.totalChunks = item.chunks.length - 1 |
|||
json.totalSize = item.size |
|||
json.fileJsonString = JSON.stringify(jsonArray) |
|||
|
|||
jsonArrayToSend.push(json) |
|||
|
|||
return json |
|||
}) |
|||
|
|||
// 等待所有文件信息处理完成 |
|||
Promise.all(processFiles) |
|||
.then((jsonArray) => { |
|||
const apiUrl = this.isBatchMount === 'true' |
|||
? `${this.baseApi}/api/collect/merge` |
|||
: `${this.baseApi}/api/re-document/merge` |
|||
|
|||
// 发送合并请求 |
|||
return this.sendRequest(apiUrl, jsonArray) |
|||
}) |
|||
.then((res) => { |
|||
// 记录整体合并结束时间并计算耗时 |
|||
this.totalMergeEndTime = new Date().getTime() |
|||
const totalMergeDuration = this.calculateDuration(this.totalMergeStartTime, this.totalMergeEndTime) |
|||
|
|||
if (res.data.code === 200) { |
|||
this.showMessage('所有文件上传成功', 'success') |
|||
this.uploadFiles.forEach(file => { |
|||
file.mergeUploadEndTime = new Date().getTime() |
|||
const mergeDuration = this.calculateDuration(file.mergeUploadStartTime, file.mergeUploadEndTime) |
|||
console.log(`【文件${file.name}】合并上传结束时间:${this.formatTime(file.mergeUploadEndTime)},合并上传耗时:${mergeDuration}`) |
|||
}) |
|||
console.log(`【整体合并】所有文件合并完成,合并结束时间:${this.formatTime(this.totalMergeEndTime)},整体合并耗时:${totalMergeDuration}`) |
|||
this.$emit('onUploadSuccess', res.data.data, this.uploadFiles.map(f => f.name), jsonArrayToSend) |
|||
} else { |
|||
this.showMessage('文件合并失败', 'error') |
|||
console.log(`【整体合并】合并失败,失败时间:${this.formatTime(this.totalMergeEndTime)},合并耗时:${totalMergeDuration}`) |
|||
} |
|||
this.resetUploadState() |
|||
}) |
|||
.catch((err) => { |
|||
// 合并失败也记录时间并打印 |
|||
this.totalMergeEndTime = new Date().getTime() |
|||
const totalMergeDuration = this.calculateDuration(this.totalMergeStartTime, this.totalMergeEndTime) |
|||
console.log(`【整体合并】合并请求异常,异常时间:${this.formatTime(this.totalMergeEndTime)},合并耗时:${totalMergeDuration},异常信息:`, err) |
|||
this.showMessage('上传服务器失败', 'error') |
|||
this.$emit('onUploadError', err) |
|||
this.resetUploadState() |
|||
}) |
|||
}, |
|||
|
|||
// 上传失败处理 |
|||
onFileError(rootFile, file, response) { |
|||
this.showMessage(`文件 ${rootFile.name} 上传失败`, 'error') |
|||
console.log(`【文件${rootFile.name}】分片上传失败,失败响应:`, response) |
|||
this.$emit('onUploadError', response) |
|||
this.removeFile(rootFile.id) |
|||
}, |
|||
|
|||
// 计算整体进度 |
|||
calculateTotalProgress() { |
|||
if (this.uploadFiles.length === 0) { |
|||
this.totalProgress = 0 |
|||
return |
|||
} |
|||
|
|||
const total = this.uploadFiles.reduce((sum, file) => sum + file.progress, 0) |
|||
this.totalProgress = Math.floor(total / this.uploadFiles.length) |
|||
}, |
|||
|
|||
// 移除文件 |
|||
removeFile(fileId) { |
|||
const removeFile = this.uploadFiles.find(f => f.id === fileId) |
|||
if (removeFile) { |
|||
console.log(`【文件${removeFile.name}】已从上传列表移除`) |
|||
} |
|||
this.uploadFiles = this.uploadFiles.filter(f => f.id !== fileId) |
|||
this.totalFiles = this.uploadFiles.length |
|||
this.completedFiles = this.uploadFiles.filter(f => f.completed).length |
|||
|
|||
if (this.uploadFiles.length === 0) { |
|||
this.isUpload = false |
|||
this.totalProgress = 0 |
|||
this.totalMergeStartTime = null |
|||
this.totalMergeEndTime = null |
|||
} else { |
|||
this.calculateTotalProgress() |
|||
} |
|||
}, |
|||
|
|||
// 重置上传状态 |
|||
resetUploadState() { |
|||
this.uploadFiles.forEach(file => { |
|||
file.isSyncing = false |
|||
}) |
|||
|
|||
// 延迟清空,让控制台打印完整 |
|||
setTimeout(() => { |
|||
this.uploadFiles = [] |
|||
this.isUpload = false |
|||
this.totalProgress = 0 |
|||
this.completedFiles = 0 |
|||
this.totalFiles = 0 |
|||
this.totalMergeStartTime = null |
|||
this.totalMergeEndTime = null |
|||
|
|||
// 清空上传组件的文件列表 |
|||
if (this.uploader) { |
|||
this.uploader.fileList.forEach(file => file.cancel()) |
|||
this.uploader.fileList = [] |
|||
this.uploader.files = [] |
|||
} |
|||
if (this.$refs.uploader) { |
|||
this.$refs.uploader.files = [] |
|||
this.$refs.uploader.fileList = [] |
|||
} |
|||
}, 2000) |
|||
}, |
|||
|
|||
// 工具方法 |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 8 }) |
|||
}, |
|||
|
|||
sendRequest(url, data) { |
|||
return axios.post(url, data, { |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
}, |
|||
|
|||
getBase64(file) { |
|||
return new Promise((resolve) => { |
|||
const reader = new FileReader() |
|||
reader.readAsDataURL(file) |
|||
reader.onload = () => resolve(reader.result) |
|||
}) |
|||
}, |
|||
|
|||
getImgPx(img) { |
|||
return new Promise((resolve) => { |
|||
const image = new Image() |
|||
image.src = img |
|||
image.onload = () => resolve({ width: image.width, height: image.height }) |
|||
}) |
|||
}, |
|||
|
|||
handleClearData() { |
|||
console.log('【上传状态重置】手动清空所有上传数据') |
|||
this.uploadFiles = [] |
|||
this.isUpload = false |
|||
this.totalProgress = 0 |
|||
this.completedFiles = 0 |
|||
this.totalFiles = 0 |
|||
this.totalMergeStartTime = null |
|||
this.totalMergeEndTime = null |
|||
|
|||
if (this.uploader) { |
|||
this.uploader.fileList.forEach(file => file.cancel()) |
|||
this.uploader.fileList = [] |
|||
this.uploader.files = [] |
|||
} |
|||
if (this.$refs.uploader) { |
|||
this.$refs.uploader.files = [] |
|||
this.$refs.uploader.fileList = [] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.uploader-big{ |
|||
position: relative; |
|||
margin-left: 10px; |
|||
|
|||
.uploader-drop { |
|||
.uploader-btn { |
|||
i { |
|||
font-size: 20px; |
|||
color: #1F55EB; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.upload_process_box{ |
|||
position: absolute; |
|||
right: 0; |
|||
top: 44px; |
|||
width: 540px; |
|||
border-radius: 5px; |
|||
border: 1px dashed #409eff; |
|||
padding: 10px; |
|||
background: #fff; // 添加白色背景,防止文字重叠 |
|||
z-index: 10; |
|||
|
|||
.file-progress-item { |
|||
margin-bottom: 15px; |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
margin-bottom: 0; |
|||
padding-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.total-progress { |
|||
margin-top: 15px; |
|||
padding-top: 10px; |
|||
border-top: 1px solid #e0e0e0; |
|||
} |
|||
} |
|||
|
|||
// 其他样式保持不变 |
|||
::v-deep .uploader-list{ |
|||
max-height: 344px; |
|||
overflow-y: scroll; |
|||
|
|||
.uploader-file-size{ |
|||
text-align: right !important; |
|||
} |
|||
|
|||
.uploader-file-name{ |
|||
text-align: left !important; |
|||
} |
|||
|
|||
.uploader-file{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/attachment.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
&.icon-image{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/image.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-excel,&.icon-xlsx,&.icon-xls{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/excel.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-pdf{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/pdf.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-ppt, &.icon-pptx{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/ppt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-word,&.icon-docx,&.icon-doc{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/word.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-zip,&.icon-rar{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/zip.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-txt{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/txt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-ofd{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/OFD.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,838 +0,0 @@ |
|||
<template> |
|||
<div class="uploader-big"> |
|||
<uploader |
|||
ref="uploader" |
|||
:options="initOptions" |
|||
:file-status-text="fileStatusText" |
|||
:auto-start="false" |
|||
class="uploader-app" |
|||
@file-added="onFileAdded" |
|||
@file-success="onUploadSuccess" |
|||
@file-progress="onFileProgress" |
|||
@file-error="onFileError" |
|||
> |
|||
<uploader-unsupport /> |
|||
<uploader-drop class="custom_uploader_drop" element-loading-text="正在读取中"> |
|||
<uploader-btn ref="uploadBtn" :attrs="attrs" :multiple="true"> |
|||
<slot> |
|||
<i class="iconfont icon-shangchuan" /> |
|||
上传 |
|||
</slot> |
|||
</uploader-btn> |
|||
</uploader-drop> |
|||
|
|||
<!-- 多文件上传进度展示 --> |
|||
<div v-if="isUpload" class="upload_process_box"> |
|||
<!-- 操作栏:清空所有 --> |
|||
<div class="upload-operate-bar"> |
|||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleClearAll"> |
|||
清空所有 |
|||
</el-button> |
|||
</div> |
|||
|
|||
<div v-for="(file, index) in uploadFiles" :key="index" class="file-progress-item"> |
|||
<!-- 文件基本信息:名称+大小 --> |
|||
<div class="file-base-info"> |
|||
<span>文件名:{{ file.name }}</span> |
|||
<span class="file-size">({{ formatFileSize(file.file.size) }})</span> |
|||
</div> |
|||
|
|||
<!-- 进度条 --> |
|||
<el-progress |
|||
:percentage="file.progress" |
|||
:status="file.errorMsg ? 'exception' : (file.completed ? 'success' : '')" |
|||
/> |
|||
|
|||
<!-- 状态提示 --> |
|||
<div class="file-status-tip"> |
|||
<span v-if="file.isMd5Processing">正在读取文件中 - {{ file.md5Progress }}</span> |
|||
<span v-else-if="!file.isSyncing && !file.errorMsg"> |
|||
上传至服务器 - <span class="upload-speed">{{ file.speed }} MB/s</span> |
|||
</span> |
|||
<span v-else-if="file.isSyncing">上传中,请稍后(合并分片)</span> |
|||
<span v-else-if="file.errorMsg" class="error-tip">❌ {{ file.errorMsg }}</span> |
|||
<span v-else-if="file.completed" class="success-tip">✅ 上传成功</span> |
|||
</div> |
|||
|
|||
<!-- 操作按钮:取消/重新上传 --> |
|||
<div class="file-operate-btn"> |
|||
<el-button |
|||
v-if="!file.completed && !file.errorMsg" |
|||
size="mini" |
|||
type="text" |
|||
icon="el-icon-circle-close" |
|||
@click="handleCancelUpload(file)" |
|||
> |
|||
取消 |
|||
</el-button> |
|||
<el-button |
|||
v-if="file.errorMsg || file.isCancelled" |
|||
size="mini" |
|||
type="text" |
|||
icon="el-icon-refresh" |
|||
@click="handleReUpload(file)" |
|||
> |
|||
重新上传 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 整体进度 --> |
|||
<div v-if="uploadFiles.length > 1" class="total-progress"> |
|||
<div>整体进度 ({{ completedFiles }}/{{ totalFiles }})</div> |
|||
<el-progress :percentage="totalProgress" /> |
|||
</div> |
|||
</div> |
|||
</uploader> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import SparkMD5 from 'spark-md5' |
|||
import { mapGetters } from 'vuex' |
|||
import axios from 'axios' |
|||
import { getToken } from '@/utils/auth' |
|||
import { getCurrentTime } from '@/utils/index' |
|||
|
|||
export default { |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 新增:允许的文件类型,如['zip', 'pdf', 'jpg'] |
|||
allowFileTypes: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
// 新增:最大文件大小(GB),默认10GB |
|||
maxFileSizeGB: { |
|||
type: Number, |
|||
default: 10 |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
initOptions: { |
|||
target: null, |
|||
headers: { |
|||
Authorization: getToken() |
|||
}, |
|||
singleFile: false, // 关闭单文件上传限制 |
|||
uploadMethod: 'post', |
|||
maxChunkRetries: 3, |
|||
testChunks: true, |
|||
parseTimeRemaining: function(timeRemaining, parsedTimeRemaining) { |
|||
return parsedTimeRemaining |
|||
.replace(/\syears?/, '年') |
|||
.replace(/\days?/, '天') |
|||
.replace(/\shours?/, '小时') |
|||
.replace(/\sminutes?/, '分钟') |
|||
.replace(/\sseconds?/, '秒') |
|||
}, |
|||
checkChunkUploadedByResponse: (chunk, message) => { |
|||
const result = JSON.parse(message) |
|||
if (result.data.skipUpload) { |
|||
this.skip = true |
|||
return true |
|||
} |
|||
return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0 |
|||
} |
|||
}, |
|||
attrs: { |
|||
accept: '' |
|||
}, |
|||
fileStatusText: { |
|||
success: '上传成功', |
|||
error: '上传失败', |
|||
uploading: '上传中', |
|||
paused: '已暂停', |
|||
waiting: '等待上传' |
|||
}, |
|||
uploadFiles: [], // 多文件上传信息 |
|||
totalProgress: 0, // 整体进度 |
|||
isUpload: false, // 是否有文件正在上传 |
|||
queryTimer: null, |
|||
socket: null, |
|||
completedFiles: 0, // 已完成上传的文件数 |
|||
totalFiles: 0, // 总文件数 |
|||
// 新增:存储MD5计算的FileReader实例,用于取消计算 |
|||
md5Readers: {} |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(['baseApi']), |
|||
uploader() { |
|||
return this.$refs.uploader?.uploader |
|||
}, |
|||
// 新增:计算最大文件大小(字节) |
|||
maxFileSize() { |
|||
return this.maxFileSizeGB * 1024 * 1024 * 1024 |
|||
} |
|||
}, |
|||
created() { |
|||
this.initOptions.target = this.baseApi + '/api/minioUpload/chunk' |
|||
// 优化:根据allowFileTypes生成accept属性 |
|||
if (this.isBatchMount === 'true') { |
|||
this.attrs.accept = '.zip' |
|||
} else if (this.allowFileTypes.length > 0) { |
|||
this.attrs.accept = this.allowFileTypes.map(type => `.${type}`).join(',') |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
clearInterval(this.queryTimer) |
|||
// 销毁时中断所有MD5计算 |
|||
Object.values(this.md5Readers).forEach(reader => { |
|||
if (reader.abort) reader.abort() |
|||
}) |
|||
}, |
|||
methods: { |
|||
clickUploader() { |
|||
this.$refs.uploadBtn.$el.click() |
|||
}, |
|||
|
|||
// 新增:格式化文件大小(字节转GB/MB/KB) |
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0 B' |
|||
const k = 1024 |
|||
const sizes = ['B', 'KB', 'MB', 'GB'] |
|||
const i = Math.floor(Math.log(bytes) / Math.log(k)) |
|||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i] |
|||
}, |
|||
|
|||
// 新增:文件去重校验 |
|||
isFileDuplicate(file) { |
|||
return this.uploadFiles.some(item => { |
|||
return item.name === file.name && item.file.size === file.size |
|||
}) |
|||
}, |
|||
|
|||
// 新增:文件类型校验 |
|||
checkFileType(file) { |
|||
if (this.allowFileTypes.length === 0) return true |
|||
const fileExt = file.name.split('.').pop().toLowerCase() |
|||
return this.allowFileTypes.includes(fileExt) |
|||
}, |
|||
|
|||
// 文件添加时触发 |
|||
async onFileAdded(file) { |
|||
// 1. 文件大小校验 |
|||
if (file.size > this.maxFileSize) { |
|||
this.$message.warning(`文件 ${file.name} 大小不能超过 ${this.maxFileSizeGB}GB!`) |
|||
return false |
|||
} |
|||
|
|||
// 2. 文件类型校验 |
|||
if (!this.checkFileType(file)) { |
|||
const allowTypes = this.allowFileTypes.join('、') |
|||
this.$message.warning(`文件 ${file.name} 类型不支持,仅允许${allowTypes}格式!`) |
|||
return false |
|||
} |
|||
|
|||
// 3. 文件去重校验 |
|||
if (this.isFileDuplicate(file)) { |
|||
this.$message.info(`文件 ${file.name} 已在上传列表中,请勿重复添加!`) |
|||
return false |
|||
} |
|||
|
|||
// 初始化文件上传信息 |
|||
const fileId = file.id || Date.now() + Math.random().toString(36).substr(2, 9) |
|||
this.uploadFiles.push({ |
|||
id: fileId, |
|||
name: file.name, |
|||
file: file, |
|||
progress: 0, |
|||
speed: 0, |
|||
isMd5Processing: true, |
|||
md5Progress: '0%', |
|||
isSyncing: false, |
|||
md5: null, |
|||
completed: false, |
|||
errorMsg: '', // 新增:错误信息 |
|||
isCancelled: false // 新增:是否被取消 |
|||
}) |
|||
|
|||
this.isUpload = true |
|||
this.totalFiles = this.uploadFiles.length |
|||
|
|||
// 计算MD5 |
|||
try { |
|||
const md5 = await this.computeMD5(file, fileId) |
|||
const fileInfo = this.uploadFiles.find(f => f.id === fileId) |
|||
if (fileInfo) { |
|||
fileInfo.md5 = md5 |
|||
fileInfo.isMd5Processing = false |
|||
file.uniqueIdentifier = md5 |
|||
// 如果是取消后重新上传,需要恢复状态 |
|||
if (fileInfo.isCancelled) { |
|||
fileInfo.isCancelled = false |
|||
fileInfo.errorMsg = '' |
|||
} |
|||
file.resume() // 开始上传 |
|||
} |
|||
} catch (error) { |
|||
const fileInfo = this.uploadFiles.find(f => f.id === fileId) |
|||
if (fileInfo) { |
|||
fileInfo.errorMsg = error.message |
|||
fileInfo.isMd5Processing = false |
|||
} |
|||
this.$message.error(`文件 ${file.name} 处理失败: ${error.message}`) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 优化:计算文件MD5(支持取消) |
|||
*/ |
|||
computeMD5(file, fileId) { |
|||
return new Promise((resolve, reject) => { |
|||
const fileReader = new FileReader() |
|||
// 存储reader实例,用于取消 |
|||
this.md5Readers[fileId] = fileReader |
|||
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice |
|||
const chunkSize = 10 * 1024 * 1024 // 10MB |
|||
const chunks = Math.ceil(file.size / chunkSize) |
|||
let currentChunk = 0 |
|||
const spark = new SparkMD5.ArrayBuffer() |
|||
|
|||
file.pause() |
|||
|
|||
const loadNext = () => { |
|||
// 如果被取消,直接拒绝 |
|||
const fileInfo = this.uploadFiles.find(f => f.id === fileId) |
|||
if (fileInfo?.isCancelled) { |
|||
reject(new Error('用户取消了MD5计算')) |
|||
fileReader.abort() |
|||
delete this.md5Readers[fileId] |
|||
return |
|||
} |
|||
const start = currentChunk * chunkSize |
|||
const end = Math.min(start + chunkSize, file.size) |
|||
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)) |
|||
} |
|||
|
|||
fileReader.onload = (e) => { |
|||
spark.append(e.target.result) |
|||
currentChunk++ |
|||
|
|||
// 更新MD5计算进度 |
|||
const progress = Math.floor((currentChunk / chunks) * 100) |
|||
const fileInfo = this.uploadFiles.find(f => f.id === fileId) |
|||
if (fileInfo) { |
|||
fileInfo.md5Progress = `${progress}%` |
|||
} |
|||
|
|||
if (currentChunk < chunks) { |
|||
loadNext() |
|||
} else { |
|||
const md5 = spark.end() |
|||
delete this.md5Readers[fileId] // 移除reader实例 |
|||
resolve(md5) |
|||
} |
|||
} |
|||
|
|||
fileReader.onerror = () => { |
|||
delete this.md5Readers[fileId] |
|||
reject(new Error(`文件读取出错: ${fileReader.error.message}`)) |
|||
} |
|||
|
|||
loadNext() |
|||
}) |
|||
}, |
|||
|
|||
// 优化:上传进度更新(修复速度计算偏差) |
|||
onFileProgress(rootFile, file, chunk) { |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo && !fileInfo.isCancelled && !fileInfo.errorMsg) { |
|||
// 优化:使用单个文件的进度而非全局进度 |
|||
const fileProgress = Math.floor((rootFile.progress() * 100)) |
|||
fileInfo.progress = fileProgress |
|||
|
|||
// 优化:计算单个文件的上传速度(避免全局速度偏差) |
|||
const averageSpeed = rootFile.averageSpeed || this.uploader?.averageSpeed || 0 |
|||
// 处理速度为0的情况,避免显示0.00 |
|||
const speed = averageSpeed > 0 ? (averageSpeed / 1024 / 1024) : 0 |
|||
fileInfo.speed = speed.toFixed(2) |
|||
} |
|||
|
|||
// 更新整体进度 |
|||
this.calculateTotalProgress() |
|||
}, |
|||
|
|||
// 单个文件上传成功 |
|||
onUploadSuccess(rootFile, file, response) { |
|||
const res1 = JSON.parse(response) |
|||
if (res1.code === 200) { |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo) { |
|||
fileInfo.completed = true |
|||
fileInfo.progress = 100 |
|||
fileInfo.speed = 0 |
|||
this.completedFiles++ |
|||
} |
|||
|
|||
// 检查是否所有文件都已上传完成 |
|||
if (this.completedFiles === this.totalFiles) { |
|||
this.handleUploadConfirm() |
|||
} |
|||
} else { |
|||
const errorMsg = res1.msg || '未知错误' |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo) { |
|||
fileInfo.errorMsg = errorMsg |
|||
} |
|||
this.$message.error(`文件 ${rootFile.name} 上传失败: ${errorMsg}`) |
|||
this.removeFile(rootFile.id, false) // 不移除,保留错误状态 |
|||
} |
|||
}, |
|||
|
|||
// 上传失败处理 |
|||
onFileError(rootFile, file, response) { |
|||
const errorMsg = response ? (JSON.parse(response)?.msg || '上传失败') : '网络异常' |
|||
const fileInfo = this.uploadFiles.find(f => f.id === rootFile.id) |
|||
if (fileInfo) { |
|||
fileInfo.errorMsg = errorMsg |
|||
fileInfo.isMd5Processing = false |
|||
} |
|||
this.showMessage(`文件 ${rootFile.name} 上传失败`, 'error') |
|||
this.$emit('onUploadError', response) |
|||
this.removeFile(rootFile.id, false) // 不移除,保留错误状态 |
|||
}, |
|||
|
|||
// 所有文件上传完成后统一执行merge |
|||
handleUploadConfirm() { |
|||
if (this.uploadFiles.length === 0) { |
|||
this.showMessage('没有可处理的文件!', 'error') |
|||
return |
|||
} |
|||
|
|||
this.nowDate = getCurrentTime() |
|||
const jsonArrayToSend = [] |
|||
|
|||
// 标记所有文件为同步中 |
|||
this.uploadFiles.forEach(file => { |
|||
if (!file.isCancelled && !file.errorMsg) { |
|||
file.isSyncing = true |
|||
} |
|||
}) |
|||
|
|||
// 处理所有文件信息 |
|||
const processFiles = this.uploadFiles |
|||
.filter(file => !file.isCancelled && !file.errorMsg) // 过滤掉取消/错误的文件 |
|||
.map(async(fileInfo) => { |
|||
const item = fileInfo.file |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
|
|||
// 处理图片文件分辨率 |
|||
if (item.file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(item.file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
item.file.px = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
item.file.px = '' |
|||
} |
|||
|
|||
// 构建文件信息 |
|||
jsonString.file_name = item.file.name |
|||
jsonString.file_size = item.file.size |
|||
jsonString.file_type = item.file.name.split('.').pop() || '' |
|||
json.last_modified = item.file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.file_dpi = item.file.px |
|||
jsonString.file_thumbnail = '' |
|||
jsonString.create_time = this.nowDate |
|||
jsonString.id = null |
|||
jsonArray.push(jsonString) |
|||
|
|||
// 根据挂载类型设置不同参数 |
|||
if (this.isBatchMount === 'true') { |
|||
json.categoryId = this.selectedCategory.id |
|||
} else { |
|||
json.documentId = this.selectedDocument.id |
|||
} |
|||
|
|||
// 构建请求参数 |
|||
json.archivesId = this.arcId |
|||
json.identifier = fileInfo.md5 |
|||
json.filename = item.name |
|||
json.totalChunks = item.chunks.length - 1 |
|||
json.totalSize = item.size |
|||
json.fileJsonString = JSON.stringify(jsonArray) |
|||
|
|||
jsonArrayToSend.push(json) |
|||
|
|||
return json |
|||
}) |
|||
|
|||
// 等待所有文件信息处理完成 |
|||
Promise.all(processFiles) |
|||
.then((jsonArray) => { |
|||
if (jsonArray.length === 0) { |
|||
this.showMessage('无有效文件进行合并', 'warning') |
|||
this.resetUploadState() |
|||
return |
|||
} |
|||
const apiUrl = this.isBatchMount === 'true' |
|||
? `${this.baseApi}/api/collect/merge` |
|||
: `${this.baseApi}/api/minioUpload/merge` |
|||
|
|||
// 发送合并请求 |
|||
return this.sendRequest(apiUrl, jsonArray) |
|||
}) |
|||
.then((res) => { |
|||
if (res.data.code === 200) { |
|||
this.showMessage('所有文件上传成功', 'success') |
|||
this.$emit('onUploadSuccess', res.data.data, this.uploadFiles.map(f => f.name), jsonArrayToSend) |
|||
} else { |
|||
this.showMessage('文件合并失败', 'error') |
|||
} |
|||
this.resetUploadState() |
|||
}) |
|||
.catch((err) => { |
|||
this.showMessage('上传服务器失败', 'error') |
|||
this.$emit('onUploadError', err) |
|||
this.resetUploadState() |
|||
}) |
|||
}, |
|||
|
|||
// 计算整体进度 |
|||
calculateTotalProgress() { |
|||
if (this.uploadFiles.length === 0) { |
|||
this.totalProgress = 0 |
|||
return |
|||
} |
|||
|
|||
// 优化:只计算有效文件的进度 |
|||
const validFiles = this.uploadFiles.filter(f => !f.isCancelled) |
|||
if (validFiles.length === 0) { |
|||
this.totalProgress = 0 |
|||
return |
|||
} |
|||
|
|||
const total = validFiles.reduce((sum, file) => sum + file.progress, 0) |
|||
this.totalProgress = Math.floor(total / validFiles.length) |
|||
}, |
|||
|
|||
// 优化:移除文件(支持是否保留列表) |
|||
removeFile(fileId, isRemoveFromList = true) { |
|||
if (isRemoveFromList) { |
|||
this.uploadFiles = this.uploadFiles.filter(f => f.id !== fileId) |
|||
} |
|||
this.totalFiles = this.uploadFiles.length |
|||
this.completedFiles = this.uploadFiles.filter(f => f.completed).length |
|||
|
|||
if (this.uploadFiles.length === 0) { |
|||
this.isUpload = false |
|||
this.totalProgress = 0 |
|||
} else { |
|||
this.calculateTotalProgress() |
|||
} |
|||
}, |
|||
|
|||
// 新增:取消上传 |
|||
handleCancelUpload(fileInfo) { |
|||
const file = fileInfo.file |
|||
// 暂停上传 |
|||
if (file.pause) file.pause() |
|||
// 中断MD5计算 |
|||
if (this.md5Readers[fileInfo.id]) { |
|||
this.md5Readers[fileInfo.id].abort() |
|||
delete this.md5Readers[fileInfo.id] |
|||
} |
|||
// 更新状态 |
|||
fileInfo.isCancelled = true |
|||
fileInfo.errorMsg = '用户取消上传' |
|||
fileInfo.isMd5Processing = false |
|||
// 重新计算进度 |
|||
this.calculateTotalProgress() |
|||
this.$message.info(`文件 ${fileInfo.name} 已取消上传`) |
|||
}, |
|||
|
|||
// 新增:重新上传 |
|||
handleReUpload(fileInfo) { |
|||
const file = fileInfo.file |
|||
// 重置状态 |
|||
fileInfo.isCancelled = false |
|||
fileInfo.errorMsg = '' |
|||
fileInfo.progress = 0 |
|||
fileInfo.isMd5Processing = true |
|||
fileInfo.md5Progress = '0%' |
|||
fileInfo.completed = false |
|||
|
|||
// 重新计算MD5并上传 |
|||
this.computeMD5(file, fileInfo.id) |
|||
.then(md5 => { |
|||
fileInfo.md5 = md5 |
|||
fileInfo.isMd5Processing = false |
|||
file.uniqueIdentifier = md5 |
|||
file.resume() |
|||
}) |
|||
.catch(error => { |
|||
fileInfo.errorMsg = error.message |
|||
fileInfo.isMd5Processing = false |
|||
this.$message.error(`文件 ${fileInfo.name} 重新处理失败: ${error.message}`) |
|||
}) |
|||
}, |
|||
|
|||
// 新增:清空所有上传文件 |
|||
handleClearAll() { |
|||
// 暂停所有上传 |
|||
this.uploadFiles.forEach(fileInfo => { |
|||
if (fileInfo.file.pause) fileInfo.file.pause() |
|||
// 中断MD5计算 |
|||
if (this.md5Readers[fileInfo.id]) { |
|||
this.md5Readers[fileInfo.id].abort() |
|||
} |
|||
}) |
|||
// 重置状态 |
|||
this.uploadFiles = [] |
|||
this.isUpload = false |
|||
this.totalProgress = 0 |
|||
this.completedFiles = 0 |
|||
this.totalFiles = 0 |
|||
this.md5Readers = {} |
|||
// 清空组件内置的文件列表 |
|||
if (this.uploader) { |
|||
this.uploader.fileList.forEach(file => file.cancel()) |
|||
this.uploader.fileList = [] |
|||
this.uploader.files = [] |
|||
} |
|||
if (this.$refs.uploader) { |
|||
this.$refs.uploader.files = [] |
|||
this.$refs.uploader.fileList = [] |
|||
} |
|||
}, |
|||
|
|||
// 重置上传状态 |
|||
resetUploadState() { |
|||
this.uploadFiles.forEach(file => { |
|||
file.isSyncing = false |
|||
}) |
|||
|
|||
// 延迟清空,让用户看到成功状态 |
|||
setTimeout(() => { |
|||
this.handleClearAll() // 复用清空方法 |
|||
}, 2000) |
|||
}, |
|||
|
|||
// 工具方法 |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 8 }) |
|||
}, |
|||
|
|||
sendRequest(url, data) { |
|||
return axios.post(url, data, { |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
}, |
|||
|
|||
getBase64(file) { |
|||
return new Promise((resolve) => { |
|||
const reader = new FileReader() |
|||
reader.readAsDataURL(file) |
|||
reader.onload = () => resolve(reader.result) |
|||
}) |
|||
}, |
|||
|
|||
getImgPx(img) { |
|||
return new Promise((resolve) => { |
|||
const image = new Image() |
|||
image.src = img |
|||
image.onload = () => resolve({ width: image.width, height: image.height }) |
|||
}) |
|||
}, |
|||
|
|||
handleClearData() { |
|||
this.handleClearAll() // 复用清空方法 |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.uploader-big{ |
|||
position: relative; |
|||
margin-left: 10px; |
|||
|
|||
.uploader-drop { |
|||
.uploader-btn { |
|||
i { |
|||
font-size: 20px; |
|||
color: #1F55EB; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.upload_process_box{ |
|||
position: absolute; |
|||
right: 0; |
|||
top: 44px; |
|||
width: 540px; |
|||
border-radius: 5px; |
|||
border: 1px dashed #409eff; |
|||
padding: 10px; |
|||
background: #fff; |
|||
z-index: 100; |
|||
|
|||
// 新增:操作栏样式 |
|||
.upload-operate-bar { |
|||
margin-bottom: 10px; |
|||
text-align: right; |
|||
} |
|||
|
|||
.file-progress-item { |
|||
margin-bottom: 15px; |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
margin-bottom: 0; |
|||
padding-bottom: 0; |
|||
} |
|||
|
|||
// 新增:文件基本信息样式 |
|||
.file-base-info { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 5px; |
|||
|
|||
.file-size { |
|||
font-size: 12px; |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
// 新增:状态提示样式 |
|||
.file-status-tip { |
|||
margin: 5px 0; |
|||
font-size: 12px; |
|||
|
|||
.upload-speed { |
|||
color: #1F55EB; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.error-tip { |
|||
color: #f56c6c; |
|||
} |
|||
|
|||
.success-tip { |
|||
color: #67c23a; |
|||
} |
|||
} |
|||
|
|||
// 新增:操作按钮样式 |
|||
.file-operate-btn { |
|||
margin-top: 5px; |
|||
text-align: right; |
|||
|
|||
.el-button { |
|||
padding: 0; |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.total-progress { |
|||
margin-top: 15px; |
|||
padding-top: 10px; |
|||
border-top: 1px solid #e0e0e0; |
|||
} |
|||
} |
|||
|
|||
// 其他样式保持不变 |
|||
::v-deep .uploader-list{ |
|||
max-height: 344px; |
|||
overflow-y: scroll; |
|||
|
|||
.uploader-file-size{ |
|||
text-align: right !important; |
|||
} |
|||
|
|||
.uploader-file-name{ |
|||
text-align: left !important; |
|||
} |
|||
|
|||
.uploader-file{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/attachment.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
&.icon-image{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/image.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-excel,&.icon-xlsx,&.icon-xls{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/excel.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-pdf{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/pdf.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-ppt, &.icon-pptx{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/ppt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-word,&.icon-docx,&.icon-doc{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/word.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-zip,&.icon-rar{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/zip.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-txt{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/txt.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
&.icon-ofd{ |
|||
.uploader-file-icon:before{ |
|||
background: url("~@/assets/images/fileIcon/OFD.png") no-repeat; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,670 +0,0 @@ |
|||
<template> |
|||
<div class="upload-minio"> |
|||
<!-- 文件选择按钮与隐藏的input --> |
|||
<el-button |
|||
type="primary" |
|||
:disabled="!isCaValid || isCheckingCa" |
|||
icon="el-icon-upload" |
|||
> |
|||
选择文件 |
|||
</el-button> |
|||
<input |
|||
type="file" |
|||
multiple |
|||
:disabled="!isCaValid || isCheckingCa" |
|||
class="file-input" |
|||
@change="handleFileSelect" |
|||
> |
|||
|
|||
<!-- 文件上传弹框:选择文件后自动打开 --> |
|||
<el-dialog |
|||
title="文件上传列表" |
|||
class="minio-file" |
|||
:close-on-click-modal="false" |
|||
:modal-append-to-body="false" |
|||
append-to-body |
|||
:visible.sync="uploadMinioVisible" |
|||
:before-close="handleCloseDialog" |
|||
width="600px" |
|||
> |
|||
<el-button |
|||
type="success" |
|||
style="margin-bottom: 20px" |
|||
:disabled="fileList.some(item => item.uploading || item.merging) || fileList.length === 0" |
|||
@click="handleUploadConfirm" |
|||
> |
|||
开始上传 |
|||
</el-button> |
|||
|
|||
<!-- 文件列表 --> |
|||
<div v-if="fileList.length === 0" class="empty-tip"> |
|||
暂无选择的文件 |
|||
</div> |
|||
<div |
|||
v-for="(fileItem, index) in fileList" |
|||
:key="index" |
|||
class="file-item" |
|||
> |
|||
<div class="file-name">{{ fileItem.file.name }} <span class="file-size">{{ formatFileSize(fileItem.file.size) }}</span></div> |
|||
<!-- 上传进度条 --> |
|||
<div v-if="fileItem.uploading" class="progress-wrapper"> |
|||
<span class="progress-text">上传进度: {{ fileItem.progress }}%</span> |
|||
<div class="progress-bar" :style="{ width: fileItem.progress + '%' }" /> |
|||
</div> |
|||
<!-- 合并中状态 --> |
|||
<div v-if="fileItem.merging" class="merge-loading "> |
|||
<span>合并中...</span> |
|||
</div> |
|||
<!-- 上传错误信息 --> |
|||
<p v-if="fileItem.errorMsg" class="error">{{ fileItem.errorMsg }}</p> |
|||
<!-- 上传成功 --> |
|||
<p v-if="fileItem.successMsg" class="success">{{ fileItem.successMsg }}</p> |
|||
</div> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { FetchCheckCaValidity } from '@/api/system/auth2' |
|||
import axios from 'axios' |
|||
import SparkMD5 from 'spark-md5' |
|||
import { getToken } from '@/utils/auth' |
|||
import { getCurrentTime } from '@/utils/index' |
|||
|
|||
export default { |
|||
name: 'MinioMultiChunkUpload', |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
uploadMinioVisible: false, // 弹框显示状态 |
|||
fileList: [], // 多文件上传列表 |
|||
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|||
totalMergeStartTime: null, // 整体批量合并开始时间 |
|||
totalMergeEndTime: null, // 整体批量合并结束时间 |
|||
allChunksUploaded: false, // 所有文件分片是否上传完成 |
|||
isCaValid: false, // CA证书是否有效 |
|||
isCheckingCa: false // 是否正在校验CA状态 |
|||
} |
|||
}, |
|||
|
|||
mounted() { |
|||
this.getCheckCaValidity() // 初始化时自动校验CA证书 |
|||
}, |
|||
|
|||
methods: { |
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0.00MB' |
|||
const mb = bytes / (1024 * 1024) // 1MB = 1024KB = 1024*1024B |
|||
return mb.toFixed(2) + 'MB' |
|||
}, |
|||
/** |
|||
* 工具方法:格式化时间戳为易读的本地时间(带毫秒) |
|||
* @param {number} timestamp - 时间戳 |
|||
* @returns {string} 格式化后的时间字符串 |
|||
*/ |
|||
formatTime(timestamp) { |
|||
if (!timestamp) return '无' |
|||
return new Date(timestamp).toLocaleString('zh-CN', { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit', |
|||
second: '2-digit', |
|||
millisecond: '3-digit' |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:计算两个时间戳的差值并格式化 |
|||
* @param {number} start - 开始时间戳 |
|||
* @param {number} end - 结束时间戳 |
|||
* @returns {string} 格式化的耗时字符串 |
|||
*/ |
|||
getTimeDiff(start, end) { |
|||
if (!start || !end) return '0ms' |
|||
const diff = end - start |
|||
if (diff < 1000) return `${diff}ms` |
|||
if (diff < 60000) return `${(diff / 1000).toFixed(2)}s` |
|||
return `${(diff / 60000).toFixed(2)}min` |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:获取图片分辨率 |
|||
* @param {string} base64 - 图片base64编码 |
|||
* @returns {Promise<{width: number, height: number}>} 图片宽高 |
|||
*/ |
|||
getImgPx(base64) { |
|||
return new Promise((resolve) => { |
|||
const img = new Image() |
|||
img.onload = () => { |
|||
resolve({ width: img.width, height: img.height }) |
|||
} |
|||
img.src = base64 |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:将文件转为base64 |
|||
* @param {File} file - 文件对象 |
|||
* @returns {Promise<string>} base64编码 |
|||
*/ |
|||
getBase64(file) { |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader() |
|||
reader.readAsDataURL(file) |
|||
reader.onload = () => resolve(reader.result) |
|||
reader.onerror = (err) => reject(err) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:显示提示信息 |
|||
* @param {string} msg - 提示内容 |
|||
* @param {string} type - 类型(success/error) |
|||
*/ |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 8 }) |
|||
}, |
|||
|
|||
/** |
|||
* 重置上传状态 |
|||
*/ |
|||
resetUploadState() { |
|||
this.allChunksUploaded = false |
|||
}, |
|||
|
|||
/** |
|||
* 校验CA证书有效性 |
|||
*/ |
|||
async getCheckCaValidity() { |
|||
try { |
|||
this.isCheckingCa = true |
|||
const res = await FetchCheckCaValidity() |
|||
this.isCaValid = !!res // 转为布尔值 |
|||
if (!this.isCaValid) { |
|||
this.showMessage('CA证书不在有效期内,无法进行分片上传', 'error') |
|||
} |
|||
} catch (err) { |
|||
this.isCaValid = false |
|||
this.showMessage('CA证书校验失败:' + err.message, 'error') |
|||
console.error('CA校验接口异常:', err) |
|||
} finally { |
|||
this.isCheckingCa = false |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 手动重新校验CA证书 |
|||
*/ |
|||
recheckCa() { |
|||
this.getCheckCaValidity() |
|||
}, |
|||
|
|||
/** |
|||
* 多文件选择处理函数:选择后打开弹框,仅初始化文件列表(不上传) |
|||
* @param {Event} e - 选择文件的事件对象 |
|||
*/ |
|||
async handleFileSelect(e) { |
|||
// 1. 校验CA状态 |
|||
if (this.isCheckingCa) { |
|||
this.showMessage('正在校验CA证书,请稍候...', 'warning') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
if (!this.isCaValid) { |
|||
this.showMessage('CA证书不在有效期内,无法上传文件', 'error') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
|
|||
// 2. 获取选择的文件并初始化列表 |
|||
const selectedFiles = Array.from(e.target.files) |
|||
if (selectedFiles.length === 0) return |
|||
|
|||
// 初始化文件上传状态(仅初始化,不上传) |
|||
const newFileList = selectedFiles.map(file => ({ |
|||
file, |
|||
uploading: false, |
|||
merging: false, |
|||
progress: 0, |
|||
errorMsg: '', |
|||
successMsg: '', |
|||
md5: '', |
|||
md5StartTime: null, |
|||
md5EndTime: null, |
|||
chunkUploadStartTime: null, |
|||
chunkUploadEndTime: null, |
|||
mergeStartTime: null, |
|||
mergeEndTime: null |
|||
})) |
|||
this.fileList = [...this.fileList, ...newFileList] |
|||
|
|||
// 3. 选择文件后立即打开弹框(核心修改点) |
|||
this.uploadMinioVisible = true |
|||
|
|||
// 清空input的文件选择(避免重复选择同一文件不触发change) |
|||
e.target.value = '' |
|||
}, |
|||
|
|||
/** |
|||
* 单个文件的分片上传流程 |
|||
* @param {Object} fileItem - 文件状态对象 |
|||
*/ |
|||
async uploadFileChunks(fileItem) { |
|||
const file = fileItem.file |
|||
fileItem.uploading = true |
|||
fileItem.progress = 0 |
|||
fileItem.errorMsg = '' |
|||
fileItem.successMsg = '' |
|||
|
|||
try { |
|||
// 计算文件MD5 |
|||
const fileMd5 = await this.calculateFileMd5(file, fileItem) |
|||
fileItem.md5 = fileMd5 |
|||
console.log(`【${file.name}】文件MD5:`, fileMd5) |
|||
|
|||
// 计算总分片数 |
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
console.log(`【${file.name}】总分片数:`, totalChunks) |
|||
|
|||
// 分片上传开始时间 |
|||
fileItem.chunkUploadStartTime = new Date().getTime() |
|||
console.log(`【${file.name}】分片上传开始时间:${this.formatTime(fileItem.chunkUploadStartTime)}`) |
|||
|
|||
const uploadedChunks = [] |
|||
for (let i = 0; i < totalChunks; i++) { |
|||
// 检查分片是否已存在 |
|||
const checkResult = await this.checkChunkExists(fileMd5, i) |
|||
if (!checkResult.exists) { |
|||
// 上传未存在的分片 |
|||
await this.uploadSingleChunk(file, fileMd5, i) |
|||
} |
|||
uploadedChunks.push(i) |
|||
fileItem.progress = Math.round((uploadedChunks.length / totalChunks) * 100) |
|||
} |
|||
|
|||
// 分片上传结束时间 |
|||
fileItem.chunkUploadEndTime = new Date().getTime() |
|||
console.log(`【${file.name}】分片上传结束时间:${this.formatTime(fileItem.chunkUploadEndTime)},耗时:${this.getTimeDiff(fileItem.chunkUploadStartTime, fileItem.chunkUploadEndTime)}`) |
|||
fileItem.progress = 100 |
|||
} catch (err) { |
|||
const fileName = file.name |
|||
if (fileItem.chunkUploadStartTime && !fileItem.chunkUploadEndTime) { |
|||
fileItem.chunkUploadEndTime = new Date().getTime() |
|||
console.log(`【${fileName}】分片上传异常结束时间:${this.formatTime(fileItem.chunkUploadEndTime)},耗时:${this.getTimeDiff(fileItem.chunkUploadStartTime, fileItem.chunkUploadEndTime)}`) |
|||
} |
|||
fileItem.errorMsg = '分片上传失败: ' + (err.message || '未知错误') |
|||
console.error(`【${fileName}】分片上传流程异常:`, err.message) |
|||
this.allChunksUploaded = false |
|||
} finally { |
|||
fileItem.uploading = false |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 计算文件MD5 |
|||
* @param {File} file - 待上传的文件 |
|||
* @param {Object} fileItem - 文件状态对象 |
|||
* @returns {Promise<string>} 文件的MD5值 |
|||
*/ |
|||
calculateFileMd5(file, fileItem) { |
|||
return new Promise((resolve, reject) => { |
|||
fileItem.md5StartTime = new Date().getTime() |
|||
console.log(`【${file.name}】MD5计算开始时间:${this.formatTime(fileItem.md5StartTime)}`) |
|||
|
|||
const spark = new SparkMD5.ArrayBuffer() |
|||
const fileReader = new FileReader() |
|||
const chunkSize = 2 * 1024 * 1024 |
|||
let offset = 0 |
|||
|
|||
const loadNextChunk = () => { |
|||
const blob = file.slice(offset, offset + chunkSize) |
|||
fileReader.readAsArrayBuffer(blob) |
|||
} |
|||
|
|||
fileReader.onload = (e) => { |
|||
spark.append(e.target.result) |
|||
offset += chunkSize |
|||
if (offset < file.size) { |
|||
loadNextChunk() |
|||
} else { |
|||
fileItem.md5EndTime = new Date().getTime() |
|||
console.log(`【${file.name}】MD5计算结束时间:${this.formatTime(fileItem.md5EndTime)},耗时:${this.getTimeDiff(fileItem.md5StartTime, fileItem.md5EndTime)}`) |
|||
resolve(spark.end()) |
|||
} |
|||
} |
|||
|
|||
fileReader.onerror = (err) => reject(err) |
|||
loadNextChunk() |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 校验所有分片是否存在 |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {number} totalChunks - 总分片数 |
|||
* @returns {Promise<boolean>} 所有分片是否都存在 |
|||
*/ |
|||
async checkAllChunksExist(fileMd5, totalChunks) { |
|||
for (let i = 0; i < totalChunks; i++) { |
|||
const result = await this.checkChunkExists(fileMd5, i) |
|||
if (!result.exists) { |
|||
console.warn(`【${fileMd5}】分片${i}未上传`) |
|||
return false |
|||
} |
|||
} |
|||
return true |
|||
}, |
|||
|
|||
/** |
|||
* 检查分片是否已存在 |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {number} chunkIndex - 分片索引 |
|||
* @returns {Promise<Object>} 分片存在状态 |
|||
*/ |
|||
async checkChunkExists(fileMd5, chunkIndex) { |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.get(linkSrc + '/api/minioUpload/chunk', { |
|||
params: { fileMd5, chunkIndex }, |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error('检查分片失败: ' + response.data.msg) |
|||
} |
|||
|
|||
return response.data.data |
|||
}, |
|||
|
|||
/** |
|||
* 上传单个分片 |
|||
* @param {File} file - 待上传的文件 |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {number} chunkIndex - 分片索引 |
|||
*/ |
|||
async uploadSingleChunk(file, fileMd5, chunkIndex) { |
|||
const start = chunkIndex * this.CHUNK_SIZE |
|||
const end = Math.min(start + this.CHUNK_SIZE, file.size) |
|||
const chunkBlob = file.slice(start, end) |
|||
|
|||
const formData = new FormData() |
|||
formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) |
|||
formData.append('fileMd5', fileMd5) |
|||
formData.append('chunkIndex', chunkIndex) |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(linkSrc + '/api/minioUpload/chunk', formData, { |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data', |
|||
'Authorization': getToken() |
|||
} |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error(`分片${chunkIndex}上传失败: ` + response.data.msg) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 批量上传并合并:弹框内点击按钮触发(核心修改点:新增串行上传逻辑) |
|||
*/ |
|||
async handleUploadConfirm() { |
|||
if (this.fileList.length === 0) { |
|||
this.showMessage('请先选择要上传的文件!', 'warning') |
|||
return |
|||
} |
|||
|
|||
// 过滤掉已上传/合并失败的文件,仅处理未上传的文件 |
|||
const pendingFiles = this.fileList.filter(item => !item.successMsg && !item.errorMsg) |
|||
if (pendingFiles.length === 0) { |
|||
this.showMessage('暂无待上传的文件!', 'warning') |
|||
return |
|||
} |
|||
|
|||
// 1. 串行上传所有文件的分片 |
|||
this.allChunksUploaded = true |
|||
for (const fileItem of pendingFiles) { |
|||
await this.uploadFileChunks(fileItem) |
|||
// 如果单个文件上传失败,标记整体上传状态为失败 |
|||
if (fileItem.errorMsg) { |
|||
this.allChunksUploaded = false |
|||
} |
|||
} |
|||
|
|||
// 2. 分片上传完成后执行合并 |
|||
if (!this.allChunksUploaded) { |
|||
this.showMessage('部分文件分片上传失败,无法执行合并', 'error') |
|||
return |
|||
} |
|||
|
|||
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) |
|||
if (validFiles.length === 0) { |
|||
this.showMessage('无有效分片上传完成的文件!', 'error') |
|||
return |
|||
} |
|||
|
|||
this.totalMergeStartTime = new Date().getTime() |
|||
console.log(`【整体合并】所有文件分片上传完成,开始合并,合并开始时间:${this.formatTime(this.totalMergeStartTime)}`) |
|||
const nowDate = getCurrentTime() |
|||
|
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = true |
|||
fileItem.mergeStartTime = new Date().getTime() |
|||
console.log(`【文件${fileItem.file.name}】合并开始时间:${this.formatTime(fileItem.mergeStartTime)}`) |
|||
}) |
|||
|
|||
try { |
|||
const processFiles = validFiles.map(async(fileItem) => { |
|||
const file = fileItem.file |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
|
|||
if (file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
jsonString.file_dpi = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
jsonString.file_dpi = '' |
|||
} |
|||
|
|||
jsonString.file_name = file.name |
|||
jsonString.file_size = file.size |
|||
jsonString.file_type = file.name.split('.').pop() || '' |
|||
jsonString.last_modified = file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.create_time = nowDate |
|||
jsonString.id = null |
|||
jsonString.file_thumbnail = '' |
|||
jsonArray.push(jsonString) |
|||
|
|||
console.log('file.lastModified', file.lastModified) |
|||
|
|||
if (this.isBatchMount === 'true') { |
|||
json.categoryId = this.selectedCategory.id |
|||
} else { |
|||
json.documentId = this.selectedDocument.id |
|||
} |
|||
|
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) |
|||
if (!chunksExist) { |
|||
throw new Error(`【${file.name}】部分分片未上传完成,无法合并`) |
|||
} |
|||
|
|||
json.archivesId = this.arcId |
|||
json.identifier = fileItem.md5 |
|||
json.filename = file.name |
|||
json.totalChunks = totalChunks |
|||
json.totalSize = file.size |
|||
json.fileJsonString = JSON.stringify(jsonArray) |
|||
|
|||
return json |
|||
}) |
|||
|
|||
const jsonArray = await Promise.all(processFiles) |
|||
// 调用合并接口 |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(linkSrc + '/api/minioUpload/merge', jsonArray, { |
|||
headers: { |
|||
'Authorization': getToken(), |
|||
'Content-Type': 'application/json' |
|||
} |
|||
}) |
|||
|
|||
this.totalMergeEndTime = new Date().getTime() |
|||
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
|||
|
|||
if (response.data.code === 200) { |
|||
this.showMessage('所有文件上传并合并成功', 'success') |
|||
validFiles.forEach((fileItem) => { |
|||
fileItem.mergeEndTime = new Date().getTime() |
|||
const mergeDuration = this.getTimeDiff(fileItem.mergeStartTime, fileItem.mergeEndTime) |
|||
console.log(`【文件${fileItem.file.name}】合并结束时间:${this.formatTime(fileItem.mergeEndTime)},合并耗时:${mergeDuration}`) |
|||
fileItem.successMsg = '上传成功!' |
|||
fileItem.merging = false |
|||
}) |
|||
console.log(`【整体合并】所有文件合并完成,整体合并耗时:${totalMergeDuration}`) |
|||
this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArray) |
|||
|
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] // 清空列表,避免下次打开弹框显示旧数据 |
|||
} else { |
|||
throw new Error(response.data.msg || '合并失败') |
|||
} |
|||
} catch (err) { |
|||
this.totalMergeEndTime = new Date().getTime() |
|||
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
|||
console.log(`【整体合并】合并失败,耗时:${totalMergeDuration},异常信息:`, err) |
|||
this.showMessage(`文件合并失败: ${err.message}`, 'error') |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = false |
|||
fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` |
|||
}) |
|||
this.$emit('onUploadError', err) |
|||
} finally { |
|||
this.resetUploadState() |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 关闭弹框时清空文件列表 |
|||
*/ |
|||
handleCloseDialog() { |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.resetUploadState() |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.upload-minio { |
|||
position: relative; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
/* 隐藏的文件选择input,覆盖在按钮上 */ |
|||
.file-input { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
width: 100px; |
|||
height: 36px; |
|||
padding: 5px; |
|||
opacity: 0; |
|||
cursor: pointer; |
|||
z-index: 10; |
|||
} |
|||
|
|||
/* 弹框内空提示 */ |
|||
.empty-tip { |
|||
text-align: center; |
|||
color: #999; |
|||
padding: 20px 0; |
|||
} |
|||
|
|||
.file-item { |
|||
width: 100%; |
|||
border: 1px dashed #409eff; |
|||
padding: 10px; |
|||
border-radius: 4px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.file-name { |
|||
font-weight: 500; |
|||
color: #333; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.progress-wrapper { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
} |
|||
|
|||
.progress-bar { |
|||
height: 10px; |
|||
background-color: #42b983; |
|||
transition: width 0.3s ease; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.progress-text { |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
|
|||
.merge-loading { |
|||
color: #1890ff; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.success { |
|||
color: #00C851; |
|||
font-size: 14px; |
|||
margin: 5px 0 0 0; |
|||
} |
|||
|
|||
.error { |
|||
color: #ff4444; |
|||
font-size: 14px; |
|||
margin: 5px 0 0 0; |
|||
} |
|||
|
|||
.minio-file{ |
|||
.el-dialog{ |
|||
.el-dialog__body{ |
|||
padding: 15px 0 30px 0 !important; |
|||
} |
|||
} |
|||
} |
|||
.file-size { |
|||
color: #666; |
|||
font-weight: normal; |
|||
font-size: 12px; |
|||
margin-left: 8px; |
|||
} |
|||
</style> |
|||
@ -1,279 +0,0 @@ |
|||
<template> |
|||
<div class="upload-container"> |
|||
<!-- 多文件选择框:添加multiple属性 --> |
|||
<input type="file" multiple @change="handleFileSelect"> |
|||
<!-- 遍历展示每个文件的上传状态 --> |
|||
<div v-for="(fileItem, index) in fileList" :key="index" class="file-item"> |
|||
<div class="file-name">{{ fileItem.file.name }}</div> |
|||
<!-- 上传进度条 --> |
|||
<div v-if="fileItem.uploading" class="progress-wrapper"> |
|||
<div class="progress-bar" :style="{ width: fileItem.progress + '%' }" /> |
|||
<span class="progress-text">上传进度: {{ fileItem.progress }}%</span> |
|||
</div> |
|||
<!-- 错误信息 --> |
|||
<p v-if="fileItem.errorMsg" class="error">{{ fileItem.errorMsg }}</p> |
|||
<!-- 成功信息 --> |
|||
<p v-if="fileItem.successMsg" class="success">{{ fileItem.successMsg }}</p> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import axios from 'axios' |
|||
import SparkMD5 from 'spark-md5' |
|||
import { getToken } from '@/utils/auth' |
|||
// import { getCurrentTime } from '@/utils/index' |
|||
|
|||
export default { |
|||
name: 'MinioMultiChunkUpload', |
|||
data() { |
|||
return { |
|||
fileList: [], // 多文件上传列表,存储每个文件的状态 |
|||
CHUNK_SIZE: 5 * 1024 * 1024 // 分片大小 (5MB) |
|||
} |
|||
}, |
|||
methods: { |
|||
/** |
|||
* 多文件选择处理函数 |
|||
* @param {Event} e - 选择文件的事件对象 |
|||
*/ |
|||
async handleFileSelect(e) { |
|||
const selectedFiles = Array.from(e.target.files) // 转为数组,支持多文件 |
|||
if (selectedFiles.length === 0) return |
|||
|
|||
// 初始化每个文件的上传状态,加入文件列表 |
|||
const newFileList = selectedFiles.map(file => ({ |
|||
file, // 文件本身 |
|||
uploading: false, // 是否正在上传 |
|||
progress: 0, // 上传进度 |
|||
errorMsg: '', // 错误信息 |
|||
successMsg: '' // 成功信息 |
|||
})) |
|||
this.fileList = [...this.fileList, ...newFileList] |
|||
|
|||
// 串行上传文件(避免同时上传过多,可改为并行) |
|||
for (const fileItem of newFileList) { |
|||
await this.uploadFile(fileItem) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 计算文件MD5(用于分片唯一标识) |
|||
* @param {File} file - 待上传的文件 |
|||
* @returns {Promise<string>} 文件的MD5值 |
|||
*/ |
|||
calculateFileMd5(file) { |
|||
return new Promise((resolve, reject) => { |
|||
const spark = new SparkMD5.ArrayBuffer() |
|||
const fileReader = new FileReader() |
|||
const chunkSize = 10 * 1024 * 1024 // 计算MD5的分片大小 |
|||
let offset = 0 |
|||
|
|||
const loadNextChunk = () => { |
|||
const blob = file.slice(offset, offset + chunkSize) |
|||
fileReader.readAsArrayBuffer(blob) |
|||
} |
|||
|
|||
fileReader.onload = (e) => { |
|||
spark.append(e.target.result) |
|||
offset += chunkSize |
|||
|
|||
if (offset < file.size) { |
|||
loadNextChunk() |
|||
} else { |
|||
resolve(spark.end()) |
|||
} |
|||
} |
|||
|
|||
fileReader.onerror = reject |
|||
loadNextChunk() |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 单个文件的上传主流程(分片检查、上传、合并) |
|||
* @param {Object} fileItem - 文件状态对象 |
|||
*/ |
|||
async uploadFile(fileItem) { |
|||
const file = fileItem.file |
|||
fileItem.uploading = true |
|||
fileItem.progress = 0 |
|||
fileItem.errorMsg = '' |
|||
fileItem.successMsg = '' |
|||
|
|||
try { |
|||
// 1. 计算文件MD5 |
|||
const fileMd5 = await this.calculateFileMd5(file) |
|||
console.log(`文件${file.name}的MD5:`, fileMd5) |
|||
|
|||
// 2. 计算总分片数 |
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
console.log(`文件${file.name}的总分片数:`, totalChunks) |
|||
|
|||
// 3. 检查并上传所有分片 |
|||
const uploadedChunks = [] |
|||
for (let i = 0; i < totalChunks; i++) { |
|||
// 检查分片是否已存在(断点续传) |
|||
const checkResult = await this.checkChunkExists(fileMd5, i) |
|||
|
|||
if (!checkResult.exists) { |
|||
// 上传未存在的分片 |
|||
await this.uploadSingleChunk(file, fileMd5, i) |
|||
} |
|||
|
|||
uploadedChunks.push(i) |
|||
fileItem.progress = Math.round((uploadedChunks.length / totalChunks) * 100) |
|||
} |
|||
|
|||
// 4. 合并所有分片 |
|||
const mergeResult = await this.mergeChunks(fileMd5, file.name, totalChunks) |
|||
console.log(`文件${file.name}的合并结果:`, mergeResult) |
|||
|
|||
fileItem.successMsg = '上传成功! 路径: ' + mergeResult.filePath |
|||
fileItem.progress = 100 |
|||
} catch (err) { |
|||
fileItem.errorMsg = '上传失败: ' + (err.message || '未知错误') |
|||
} finally { |
|||
fileItem.uploading = false |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 检查分片是否已存在(断点续传核心) |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {number} chunkIndex - 分片索引 |
|||
* @returns {Promise<Object>} 分片存在状态 |
|||
*/ |
|||
async checkChunkExists(fileMd5, chunkIndex) { |
|||
const response = await axios.get('/api/minioUpload/chunk', { |
|||
params: { |
|||
fileMd5, |
|||
chunkIndex |
|||
}, |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error('检查分片失败: ' + response.data.msg) |
|||
} |
|||
|
|||
return response.data.data |
|||
}, |
|||
|
|||
/** |
|||
* 上传单个分片 |
|||
* @param {File} file - 待上传的文件 |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {number} chunkIndex - 分片索引 |
|||
*/ |
|||
async uploadSingleChunk(file, fileMd5, chunkIndex) { |
|||
// 计算分片的起始和结束位置 |
|||
const start = chunkIndex * this.CHUNK_SIZE |
|||
const end = Math.min(start + this.CHUNK_SIZE, file.size) |
|||
const chunkBlob = file.slice(start, end) |
|||
|
|||
// 创建FormData |
|||
const formData = new FormData() |
|||
formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) |
|||
formData.append('fileMd5', fileMd5) |
|||
formData.append('chunkIndex', chunkIndex) |
|||
|
|||
const response = await axios.post('/api/minioUpload/chunk', formData, { |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data', |
|||
Authorization: getToken() |
|||
} |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error('分片上传失败: ' + response.data.msg) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 合并所有分片 |
|||
* @param {string} fileMd5 - 文件MD5 |
|||
* @param {string} fileName - 原文件名 |
|||
* @param {number} totalChunks - 总分片数 |
|||
* @returns {Promise<Object>} 合并结果 |
|||
*/ |
|||
async mergeChunks(fileMd5, fileName, totalChunks) { |
|||
try { |
|||
const response = await axios.post('/api/minioUpload/merge', null, { |
|||
params: { |
|||
fileMd5, |
|||
fileName, |
|||
totalChunks |
|||
}, |
|||
headers: { 'Authorization': getToken() } |
|||
|
|||
}) |
|||
console.log('合并分片请求响应:', JSON.stringify(response.data)) // 打印完整响应 |
|||
if (response.data.code !== 200) { |
|||
throw new Error(`合并失败: ${response.data.msg || '无错误信息'}`) |
|||
} |
|||
return response.data.data |
|||
} catch (err) { |
|||
console.error(`文件${fileName}合并请求异常:`, err.message, err.response?.data) // 打印请求异常 |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.upload-container { |
|||
max-width: 800px; |
|||
margin: 20px auto; |
|||
padding: 20px; |
|||
border: 1px solid #e0e0e0; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
/* 单个文件项样式 */ |
|||
.file-item { |
|||
margin-top: 20px; |
|||
padding: 15px; |
|||
border: 1px solid #f0f0f0; |
|||
border-radius: 6px; |
|||
} |
|||
|
|||
.file-name { |
|||
font-weight: 500; |
|||
margin-bottom: 10px; |
|||
color: #333; |
|||
} |
|||
|
|||
/* 进度条容器 */ |
|||
.progress-wrapper { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
} |
|||
|
|||
.progress-bar { |
|||
height: 20px; |
|||
background-color: #42b983; |
|||
transition: width 0.3s ease; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.progress-text { |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
|
|||
/* 提示文字样式 */ |
|||
.error { |
|||
color: #ff4444; |
|||
margin-top: 10px; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.success { |
|||
color: #00C851; |
|||
margin-top: 10px; |
|||
font-size: 14px; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue