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
-
26src/views/collectReorganizi/batchConnection/module/form.vue
-
2src/views/components/category/PreviewForm.vue
-
73src/views/components/category/preUpload.vue
-
808src/views/components/category/preUpload2.vue
-
150src/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