|
|
|
@ -1,5 +1,13 @@ |
|
|
|
<template> |
|
|
|
<div class="upload-minio"> |
|
|
|
<!-- 文件选择按钮与隐藏的input --> |
|
|
|
<el-button |
|
|
|
type="primary" |
|
|
|
:disabled="!isCaValid || isCheckingCa" |
|
|
|
icon="el-icon-upload" |
|
|
|
> |
|
|
|
选择文件 |
|
|
|
</el-button> |
|
|
|
<input |
|
|
|
type="file" |
|
|
|
multiple |
|
|
|
@ -7,25 +15,52 @@ |
|
|
|
class="file-input" |
|
|
|
@change="handleFileSelect" |
|
|
|
> |
|
|
|
<el-button>上传文件</el-button> |
|
|
|
|
|
|
|
<!-- 遍历展示每个文件的上传状态(移除验签相关渲染) --> |
|
|
|
<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> |
|
|
|
|
|
|
|
<!-- 文件上传弹框:选择文件后自动打开 --> |
|
|
|
<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-if="fileItem.merging" class="merge-loading"> |
|
|
|
<span>合并中...</span> |
|
|
|
<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> |
|
|
|
<!-- 上传错误信息 --> |
|
|
|
<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> |
|
|
|
|
|
|
|
@ -59,7 +94,8 @@ export default { |
|
|
|
|
|
|
|
data() { |
|
|
|
return { |
|
|
|
fileList: [], // 多文件上传列表,存储每个文件的状态 |
|
|
|
uploadMinioVisible: false, // 弹框显示状态 |
|
|
|
fileList: [], // 多文件上传列表 |
|
|
|
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|
|
|
totalMergeStartTime: null, // 整体批量合并开始时间 |
|
|
|
totalMergeEndTime: null, // 整体批量合并结束时间 |
|
|
|
@ -74,6 +110,11 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
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 - 时间戳 |
|
|
|
@ -136,7 +177,7 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 工具方法:显示提示信息(可替换为项目的Message组件) |
|
|
|
* 工具方法:显示提示信息 |
|
|
|
* @param {string} msg - 提示内容 |
|
|
|
* @param {string} type - 类型(success/error) |
|
|
|
*/ |
|
|
|
@ -158,10 +199,8 @@ export default { |
|
|
|
try { |
|
|
|
this.isCheckingCa = true |
|
|
|
const res = await FetchCheckCaValidity() |
|
|
|
this.isCaValid = !!res // 转为布尔值,确保结果是true/false |
|
|
|
if (this.isCaValid) { |
|
|
|
// this.showMessage('CA证书有效,可进行分片上传', 'success') |
|
|
|
} else { |
|
|
|
this.isCaValid = !!res // 转为布尔值 |
|
|
|
if (!this.isCaValid) { |
|
|
|
this.showMessage('CA证书不在有效期内,无法进行分片上传', 'error') |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
@ -181,26 +220,27 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 多文件选择处理函数 |
|
|
|
* 多文件选择处理函数:选择后打开弹框,仅初始化文件列表(不上传) |
|
|
|
* @param {Event} e - 选择文件的事件对象 |
|
|
|
*/ |
|
|
|
async handleFileSelect(e) { |
|
|
|
// 1. 校验CA状态,无效则拦截上传 |
|
|
|
// 1. 校验CA状态 |
|
|
|
if (this.isCheckingCa) { |
|
|
|
this.showMessage('正在校验CA证书,请稍候...', 'error') |
|
|
|
this.showMessage('正在校验CA证书,请稍候...', 'warning') |
|
|
|
e.target.value = '' |
|
|
|
return |
|
|
|
} |
|
|
|
if (!this.isCaValid) { |
|
|
|
this.showMessage('CA证书不在有效期内,无法上传文件', 'error') |
|
|
|
e.target.value = '' // 清空文件选择框 |
|
|
|
e.target.value = '' |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 原有的文件上传逻辑 |
|
|
|
// 2. 获取选择的文件并初始化列表 |
|
|
|
const selectedFiles = Array.from(e.target.files) |
|
|
|
if (selectedFiles.length === 0) return |
|
|
|
|
|
|
|
// 初始化每个文件的上传状态(移除验签相关变量) |
|
|
|
// 初始化文件上传状态(仅初始化,不上传) |
|
|
|
const newFileList = selectedFiles.map(file => ({ |
|
|
|
file, |
|
|
|
uploading: false, |
|
|
|
@ -218,14 +258,11 @@ export default { |
|
|
|
})) |
|
|
|
this.fileList = [...this.fileList, ...newFileList] |
|
|
|
|
|
|
|
// 串行上传文件分片 |
|
|
|
for (const fileItem of newFileList) { |
|
|
|
await this.uploadFileChunks(fileItem) |
|
|
|
} |
|
|
|
// 3. 选择文件后立即打开弹框(核心修改点) |
|
|
|
this.uploadMinioVisible = true |
|
|
|
|
|
|
|
// 所有文件分片上传完成后,标记状态并触发批量合并 |
|
|
|
this.allChunksUploaded = true |
|
|
|
this.handleUploadConfirm() |
|
|
|
// 清空input的文件选择(避免重复选择同一文件不触发change) |
|
|
|
e.target.value = '' |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
@ -386,11 +423,34 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 所有文件分片上传完成后自动执行批量合并 |
|
|
|
* 批量上传并合并:弹框内点击按钮触发(核心修改点:新增串行上传逻辑) |
|
|
|
*/ |
|
|
|
async handleUploadConfirm() { |
|
|
|
if (this.fileList.length === 0 || !this.allChunksUploaded) { |
|
|
|
this.showMessage('没有可处理的文件或分片上传未完成!', 'error') |
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
@ -428,7 +488,7 @@ export default { |
|
|
|
jsonString.file_name = file.name |
|
|
|
jsonString.file_size = file.size |
|
|
|
jsonString.file_type = file.name.split('.').pop() || '' |
|
|
|
json.last_modified = file.lastModified |
|
|
|
jsonString.last_modified = file.lastModified |
|
|
|
jsonString.file_path = '' |
|
|
|
jsonString.sequence = null |
|
|
|
jsonString.archive_id = this.arcId |
|
|
|
@ -437,6 +497,8 @@ export default { |
|
|
|
jsonString.file_thumbnail = '' |
|
|
|
jsonArray.push(jsonString) |
|
|
|
|
|
|
|
console.log('file.lastModified', file.lastModified) |
|
|
|
|
|
|
|
if (this.isBatchMount === 'true') { |
|
|
|
json.categoryId = this.selectedCategory.id |
|
|
|
} else { |
|
|
|
@ -460,8 +522,8 @@ export default { |
|
|
|
}) |
|
|
|
|
|
|
|
const jsonArray = await Promise.all(processFiles) |
|
|
|
// 注意:若后端支持批量合并,需将jsonArray整体传入,此处原代码传了第一个元素,需根据后端接口调整 |
|
|
|
const response = await axios.post('/api/minioUpload/merge', jsonArray[0], { |
|
|
|
// 调用合并接口 |
|
|
|
const response = await axios.post('/api/minioUpload/merge', jsonArray, { |
|
|
|
headers: { |
|
|
|
'Authorization': getToken(), |
|
|
|
'Content-Type': 'application/json' |
|
|
|
@ -472,26 +534,27 @@ export default { |
|
|
|
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
|
|
|
|
|
|
|
if (response.data.code === 200) { |
|
|
|
this.showMessage('所有文件合并成功', 'success') |
|
|
|
validFiles.forEach((fileItem, index) => { |
|
|
|
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}`) |
|
|
|
// const mergeResult = response.data.data || {} |
|
|
|
// fileItem.successMsg = '上传成功! 路径: ' + (mergeResult.filePath || '未知路径') |
|
|
|
fileItem.successMsg = '上传成功!' |
|
|
|
fileItem.merging = false |
|
|
|
}) |
|
|
|
console.log(`【整体合并】所有文件合并完成,合并结束时间:${this.formatTime(this.totalMergeEndTime)},整体合并耗时:${totalMergeDuration}`) |
|
|
|
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(`【整体合并】合并失败,失败时间:${this.formatTime(this.totalMergeEndTime)},合并耗时:${totalMergeDuration},异常信息:`, err) |
|
|
|
this.showMessage(`合并失败: ${err.message}`, 'error') |
|
|
|
console.log(`【整体合并】合并失败,耗时:${totalMergeDuration},异常信息:`, err) |
|
|
|
this.showMessage(`文件合并失败: ${err.message}`, 'error') |
|
|
|
validFiles.forEach(fileItem => { |
|
|
|
fileItem.merging = false |
|
|
|
fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` |
|
|
|
@ -501,8 +564,14 @@ export default { |
|
|
|
this.resetUploadState() |
|
|
|
} |
|
|
|
}, |
|
|
|
handleClearData() { |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭弹框时清空文件列表 |
|
|
|
*/ |
|
|
|
handleCloseDialog() { |
|
|
|
this.uploadMinioVisible = false |
|
|
|
this.fileList = [] |
|
|
|
this.resetUploadState() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -511,58 +580,41 @@ export default { |
|
|
|
<style scoped> |
|
|
|
.upload-minio { |
|
|
|
position: relative; |
|
|
|
/* max-width: 800px; */ |
|
|
|
margin-top: 20px; |
|
|
|
/* background-color: red; */ |
|
|
|
.el-button{ |
|
|
|
cursor: pointer; |
|
|
|
} |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
.ca-checking { |
|
|
|
color: #1890ff; |
|
|
|
font-size: 14px; |
|
|
|
margin: 5px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.ca-invalid { |
|
|
|
color: #ff4444; |
|
|
|
font-size: 14px; |
|
|
|
margin: 5px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.recheck-btn { |
|
|
|
margin: 5px 0 20px; |
|
|
|
padding: 5px 10px; |
|
|
|
cursor: pointer; |
|
|
|
background: #42b983; |
|
|
|
color: #fff; |
|
|
|
border: none; |
|
|
|
border-radius: 4px; |
|
|
|
} |
|
|
|
|
|
|
|
.recheck-btn:disabled { |
|
|
|
background: #ccc; |
|
|
|
cursor: not-allowed; |
|
|
|
/* 弹框内空提示 */ |
|
|
|
.empty-tip { |
|
|
|
text-align: center; |
|
|
|
color: #999; |
|
|
|
padding: 20px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.file-item { |
|
|
|
padding: 15px; |
|
|
|
width: 100%; |
|
|
|
border: 1px dashed #409eff; |
|
|
|
padding: 10px; |
|
|
|
border-radius: 4px; |
|
|
|
margin-bottom: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.file-name { |
|
|
|
font-weight: 500; |
|
|
|
margin-bottom: 10px; |
|
|
|
color: #333; |
|
|
|
margin-bottom: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.progress-wrapper { |
|
|
|
@ -572,7 +624,6 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
.progress-bar { |
|
|
|
width: 100%; |
|
|
|
height: 10px; |
|
|
|
background-color: #42b983; |
|
|
|
transition: width 0.3s ease; |
|
|
|
@ -585,20 +636,33 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
.merge-loading { |
|
|
|
margin: 10px 0; |
|
|
|
color: #1890ff; |
|
|
|
font-size: 14px; |
|
|
|
} |
|
|
|
|
|
|
|
.success { |
|
|
|
color: #00C851; |
|
|
|
margin: 10px 0 0 0; |
|
|
|
font-size: 14px; |
|
|
|
margin: 5px 0 0 0; |
|
|
|
} |
|
|
|
|
|
|
|
.error { |
|
|
|
color: #ff4444; |
|
|
|
margin: 10px 0 0 0; |
|
|
|
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> |