7 changed files with 828 additions and 760 deletions
-
63src/views/collectReorganizi/collectionLibrary/file/index.vue
-
63src/views/collectReorganizi/collectionLibrary/module/uploadFile/index.vue
-
666src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload22.vue
-
645src/views/components/category/preUpload4 -数组.vue
-
85src/views/prearchiveLibrary/file/index.vue
-
3src/views/prearchiveLibrary/index.vue
-
63src/views/prearchiveLibrary/module/detail.vue
@ -0,0 +1,666 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<!--上传组件--> |
||||
|
<el-dialog class="big-file" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="uploadBigVisible" :before-close="handleCloseDialog"> |
||||
|
<template #title> |
||||
|
{{ uploadTitle }} |
||||
|
<span style="color: red;font-size: 12px; ">单个文件不可超过10GB</span> |
||||
|
</template> |
||||
|
<div class="setting-dialog"> |
||||
|
<div class="uploader-big"> |
||||
|
<uploader |
||||
|
ref="uploader" |
||||
|
:key="uploaderKey" |
||||
|
:auto-start="false" |
||||
|
:options="options" |
||||
|
:file-status-text="statusText" |
||||
|
@file-success="fileSuccess" |
||||
|
@files-added="filesAdded" |
||||
|
@file-error="onFileError" |
||||
|
@file-removed="filesRemove" |
||||
|
> |
||||
|
<uploader-unsupport /> |
||||
|
<uploader-drop> |
||||
|
<p>{{ !isCatalogUpload ? '将文件拖到此处,或点击上传(可多文件上传)' : "将ZIP包拖到此处, 或点击上传(只可单文件上传)" }}</p> |
||||
|
<uploader-btn :attrs="attrs"> |
||||
|
<slot> |
||||
|
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
||||
|
</slot> |
||||
|
</uploader-btn> |
||||
|
<div class="el-upload__tip">上传限制文件大小:最大10GB/个</div> |
||||
|
</uploader-drop> |
||||
|
<!-- <uploader-files /> --> |
||||
|
<uploader-list> |
||||
|
<!--通过slot-scope绑定文件实例--> |
||||
|
<div slot-scope="props" class="set-file"> |
||||
|
<div v-for="(file,i) in props.fileList" :key="i"> |
||||
|
<uploader-file |
||||
|
:list="true" |
||||
|
:file="file" |
||||
|
:class=" file.fileType.substring(0, file.fileType.indexOf('/')) === 'image' ? 'icon-image' : `icon-`+file.name.substring( |
||||
|
file.name.lastIndexOf('.') + 1, |
||||
|
file.name.length |
||||
|
)" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</uploader-list> |
||||
|
</uploader> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="handleCloseDialog">取消</el-button> |
||||
|
<el-button :loading="btnLoading" type="primary" @click="handleUploadConfirm">保存</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
|
||||
|
<!-- 判断是否有重复上传的文件 --> |
||||
|
<el-dialog class="collectUpload-dialog" title="文件上传" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="repeatFileVisible"> |
||||
|
<div class="setting-dialog"> |
||||
|
<p style="color:#f00;margin-bottom: 20px;display:block">提示:以下所选文件在当前档案文件列表已存在</p> |
||||
|
<div v-for="item in repeatFileData" :key="item.name" class="file-list" style="margin-bottom: 10px;"> |
||||
|
<i class="iconfont icon-xiaowenjian" /> |
||||
|
{{ item.name }} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="primary" @click="handleRepeatFile(0)">直接上传</el-button> |
||||
|
<el-button style="width: 85px;" type="primary" @click="handleRepeatFile(1)">去重后上传</el-button> |
||||
|
<el-button type="text" @click="repeatFileVisible = false">取消</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
|
||||
|
<!-- 目录上传报错 --> |
||||
|
<el-dialog class="catalog-dialog" title="目录上传-失败列表" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="catalogErrorVisible"> |
||||
|
<div class="setting-dialog"> |
||||
|
<div style="margin-bottom: 20px; display:flex; justify-content: flex-start;"> |
||||
|
<p style="margin-right: 20px;">总条数: <span style=" font-weight: bold;">{{ resultCatalog && resultCatalog.mountFile.total }} </span> 条</p> |
||||
|
<p style="margin-right: 20px;">成功: <span style="color:#1AAE93; font-weight: bold;">{{ resultCatalog && resultCatalog.mountFile.successNum }} </span> 条</p> |
||||
|
<p>失败: <span style="color:#f00; font-weight: bold; ">{{ resultCatalog && resultCatalog.mountFile.failNum }}</span> 条 </p> |
||||
|
</div> |
||||
|
<el-table class="archives-table" :data="catalogInfoData" style="min-width: 100%" height="calc(100vh - 676px)"> |
||||
|
<el-table-column type="expand"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-row style="padding-left: 20px;"> |
||||
|
<el-col :span="24" style="line-height: 30px;"> |
||||
|
<div v-for="(file, index) in row.children" :key="index"><i class="iconfont icon-xiaowenjian" />{{ file }}</div> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="archives" label="档案/电子原文" /> |
||||
|
<el-table-column prop="description" label="失败原因" /> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="primary" @click="catalogErrorVisible = false">确定</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { FetchInitFileCategoryView } from '@/api/collect/collect' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import axios from 'axios' |
||||
|
import SparkMD5 from 'spark-md5' |
||||
|
import { getToken } from '@/utils/auth' |
||||
|
import { getCurrentTime } from '@/utils/index' |
||||
|
import { catalogUpload } from '@/utils/upload' |
||||
|
// https://juejin.cn/post/7040817922540830728 |
||||
|
export default { |
||||
|
props: { |
||||
|
selectedCategory: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
}, |
||||
|
arcId: { |
||||
|
type: String, |
||||
|
default: function() { |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isCatalogUpload: false, |
||||
|
uploadTitle: '文件上传', |
||||
|
btnLoading: false, |
||||
|
uploadBigVisible: false, |
||||
|
skip: false, |
||||
|
options: { |
||||
|
target: null, |
||||
|
// 开启服务端分片校验功能 |
||||
|
testChunks: true, // 是否分片 |
||||
|
singleFile: false, // 多文件上传 |
||||
|
uploadMethod: 'post', // 真正上传的时候使用的 HTTP 方法,默认 POST |
||||
|
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 |
||||
|
}, |
||||
|
headers: { |
||||
|
'Authorization': getToken() |
||||
|
}, |
||||
|
processParams: (params, file, chunk) => { |
||||
|
params.fileMd5 = chunk.file.uniqueIdentifier |
||||
|
params.chunkIndex = chunk.offset |
||||
|
return params |
||||
|
} |
||||
|
}, |
||||
|
attrs: { |
||||
|
accept: '', |
||||
|
multiple: true |
||||
|
}, |
||||
|
// 修改上传状态 |
||||
|
statusText: { |
||||
|
success: '上传成功', |
||||
|
error: '上传出错了', |
||||
|
uploading: '上传中...', |
||||
|
paused: '暂停中...', |
||||
|
waiting: '等待中...', |
||||
|
cmd5: '计算文件MD5中...' |
||||
|
}, |
||||
|
fileList: [], |
||||
|
nowDate: null, |
||||
|
submitted: false, |
||||
|
// 重复的文件 |
||||
|
repeatFileVisible: false, |
||||
|
repeatFileData: [], |
||||
|
originFileData: [], |
||||
|
uploaderKey: 0, |
||||
|
resultCatalog: { |
||||
|
mountFile: {}, |
||||
|
failArchives: [] |
||||
|
}, |
||||
|
catalogErrorVisible: false, |
||||
|
catalogInfoData: [] |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'baseApi' |
||||
|
]) |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.options.target = this.baseApi + '/api/collect/chunk' |
||||
|
}, |
||||
|
methods: { |
||||
|
opened() { |
||||
|
this.$nextTick(() => { |
||||
|
if (!this.isCatalogUpload) { |
||||
|
this.uploadTitle = '文件上传' |
||||
|
this.attrs = { accept: '', multiple: true } |
||||
|
this.options.singleFile = false |
||||
|
} else { |
||||
|
this.uploadTitle = '原文目录上传' |
||||
|
this.attrs = { accept: '.zip', multiple: false } |
||||
|
this.options.singleFile = true |
||||
|
} |
||||
|
this.uploaderKey++ |
||||
|
console.log('this.isCatalogUpload', this.isCatalogUpload) |
||||
|
this.uploadBigVisible = true |
||||
|
}) |
||||
|
}, |
||||
|
updateUploadOptions(uploadType) { |
||||
|
console.log('uploadType', uploadType) |
||||
|
if (uploadType === 1) { |
||||
|
this.isCatalogUpload = false |
||||
|
this.uploadTitle = '文件上传' |
||||
|
this.attrs = { accept: '' } |
||||
|
this.options.singleFile = false |
||||
|
} else { |
||||
|
this.isCatalogUpload = true |
||||
|
this.uploadTitle = '原文目录上传' |
||||
|
this.attrs = { accept: '.zip' } |
||||
|
this.options.singleFile = true |
||||
|
} |
||||
|
console.log('this.attrs.accept', this.attrs.accept) |
||||
|
this.uploaderKey++ |
||||
|
this.uploadBigVisible = true |
||||
|
}, |
||||
|
// 电子原文列表 |
||||
|
getFileList() { |
||||
|
const params = { |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'archivesId': this.arcId |
||||
|
} |
||||
|
return FetchInitFileCategoryView(params).then(data => { |
||||
|
this.originFileData = data.returnlist |
||||
|
}) |
||||
|
}, |
||||
|
fileSuccess(rootFile, file, response, chunk) { |
||||
|
this.chunkOffset = [] |
||||
|
const result = JSON.parse(response) |
||||
|
console.log('result', result) |
||||
|
this.fileList.push(file) |
||||
|
if (result.code === 200 && this.fileList.length !== 0) { |
||||
|
this.submitted = true |
||||
|
} else { |
||||
|
this.submitted = false |
||||
|
} |
||||
|
if (this.skip) { |
||||
|
this.skip = false |
||||
|
} |
||||
|
}, |
||||
|
filesRemove(file, index) { |
||||
|
const uploaderInstance = this.$refs.uploader.uploader |
||||
|
const temp = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === file.uniqueIdentifier) |
||||
|
if (temp > -1) { |
||||
|
uploaderInstance.fileList[temp].cancel() // 这句代码是删除所选上传文件的关键 |
||||
|
} |
||||
|
this.fileList = this.fileList.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier) |
||||
|
}, |
||||
|
handleUploadConfirm() { |
||||
|
if (this.$refs.uploader.fileList.length === 0) { |
||||
|
this.$message({ message: '请选择要上传的文件!', type: 'error', offset: 8 }) |
||||
|
return false |
||||
|
} |
||||
|
this.nowDate = getCurrentTime() |
||||
|
const jsonArrayToSend = [] |
||||
|
console.log('this.$refs.uploader.fileList', this.$refs.uploader.fileList) |
||||
|
|
||||
|
// 使用 Promise.all 确保所有异步操作完成 |
||||
|
Promise.all(this.$refs.uploader.fileList.map(async(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) |
||||
|
|
||||
|
json.categoryId = this.selectedCategory.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.submitted) { |
||||
|
this.btnLoading = true |
||||
|
this.submitted = false |
||||
|
axios.post(this.baseApi + '/api/collect/merge', jsonArrayToSend, { |
||||
|
headers: { |
||||
|
'Authorization': getToken() |
||||
|
} |
||||
|
}).then((res) => { |
||||
|
console.log('merge', res.data.data) |
||||
|
if (res.data.code === 200) { |
||||
|
if (this.isCatalogUpload && res.data.data.length === 1 && res.data.data[0] !== '') { |
||||
|
// 目录上传 |
||||
|
catalogUpload(this.baseApi + '/api/collect/catalogUpload', |
||||
|
res.data.data[0], |
||||
|
this.selectedCategory.fondsId |
||||
|
).then(res => { |
||||
|
if (res.data.code === 200) { |
||||
|
this.resultCatalog = res.data.data |
||||
|
if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.successNum) { |
||||
|
this.$message({ message: '目录上传操作成功', type: 'success', offset: 8 }) |
||||
|
} else if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.failNum) { |
||||
|
this.catalogErrorVisible = true |
||||
|
} else { |
||||
|
this.catalogErrorVisible = true |
||||
|
} |
||||
|
this.catalogInfoData = [] |
||||
|
this.resultCatalog.failArchives.forEach(item => { |
||||
|
const parts = item.split(':') |
||||
|
if (parts.length === 2) { |
||||
|
const field = parts[0] |
||||
|
let fileStr = parts[1] |
||||
|
// 去除方括号 |
||||
|
fileStr = fileStr.replace(/\[|\]/g, '') |
||||
|
// 按逗号分割成数组 |
||||
|
const fileArray = fileStr.split(',').map(file => file.trim()) |
||||
|
// 拆分 field 为档号和文案 |
||||
|
const match = field.match(/^([\w-·]+)(.*)$/) |
||||
|
let archives = '' |
||||
|
let description = '' |
||||
|
if (match) { |
||||
|
archives = match[1] |
||||
|
description = match[2] |
||||
|
} |
||||
|
this.catalogInfoData.push({ |
||||
|
archives, |
||||
|
description, |
||||
|
children: fileArray |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
console.log('catalogInfoData', this.catalogInfoData) |
||||
|
this.$emit('close-dialog') |
||||
|
} else { |
||||
|
console.log('result1', res.data) |
||||
|
console.log('result2', res.data.message) |
||||
|
this.$message({ message: res.data.message, type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCloseDialog() |
||||
|
}) |
||||
|
} else { |
||||
|
this.$message({ message: '所有文件上传成功', type: 'success', offset: 8 }) |
||||
|
this.$emit('close-dialog') |
||||
|
this.handleCloseDialog() |
||||
|
} |
||||
|
} else { |
||||
|
this.$message({ message: '部分或全部文件上传失败', type: 'error', offset: 8 }) |
||||
|
this.handleCloseDialog() |
||||
|
} |
||||
|
}) |
||||
|
} else { |
||||
|
this.submitted = false |
||||
|
this.$message({ message: '请耐心等待文件上传完成后再保存!', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
onFileError(rootFile, file, message, chunk) { |
||||
|
this.$message({ message: `上传出错:${file.name} - ${message}`, type: 'error', offset: 8 }) |
||||
|
}, |
||||
|
handleRepeatFileCancel() { |
||||
|
this.repeatFileVisible = false |
||||
|
this.repeatFileData = [] |
||||
|
this.originFileData = [] |
||||
|
const uploaderInstance = this.$refs.uploader.uploader |
||||
|
uploaderInstance.fileList.forEach(file => file.cancel()) |
||||
|
}, |
||||
|
handleRepeatFile(type) { |
||||
|
if (type === 0) { |
||||
|
// 直接上传 |
||||
|
this.fileList.forEach(file => this.computeMD5(file)) |
||||
|
} else { |
||||
|
// 去重后上传 |
||||
|
const nonRepeatFileData = this.fileList.filter(file => !this.repeatFileData.some(repeatFile => repeatFile.name === file.name)) |
||||
|
if (nonRepeatFileData.length === 0) { |
||||
|
this.$message({ message: '当前所选文件去重后无可上传的文件', type: 'error', offset: 8 }) |
||||
|
this.handleRepeatFileCancel() |
||||
|
this.handleCloseDialog() |
||||
|
} else { |
||||
|
// 更新上传组件的文件列表 |
||||
|
const uploaderInstance = this.$refs.uploader |
||||
|
uploaderInstance.fileList = uploaderInstance.fileList.filter(file => nonRepeatFileData.some(nonRepeatFile => nonRepeatFile.uniqueIdentifier === file.uniqueIdentifier)) |
||||
|
this.$refs.uploader.files = this.$refs.uploader.files.filter(file => nonRepeatFileData.some(nonRepeatFile => nonRepeatFile.uniqueIdentifier === file.uniqueIdentifier)) |
||||
|
nonRepeatFileData.forEach(file => this.computeMD5(file)) |
||||
|
} |
||||
|
} |
||||
|
this.repeatFileVisible = false |
||||
|
}, |
||||
|
async filesAdded(file, fileList, event) { |
||||
|
if (!this.isCatalogUpload) { |
||||
|
// 先获取原文件列表 |
||||
|
await this.getFileList() |
||||
|
|
||||
|
file.forEach((e) => { |
||||
|
this.fileList.push(e) |
||||
|
const existingFileNames = this.originFileData.map(file => file.file_name) |
||||
|
|
||||
|
this.repeatFileData = this.fileList.filter(file => existingFileNames.includes(file.name)) |
||||
|
console.log('filteredFileList', this.repeatFileData) |
||||
|
|
||||
|
if (this.repeatFileData.length === 0) { |
||||
|
this.computeMD5(e) |
||||
|
} else { |
||||
|
this.repeatFileVisible = true |
||||
|
} |
||||
|
}) |
||||
|
} else { |
||||
|
file.forEach((e) => { |
||||
|
this.fileList.push(e) |
||||
|
this.computeMD5(e) |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
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 = 10 * 1024 * 1024 |
||||
|
const chunks = Math.ceil(file.size / chunkSize) |
||||
|
const spark = new SparkMD5.ArrayBuffer() |
||||
|
// 文件状态设为"计算MD5" |
||||
|
file.cmd5 = true |
||||
|
file.pause() |
||||
|
|
||||
|
// 定义 loadNext 函数为箭头函数 |
||||
|
const loadNext = () => { |
||||
|
const start = currentChunk * chunkSize |
||||
|
const end = start + chunkSize >= file.size ? file.size : start + chunkSize |
||||
|
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)) |
||||
|
} |
||||
|
|
||||
|
loadNext() |
||||
|
fileReader.onload = (e) => { |
||||
|
spark.append(e.target.result) |
||||
|
if (currentChunk < chunks) { |
||||
|
currentChunk++ |
||||
|
loadNext() |
||||
|
// 实时展示MD5的计算进度 |
||||
|
console.log( |
||||
|
`第${currentChunk}分片解析完成, 开始第${ |
||||
|
currentChunk + 1 |
||||
|
} / ${chunks}分片解析` |
||||
|
) |
||||
|
} else { |
||||
|
const md5 = spark.end() |
||||
|
console.log( |
||||
|
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${ |
||||
|
file.size |
||||
|
} 用时:${new Date().getTime() - time} ms` |
||||
|
) |
||||
|
console.log('md5', md5) |
||||
|
spark.destroy() // 释放缓存 |
||||
|
file.uniqueIdentifier = md5 // 将文件md5赋值给文件唯一标识 |
||||
|
file.cmd5 = false // 取消计算md5状态 |
||||
|
file.resume() // 开始上传 |
||||
|
} |
||||
|
} |
||||
|
fileReader.onerror = function() { |
||||
|
this.error(`文件${file.name}读取出错,请检查该文件`) |
||||
|
file.cancel() |
||||
|
} |
||||
|
}, |
||||
|
// 将上传的图片转为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 }) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
handleCloseDialog(done) { |
||||
|
this.btnLoading = false |
||||
|
this.uploadBigVisible = false |
||||
|
const uploaderInstance = this.$refs.uploader.uploader |
||||
|
console.log('uploaderInstance.fileList', uploaderInstance.fileList) |
||||
|
uploaderInstance.fileList.forEach(file => file.cancel()) |
||||
|
uploaderInstance.fileList = [] |
||||
|
uploaderInstance.files = [] |
||||
|
this.fileList = [] |
||||
|
this.$refs.uploader.files = [] |
||||
|
this.$refs.uploader.fileList = [] |
||||
|
// 重置 isCatalogUpload 和 attrs |
||||
|
this.isCatalogUpload = false |
||||
|
this.attrs = { accept: '', multiple: true } |
||||
|
this.uploaderKey++ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.uploader-big{ |
||||
|
width: 100%; |
||||
|
.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; |
||||
|
height: 180px; |
||||
|
// padding: 20px 0; |
||||
|
border: none; |
||||
|
.uploader-btn{ |
||||
|
// width: 120px; |
||||
|
margin: 20px 0 20px 0; |
||||
|
padding: 0; |
||||
|
border: none; |
||||
|
i{ |
||||
|
font-size: 32px; |
||||
|
color: #1F55EB; |
||||
|
} |
||||
|
&:hover{ |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
.el-upload__tip{ |
||||
|
font-size: 12px; |
||||
|
color: #A6ADB6; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.upload-big-button{ |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin: 10px 0 5px 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> |
||||
@ -1,645 +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> |
|
||||
<!-- 合并中状态 --> |
|
||||
<div v-if="fileItem.merging" class="merge-loading"> |
|
||||
<span>合并中...</span> |
|
||||
</div> |
|
||||
<!-- 验签中状态 --> |
|
||||
<div v-if="fileItem.verifying" class="verify-loading"> |
|
||||
<span>验签中...</span> |
|
||||
</div> |
|
||||
<!-- 上传错误信息 --> |
|
||||
<p v-if="fileItem.errorMsg" class="error">{{ fileItem.errorMsg }}</p> |
|
||||
<!-- 上传成功+验签结果 --> |
|
||||
<div v-if="fileItem.successMsg" class="success-section"> |
|
||||
<p class="success">{{ fileItem.successMsg }}</p> |
|
||||
<!-- 验签成功 --> |
|
||||
<p v-if="fileItem.verifySuccess" class="verify-success">✓ 验签成功</p> |
|
||||
<!-- 验签失败 --> |
|
||||
<p v-if="fileItem.verifyError" class="verify-error">✗ 验签失败: {{ fileItem.verifyError }}</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import axios from 'axios' |
|
||||
import SparkMD5 from 'spark-md5' |
|
||||
import { getToken } from '@/utils/auth' |
|
||||
|
|
||||
// 模拟getCurrentTime方法(若项目中有该方法可直接导入) |
|
||||
const getCurrentTime = () => { |
|
||||
return new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '') |
|
||||
} |
|
||||
|
|
||||
export default { |
|
||||
name: 'MinioMultiChunkUpload', |
|
||||
props: { |
|
||||
selectedDocument: { |
|
||||
type: Object, |
|
||||
default: () => ({}) |
|
||||
}, |
|
||||
arcId: { |
|
||||
type: String, |
|
||||
default: '' |
|
||||
}, |
|
||||
selectedCategory: { |
|
||||
type: Object, |
|
||||
default: () => ({}) |
|
||||
}, |
|
||||
isBatchMount: { |
|
||||
type: String, |
|
||||
default: '' |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
data() { |
|
||||
return { |
|
||||
fileList: [], // 多文件上传列表,存储每个文件的状态 |
|
||||
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|
||||
totalMergeStartTime: null, // 整体批量合并开始时间 |
|
||||
totalMergeEndTime: null, // 整体批量合并结束时间 |
|
||||
baseApi: '', // 接口基础路径(根据项目实际配置) |
|
||||
allChunksUploaded: false // 所有文件分片是否上传完成 |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
/** |
|
||||
* 工具方法:格式化时间戳为易读的本地时间(带毫秒) |
|
||||
* @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) |
|
||||
}) |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 工具方法:显示提示信息(可替换为项目的Message组件) |
|
||||
* @param {string} msg - 提示内容 |
|
||||
* @param {string} type - 类型(success/error) |
|
||||
*/ |
|
||||
showMessage(msg, type) { |
|
||||
if (type === 'error') { |
|
||||
console.error(msg) |
|
||||
} else { |
|
||||
console.log(msg) |
|
||||
} |
|
||||
// 若项目有ElementUI/NaiveUI等组件,可替换为: |
|
||||
// this.$message[type](msg) |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 重置上传状态 |
|
||||
*/ |
|
||||
resetUploadState() { |
|
||||
this.allChunksUploaded = false |
|
||||
// 可根据需求重置其他状态 |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 多文件选择处理函数 |
|
||||
* @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, // 是否正在上传 |
|
||||
merging: false, // 是否正在合并 |
|
||||
progress: 0, // 上传进度 |
|
||||
errorMsg: '', // 上传/合并错误信息 |
|
||||
successMsg: '', // 上传成功信息 |
|
||||
verifying: false, // 是否正在验签 |
|
||||
verifySuccess: false, // 验签是否成功 |
|
||||
verifyError: '', // 验签失败信息 |
|
||||
md5: '', // 文件MD5(新增,用于合并参数) |
|
||||
// 新增:每个文件的时间记录 |
|
||||
md5StartTime: null, // MD5计算开始时间 |
|
||||
md5EndTime: null, // MD5计算结束时间 |
|
||||
chunkUploadStartTime: null, // 分片上传开始时间 |
|
||||
chunkUploadEndTime: null, // 分片上传结束时间 |
|
||||
mergeStartTime: null, // 合并开始时间 |
|
||||
mergeEndTime: null, // 合并结束时间 |
|
||||
verifyStartTime: null, // 验签开始时间 |
|
||||
verifyEndTime: null // 验签结束时间 |
|
||||
})) |
|
||||
this.fileList = [...this.fileList, ...newFileList] |
|
||||
|
|
||||
// 串行上传文件分片(避免同时上传过多) |
|
||||
for (const fileItem of newFileList) { |
|
||||
await this.uploadFileChunks(fileItem) |
|
||||
} |
|
||||
|
|
||||
// 所有文件分片上传完成后,标记状态并触发批量合并 |
|
||||
this.allChunksUploaded = true |
|
||||
this.handleUploadConfirm() |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 单个文件的分片上传流程(仅上传分片,不立即合并) |
|
||||
* @param {Object} fileItem - 文件状态对象 |
|
||||
*/ |
|
||||
async uploadFileChunks(fileItem) { |
|
||||
const file = fileItem.file |
|
||||
fileItem.uploading = true |
|
||||
fileItem.progress = 0 |
|
||||
fileItem.errorMsg = '' |
|
||||
fileItem.successMsg = '' |
|
||||
|
|
||||
try { |
|
||||
// 1. 计算文件MD5 |
|
||||
const fileMd5 = await this.calculateFileMd5(file, fileItem) |
|
||||
fileItem.md5 = fileMd5 // 存储MD5用于后续合并 |
|
||||
console.log(`【${file.name}】文件MD5:`, fileMd5) |
|
||||
|
|
||||
// 2. 计算总分片数 |
|
||||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|
||||
console.log(`【${file.name}】总分片数:`, totalChunks) |
|
||||
|
|
||||
// 3. 检查并上传所有分片(记录分片上传开始时间) |
|
||||
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 // 分片上传完成,进度置为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) => { |
|
||||
// 记录MD5计算开始时间 |
|
||||
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 // 计算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 { |
|
||||
// 记录MD5计算结束时间 |
|
||||
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 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(`分片${chunkIndex}上传失败: ` + response.data.msg) |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 所有文件分片上传完成后自动执行批量合并(核心改造方法) |
|
||||
*/ |
|
||||
async handleUploadConfirm() { |
|
||||
if (this.fileList.length === 0 || !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() |
|
||||
const jsonArrayToSend = [] |
|
||||
|
|
||||
// 标记所有文件为合并中,并记录单个文件合并开始时间 |
|
||||
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 = '' |
|
||||
} |
|
||||
|
|
||||
// 构建文件信息(按handleUploadConfirm的参数结构) |
|
||||
jsonString.file_name = file.name |
|
||||
jsonString.file_size = file.size |
|
||||
jsonString.file_type = file.name.split('.').pop() || '' |
|
||||
json.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) |
|
||||
|
|
||||
// 根据挂载类型设置不同参数 |
|
||||
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) |
|
||||
|
|
||||
jsonArrayToSend.push(json) |
|
||||
return json |
|
||||
}) |
|
||||
|
|
||||
// 等待所有文件参数构建完成 |
|
||||
const jsonArray = await Promise.all(processFiles) |
|
||||
|
|
||||
// 调用批量合并接口(保留原/api/minioUpload/merge接口) |
|
||||
const response = await axios.post('/api/minioUpload/merge', jsonArray, { |
|
||||
headers: { |
|
||||
'Authorization': getToken(), |
|
||||
'Content-Type': 'application/json' // 批量合并传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, index) => { |
|
||||
fileItem.mergeEndTime = new Date().getTime() |
|
||||
const mergeDuration = this.getTimeDiff(fileItem.mergeStartTime, fileItem.mergeEndTime) |
|
||||
console.log(`【文件${fileItem.file.name}】合并结束时间:${this.formatTime(fileItem.mergeEndTime)},合并耗时:${mergeDuration}`) |
|
||||
// 从合并结果中获取文件路径(需后端返回对应文件的路径) |
|
||||
const mergeResult = response.data.data[index] || {} |
|
||||
fileItem.successMsg = '上传成功! 路径: ' + (mergeResult.filePath || '未知路径') |
|
||||
fileItem.merging = false |
|
||||
|
|
||||
// 合并成功后立即触发验签 |
|
||||
this.verifySignature(fileItem, mergeResult) |
|
||||
}) |
|
||||
console.log(`【整体合并】所有文件合并完成,合并结束时间:${this.formatTime(this.totalMergeEndTime)},整体合并耗时:${totalMergeDuration}`) |
|
||||
this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArrayToSend) |
|
||||
} else { |
|
||||
throw new Error(response.data.msg || '合并失败') |
|
||||
} |
|
||||
} catch (err) { |
|
||||
// 合并失败处理 |
|
||||
this.totalMergeEndTime = new Date().getTime() |
|
||||
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
|
||||
console.log(`【整体合并】合并失败,失败时间:${this.formatTime(this.totalMergeEndTime)},合并耗时:${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() |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 调用验签接口验证签名 |
|
||||
* @param {Object} fileItem - 文件状态对象 |
|
||||
* @param {Object} mergeResult - 合并成功后的返回数据 |
|
||||
*/ |
|
||||
async verifySignature(fileItem, mergeResult) { |
|
||||
const file = fileItem.file |
|
||||
const verifyParams = { |
|
||||
certFingerprint: mergeResult.certFingerprint, |
|
||||
filePath: mergeResult.filePath, |
|
||||
signature: mergeResult.signature, |
|
||||
timestamp: mergeResult.timestamp |
|
||||
} |
|
||||
console.log(`【${file.name}】验签参数:`, verifyParams) |
|
||||
|
|
||||
fileItem.verifying = true |
|
||||
try { |
|
||||
const response = await axios.post( |
|
||||
'/api/minioUpload/verify-signature', |
|
||||
null, |
|
||||
{ |
|
||||
params: verifyParams, |
|
||||
headers: { 'Authorization': getToken() } |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
console.log(`【${file.name}】验签接口响应:`, response.data) |
|
||||
|
|
||||
if (response.data.code === 200) { // 调整为实际的成功码 |
|
||||
fileItem.verifySuccess = true |
|
||||
} else { |
|
||||
fileItem.verifyError = response.data.error || '验签失败,原因未知' |
|
||||
} |
|
||||
} catch (err) { |
|
||||
fileItem.verifyError = `请求异常: ${err.message || '网络错误'}` |
|
||||
console.error(`【${file.name}】验签请求异常:`, err) |
|
||||
} finally { |
|
||||
// 记录验签结束时间 |
|
||||
fileItem.verifyEndTime = new Date().getTime() |
|
||||
fileItem.verifying = false |
|
||||
// 打印文件全流程耗时 |
|
||||
const totalTime = this.getTimeDiff(fileItem.md5StartTime, fileItem.verifyEndTime) |
|
||||
console.log(`【${file.name}】上传+合并+验签全流程总耗时:${totalTime}`) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</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; |
|
||||
} |
|
||||
|
|
||||
/* 合并中状态 */ |
|
||||
.merge-loading { |
|
||||
margin: 10px 0; |
|
||||
color: #1890ff; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
|
|
||||
/* 验签中状态 */ |
|
||||
.verify-loading { |
|
||||
margin: 10px 0; |
|
||||
color: #42b983; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
|
|
||||
/* 上传和验签提示样式 */ |
|
||||
.success-section { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
gap: 5px; |
|
||||
} |
|
||||
|
|
||||
.success { |
|
||||
color: #00C851; |
|
||||
margin: 0; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
|
|
||||
.error { |
|
||||
color: #ff4444; |
|
||||
margin: 10px 0 0 0; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
|
|
||||
.verify-success { |
|
||||
color: #00C851; |
|
||||
font-size: 14px; |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
.verify-error { |
|
||||
color: #ff4444; |
|
||||
font-size: 14px; |
|
||||
margin: 0; |
|
||||
} |
|
||||
</style> |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue