8 changed files with 1316 additions and 274 deletions
-
20src/api/system/sql.js
-
57src/utils/upload.js
-
2src/views/collectReorganizi/batchConnection/module/detail.vue
-
38src/views/collectReorganizi/batchConnection/module/form.vue
-
276src/views/components/category/PreviewForm.vue
-
203src/views/components/category/preUpload.vue
-
808src/views/components/category/preUpload2.vue
-
186src/views/system/database/index.vue
@ -0,0 +1,808 @@ |
|||||
|
<template> |
||||
|
<div class="upload-minio"> |
||||
|
<!-- 文件选择按钮与隐藏的input --> |
||||
|
<el-button |
||||
|
v-if="isPreFile !== 'true'" |
||||
|
type="primary" |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
icon="el-icon-upload" |
||||
|
@click="uploadMinioVisible=true" |
||||
|
> |
||||
|
选择文件 |
||||
|
</el-button> |
||||
|
<!--<input |
||||
|
type="file" |
||||
|
multiple |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
:accept="fileAcceptType" |
||||
|
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" |
||||
|
> |
||||
|
<div class="uploader-drop" style="margin-bottom: 20px;"> |
||||
|
<div class="uploader-btn" @click="triggerFileInput"> |
||||
|
<p>{{ isBatchMount !== 'true' ? '点击上传(可多文件上传)' : "点击上传ZIP包(可多文件上传)" }}</p> |
||||
|
<div style="margin: 20px 0 0 0;"> |
||||
|
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
||||
|
<input |
||||
|
ref="fileInput" |
||||
|
type="file" |
||||
|
multiple |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
:accept="fileAcceptType" |
||||
|
class="file-input" |
||||
|
style="display: none;" |
||||
|
@change="handleFileSelect" |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- <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" style="max-height: 400px; overflow: hidden; overflow-y: scroll;"> |
||||
|
<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> |
||||
|
<i class="iconfont icon-shanchu" @click="handleDeleteFile(index)" /> |
||||
|
</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> |
||||
|
</div> |
||||
|
<div slot="footer" class="dialog-footer" style="margin-top: 0;"> |
||||
|
<el-button type="text" @click="handleCloseDialog">取消</el-button> |
||||
|
<el-button :disabled="fileList.some(item => item.uploading || item.merging) || fileList.length === 0" :loading="btnLoading" type="primary" @click="handleUploadConfirm">保存</el-button> |
||||
|
</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: '' |
||||
|
}, |
||||
|
isPreFile: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
btnLoading: false, |
||||
|
uploadMinioVisible: false, // 弹框显示状态 |
||||
|
fileList: [], // 多文件上传列表 |
||||
|
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
||||
|
totalMergeStartTime: null, // 整体批量合并开始时间 |
||||
|
totalMergeEndTime: null, // 整体批量合并结束时间 |
||||
|
allChunksUploaded: false, // 所有文件分片是否上传完成 |
||||
|
isCaValid: false, // CA证书是否有效 |
||||
|
isCheckingCa: false, // 是否正在校验CA状态 |
||||
|
baseApi: process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 计算属性:动态生成文件类型限制 |
||||
|
computed: { |
||||
|
fileAcceptType() { |
||||
|
return this.isBatchMount === 'true' ? '.zip' : '' |
||||
|
}, |
||||
|
// 新增计算属性:动态获取分片接口路径 |
||||
|
chunkApiPath() { |
||||
|
return this.isBatchMount === 'true' ? '/api/collect/chunk' : '/api/minioUpload/chunk' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
mounted() { |
||||
|
this.getCheckCaValidity() // 初始化时自动校验CA证书 |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
triggerFileInput() { |
||||
|
this.$refs.fileInput.click() |
||||
|
}, |
||||
|
handleDeleteFile(index) { |
||||
|
// 校验是否正在上传/合并,避免删除操作冲突 |
||||
|
const fileItem = this.fileList[index] |
||||
|
if (fileItem.uploading || fileItem.merging) { |
||||
|
this.showMessage('当前文件正在上传/合并中,无法删除', 'warning') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 删除对应索引的文件项 |
||||
|
this.fileList.splice(index, 1) |
||||
|
this.showMessage('文件已移除', 'success') |
||||
|
}, |
||||
|
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 |
||||
|
this.btnLoading = 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) { |
||||
|
// 修改点:使用计算属性chunkApiPath |
||||
|
const response = await axios.get(`${this.baseApi}${this.chunkApiPath}`, { |
||||
|
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) |
||||
|
// 修改点:使用计算属性chunkApiPath |
||||
|
const response = await axios.post(`${this.baseApi}${this.chunkApiPath}`, 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 |
||||
|
} |
||||
|
|
||||
|
this.btnLoading = true |
||||
|
|
||||
|
// 1. 串行上传所有文件的分片 |
||||
|
this.allChunksUploaded = true |
||||
|
for (const fileItem of pendingFiles) { |
||||
|
await this.uploadFileChunks(fileItem) |
||||
|
// 如果单个文件上传失败,标记整体上传状态为失败 |
||||
|
if (fileItem.errorMsg) { |
||||
|
this.allChunksUploaded = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. 分片上传完成后执行合并 |
||||
|
if (!this.allChunksUploaded) { |
||||
|
this.btnLoading = false |
||||
|
this.showMessage('部分文件分片上传失败,无法执行合并', 'error') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) |
||||
|
if (validFiles.length === 0) { |
||||
|
this.btnLoading = false |
||||
|
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 { |
||||
|
// 处理批量挂载场景(单个对象传参) |
||||
|
if (this.isBatchMount === 'true') { |
||||
|
// 只处理第一个有效文件(因为接口只接收单个对象) |
||||
|
const fileItem = validFiles[0] |
||||
|
const file = fileItem.file |
||||
|
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
||||
|
const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) |
||||
|
|
||||
|
if (!chunksExist) { |
||||
|
throw new Error(`【${file.name}】部分分片未上传完成,无法合并`) |
||||
|
} |
||||
|
|
||||
|
// 构建单个对象参数(仅三个字段) |
||||
|
const mergeParams = { |
||||
|
identifier: fileItem.md5, |
||||
|
totalChunks: totalChunks, |
||||
|
filename: file.name |
||||
|
} |
||||
|
|
||||
|
// 调用合并接口(传递单个对象而非数组) |
||||
|
const mergeApiUrl = `${this.baseApi}/api/collect/mergeZip` |
||||
|
const response = await axios.post(mergeApiUrl, mergeParams, { |
||||
|
headers: { |
||||
|
'Authorization': getToken(), |
||||
|
'Content-Type': 'application/json' |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
this.handleMergeSuccess(response, validFiles, nowDate) |
||||
|
} else { |
||||
|
// 原有逻辑:多文件数组传参 |
||||
|
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) |
||||
|
|
||||
|
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.documentId = this.selectedDocument.id |
||||
|
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 mergeApiUrl = `${this.baseApi}/api/minioUpload/merge` |
||||
|
const response = await axios.post(mergeApiUrl, jsonArray, { |
||||
|
headers: { |
||||
|
'Authorization': getToken(), |
||||
|
'Content-Type': 'application/json' |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
this.handleMergeSuccess(response, validFiles, nowDate, jsonArray) |
||||
|
} |
||||
|
} catch (err) { |
||||
|
this.btnLoading = false |
||||
|
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() |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 处理合并成功的通用逻辑 |
||||
|
*/ |
||||
|
handleMergeSuccess(response, validFiles, nowDate, jsonArray = null) { |
||||
|
this.totalMergeEndTime = new Date().getTime() |
||||
|
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
||||
|
|
||||
|
if (response.data.code === 200) { |
||||
|
this.btnLoading = false |
||||
|
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}`) |
||||
|
|
||||
|
// 触发成功回调 |
||||
|
const fileNames = validFiles.map(f => f.file.name) |
||||
|
this.$emit('onUploadSuccess', response.data.data, fileNames, jsonArray || response.data.data) |
||||
|
|
||||
|
this.uploadMinioVisible = false |
||||
|
this.fileList = [] // 清空列表,避免下次打开弹框显示旧数据 |
||||
|
} else { |
||||
|
this.btnLoading = false |
||||
|
throw new Error(response.data.msg || '合并失败') |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 关闭弹框时清空文件列表 |
||||
|
*/ |
||||
|
handleCloseDialog() { |
||||
|
this.uploadMinioVisible = false |
||||
|
this.fileList = [] |
||||
|
this.resetUploadState() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.upload-minio { |
||||
|
position: relative; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.uploader-drop{ |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
text-align: center; |
||||
|
height: 120px; |
||||
|
border: none; |
||||
|
background-color: #f5f5f5; |
||||
|
.uploader-btn{ |
||||
|
border: none; |
||||
|
i{ |
||||
|
font-size: 32px; |
||||
|
color: #1F55EB; |
||||
|
} |
||||
|
&:hover{ |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
.el-upload__tip{ |
||||
|
font-size: 12px; |
||||
|
color: #A6ADB6; |
||||
|
} |
||||
|
} |
||||
|
.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 { |
||||
|
position: relative; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
.icon-shanchu{ |
||||
|
position: absolute; |
||||
|
right: -7px; |
||||
|
top: 0; |
||||
|
font-size: 20px; |
||||
|
color: #1F55EB; |
||||
|
cursor: pointer; |
||||
|
&:hover { |
||||
|
color: #ff4444; |
||||
|
transform: scale(1.1); |
||||
|
transition: all 0.2s ease; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue