8 changed files with 2760 additions and 16 deletions
-
10src/views/collectReorganizi/collectionLibrary/module/collectHeader.vue
-
898src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload-20260129.vue
-
910src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload-bottom-1.vue
-
910src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload-bottom.vue
-
12src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/bigUpload.vue
-
10src/views/components/category/PreviewForm.vue
-
23src/views/prearchiveLibrary/index.vue
-
1src/views/prearchiveLibrary/treeList.vue
@ -0,0 +1,898 @@ |
|||
<template> |
|||
<div class="upload-minio"> |
|||
<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" |
|||
> |
|||
<template #title> |
|||
{{ uploadTitle }} |
|||
<span style="color: red;font-size: 12px; ">单个文件不可超过10GB</span> |
|||
</template> |
|||
|
|||
<div class="uploader-drop" style="margin-bottom: 20px;"> |
|||
<div class="uploader-btn" @click="triggerFileInput"> |
|||
<p>{{ !isCatalogUpload ? '点击上传(可多文件上传)' : "点击上传ZIP包(只可单文件上传)" }}</p> |
|||
<div style="margin: 20px 0 20px 0;"> |
|||
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
|||
<input |
|||
ref="fileInput" |
|||
type="file" |
|||
:accept="isCatalogUpload ? '.zip' : ''" |
|||
:multiple="!isCatalogUpload" |
|||
:disabled="!isCaValid || isCheckingCa" |
|||
class="file-input" |
|||
style="display: none;" |
|||
@change="handleFileSelect" |
|||
> |
|||
</div> |
|||
</div> |
|||
<div class="el-upload__tip">上传限制文件大小:最大10GB/个</div> |
|||
</div> |
|||
|
|||
<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> |
|||
|
|||
<!-- 判断是否有重复上传的文件 --> |
|||
<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 { FetchCheckCaValidity } from '@/api/system/auth2' |
|||
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' |
|||
|
|||
export default { |
|||
name: 'MinioMultiChunkUpload', |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
uploadMinioVisible: false, // 弹框显示状态 |
|||
fileList: [], // 多文件上传列表 |
|||
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|||
totalMergeStartTime: null, // 整体批量合并开始时间 |
|||
totalMergeEndTime: null, // 整体批量合并结束时间 |
|||
allChunksUploaded: false, // 所有文件分片是否上传完成 |
|||
isCaValid: false, // CA证书是否有效 |
|||
isCheckingCa: false, // 是否正在校验CA状态 |
|||
|
|||
// 新增目录上传相关 |
|||
isCatalogUpload: false, |
|||
uploadTitle: '文件上传', |
|||
btnLoading: false, |
|||
|
|||
// 重复文件相关 |
|||
repeatFileVisible: false, |
|||
repeatFileData: [], |
|||
originFileData: [], |
|||
tempSelectedFiles: null, // 临时存储选择的文件 |
|||
|
|||
// 目录上传报错相关 |
|||
resultCatalog: { |
|||
mountFile: {}, |
|||
failArchives: [] |
|||
}, |
|||
catalogErrorVisible: false, |
|||
catalogInfoData: [] |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters([ |
|||
'baseApi' |
|||
]) |
|||
}, |
|||
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') |
|||
}, |
|||
// 切换上传类型(普通文件/目录ZIP包) |
|||
updateUploadOptions(uploadType) { |
|||
this.isCatalogUpload = uploadType !== 1 |
|||
this.uploadTitle = this.isCatalogUpload ? '原文目录上传' : '文件上传' |
|||
this.fileList = [] |
|||
this.uploadMinioVisible = true |
|||
}, |
|||
|
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0.00MB' |
|||
const mb = bytes / (1024 * 1024) |
|||
return mb.toFixed(2) + 'MB' |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:格式化时间戳为易读的本地时间(带毫秒) |
|||
*/ |
|||
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' |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:计算两个时间戳的差值并格式化 |
|||
*/ |
|||
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` |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:获取图片分辨率 |
|||
*/ |
|||
getImgPx(base64) { |
|||
return new Promise((resolve) => { |
|||
const img = new Image() |
|||
img.onload = () => { |
|||
resolve({ width: img.width, height: img.height }) |
|||
} |
|||
img.src = base64 |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:将文件转为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) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 工具方法:显示提示信息 |
|||
*/ |
|||
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 |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 获取已存在的文件列表(用于重复文件检测) |
|||
*/ |
|||
async getFileList() { |
|||
const params = { |
|||
'categoryId': this.selectedCategory.id, |
|||
'archivesId': this.arcId |
|||
} |
|||
const res = await FetchInitFileCategoryView(params) |
|||
this.originFileData = res.returnlist || [] |
|||
}, |
|||
|
|||
/** |
|||
* 多文件选择处理函数:选择后打开弹框,检测重复文件 |
|||
*/ |
|||
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 |
|||
|
|||
// 3. 重复文件检测(原有逻辑不变) |
|||
if (!this.isCatalogUpload) { |
|||
await this.getFileList() |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
this.repeatFileData = selectedFiles.filter(file => existingFileNames.includes(file.name)) |
|||
|
|||
if (this.repeatFileData.length > 0) { |
|||
this.repeatFileVisible = true |
|||
this.tempSelectedFiles = selectedFiles |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
} |
|||
|
|||
// 4. 初始化文件列表(原有逻辑不变) |
|||
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] |
|||
e.target.value = '' |
|||
|
|||
// this.handleUploadConfirm() |
|||
}, |
|||
|
|||
/** |
|||
* 处理重复文件选择 |
|||
* @param {number} type 0-直接上传 1-去重后上传 |
|||
*/ |
|||
handleRepeatFile(type) { |
|||
this.repeatFileVisible = false |
|||
|
|||
if (!this.tempSelectedFiles) return |
|||
|
|||
let filesToUpload = [] |
|||
|
|||
if (type === 0) { |
|||
// 直接上传所有文件 |
|||
filesToUpload = this.tempSelectedFiles |
|||
} else { |
|||
// 去重后上传 |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
filesToUpload = this.tempSelectedFiles.filter(file => !existingFileNames.includes(file.name)) |
|||
|
|||
if (filesToUpload.length === 0) { |
|||
this.showMessage('当前所选文件去重后无可上传的文件', 'error') |
|||
this.$emit('close-dialog') |
|||
return |
|||
} |
|||
} |
|||
|
|||
// 初始化去重后的文件列表 |
|||
const newFileList = filesToUpload.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 = newFileList |
|||
this.uploadMinioVisible = true |
|||
this.tempSelectedFiles = null |
|||
|
|||
// this.handleUploadConfirm() |
|||
}, |
|||
|
|||
/** |
|||
* 计算文件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() |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 检查分片是否已存在 |
|||
*/ |
|||
async checkChunkExists(fileMd5, chunkIndex) { |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.get(`${linkSrc}/api/collect/chunk`, { |
|||
params: { fileMd5, chunkIndex }, |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error('检查分片失败: ' + response.data.msg) |
|||
} |
|||
|
|||
return response.data.data |
|||
}, |
|||
|
|||
/** |
|||
* 上传单个分片 |
|||
*/ |
|||
async uploadSingleChunk(file, fileMd5, chunkIndex) { |
|||
const start = chunkIndex * this.CHUNK_SIZE |
|||
const end = Math.min(start + this.CHUNK_SIZE, file.size) |
|||
const chunkBlob = file.slice(start, end) |
|||
|
|||
const formData = new FormData() |
|||
formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) |
|||
formData.append('fileMd5', fileMd5) |
|||
formData.append('chunkIndex', chunkIndex) |
|||
|
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/chunk`, formData, { |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data', |
|||
'Authorization': getToken() |
|||
} |
|||
}) |
|||
|
|||
if (response.data.code !== 200) { |
|||
throw new Error(`分片${chunkIndex}上传失败: ` + response.data.msg) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 校验所有分片是否存在 |
|||
*/ |
|||
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 |
|||
}, |
|||
|
|||
/** |
|||
* 单个文件的分片上传流程 |
|||
*/ |
|||
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 |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 批量上传并合并:整合目录上传逻辑 |
|||
*/ |
|||
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 { |
|||
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) |
|||
|
|||
// 校验分片完整性 |
|||
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.categoryId = this.selectedCategory.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 linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/merge`, jsonArray, { |
|||
headers: { |
|||
'Authorization': getToken(), |
|||
'Content-Type': 'application/json' |
|||
} |
|||
}) |
|||
|
|||
this.totalMergeEndTime = new Date().getTime() |
|||
const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) |
|||
|
|||
if (response.data.code === 200) { |
|||
// 处理目录上传逻辑 |
|||
if (this.isCatalogUpload && response.data.data.length === 1 && response.data.data[0] !== '') { |
|||
// 调用目录上传接口 |
|||
catalogUpload(`${linkSrc}/api/collect/catalogUpload`, |
|||
response.data.data[0], |
|||
this.selectedCategory.fondsId |
|||
).then(res => { |
|||
this.btnLoading = false |
|||
if (res.data.code === 200) { |
|||
this.resultCatalog = res.data.data |
|||
if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.successNum) { |
|||
this.showMessage('目录上传操作成功', 'success') |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.$emit('close-dialog') |
|||
} else { |
|||
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()) |
|||
const match = field.match(/^([\w-·]+)(.*)$/) |
|||
let archives = '' |
|||
let description = '' |
|||
if (match) { |
|||
archives = match[1] |
|||
description = match[2] |
|||
} |
|||
this.catalogInfoData.push({ |
|||
archives, |
|||
description, |
|||
children: fileArray |
|||
}) |
|||
} |
|||
}) |
|||
this.catalogErrorVisible = true |
|||
this.$emit('close-dialog') |
|||
} |
|||
} else { |
|||
this.showMessage(`目录上传失败: ${res.data.message}`, 'error') |
|||
} |
|||
}) |
|||
} else { |
|||
// 普通文件上传成功 |
|||
this.showMessage('所有文件上传并合并成功', 'success') |
|||
validFiles.forEach((fileItem) => { |
|||
fileItem.mergeEndTime = new Date().getTime() |
|||
const mergeDuration = this.getTimeDiff(fileItem.mergeStartTime, fileItem.mergeEndTime) |
|||
console.log(`【文件${fileItem.file.name}】合并结束时间:${this.formatTime(fileItem.mergeEndTime)},合并耗时:${mergeDuration}`) |
|||
fileItem.successMsg = '上传成功!' |
|||
fileItem.merging = false |
|||
}) |
|||
console.log(`【整体合并】所有文件合并完成,整体合并耗时:${totalMergeDuration}`) |
|||
|
|||
this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArray) |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.$emit('close-dialog') |
|||
} |
|||
} else { |
|||
throw new Error(response.data.msg || '合并失败') |
|||
} |
|||
} 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() |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 关闭弹框时清空文件列表 |
|||
*/ |
|||
handleCloseDialog() { |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.resetUploadState() |
|||
this.repeatFileData = [] |
|||
this.tempSelectedFiles = null |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.upload-minio { |
|||
position: relative; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.uploader-drop{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
text-align: center; |
|||
height: 180px; |
|||
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; |
|||
} |
|||
|
|||
.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; |
|||
} |
|||
|
|||
/* 重复文件弹窗样式 */ |
|||
.collectUpload-dialog .file-list { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
/* 目录上传失败弹窗样式 */ |
|||
.catalog-dialog .archives-table { |
|||
--el-table-header-text-color: #666; |
|||
--el-table-row-hover-bg-color: #f5f7fa; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,910 @@ |
|||
<template> |
|||
<div class="upload-minio"> |
|||
<!-- 上传窗口:默认居中,最小化后固定底部右侧 --> |
|||
<div |
|||
v-if="uploadMinioVisible" |
|||
class="upload-container" |
|||
:class="{ 'minimized': isMinimized }" |
|||
> |
|||
<!-- 窗口头部:居中显示标题+操作按钮,最小化后显示上传状态 --> |
|||
<div class="upload-header"> |
|||
<!-- 最大化状态头部 --> |
|||
<div v-if="!isMinimized" class="header-normal"> |
|||
<div class="title"> |
|||
{{ uploadTitle }} |
|||
<span style="color: red;font-size: 12px; margin-left: 8px;">单个文件不可超过10GB</span> |
|||
</div> |
|||
<div class="header-controls"> |
|||
<el-button type="text" size="small" @click="handleMinimize">最小化</el-button> |
|||
<el-button type="text" size="small" @click="handleClose">关闭</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 最小化状态头部(底部右侧显示) --> |
|||
<div v-if="isMinimized" class="header-mini"> |
|||
<!-- 核心修改:显示maintitle+archive_no 上传中 --> |
|||
<span class="uploading-tip">{{ uploadTipText }}上传中</span> |
|||
<div class="mini-progress"> |
|||
<div class="progress-bar" :style="{ width: totalProgress + '%' }" /> |
|||
</div> |
|||
<div class="mini-controls"> |
|||
<el-button type="text" size="mini" @click="handleRestore">恢复</el-button> |
|||
<el-button type="text" size="mini" @click="handleCancelUpload">取消</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 最大化状态:原上传内容(居中完整显示) --> |
|||
<div v-if="!isMinimized" class="upload-content"> |
|||
<div class="uploader-drop" style="margin-bottom: 20px;"> |
|||
<div class="uploader-btn" @click="triggerFileInput"> |
|||
<p>{{ !isCatalogUpload ? '点击上传(可多文件上传)' : "点击上传ZIP包(只可单文件上传)" }}</p> |
|||
<div style="margin: 20px 0 20px 0;"> |
|||
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
|||
<input |
|||
ref="fileInput" |
|||
type="file" |
|||
:accept="isCatalogUpload ? '.zip' : ''" |
|||
:multiple="!isCatalogUpload" |
|||
:disabled="!isCaValid || isCheckingCa || isUploading" |
|||
class="file-input" |
|||
style="display: none;" |
|||
@change="handleFileSelect" |
|||
> |
|||
</div> |
|||
</div> |
|||
<div class="el-upload__tip">上传限制文件大小:最大10GB/个</div> |
|||
</div> |
|||
|
|||
<div v-if="fileList.length !== 0" style="max-height: 300px; 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" |
|||
:disabled="fileItem.uploading || fileItem.merging" |
|||
@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 class="dialog-footer" style="margin-top: 20px; text-align: right;"> |
|||
<el-button type="text" @click="handleClose">取消</el-button> |
|||
<el-button |
|||
:disabled="fileList.some(item => item.uploading || item.merging) || fileList.length === 0 || isUploading" |
|||
:loading="btnLoading" |
|||
type="primary" |
|||
@click="handleUploadConfirm" |
|||
> |
|||
保存 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 重复文件弹窗(保留原逻辑) --> |
|||
<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 { FetchCheckCaValidity } from '@/api/system/auth2' |
|||
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' |
|||
|
|||
export default { |
|||
name: 'MinioMultiChunkUpload', |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 新增:接收父组件的archiveInfo对象 |
|||
archiveInfo: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(['baseApi']), |
|||
// 总上传进度(所有文件平均)- 保留 |
|||
totalProgress() { |
|||
if (this.fileList.length === 0) return 0 |
|||
const total = this.fileList.reduce((sum, item) => sum + item.progress, 0) |
|||
return Math.round(total / this.fileList.length) |
|||
}, |
|||
// 是否正在上传/合并 - 保留 |
|||
isUploading() { |
|||
return this.fileList.some(item => item.uploading || item.merging) |
|||
}, |
|||
// 核心修改:上传提示文本,优先显示maintitle+archive_no |
|||
uploadTipText() { |
|||
// 优先使用archiveInfo中的maintitle和archive_no |
|||
const { maintitle, archive_no } = this.archiveInfo |
|||
if (maintitle && archive_no) { |
|||
return `${maintitle}${archive_no} ` |
|||
} |
|||
// 兜底:原逻辑(普通/目录上传) |
|||
return this.isCatalogUpload ? '原文目录' : '档案电子原文' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
uploadMinioVisible: false, // 上传窗显示状态 |
|||
fileList: [], // 多文件上传列表 |
|||
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|||
totalMergeStartTime: null, |
|||
totalMergeEndTime: null, |
|||
allChunksUploaded: false, |
|||
isCaValid: false, // CA证书是否有效 |
|||
isCheckingCa: false, // 是否正在校验CA状态 |
|||
|
|||
isCatalogUpload: false, // 是否目录上传 |
|||
uploadTitle: '文件上传', |
|||
btnLoading: false, |
|||
|
|||
// 重复文件相关 |
|||
repeatFileVisible: false, |
|||
repeatFileData: [], |
|||
originFileData: [], |
|||
tempSelectedFiles: null, |
|||
|
|||
// 目录上传报错相关 |
|||
resultCatalog: { mountFile: {}, failArchives: [] }, |
|||
catalogErrorVisible: false, |
|||
catalogInfoData: [], |
|||
|
|||
// 窗口核心状态 |
|||
isMinimized: false // 是否最小化到底部右侧 |
|||
} |
|||
}, |
|||
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') |
|||
}, |
|||
// 切换上传类型(普通/目录)- 父组件调用 |
|||
updateUploadOptions(uploadType) { |
|||
this.isCatalogUpload = uploadType !== 1 |
|||
this.uploadTitle = this.isCatalogUpload ? '原文目录上传' : '文件上传' |
|||
this.fileList = [] |
|||
this.isMinimized = false |
|||
this.uploadMinioVisible = true |
|||
}, |
|||
// 格式化文件大小为MB |
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0.00MB' |
|||
const mb = bytes / (1024 * 1024) |
|||
return mb.toFixed(2) + 'MB' |
|||
}, |
|||
|
|||
// 工具方法:提示框/时间格式化/Base64/图片分辨率(保留原逻辑) |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 80 }) // 提示框上移,避免遮挡底部小窗 |
|||
}, |
|||
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' |
|||
}) |
|||
}, |
|||
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` |
|||
}, |
|||
getImgPx(base64) { |
|||
return new Promise((resolve) => { |
|||
const img = new Image() |
|||
img.onload = () => resolve({ width: img.width, height: img.height }) |
|||
img.src = 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) |
|||
}) |
|||
}, |
|||
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 |
|||
} |
|||
}, |
|||
// 获取已存在文件列表(重复检测,保留原逻辑) |
|||
async getFileList() { |
|||
const params = { categoryId: this.selectedCategory.id, archivesId: this.arcId } |
|||
const res = await FetchInitFileCategoryView(params) |
|||
this.originFileData = res.returnlist || [] |
|||
}, |
|||
|
|||
// 选择文件处理(保留原逻辑,不自动上传) |
|||
async handleFileSelect(e) { |
|||
if (this.isCheckingCa) { |
|||
this.showMessage('正在校验CA证书,请稍候...', 'warning') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
if (!this.isCaValid) { |
|||
this.showMessage('CA证书不在有效期内,无法上传文件', 'error') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
const selectedFiles = Array.from(e.target.files) |
|||
if (selectedFiles.length === 0) return |
|||
|
|||
// 重复文件检测 |
|||
if (!this.isCatalogUpload) { |
|||
await this.getFileList() |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
this.repeatFileData = selectedFiles.filter(file => existingFileNames.includes(file.name)) |
|||
if (this.repeatFileData.length > 0) { |
|||
this.repeatFileVisible = true |
|||
this.tempSelectedFiles = selectedFiles |
|||
e.target.value = '' |
|||
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] |
|||
e.target.value = '' |
|||
}, |
|||
// 处理重复文件选择(保留原逻辑,不自动上传) |
|||
handleRepeatFile(type) { |
|||
this.repeatFileVisible = false |
|||
if (!this.tempSelectedFiles) return |
|||
let filesToUpload = [] |
|||
if (type === 0) { |
|||
filesToUpload = this.tempSelectedFiles |
|||
} else { |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
filesToUpload = this.tempSelectedFiles.filter(file => !existingFileNames.includes(file.name)) |
|||
if (filesToUpload.length === 0) { |
|||
this.showMessage('当前所选文件去重后无可上传的文件', 'error') |
|||
this.handleClose() |
|||
return |
|||
} |
|||
} |
|||
const newFileList = filesToUpload.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 = newFileList |
|||
this.tempSelectedFiles = null |
|||
}, |
|||
|
|||
// 计算文件MD5(保留原逻辑) |
|||
calculateFileMd5(file, fileItem) { |
|||
return new Promise((resolve, reject) => { |
|||
fileItem.md5StartTime = new Date().getTime() |
|||
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 |
|||
offset < file.size ? loadNextChunk() : resolve(spark.end()) |
|||
} |
|||
fileReader.onerror = (err) => reject(err) |
|||
loadNextChunk() |
|||
}) |
|||
}, |
|||
// 检查分片是否存在(保留原逻辑) |
|||
async checkChunkExists(fileMd5, chunkIndex) { |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.get(`${linkSrc}/api/collect/chunk`, { |
|||
params: { fileMd5, chunkIndex }, |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
if (response.data.code !== 200) throw new Error('检查分片失败: ' + response.data.msg) |
|||
return response.data.data |
|||
}, |
|||
// 上传单个分片(保留原逻辑) |
|||
async uploadSingleChunk(file, fileMd5, chunkIndex) { |
|||
const start = chunkIndex * this.CHUNK_SIZE |
|||
const end = Math.min(start + this.CHUNK_SIZE, file.size) |
|||
const chunkBlob = file.slice(start, end) |
|||
const formData = new FormData() |
|||
formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) |
|||
formData.append('fileMd5', fileMd5) |
|||
formData.append('chunkIndex', chunkIndex) |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/chunk`, formData, { |
|||
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': getToken() } |
|||
}) |
|||
if (response.data.code !== 200) throw new Error(`分片${chunkIndex}上传失败: ` + response.data.msg) |
|||
}, |
|||
// 校验所有分片是否存在(保留原逻辑) |
|||
async checkAllChunksExist(fileMd5, totalChunks) { |
|||
for (let i = 0; i < totalChunks; i++) { |
|||
const result = await this.checkChunkExists(fileMd5, i) |
|||
if (!result.exists) return false |
|||
} |
|||
return true |
|||
}, |
|||
|
|||
// 单个文件分片上传(保留原逻辑) |
|||
async uploadFileChunks(fileItem) { |
|||
const file = fileItem.file |
|||
fileItem.uploading = true |
|||
fileItem.progress = 0 |
|||
fileItem.errorMsg = '' |
|||
fileItem.successMsg = '' |
|||
try { |
|||
const fileMd5 = await this.calculateFileMd5(file, fileItem) |
|||
fileItem.md5 = fileMd5 |
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
fileItem.chunkUploadStartTime = new Date().getTime() |
|||
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() |
|||
fileItem.progress = 100 |
|||
} catch (err) { |
|||
if (fileItem.chunkUploadStartTime && !fileItem.chunkUploadEndTime) { |
|||
fileItem.chunkUploadEndTime = new Date().getTime() |
|||
} |
|||
fileItem.errorMsg = '分片上传失败: ' + (err.message || '未知错误') |
|||
this.allChunksUploaded = false |
|||
console.error(`【${file.name}】分片上传异常:`, err) |
|||
} finally { |
|||
fileItem.uploading = false |
|||
} |
|||
}, |
|||
|
|||
// 点击保存:开始上传(核心保留手动保存逻辑) |
|||
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 |
|||
// 提示用户可最小化窗口查看后台进度 |
|||
if (!this.isMinimized) { |
|||
this.showMessage('文件开始上传,您可点击最小化按钮在底部右侧查看进度', 'info') |
|||
} |
|||
|
|||
// 串行上传所有文件分片 |
|||
this.allChunksUploaded = true |
|||
for (const fileItem of pendingFiles) { |
|||
await this.uploadFileChunks(fileItem) |
|||
if (fileItem.errorMsg) this.allChunksUploaded = false |
|||
} |
|||
|
|||
// 分片上传失败处理:恢复最大化显示错误 |
|||
if (!this.allChunksUploaded) { |
|||
this.btnLoading = false |
|||
this.showMessage('部分文件分片上传失败,无法执行合并', 'error') |
|||
this.isMinimized = false |
|||
return |
|||
} |
|||
|
|||
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) |
|||
if (validFiles.length === 0) { |
|||
this.btnLoading = false |
|||
this.showMessage('无有效分片上传完成的文件!', 'error') |
|||
this.isMinimized = false |
|||
return |
|||
} |
|||
|
|||
// 执行文件合并 |
|||
this.totalMergeStartTime = new Date().getTime() |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = true |
|||
fileItem.mergeStartTime = new Date().getTime() |
|||
}) |
|||
|
|||
try { |
|||
const processFiles = validFiles.map(async(fileItem) => { |
|||
const file = fileItem.file |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
// 图片分辨率处理 |
|||
if (file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
jsonString.file_dpi = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
jsonString.file_dpi = '' |
|||
} |
|||
// 构建文件信息 |
|||
jsonString.file_name = file.name |
|||
jsonString.file_size = file.size |
|||
jsonString.file_type = file.name.split('.').pop() || '' |
|||
jsonString.last_modified = file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.create_time = getCurrentTime() |
|||
jsonString.id = null |
|||
jsonString.file_thumbnail = '' |
|||
jsonArray.push(jsonString) |
|||
// 校验分片完整性 |
|||
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.categoryId = this.selectedCategory.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 linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/merge`, jsonArray, { |
|||
headers: { 'Authorization': getToken(), 'Content-Type': 'application/json' } |
|||
}) |
|||
|
|||
this.totalMergeEndTime = new Date().getTime() |
|||
if (response.data.code === 200) { |
|||
// 目录上传逻辑(保留原逻辑) |
|||
if (this.isCatalogUpload && response.data.data.length === 1 && response.data.data[0] !== '') { |
|||
catalogUpload(`${linkSrc}/api/collect/catalogUpload`, |
|||
response.data.data[0], |
|||
this.selectedCategory.fondsId |
|||
).then(res => { |
|||
this.btnLoading = false |
|||
if (res.data.code === 200) { |
|||
this.resultCatalog = res.data.data |
|||
if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.successNum) { |
|||
this.uploadSuccessHandle() // 目录上传成功:提示+关窗 |
|||
} else { |
|||
this.catalogInfoData = [] |
|||
this.resultCatalog.failArchives.forEach(item => { |
|||
const parts = item.split(':') |
|||
if (parts.length === 2) { |
|||
const field = parts[0] |
|||
const fileStr = parts[1].replace(/\[|\]/g, '').trim() |
|||
const fileArray = fileStr.split(',').map(file => file.trim()) |
|||
const match = field.match(/^([\w-·]+)(.*)$/) |
|||
this.catalogInfoData.push({ |
|||
archives: match ? match[1] : field, |
|||
description: match ? match[2] : '', |
|||
children: fileArray |
|||
}) |
|||
} |
|||
}) |
|||
this.catalogErrorVisible = true |
|||
this.handleClose() |
|||
} |
|||
} else { |
|||
this.showMessage(`目录上传失败: ${res.data.message}`, 'error') |
|||
this.isMinimized = false |
|||
} |
|||
}) |
|||
} else { |
|||
// 普通文件上传成功:统一处理 |
|||
validFiles.forEach((fileItem) => { |
|||
fileItem.mergeEndTime = new Date().getTime() |
|||
fileItem.successMsg = '上传成功!' |
|||
fileItem.merging = false |
|||
}) |
|||
this.uploadSuccessHandle() |
|||
} |
|||
} else { |
|||
throw new Error(response.data.msg || '合并失败') |
|||
} |
|||
} catch (err) { |
|||
this.btnLoading = false |
|||
this.showMessage(`文件合并失败: ${err.message}`, 'error') |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = false |
|||
fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` |
|||
}) |
|||
this.isMinimized = false // 恢复最大化显示错误 |
|||
this.$emit('onUploadError', err) |
|||
} finally { |
|||
this.resetUploadState() |
|||
} |
|||
}, |
|||
|
|||
// 上传成功统一处理:提示+延迟1.5秒关窗 |
|||
uploadSuccessHandle() { |
|||
// 成功提示也适配archiveInfo |
|||
const { maintitle, archive_no } = this.archiveInfo |
|||
let successText = '' |
|||
if (maintitle && archive_no) { |
|||
successText = `${maintitle}${archive_no} 上传完成` |
|||
} else { |
|||
successText = `${this.isCatalogUpload ? '原文目录' : '档案电子原文'}上传完成` |
|||
} |
|||
this.showMessage(successText, 'success') |
|||
this.$emit('onUploadSuccess') |
|||
// 延迟关窗,让用户看到成功提示 |
|||
setTimeout(() => { |
|||
this.handleClose() |
|||
}, 1500) |
|||
}, |
|||
|
|||
// 窗口核心操作方法 |
|||
// 手动最小化:缩至底部右侧 |
|||
handleMinimize() { |
|||
this.isMinimized = true |
|||
this.showMessage('上传窗口已最小化,可在底部右侧查看进度', 'info') |
|||
}, |
|||
// 恢复最大化:回到屏幕居中 |
|||
handleRestore() { |
|||
this.isMinimized = false |
|||
}, |
|||
// 关闭窗口(上传中禁止,自动最小化) |
|||
handleClose() { |
|||
if (this.isUploading) { |
|||
this.showMessage('文件正在上传,无法直接关闭,可点击取消上传', 'warning') |
|||
this.isMinimized = true |
|||
return |
|||
} |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.isMinimized = false |
|||
this.resetUploadState() |
|||
this.$emit('close-dialog') |
|||
}, |
|||
// 取消上传:二次确认,清空文件并关窗 |
|||
handleCancelUpload() { |
|||
this.$confirm('确定要取消本次上传吗?已上传的分片将被丢弃', '提示', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.fileList = [] |
|||
this.resetUploadState() |
|||
this.uploadMinioVisible = false |
|||
this.isMinimized = false |
|||
this.showMessage('已取消上传', 'info') |
|||
this.$emit('close-dialog') |
|||
}).catch(() => {}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.upload-minio { |
|||
position: relative; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
/* 核心:上传窗口基础样式 - 最大化居中,最小化底部右侧 */ |
|||
.upload-container { |
|||
position: fixed; |
|||
z-index: 9999; |
|||
background: #fff; |
|||
border-radius: 8px; |
|||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|||
border: 1px solid #e6e6e6; |
|||
transition: all 0.3s ease; |
|||
/* 最大化状态:屏幕居中 */ |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 600px; |
|||
min-height: 400px; |
|||
} |
|||
/* 最小化状态:屏幕底部右侧 */ |
|||
.upload-container.minimized { |
|||
top: auto; |
|||
left: auto; |
|||
right: 20px; |
|||
bottom: 20px; |
|||
transform: none; |
|||
width: 420px; /* 微调宽度,适配长文本显示 */ |
|||
min-height: 60px; |
|||
height: 60px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
/* 窗口头部通用样式 */ |
|||
.upload-header { |
|||
height: 40px; |
|||
line-height: 40px; |
|||
padding: 0 16px; |
|||
background: #f5f7fa; |
|||
border-bottom: 1px solid #e6e6e6; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
/* 最大化头部:标题+最小化/关闭按钮 */ |
|||
.header-normal { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.header-normal .title { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
/* 最小化头部:上传状态+进度条+恢复/取消按钮 */ |
|||
.header-mini { |
|||
width: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
height: 100%; |
|||
} |
|||
.uploading-tip { |
|||
font-size: 13px; |
|||
color: #333; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
flex-shrink: 0; |
|||
max-width: 180px; /* 限制文本宽度,避免挤压进度条 */ |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
.mini-progress { |
|||
flex: 1; |
|||
height: 6px; |
|||
background: #e5e6eb; |
|||
border-radius: 3px; |
|||
overflow: hidden; |
|||
} |
|||
.mini-controls { |
|||
display: flex; |
|||
gap: 6px; |
|||
flex-shrink: 0; |
|||
} |
|||
.mini-controls el-button { |
|||
padding: 0 6px !important; |
|||
font-size: 12px !important; |
|||
} |
|||
|
|||
/* 窗口内容区(仅最大化显示) */ |
|||
.upload-content { |
|||
padding: 16px; |
|||
} |
|||
|
|||
/* 原有上传样式保留,微调适配 */ |
|||
.uploader-drop{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
text-align: center; |
|||
height: 180px; |
|||
border: none; |
|||
background-color: #f5f5f5; |
|||
border-radius: 4px; |
|||
.uploader-btn{ |
|||
border: none; |
|||
i{ |
|||
font-size: 32px; |
|||
color: #1F55EB; |
|||
} |
|||
&:hover{ |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
.el-upload__tip{ |
|||
font-size: 12px; |
|||
color: #A6ADB6; |
|||
margin-top: 8px; |
|||
} |
|||
} |
|||
.file-input { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
width: 100px; |
|||
height: 36px; |
|||
padding: 5px; |
|||
opacity: 0; |
|||
cursor: pointer; |
|||
z-index: 10; |
|||
} |
|||
.file-item { |
|||
width: 100%; |
|||
border: 1px dashed #409eff; |
|||
padding: 12px; |
|||
border-radius: 4px; |
|||
margin-bottom: 12px; |
|||
} |
|||
.file-name { |
|||
position: relative; |
|||
font-weight: 500; |
|||
color: #333; |
|||
margin-bottom: 8px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
.file-size { |
|||
color: #666; |
|||
font-weight: normal; |
|||
font-size: 12px; |
|||
margin-left: 8px; |
|||
} |
|||
.icon-shanchu{ |
|||
font-size: 18px; |
|||
color: #1F55EB; |
|||
cursor: pointer; |
|||
&:hover { |
|||
color: #ff4444; |
|||
transform: scale(1.1); |
|||
transition: all 0.2s ease; |
|||
} |
|||
&[disabled] { |
|||
color: #ccc; |
|||
cursor: not-allowed; |
|||
transform: none; |
|||
} |
|||
} |
|||
.progress-wrapper { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
} |
|||
.progress-bar { |
|||
height: 10px; |
|||
background-color: #42b983; |
|||
transition: width 0.3s ease; |
|||
border-radius: 5px; |
|||
} |
|||
.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; |
|||
} |
|||
.dialog-footer { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
/* 重复文件/目录报错弹窗样式保留 */ |
|||
.collectUpload-dialog .file-list { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
.catalog-dialog .archives-table { |
|||
--el-table-header-text-color: #666; |
|||
--el-table-row-hover-bg-color: #f5f7fa; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,910 @@ |
|||
<template> |
|||
<div class="upload-minio"> |
|||
<!-- 上传窗口:默认居中,最小化后固定底部右侧 --> |
|||
<div |
|||
v-if="uploadMinioVisible" |
|||
class="upload-container" |
|||
:class="{ 'minimized': isMinimized }" |
|||
> |
|||
<!-- 窗口头部:居中显示标题+操作按钮,最小化后显示上传状态 --> |
|||
<div class="upload-header"> |
|||
<!-- 最大化状态头部 --> |
|||
<div v-if="!isMinimized" class="header-normal"> |
|||
<div class="title"> |
|||
{{ uploadTitle }} |
|||
<span style="color: red;font-size: 12px; margin-left: 8px;">单个文件不可超过10GB</span> |
|||
</div> |
|||
<div class="header-controls"> |
|||
<el-button type="text" size="small" @click="handleMinimize">最小化</el-button> |
|||
<el-button type="text" size="small" @click="handleClose">关闭</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 最小化状态头部(底部右侧显示) --> |
|||
<div v-if="isMinimized" class="header-mini"> |
|||
<!-- 核心修改:显示maintitle+archive_no 上传中 --> |
|||
<span class="uploading-tip">{{ uploadTipText }}上传中</span> |
|||
<div class="mini-progress"> |
|||
<div class="progress-bar" :style="{ width: totalProgress + '%' }" /> |
|||
</div> |
|||
<div class="mini-controls"> |
|||
<el-button type="text" size="mini" @click="handleRestore">恢复</el-button> |
|||
<el-button type="text" size="mini" @click="handleCancelUpload">取消</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 最大化状态:原上传内容(居中完整显示) --> |
|||
<div v-if="!isMinimized" class="upload-content"> |
|||
<div class="uploader-drop" style="margin-bottom: 20px;"> |
|||
<div class="uploader-btn" @click="triggerFileInput"> |
|||
<p>{{ !isCatalogUpload ? '点击上传(可多文件上传)' : "点击上传ZIP包(只可单文件上传)" }}</p> |
|||
<div style="margin: 20px 0 20px 0;"> |
|||
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
|||
<input |
|||
ref="fileInput" |
|||
type="file" |
|||
:accept="isCatalogUpload ? '.zip' : ''" |
|||
:multiple="!isCatalogUpload" |
|||
:disabled="!isCaValid || isCheckingCa || isUploading" |
|||
class="file-input" |
|||
style="display: none;" |
|||
@change="handleFileSelect" |
|||
> |
|||
</div> |
|||
</div> |
|||
<div class="el-upload__tip">上传限制文件大小:最大10GB/个</div> |
|||
</div> |
|||
|
|||
<div v-if="fileList.length !== 0" style="max-height: 300px; 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" |
|||
:disabled="fileItem.uploading || fileItem.merging" |
|||
@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 class="dialog-footer" style="margin-top: 20px; text-align: right;"> |
|||
<el-button type="text" @click="handleClose">取消</el-button> |
|||
<el-button |
|||
:disabled="fileList.some(item => item.uploading || item.merging) || fileList.length === 0 || isUploading" |
|||
:loading="btnLoading" |
|||
type="primary" |
|||
@click="handleUploadConfirm" |
|||
> |
|||
保存 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 重复文件弹窗(保留原逻辑) --> |
|||
<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 { FetchCheckCaValidity } from '@/api/system/auth2' |
|||
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' |
|||
|
|||
export default { |
|||
name: 'MinioMultiChunkUpload', |
|||
props: { |
|||
selectedDocument: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
isBatchMount: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 新增:接收父组件的archiveInfo对象 |
|||
archiveInfo: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(['baseApi']), |
|||
// 总上传进度(所有文件平均)- 保留 |
|||
totalProgress() { |
|||
if (this.fileList.length === 0) return 0 |
|||
const total = this.fileList.reduce((sum, item) => sum + item.progress, 0) |
|||
return Math.round(total / this.fileList.length) |
|||
}, |
|||
// 是否正在上传/合并 - 保留 |
|||
isUploading() { |
|||
return this.fileList.some(item => item.uploading || item.merging) |
|||
}, |
|||
// 核心修改:上传提示文本,优先显示maintitle+archive_no |
|||
uploadTipText() { |
|||
// 优先使用archiveInfo中的maintitle和archive_no |
|||
const { maintitle, archive_no } = this.archiveInfo |
|||
if (maintitle && archive_no) { |
|||
return `${maintitle}${archive_no} ` |
|||
} |
|||
// 兜底:原逻辑(普通/目录上传) |
|||
return this.isCatalogUpload ? '原文目录' : '档案电子原文' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
uploadMinioVisible: false, // 上传窗显示状态 |
|||
fileList: [], // 多文件上传列表 |
|||
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB) |
|||
totalMergeStartTime: null, |
|||
totalMergeEndTime: null, |
|||
allChunksUploaded: false, |
|||
isCaValid: false, // CA证书是否有效 |
|||
isCheckingCa: false, // 是否正在校验CA状态 |
|||
|
|||
isCatalogUpload: false, // 是否目录上传 |
|||
uploadTitle: '文件上传', |
|||
btnLoading: false, |
|||
|
|||
// 重复文件相关 |
|||
repeatFileVisible: false, |
|||
repeatFileData: [], |
|||
originFileData: [], |
|||
tempSelectedFiles: null, |
|||
|
|||
// 目录上传报错相关 |
|||
resultCatalog: { mountFile: {}, failArchives: [] }, |
|||
catalogErrorVisible: false, |
|||
catalogInfoData: [], |
|||
|
|||
// 窗口核心状态 |
|||
isMinimized: false // 是否最小化到底部右侧 |
|||
} |
|||
}, |
|||
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') |
|||
}, |
|||
// 切换上传类型(普通/目录)- 父组件调用 |
|||
updateUploadOptions(uploadType) { |
|||
this.isCatalogUpload = uploadType !== 1 |
|||
this.uploadTitle = this.isCatalogUpload ? '原文目录上传' : '文件上传' |
|||
this.fileList = [] |
|||
this.isMinimized = false |
|||
this.uploadMinioVisible = true |
|||
}, |
|||
// 格式化文件大小为MB |
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0.00MB' |
|||
const mb = bytes / (1024 * 1024) |
|||
return mb.toFixed(2) + 'MB' |
|||
}, |
|||
|
|||
// 工具方法:提示框/时间格式化/Base64/图片分辨率(保留原逻辑) |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 80 }) // 提示框上移,避免遮挡底部小窗 |
|||
}, |
|||
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' |
|||
}) |
|||
}, |
|||
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` |
|||
}, |
|||
getImgPx(base64) { |
|||
return new Promise((resolve) => { |
|||
const img = new Image() |
|||
img.onload = () => resolve({ width: img.width, height: img.height }) |
|||
img.src = 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) |
|||
}) |
|||
}, |
|||
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 |
|||
} |
|||
}, |
|||
// 获取已存在文件列表(重复检测,保留原逻辑) |
|||
async getFileList() { |
|||
const params = { categoryId: this.selectedCategory.id, archivesId: this.arcId } |
|||
const res = await FetchInitFileCategoryView(params) |
|||
this.originFileData = res.returnlist || [] |
|||
}, |
|||
|
|||
// 选择文件处理(保留原逻辑,不自动上传) |
|||
async handleFileSelect(e) { |
|||
if (this.isCheckingCa) { |
|||
this.showMessage('正在校验CA证书,请稍候...', 'warning') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
if (!this.isCaValid) { |
|||
this.showMessage('CA证书不在有效期内,无法上传文件', 'error') |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
const selectedFiles = Array.from(e.target.files) |
|||
if (selectedFiles.length === 0) return |
|||
|
|||
// 重复文件检测 |
|||
if (!this.isCatalogUpload) { |
|||
await this.getFileList() |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
this.repeatFileData = selectedFiles.filter(file => existingFileNames.includes(file.name)) |
|||
if (this.repeatFileData.length > 0) { |
|||
this.repeatFileVisible = true |
|||
this.tempSelectedFiles = selectedFiles |
|||
e.target.value = '' |
|||
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] |
|||
e.target.value = '' |
|||
}, |
|||
// 处理重复文件选择(保留原逻辑,不自动上传) |
|||
handleRepeatFile(type) { |
|||
this.repeatFileVisible = false |
|||
if (!this.tempSelectedFiles) return |
|||
let filesToUpload = [] |
|||
if (type === 0) { |
|||
filesToUpload = this.tempSelectedFiles |
|||
} else { |
|||
const existingFileNames = this.originFileData.map(file => file.file_name) |
|||
filesToUpload = this.tempSelectedFiles.filter(file => !existingFileNames.includes(file.name)) |
|||
if (filesToUpload.length === 0) { |
|||
this.showMessage('当前所选文件去重后无可上传的文件', 'error') |
|||
this.handleClose() |
|||
return |
|||
} |
|||
} |
|||
const newFileList = filesToUpload.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 = newFileList |
|||
this.tempSelectedFiles = null |
|||
}, |
|||
|
|||
// 计算文件MD5(保留原逻辑) |
|||
calculateFileMd5(file, fileItem) { |
|||
return new Promise((resolve, reject) => { |
|||
fileItem.md5StartTime = new Date().getTime() |
|||
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 |
|||
offset < file.size ? loadNextChunk() : resolve(spark.end()) |
|||
} |
|||
fileReader.onerror = (err) => reject(err) |
|||
loadNextChunk() |
|||
}) |
|||
}, |
|||
// 检查分片是否存在(保留原逻辑) |
|||
async checkChunkExists(fileMd5, chunkIndex) { |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.get(`${linkSrc}/api/collect/chunk`, { |
|||
params: { fileMd5, chunkIndex }, |
|||
headers: { 'Authorization': getToken() } |
|||
}) |
|||
if (response.data.code !== 200) throw new Error('检查分片失败: ' + response.data.msg) |
|||
return response.data.data |
|||
}, |
|||
// 上传单个分片(保留原逻辑) |
|||
async uploadSingleChunk(file, fileMd5, chunkIndex) { |
|||
const start = chunkIndex * this.CHUNK_SIZE |
|||
const end = Math.min(start + this.CHUNK_SIZE, file.size) |
|||
const chunkBlob = file.slice(start, end) |
|||
const formData = new FormData() |
|||
formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) |
|||
formData.append('fileMd5', fileMd5) |
|||
formData.append('chunkIndex', chunkIndex) |
|||
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/chunk`, formData, { |
|||
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': getToken() } |
|||
}) |
|||
if (response.data.code !== 200) throw new Error(`分片${chunkIndex}上传失败: ` + response.data.msg) |
|||
}, |
|||
// 校验所有分片是否存在(保留原逻辑) |
|||
async checkAllChunksExist(fileMd5, totalChunks) { |
|||
for (let i = 0; i < totalChunks; i++) { |
|||
const result = await this.checkChunkExists(fileMd5, i) |
|||
if (!result.exists) return false |
|||
} |
|||
return true |
|||
}, |
|||
|
|||
// 单个文件分片上传(保留原逻辑) |
|||
async uploadFileChunks(fileItem) { |
|||
const file = fileItem.file |
|||
fileItem.uploading = true |
|||
fileItem.progress = 0 |
|||
fileItem.errorMsg = '' |
|||
fileItem.successMsg = '' |
|||
try { |
|||
const fileMd5 = await this.calculateFileMd5(file, fileItem) |
|||
fileItem.md5 = fileMd5 |
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
fileItem.chunkUploadStartTime = new Date().getTime() |
|||
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() |
|||
fileItem.progress = 100 |
|||
} catch (err) { |
|||
if (fileItem.chunkUploadStartTime && !fileItem.chunkUploadEndTime) { |
|||
fileItem.chunkUploadEndTime = new Date().getTime() |
|||
} |
|||
fileItem.errorMsg = '分片上传失败: ' + (err.message || '未知错误') |
|||
this.allChunksUploaded = false |
|||
console.error(`【${file.name}】分片上传异常:`, err) |
|||
} finally { |
|||
fileItem.uploading = false |
|||
} |
|||
}, |
|||
|
|||
// 点击保存:开始上传(核心保留手动保存逻辑) |
|||
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 |
|||
// 提示用户可最小化窗口查看后台进度 |
|||
if (!this.isMinimized) { |
|||
this.showMessage('文件开始上传,您可点击最小化按钮在底部右侧查看进度', 'info') |
|||
} |
|||
|
|||
// 串行上传所有文件分片 |
|||
this.allChunksUploaded = true |
|||
for (const fileItem of pendingFiles) { |
|||
await this.uploadFileChunks(fileItem) |
|||
if (fileItem.errorMsg) this.allChunksUploaded = false |
|||
} |
|||
|
|||
// 分片上传失败处理:恢复最大化显示错误 |
|||
if (!this.allChunksUploaded) { |
|||
this.btnLoading = false |
|||
this.showMessage('部分文件分片上传失败,无法执行合并', 'error') |
|||
this.isMinimized = false |
|||
return |
|||
} |
|||
|
|||
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) |
|||
if (validFiles.length === 0) { |
|||
this.btnLoading = false |
|||
this.showMessage('无有效分片上传完成的文件!', 'error') |
|||
this.isMinimized = false |
|||
return |
|||
} |
|||
|
|||
// 执行文件合并 |
|||
this.totalMergeStartTime = new Date().getTime() |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = true |
|||
fileItem.mergeStartTime = new Date().getTime() |
|||
}) |
|||
|
|||
try { |
|||
const processFiles = validFiles.map(async(fileItem) => { |
|||
const file = fileItem.file |
|||
const json = {} |
|||
const jsonArray = [] |
|||
const jsonString = {} |
|||
// 图片分辨率处理 |
|||
if (file.type.startsWith('image')) { |
|||
const fileBase64 = await this.getBase64(file) |
|||
const imgRes = await this.getImgPx(fileBase64) |
|||
jsonString.file_dpi = `${imgRes.width}px*${imgRes.height}px` |
|||
} else { |
|||
jsonString.file_dpi = '' |
|||
} |
|||
// 构建文件信息 |
|||
jsonString.file_name = file.name |
|||
jsonString.file_size = file.size |
|||
jsonString.file_type = file.name.split('.').pop() || '' |
|||
jsonString.last_modified = file.lastModified |
|||
jsonString.file_path = '' |
|||
jsonString.sequence = null |
|||
jsonString.archive_id = this.arcId |
|||
jsonString.create_time = getCurrentTime() |
|||
jsonString.id = null |
|||
jsonString.file_thumbnail = '' |
|||
jsonArray.push(jsonString) |
|||
// 校验分片完整性 |
|||
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.categoryId = this.selectedCategory.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 linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API |
|||
const response = await axios.post(`${linkSrc}/api/collect/merge`, jsonArray, { |
|||
headers: { 'Authorization': getToken(), 'Content-Type': 'application/json' } |
|||
}) |
|||
|
|||
this.totalMergeEndTime = new Date().getTime() |
|||
if (response.data.code === 200) { |
|||
// 目录上传逻辑(保留原逻辑) |
|||
if (this.isCatalogUpload && response.data.data.length === 1 && response.data.data[0] !== '') { |
|||
catalogUpload(`${linkSrc}/api/collect/catalogUpload`, |
|||
response.data.data[0], |
|||
this.selectedCategory.fondsId |
|||
).then(res => { |
|||
this.btnLoading = false |
|||
if (res.data.code === 200) { |
|||
this.resultCatalog = res.data.data |
|||
if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.successNum) { |
|||
this.uploadSuccessHandle() // 目录上传成功:提示+关窗 |
|||
} else { |
|||
this.catalogInfoData = [] |
|||
this.resultCatalog.failArchives.forEach(item => { |
|||
const parts = item.split(':') |
|||
if (parts.length === 2) { |
|||
const field = parts[0] |
|||
const fileStr = parts[1].replace(/\[|\]/g, '').trim() |
|||
const fileArray = fileStr.split(',').map(file => file.trim()) |
|||
const match = field.match(/^([\w-·]+)(.*)$/) |
|||
this.catalogInfoData.push({ |
|||
archives: match ? match[1] : field, |
|||
description: match ? match[2] : '', |
|||
children: fileArray |
|||
}) |
|||
} |
|||
}) |
|||
this.catalogErrorVisible = true |
|||
this.handleClose() |
|||
} |
|||
} else { |
|||
this.showMessage(`目录上传失败: ${res.data.message}`, 'error') |
|||
this.isMinimized = false |
|||
} |
|||
}) |
|||
} else { |
|||
// 普通文件上传成功:统一处理 |
|||
validFiles.forEach((fileItem) => { |
|||
fileItem.mergeEndTime = new Date().getTime() |
|||
fileItem.successMsg = '上传成功!' |
|||
fileItem.merging = false |
|||
}) |
|||
this.uploadSuccessHandle() |
|||
} |
|||
} else { |
|||
throw new Error(response.data.msg || '合并失败') |
|||
} |
|||
} catch (err) { |
|||
this.btnLoading = false |
|||
this.showMessage(`文件合并失败: ${err.message}`, 'error') |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = false |
|||
fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` |
|||
}) |
|||
this.isMinimized = false // 恢复最大化显示错误 |
|||
this.$emit('onUploadError', err) |
|||
} finally { |
|||
this.resetUploadState() |
|||
} |
|||
}, |
|||
|
|||
// 上传成功统一处理:提示+延迟1.5秒关窗 |
|||
uploadSuccessHandle() { |
|||
// 成功提示也适配archiveInfo |
|||
const { maintitle, archive_no } = this.archiveInfo |
|||
let successText = '' |
|||
if (maintitle && archive_no) { |
|||
successText = `${maintitle}${archive_no} 上传完成` |
|||
} else { |
|||
successText = `${this.isCatalogUpload ? '原文目录' : '档案电子原文'}上传完成` |
|||
} |
|||
this.showMessage(successText, 'success') |
|||
this.$emit('onUploadSuccess') |
|||
// 延迟关窗,让用户看到成功提示 |
|||
setTimeout(() => { |
|||
this.handleClose() |
|||
}, 1500) |
|||
}, |
|||
|
|||
// 窗口核心操作方法 |
|||
// 手动最小化:缩至底部右侧 |
|||
handleMinimize() { |
|||
this.isMinimized = true |
|||
this.showMessage('上传窗口已最小化,可在底部右侧查看进度', 'info') |
|||
}, |
|||
// 恢复最大化:回到屏幕居中 |
|||
handleRestore() { |
|||
this.isMinimized = false |
|||
}, |
|||
// 关闭窗口(上传中禁止,自动最小化) |
|||
handleClose() { |
|||
if (this.isUploading) { |
|||
this.showMessage('文件正在上传,无法直接关闭,可点击取消上传', 'warning') |
|||
this.isMinimized = true |
|||
return |
|||
} |
|||
this.uploadMinioVisible = false |
|||
this.fileList = [] |
|||
this.isMinimized = false |
|||
this.resetUploadState() |
|||
this.$emit('close-dialog') |
|||
}, |
|||
// 取消上传:二次确认,清空文件并关窗 |
|||
handleCancelUpload() { |
|||
this.$confirm('确定要取消本次上传吗?已上传的分片将被丢弃', '提示', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.fileList = [] |
|||
this.resetUploadState() |
|||
this.uploadMinioVisible = false |
|||
this.isMinimized = false |
|||
this.showMessage('已取消上传', 'info') |
|||
this.$emit('close-dialog') |
|||
}).catch(() => {}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.upload-minio { |
|||
position: relative; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
/* 核心:上传窗口基础样式 - 最大化居中,最小化底部右侧 */ |
|||
.upload-container { |
|||
position: fixed; |
|||
z-index: 9999; |
|||
background: #fff; |
|||
border-radius: 8px; |
|||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|||
border: 1px solid #e6e6e6; |
|||
transition: all 0.3s ease; |
|||
/* 最大化状态:屏幕居中 */ |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 600px; |
|||
min-height: 400px; |
|||
} |
|||
/* 最小化状态:屏幕底部右侧 */ |
|||
.upload-container.minimized { |
|||
top: auto; |
|||
left: auto; |
|||
right: 20px; |
|||
bottom: 20px; |
|||
transform: none; |
|||
width: 420px; /* 微调宽度,适配长文本显示 */ |
|||
min-height: 60px; |
|||
height: 60px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
/* 窗口头部通用样式 */ |
|||
.upload-header { |
|||
height: 40px; |
|||
line-height: 40px; |
|||
padding: 0 16px; |
|||
background: #f5f7fa; |
|||
border-bottom: 1px solid #e6e6e6; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
/* 最大化头部:标题+最小化/关闭按钮 */ |
|||
.header-normal { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.header-normal .title { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
/* 最小化头部:上传状态+进度条+恢复/取消按钮 */ |
|||
.header-mini { |
|||
width: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
height: 100%; |
|||
} |
|||
.uploading-tip { |
|||
font-size: 13px; |
|||
color: #333; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
flex-shrink: 0; |
|||
max-width: 180px; /* 限制文本宽度,避免挤压进度条 */ |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
.mini-progress { |
|||
flex: 1; |
|||
height: 6px; |
|||
background: #e5e6eb; |
|||
border-radius: 3px; |
|||
overflow: hidden; |
|||
} |
|||
.mini-controls { |
|||
display: flex; |
|||
gap: 6px; |
|||
flex-shrink: 0; |
|||
} |
|||
.mini-controls el-button { |
|||
padding: 0 6px !important; |
|||
font-size: 12px !important; |
|||
} |
|||
|
|||
/* 窗口内容区(仅最大化显示) */ |
|||
.upload-content { |
|||
padding: 16px; |
|||
} |
|||
|
|||
/* 原有上传样式保留,微调适配 */ |
|||
.uploader-drop{ |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
text-align: center; |
|||
height: 180px; |
|||
border: none; |
|||
background-color: #f5f5f5; |
|||
border-radius: 4px; |
|||
.uploader-btn{ |
|||
border: none; |
|||
i{ |
|||
font-size: 32px; |
|||
color: #1F55EB; |
|||
} |
|||
&:hover{ |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
.el-upload__tip{ |
|||
font-size: 12px; |
|||
color: #A6ADB6; |
|||
margin-top: 8px; |
|||
} |
|||
} |
|||
.file-input { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
width: 100px; |
|||
height: 36px; |
|||
padding: 5px; |
|||
opacity: 0; |
|||
cursor: pointer; |
|||
z-index: 10; |
|||
} |
|||
.file-item { |
|||
width: 100%; |
|||
border: 1px dashed #409eff; |
|||
padding: 12px; |
|||
border-radius: 4px; |
|||
margin-bottom: 12px; |
|||
} |
|||
.file-name { |
|||
position: relative; |
|||
font-weight: 500; |
|||
color: #333; |
|||
margin-bottom: 8px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
.file-size { |
|||
color: #666; |
|||
font-weight: normal; |
|||
font-size: 12px; |
|||
margin-left: 8px; |
|||
} |
|||
.icon-shanchu{ |
|||
font-size: 18px; |
|||
color: #1F55EB; |
|||
cursor: pointer; |
|||
&:hover { |
|||
color: #ff4444; |
|||
transform: scale(1.1); |
|||
transition: all 0.2s ease; |
|||
} |
|||
&[disabled] { |
|||
color: #ccc; |
|||
cursor: not-allowed; |
|||
transform: none; |
|||
} |
|||
} |
|||
.progress-wrapper { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
} |
|||
.progress-bar { |
|||
height: 10px; |
|||
background-color: #42b983; |
|||
transition: width 0.3s ease; |
|||
border-radius: 5px; |
|||
} |
|||
.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; |
|||
} |
|||
.dialog-footer { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
/* 重复文件/目录报错弹窗样式保留 */ |
|||
.collectUpload-dialog .file-list { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
.catalog-dialog .archives-table { |
|||
--el-table-header-text-color: #666; |
|||
--el-table-row-hover-bg-color: #f5f7fa; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue