9 changed files with 3266 additions and 335 deletions
-
36src/utils/upload.js
-
733src/views/collectReorganizi/batchConnection/module/form copy.vue
-
504src/views/collectReorganizi/batchConnection/module/form-zip.vue
-
673src/views/collectReorganizi/batchConnection/module/form.vue
-
850src/views/components/category/preUpload-非zip-test.vue
-
8src/views/features/ssoLogin.vue
-
8src/views/system/fileLibraryManage/index.vue
-
167src/views/system/fileLibraryManage/processManage/index.vue
-
374src/views/system/fileLibraryManage/processManage/module/form.vue
@ -0,0 +1,733 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<!-- 本地挂接 / 远程挂接 主弹窗 --> |
||||
|
<el-dialog |
||||
|
:title="connectionTitle" |
||||
|
:close-on-click-modal="false" |
||||
|
:modal-append-to-body="false" |
||||
|
append-to-body |
||||
|
:visible.sync="localHitchVisible" |
||||
|
:before-close="handleCancel" |
||||
|
@opened="opened" |
||||
|
> |
||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="120px"> |
||||
|
<el-form-item label="重复验证方式" prop="checkRepeatType"> |
||||
|
<el-select v-model="form.checkRepeatType" placeholder="请选择" style="width: 400px;"> |
||||
|
<el-option |
||||
|
v-for="item in typeOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="挂接字段" prop="hitchFiled"> |
||||
|
<el-select v-model="form.hitchFiled" placeholder="请选择" style="width: 400px;" @change="selectFiled"> |
||||
|
<el-option |
||||
|
v-for="item in fieldOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="挂接详情" prop="fields"> |
||||
|
<div class="tag-wrapper"> |
||||
|
<el-tag v-for="item in hitchRemarkArray" :key="item" :closable="item !== '*'" @close="removeTag(item)"> |
||||
|
{{ item }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
<el-input v-model="form.fields" style="display: none;" type="hidden" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="匹配模式" prop="matchingMode"> |
||||
|
<el-select v-model="form.matchingMode" placeholder="请选择" style="width: 400px;" @change="changeMatchedType"> |
||||
|
<el-option |
||||
|
v-for="item in matchedOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="!connectionType" label="上传附件" prop="file" class="upload-wm"> |
||||
|
<!-- 打开子选择弹窗 --> |
||||
|
<el-button size="small" @click="openFileSelectDialog">选择文件/文件夹</el-button> |
||||
|
|
||||
|
<!-- 主弹窗展示:展示【文件名】,不展示完整路径;提交接口传完整路径 uploadResultPaths --> |
||||
|
<div v-if="uploadResultPaths.length" class="file-list"> |
||||
|
<div v-for="(path, idx) in uploadResultPaths" :key="idx" class="file-item"> |
||||
|
<i class="iconfont icon-xiaowenjian" /> |
||||
|
{{ getFileNameFromPath(path) }} |
||||
|
</div> |
||||
|
</div> |
||||
|
<el-input v-show="false" v-model="form.file" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="connectionType" label="挂接类型" prop="protocol"> |
||||
|
<el-select v-model="form.protocol" placeholder="请选择" style="width: 400px;" @change="changeProtocol"> |
||||
|
<el-option |
||||
|
v-for="item in batchTypeOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="connectionType" label="文件路径" prop="filePath"> |
||||
|
<el-select v-model="form.filePath" placeholder="请选择" value-key="id" style="width: 400px;" @change="changeFilePath"> |
||||
|
<el-option |
||||
|
v-for="(item,index) in filePathOptions" |
||||
|
:key="index" |
||||
|
:label="item.name" |
||||
|
:value="item" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item |
||||
|
v-if="connectionType" |
||||
|
label="" |
||||
|
prop="fileRemote" |
||||
|
class="file_remote-style" |
||||
|
:rules="form.filePath !== null ? rules.fileRemote : []" |
||||
|
:required="form.filePath !== null" |
||||
|
> |
||||
|
<el-button v-if="form.filePath && fileRemoteLoading" type="primary" :loading="fileRemoteLoading" style="border: none; display: block; padding: 0 0 4px 0;">文件列表加载中</el-button> |
||||
|
<el-radio-group v-model="form.fileRemote"> |
||||
|
<el-radio |
||||
|
v-for="item in fileRemoteOptions" |
||||
|
:key="item.value" |
||||
|
:value="item.value" |
||||
|
:label="item.name" |
||||
|
>{{ item.name }}</el-radio> |
||||
|
</el-radio-group> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="handleCancel">取消</el-button> |
||||
|
<el-button :loading="btnLoading" type="primary" @click.native="handleComfired">确定</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
|
||||
|
<!-- =========子弹窗:文件选择+上传弹窗========= --> |
||||
|
<el-dialog |
||||
|
title="选择文件/文件夹" |
||||
|
:visible.sync="fileSelectDialogVisible" |
||||
|
width="650px" |
||||
|
:close-on-click-modal="false" |
||||
|
:modal-append-to-body="false" |
||||
|
append-to-body |
||||
|
@closed="resetFileDialog" |
||||
|
> |
||||
|
<!-- 选择文件 input 普通多选,不带webkitdirectory --> |
||||
|
<input |
||||
|
ref="fileOnlyInput" |
||||
|
type="file" |
||||
|
multiple |
||||
|
style="display: none" |
||||
|
@change="onSelectFileOnly" |
||||
|
> |
||||
|
<!-- 选择文件夹 input --> |
||||
|
<input |
||||
|
ref="folderInput" |
||||
|
type="file" |
||||
|
webkitdirectory |
||||
|
directory |
||||
|
multiple |
||||
|
style="display: none" |
||||
|
@change="onSelectFolder" |
||||
|
> |
||||
|
|
||||
|
<div class="select-btn-group"> |
||||
|
<el-button type="primary" :disabled="fileDialogUploading" @click="$refs.fileOnlyInput.click()">选择文件</el-button> |
||||
|
<el-button type="primary" :disabled="fileDialogUploading" @click="$refs.folderInput.click()">选择文件夹</el-button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 子弹窗上传进度条 --> |
||||
|
<div v-if="fileDialogUploading" class="dialog-upload-tip"> |
||||
|
<el-progress :percentage="dialogUploadProgress" /> |
||||
|
<div>正在上传:{{ dialogUploadingFileName }}</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="select-file-list"> |
||||
|
<div v-if="tempSelectedFiles.length === 0" class="empty-tip">暂未选择文件</div> |
||||
|
<div v-for="(f, idx) in tempSelectedFiles" :key="idx" class="temp-file-item"> |
||||
|
<i class="iconfont icon-xiaowenjian" /> |
||||
|
<span class="file-name">{{ f.name }}</span> |
||||
|
<i class="iconfont icon-shanchu" :disabled="fileDialogUploading" @click="deleteTempFile(idx)" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div slot="footer"> |
||||
|
<el-button @click="fileSelectDialogVisible = false">取消</el-button> |
||||
|
<el-button type="primary" :loading="fileDialogUploading" :disabled="tempSelectedFiles.length === 0" @click="startUploadFiles">开始上传</el-button> |
||||
|
<el-button type="primary" :disabled="!dialogUploadSuccess" @click="confirmSelectFiles">确认</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { crud } from '@crud/crud' |
||||
|
import { FetchInitCategoryInputFieldByPid } from '@/api/system/category/category' |
||||
|
import { FetchFindDetailsByProtocol, FetchFileByRemoteConnection } from '@/api/system/remoteConnection' |
||||
|
import { batchMountUpload, remoteConnectiontUpload } from '@/utils/upload' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import axios from 'axios' |
||||
|
import { getToken } from '@/utils/auth' |
||||
|
|
||||
|
// 分片大小 5MB |
||||
|
const CHUNK_SIZE = 5 * 1024 * 1024 |
||||
|
|
||||
|
export default { |
||||
|
name: 'LocalHitch', |
||||
|
mixins: [crud()], |
||||
|
props: { |
||||
|
selectedCategory: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
btnLoading: false, |
||||
|
connectionTitle: '本地挂接', |
||||
|
localHitchVisible: false, |
||||
|
hitchRemarkArray: [], |
||||
|
connectionType: 0, |
||||
|
fileRemoteLoading: false, |
||||
|
fileRemoteOptions: [], |
||||
|
|
||||
|
// =========子弹窗变量========= |
||||
|
fileSelectDialogVisible: false, |
||||
|
tempSelectedFiles: [], // 子弹窗临时文件列表 |
||||
|
fileDialogUploading: false, // 子弹窗上传中 |
||||
|
dialogUploadProgress: 0, |
||||
|
dialogUploadingFileName: '', |
||||
|
dialogUploadSuccess: false, // 是否上传合并完成 |
||||
|
dialogResultPaths: [], // 子弹窗上传完成返回的filePaths(完整路径) |
||||
|
|
||||
|
// ✅新增:保存上一次合并成功快照 |
||||
|
lastMergeSnapshot: null, |
||||
|
|
||||
|
// 主弹窗:最终上传完成的文件路径数组(完整路径,提交接口使用) |
||||
|
uploadResultPaths: [], |
||||
|
|
||||
|
form: { |
||||
|
checkRepeatType: 1, |
||||
|
hitchFiled: null, |
||||
|
matchingMode: 2, |
||||
|
fields: null, |
||||
|
file: null, |
||||
|
protocol: null, |
||||
|
filePath: null, |
||||
|
fileRemote: null |
||||
|
}, |
||||
|
typeOptions: [ |
||||
|
{ value: 1, label: '跳过' }, |
||||
|
{ value: 2, label: '覆盖' }, |
||||
|
{ value: 3, label: '追加' } |
||||
|
], |
||||
|
matchedOptions: [ |
||||
|
{ value: 1, label: '全量匹配' }, |
||||
|
{ value: 2, label: '前缀模糊匹配' }, |
||||
|
{ value: 3, label: '后缀模糊匹配' } |
||||
|
], |
||||
|
batchTypeOptions: [ |
||||
|
{ value: 1, label: '服务端挂接' }, |
||||
|
{ value: 2, label: 'FTP挂接' }, |
||||
|
{ value: 3, label: 'SFTP挂接' } |
||||
|
], |
||||
|
fieldOptions: [], |
||||
|
filePathOptions: [], |
||||
|
rules: { |
||||
|
checkRepeatType: [{ required: true, message: '请选择', trigger: 'change' }], |
||||
|
hitchFiled: [{ required: true, message: '请选择', trigger: 'change' }], |
||||
|
fields: [{ required: true, message: '请选择上面挂接字段生成挂接详情', trigger: 'blur' }], |
||||
|
matchingMode: [{ required: true, message: '请选择', trigger: 'change' }], |
||||
|
protocol: [{ required: true, message: '请选择', trigger: 'change' }], |
||||
|
filePath: [{ required: true, message: '请选择', trigger: 'change' }], |
||||
|
fileRemote: [{ required: true, message: '请选择指定文件', trigger: 'change' }] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters(['baseApi']), |
||||
|
collectLevel() { |
||||
|
if (this.selectedCategory.arrangeType === 1) return 3 |
||||
|
if (this.selectedCategory.arrangeType === 2) return 2 |
||||
|
if (this.selectedCategory.arrangeType === 3) return 1 |
||||
|
return null |
||||
|
} |
||||
|
}, |
||||
|
created() { }, |
||||
|
mounted() { }, |
||||
|
methods: { |
||||
|
opened() { |
||||
|
this.getFieldCommon() |
||||
|
}, |
||||
|
// 比对两个文件数组是否完全一致(文件名+大小) |
||||
|
isFileListEqual(filesA, filesB) { |
||||
|
if (filesA.length !== filesB.length) return false |
||||
|
const listA = filesA.map(f => ({ name: f.name, size: f.size })) |
||||
|
const listB = filesB.map(f => ({ name: f.name, size: f.size })) |
||||
|
for (let i = 0; i < listA.length; i++) { |
||||
|
if (listA[i].name !== listB[i].name || listA[i].size !== listB[i].size) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
}, |
||||
|
// 从完整路径截取文件名,兼容 / 和 \ 分隔符 |
||||
|
getFileNameFromPath(path) { |
||||
|
if (!path) return '' |
||||
|
const splitArr = path.split(/[\\/]/) |
||||
|
return splitArr[splitArr.length - 1] |
||||
|
}, |
||||
|
|
||||
|
// ==========子弹窗方法========== |
||||
|
openFileSelectDialog() { |
||||
|
this.resetFileDialog() |
||||
|
this.fileSelectDialogVisible = true |
||||
|
}, |
||||
|
|
||||
|
// 重置子弹窗全部状态 |
||||
|
resetFileDialog() { |
||||
|
this.tempSelectedFiles = [] |
||||
|
this.fileDialogUploading = false |
||||
|
this.dialogUploadProgress = 0 |
||||
|
this.dialogUploadingFileName = '' |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
this.lastMergeSnapshot = null // ✅清空快照 |
||||
|
}, |
||||
|
|
||||
|
// 选择普通文件(多选) |
||||
|
onSelectFileOnly(e) { |
||||
|
const files = e.target.files |
||||
|
if (!files || files.length === 0) return |
||||
|
const arr = Array.from(files) |
||||
|
this.tempSelectedFiles = [...this.tempSelectedFiles, ...arr] |
||||
|
e.target.value = '' |
||||
|
// 新增文件,旧合并结果作废 |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
}, |
||||
|
|
||||
|
// 选择文件夹 |
||||
|
onSelectFolder(e) { |
||||
|
const files = e.target.files |
||||
|
if (!files || files.length === 0) return |
||||
|
const arr = Array.from(files) |
||||
|
this.tempSelectedFiles = [...this.tempSelectedFiles, ...arr] |
||||
|
e.target.value = '' |
||||
|
// 新增文件,旧合并结果作废 |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
}, |
||||
|
|
||||
|
// 删除子弹窗列表里的文件 |
||||
|
deleteTempFile(idx) { |
||||
|
// 先删除临时文件 |
||||
|
this.tempSelectedFiles.splice(idx, 1) |
||||
|
|
||||
|
// 如果有快照,并且当前文件列表和快照一致 → 恢复上次合并状态 |
||||
|
if (this.lastMergeSnapshot && this.isFileListEqual(this.tempSelectedFiles, this.lastMergeSnapshot.files)) { |
||||
|
this.dialogUploadSuccess = true |
||||
|
this.dialogResultPaths = [...this.lastMergeSnapshot.paths] |
||||
|
} else { |
||||
|
// 不相等,需要重传 |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// ==========分片上传接口函数(全部在子弹窗内执行)========== |
||||
|
async initUpload(fileName, totalChunks) { |
||||
|
const res = await axios.post('/api/uploadtemp/init', null, { |
||||
|
params: { fileName, totalChunks }, |
||||
|
headers: { 'Authorization': getToken() } |
||||
|
}) |
||||
|
return res.data.data |
||||
|
}, |
||||
|
|
||||
|
async uploadChunk(uploadId, chunkIndex, fileBlob) { |
||||
|
const formData = new FormData() |
||||
|
formData.append('file', fileBlob) |
||||
|
formData.append('uploadId', uploadId) |
||||
|
formData.append('chunkIndex', chunkIndex) |
||||
|
const res = await axios.post('/api/uploadtemp/chunk', formData, { |
||||
|
headers: { |
||||
|
'Content-Type': 'multipart/form-data', |
||||
|
'Authorization': getToken() |
||||
|
} |
||||
|
}) |
||||
|
return res.data |
||||
|
}, |
||||
|
|
||||
|
async batchMerge(uploadIdToFileNameMap) { |
||||
|
const res = await axios.post('/api/uploadtemp/batchMerge', uploadIdToFileNameMap, { |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization': getToken() |
||||
|
} |
||||
|
}) |
||||
|
return res.data.data |
||||
|
}, |
||||
|
|
||||
|
// 子弹窗:开始上传全部文件 |
||||
|
// 子弹窗:开始上传全部文件 |
||||
|
async startUploadFiles() { |
||||
|
if (this.tempSelectedFiles.length === 0) { |
||||
|
this.$message.warning('请选择文件') |
||||
|
return |
||||
|
} |
||||
|
this.fileDialogUploading = true |
||||
|
this.dialogUploadProgress = 0 |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
const uploadIdMap = {} |
||||
|
|
||||
|
try { |
||||
|
for (let i = 0; i < this.tempSelectedFiles.length; i++) { |
||||
|
const file = this.tempSelectedFiles[i] |
||||
|
this.dialogUploadingFileName = file.name |
||||
|
const fileSize = file.size |
||||
|
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE) |
||||
|
const uploadId = await this.initUpload(file.name, totalChunks) |
||||
|
uploadIdMap[uploadId] = file.name |
||||
|
|
||||
|
for (let chunkIdx = 0; chunkIdx < totalChunks; chunkIdx++) { |
||||
|
const start = chunkIdx * CHUNK_SIZE |
||||
|
const end = Math.min(start + CHUNK_SIZE, fileSize) |
||||
|
const blob = file.slice(start, end) |
||||
|
await this.uploadChunk(uploadId, chunkIdx, blob) |
||||
|
|
||||
|
const totalAllChunks = this.tempSelectedFiles.reduce((sum, f) => sum + Math.ceil(f.size / CHUNK_SIZE), 0) |
||||
|
const doneAllChunks = this.tempSelectedFiles.slice(0, i).reduce((sum, f) => sum + Math.ceil(f.size / CHUNK_SIZE), 0) + chunkIdx + 1 |
||||
|
this.dialogUploadProgress = Math.round((doneAllChunks / totalAllChunks) * 100) |
||||
|
} |
||||
|
} |
||||
|
// 批量合并 |
||||
|
const pathList = await this.batchMerge(uploadIdMap) |
||||
|
this.dialogResultPaths = pathList |
||||
|
this.dialogUploadSuccess = true |
||||
|
// ✅保存合并快照 |
||||
|
this.lastMergeSnapshot = { |
||||
|
files: [...this.tempSelectedFiles], |
||||
|
paths: [...pathList] |
||||
|
} |
||||
|
this.$message.success('文件上传合并完成') |
||||
|
} catch (err) { |
||||
|
console.error('上传失败', err) |
||||
|
this.$message.error('文件上传失败,请重试') |
||||
|
this.dialogUploadSuccess = false |
||||
|
this.dialogResultPaths = [] |
||||
|
this.lastMergeSnapshot = null |
||||
|
} finally { |
||||
|
this.fileDialogUploading = false |
||||
|
this.dialogUploadingFileName = '' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 子弹窗确认,把返回的filePaths给到主弹窗 |
||||
|
confirmSelectFiles() { |
||||
|
if (!this.dialogUploadSuccess) { |
||||
|
this.$message.warning('请先完成文件上传') |
||||
|
return |
||||
|
} |
||||
|
this.uploadResultPaths = [...this.dialogResultPaths] |
||||
|
this.fileSelectDialogVisible = false |
||||
|
}, |
||||
|
|
||||
|
// =========主弹窗提交========== |
||||
|
async handleComfired() { |
||||
|
if (!this.connectionType) { |
||||
|
if (this.uploadResultPaths.length === 0) { |
||||
|
this.$message({ message: '请选择并上传文件或者文件夹!', offset: 8 }) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
if (this.hitchRemarkArray.length === 1 && this.hitchRemarkArray[0] === '*') { |
||||
|
this.form.fields = '' |
||||
|
} else { |
||||
|
this.form.fields = this.hitchRemarkArray.join('') |
||||
|
} |
||||
|
this.btnLoading = true |
||||
|
this.$refs['form'].validate(async(valid) => { |
||||
|
if (!valid) { |
||||
|
this.btnLoading = false |
||||
|
return false |
||||
|
} |
||||
|
let newhitchRemark = this.hitchRemarkArray.map(item => item.replace(/^\$|\$$|\*$/g, '')) |
||||
|
newhitchRemark = newhitchRemark.filter(item => item !== '') |
||||
|
|
||||
|
if (!this.connectionType) { |
||||
|
try { |
||||
|
const params = { |
||||
|
categoryId: this.selectedCategory.id, |
||||
|
mountType: 1, // 挂接方式 1.本地挂接 2.远程挂接 3.上传目录 (目前只有1) |
||||
|
checkRepeatType: this.form.checkRepeatType, |
||||
|
matchingMode: this.form.matchingMode, |
||||
|
fields: newhitchRemark, |
||||
|
fondsId: this.selectedCategory.fondsId, |
||||
|
filePaths: this.uploadResultPaths // 提交接口仍然传完整路径数组 |
||||
|
} |
||||
|
console.log('批量挂接参数', params) |
||||
|
// 调用批量挂接接口 |
||||
|
batchMountUpload(this.baseApi + '/api/collect/batchMountFolder', params).then(res => { |
||||
|
if (res.data.code === 200) { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '挂接失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCancel() |
||||
|
this.crud.refresh() |
||||
|
this.btnLoading = false |
||||
|
}).catch(err => { |
||||
|
console.error('请求异常', err) |
||||
|
this.$message.error('请求异常') |
||||
|
this.btnLoading = false |
||||
|
}) |
||||
|
} catch (err) { |
||||
|
console.error('提交异常', err) |
||||
|
this.$message.error('提交失败,请重试') |
||||
|
this.btnLoading = false |
||||
|
} |
||||
|
} else { |
||||
|
// 远程挂接逻辑不变 |
||||
|
const params = { |
||||
|
account: this.form.filePath.account, |
||||
|
caddress: this.form.filePath.caddress, |
||||
|
cpassword: this.form.filePath.cpassword, |
||||
|
cport: this.form.filePath.cport, |
||||
|
categoryId: this.selectedCategory.id, |
||||
|
fondsId: this.selectedCategory.fondsId, |
||||
|
checkRepeatType: this.form.checkRepeatType, |
||||
|
matchingMode: this.form.matchingMode, |
||||
|
fields: newhitchRemark, |
||||
|
protocol: this.form.protocol, |
||||
|
zipFilePath: this.form.filePath.rootDirectory + '/' + this.form.fileRemote |
||||
|
} |
||||
|
console.log(params) |
||||
|
remoteConnectiontUpload(this.baseApi + '/api/collect/remoteMount', params).then(res => { |
||||
|
if (res.data.code === 200) { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '操作失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCancel() |
||||
|
this.crud.refresh() |
||||
|
this.btnLoading = false |
||||
|
}).catch(err => { |
||||
|
console.error('请求异常', err) |
||||
|
this.$message.error('请求异常') |
||||
|
this.btnLoading = false |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
getFieldCommon() { |
||||
|
const params = { |
||||
|
categoryId: this.selectedCategory.id, |
||||
|
categoryLevel: this.collectLevel |
||||
|
} |
||||
|
FetchInitCategoryInputFieldByPid(params).then(data => { |
||||
|
this.fieldOptions = data.map(item => { |
||||
|
return { |
||||
|
id: item.id, |
||||
|
label: item.fieldCnName, |
||||
|
value: item.fieldName |
||||
|
} |
||||
|
}) |
||||
|
const archiveNoOption = this.fieldOptions.find(item => item.value === 'archive_no') |
||||
|
if (archiveNoOption) { |
||||
|
this.form.hitchFiled = archiveNoOption.value |
||||
|
this.selectFiled(archiveNoOption.value) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
selectFiled(val) { |
||||
|
if (!val) return |
||||
|
if (this.hitchRemarkArray.includes('$' + val + '$')) { |
||||
|
this.$message({ message: '请注意当前选择的挂接字段,在挂接详情内已存在!', offset: 8 }) |
||||
|
return |
||||
|
} |
||||
|
const lastItemIndex = this.hitchRemarkArray.length - 1 |
||||
|
const index = this.hitchRemarkArray.indexOf('*') |
||||
|
if (index !== -1) { |
||||
|
if (index === lastItemIndex) { |
||||
|
if (this.form.matchingMode === 2) { |
||||
|
this.hitchRemarkArray.unshift('$' + val + '$') |
||||
|
} else if (this.form.matchingMode === 3) { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} else { |
||||
|
this.hitchRemarkArray.splice(index > 0 ? index : 0, 0, '$' + val + '$') |
||||
|
} |
||||
|
} else { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} |
||||
|
} else { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
changeMatchedType(val) { |
||||
|
const symbol = '*' |
||||
|
const index = this.hitchRemarkArray.indexOf(symbol) |
||||
|
if (index !== -1) this.hitchRemarkArray.splice(index, 1) |
||||
|
if (val === 2) this.hitchRemarkArray.push(symbol) |
||||
|
if (val === 3) this.hitchRemarkArray.unshift(symbol) |
||||
|
}, |
||||
|
|
||||
|
changeProtocol(val) { |
||||
|
if (val) { |
||||
|
this.form.filePath = null |
||||
|
this.filePathOptions = [] |
||||
|
this.fileRemoteOptions = [] |
||||
|
FetchFindDetailsByProtocol({ protocol: val }).then(res => { |
||||
|
this.filePathOptions = res |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
changeFilePath(val) { |
||||
|
this.fileRemoteLoading = true |
||||
|
if (val) { |
||||
|
this.fileRemoteOptions = [] |
||||
|
FetchFileByRemoteConnection({ id: val.id }).then(res => { |
||||
|
if (res) { |
||||
|
this.fileRemoteOptions = res.map(item => ({ value: item, name: item })) |
||||
|
} else { |
||||
|
this.fileRemoteOptions = [] |
||||
|
} |
||||
|
this.fileRemoteLoading = false |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
removeTag(tag) { |
||||
|
const index = this.hitchRemarkArray.indexOf(tag) |
||||
|
if (index !== -1) this.hitchRemarkArray.splice(index, 1) |
||||
|
}, |
||||
|
|
||||
|
handleCancel() { |
||||
|
this.$refs.form.resetFields() |
||||
|
this.hitchRemarkArray = [] |
||||
|
this.uploadResultPaths = [] |
||||
|
this.fileSelectDialogVisible = false |
||||
|
this.localHitchVisible = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang='scss' scoped> |
||||
|
::v-deep .el-dialog { |
||||
|
width: 585px !important; |
||||
|
.el-dialog__body { |
||||
|
padding: 30px 0 !important; |
||||
|
} |
||||
|
} |
||||
|
.dialog-footer { |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
.file-list { |
||||
|
width: 400px; |
||||
|
height: 110px; |
||||
|
font-size: 12px; |
||||
|
color: #A6ADB6; |
||||
|
margin-top: 8px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
.file-item { |
||||
|
line-height: 26px; |
||||
|
// margin: 2px 0; |
||||
|
} |
||||
|
.tag-wrapper { |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
flex-wrap: wrap; |
||||
|
width: 400px; |
||||
|
min-height: 32px; |
||||
|
padding: 0 10px; |
||||
|
border-radius: 3px; |
||||
|
border: 1px solid #e6e8ed; |
||||
|
.el-tag { |
||||
|
margin-right: 6px; |
||||
|
} |
||||
|
} |
||||
|
.file_remote-style { |
||||
|
::v-deep .el-form-item__content { |
||||
|
width: 400px !important; |
||||
|
.el-radio-group { |
||||
|
display: block; |
||||
|
max-height: 60px; |
||||
|
overflow: hidden; |
||||
|
overflow-y: scroll; |
||||
|
.el-radio { |
||||
|
display: block; |
||||
|
padding: 3px 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.upload-wm { |
||||
|
height: 100px; |
||||
|
} |
||||
|
|
||||
|
/* 子弹窗样式 */ |
||||
|
.select-btn-group { |
||||
|
margin-bottom: 16px; |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
.dialog-upload-tip { |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
.select-file-list { |
||||
|
max-height: 300px; |
||||
|
overflow-y: auto; |
||||
|
border: 1px solid #e6e6e6; |
||||
|
padding: 8px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
.temp-file-item { |
||||
|
padding: 4px 0; |
||||
|
font-size: 14px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.file-name { |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
// .temp-del-icon { |
||||
|
// color: #f56c6c; |
||||
|
// cursor: pointer; |
||||
|
// margin-left: 10px; |
||||
|
// } |
||||
|
.empty-tip { |
||||
|
text-align: center; |
||||
|
color: #999; |
||||
|
padding: 20px 0; |
||||
|
} |
||||
|
.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; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,504 @@ |
|||||
|
<template> |
||||
|
<!-- 本地挂接 / 远程挂接 --> |
||||
|
<el-dialog :title="connectionTitle" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="localHitchVisible" :before-close="handleCancel" @opened="opened"> |
||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="120px"> |
||||
|
<el-form-item label="重复验证方式" prop="checkRepeatType"> |
||||
|
<el-select v-model="form.checkRepeatType" placeholder="请选择" style="width: 400px;"> |
||||
|
<el-option |
||||
|
v-for="item in typeOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="挂接字段" prop="hitchFiled"> |
||||
|
<el-select v-model="form.hitchFiled" placeholder="请选择" style="width: 400px;" @change="selectFiled"> |
||||
|
<el-option |
||||
|
v-for="item in fieldOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="挂接详情" prop="fields"> |
||||
|
<div class="tag-wrapper"> |
||||
|
<el-tag v-for="item in hitchRemarkArray" :key="item" :closable="item !== '*'" @close="removeTag(item)"> |
||||
|
{{ item }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
<el-input v-model="form.fields" style="display: none;" type="hidden" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="匹配模式" prop="matchingMode"> |
||||
|
<el-select v-model="form.matchingMode" placeholder="请选择" style="width: 400px;" @change="changeMatchedType"> |
||||
|
<el-option |
||||
|
v-for="item in matchedOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="!connectionType" label="上传附件" prop="file" class="upload-wm"> |
||||
|
<!-- <div class="upload-bgColor"> |
||||
|
<input class="upload-input" type="file" accept=".zip" style="width: 110px; height: 32px;" @change="handleFile"> |
||||
|
<el-button size="small" type="primary"><i class="iconfont icon-shangchuan" />点击上传</el-button> |
||||
|
</div> --> |
||||
|
<div v-for="item in fileList" :key="item.name" class="file-list"> |
||||
|
<i class="iconfont icon-xiaowenjian" /> |
||||
|
<!-- {{ item.file_name }} --> |
||||
|
{{ item }} |
||||
|
</div> |
||||
|
<el-input v-show="false" v-model="form.file" /> |
||||
|
<!-- 大文件上传 --> |
||||
|
<PreUpload ref="uploadBigRef" is-batch-mount="true" :selected-category="selectedCategory" @onUploadSuccess="handleSuccessResource" @onUploadError="handleErrorResource" /> |
||||
|
<!-- <div class="file-list"> |
||||
|
<i class="iconfont icon-xiaowenjian" /> |
||||
|
{{ form.file && form.file.name }} |
||||
|
</div> --> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="connectionType" label="挂接类型" prop="protocol"> |
||||
|
<el-select v-model="form.protocol" placeholder="请选择" style="width: 400px;" @change="changeProtocol"> |
||||
|
<el-option |
||||
|
v-for="item in batchTypeOptions" |
||||
|
:key="item.value" |
||||
|
:label="item.label" |
||||
|
:value="item.value" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="connectionType" label="文件路径" prop="filePath"> |
||||
|
<el-select v-model="form.filePath" placeholder="请选择" value-key="id" style="width: 400px;" @change="changeFilePath"> |
||||
|
<el-option |
||||
|
v-for="(item,index) in filePathOptions" |
||||
|
:key="index" |
||||
|
:label="item.name" |
||||
|
:value="item" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item |
||||
|
v-if="connectionType" |
||||
|
label="" |
||||
|
prop="fileRemote" |
||||
|
class="file_remote-style" |
||||
|
:rules="form.filePath !== null ? rules.fileRemote : []" |
||||
|
:required="form.filePath !== null" |
||||
|
> |
||||
|
<el-button v-if="form.filePath && fileRemoteLoading" type="primary" :loading="fileRemoteLoading" style="border: none; display: block; padding: 0 0 4px 0;">文件列表加载中</el-button> |
||||
|
<el-radio-group v-model="form.fileRemote"> |
||||
|
<el-radio |
||||
|
v-for="item in fileRemoteOptions" |
||||
|
:key="item.value" |
||||
|
:value="item.value" |
||||
|
:label="item.name" |
||||
|
>{{ item.name }}</el-radio> |
||||
|
</el-radio-group> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="handleCancel">取消</el-button> |
||||
|
<el-button :loading="btnLoading" type="primary" @click.native="handleComfired">确定</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { crud } from '@crud/crud' |
||||
|
import { FetchInitCategoryInputFieldByPid } from '@/api/system/category/category' |
||||
|
import { FetchFindDetailsByProtocol, FetchFileByRemoteConnection } from '@/api/system/remoteConnection' |
||||
|
import { batchMountUpload, remoteConnectiontUpload } from '@/utils/upload' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import PreUpload from '../../../components/category/preUpload.vue' |
||||
|
|
||||
|
export default { |
||||
|
name: 'LocalHitch', |
||||
|
components: { PreUpload }, |
||||
|
mixins: [crud()], |
||||
|
props: { |
||||
|
selectedCategory: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
btnLoading: false, |
||||
|
connectionTitle: '本地挂接', |
||||
|
localHitchVisible: false, |
||||
|
hitchRemarkArray: [], |
||||
|
connectionType: 0, |
||||
|
form: { |
||||
|
checkRepeatType: 1, |
||||
|
hitchFiled: null, |
||||
|
matchingMode: 2, |
||||
|
fields: null, |
||||
|
file: null, |
||||
|
protocol: null, |
||||
|
filePath: null, |
||||
|
fileRemote: null |
||||
|
}, |
||||
|
upfilePath: null, |
||||
|
fileList: [], |
||||
|
typeOptions: [ |
||||
|
{ |
||||
|
value: 1, |
||||
|
label: '跳过' |
||||
|
}, |
||||
|
{ |
||||
|
value: 2, |
||||
|
label: '覆盖' |
||||
|
}, |
||||
|
{ |
||||
|
value: 3, |
||||
|
label: '追加' |
||||
|
} |
||||
|
], |
||||
|
matchedOptions: [ |
||||
|
{ |
||||
|
value: 1, |
||||
|
label: '全量匹配' |
||||
|
}, |
||||
|
{ |
||||
|
value: 2, |
||||
|
label: '前缀模糊匹配' |
||||
|
}, |
||||
|
{ |
||||
|
value: 3, |
||||
|
label: '后缀模糊匹配' |
||||
|
} |
||||
|
], |
||||
|
batchTypeOptions: [ |
||||
|
{ |
||||
|
value: 1, |
||||
|
label: '服务端挂接' |
||||
|
}, |
||||
|
{ |
||||
|
value: 2, |
||||
|
label: 'FTP挂接' |
||||
|
}, |
||||
|
{ |
||||
|
value: 3, |
||||
|
label: 'SFTP挂接' |
||||
|
} |
||||
|
], |
||||
|
fieldOptions: [], |
||||
|
filePathOptions: [], |
||||
|
fileRemoteLoading: false, |
||||
|
fileRemoteOptions: [], |
||||
|
rules: { |
||||
|
checkRepeatType: [ |
||||
|
{ required: true, message: '请选择', trigger: 'change' } |
||||
|
], |
||||
|
hitchFiled: [ |
||||
|
{ required: true, message: '请选择', trigger: 'change' } |
||||
|
], |
||||
|
fields: [ |
||||
|
{ required: true, message: '请选择上面挂接字段生成挂接详情', trigger: 'blur' } |
||||
|
], |
||||
|
matchingMode: [ |
||||
|
{ required: true, message: '请选择', trigger: 'change' } |
||||
|
], |
||||
|
protocol: [ |
||||
|
{ required: true, message: '请选择', trigger: 'change' } |
||||
|
], |
||||
|
filePath: [ |
||||
|
{ required: true, message: '请选择', trigger: 'change' } |
||||
|
], |
||||
|
fileRemote: [ |
||||
|
{ required: true, message: '请选择指定文件', trigger: 'change' } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'baseApi' |
||||
|
]), |
||||
|
collectLevel() { |
||||
|
if (this.selectedCategory.arrangeType === 1) { |
||||
|
return 3 |
||||
|
} else if (this.selectedCategory.arrangeType === 2) { |
||||
|
return 2 |
||||
|
} else if (this.selectedCategory.arrangeType === 3) { |
||||
|
return 1 |
||||
|
} |
||||
|
return null |
||||
|
} |
||||
|
}, |
||||
|
created() { |
||||
|
}, |
||||
|
mounted() { |
||||
|
}, |
||||
|
methods: { |
||||
|
opened() { |
||||
|
this.getFieldCommon() |
||||
|
}, |
||||
|
handleSuccessResource(filePath, fileName, jsonArrayToSend) { |
||||
|
console.log('handleSuccessResource', filePath) |
||||
|
console.log('handleSuccessResource', fileName) |
||||
|
console.log('handleSuccessResource', jsonArrayToSend) |
||||
|
// console.log('handleSuccessResource', jsonArrayToSend[0].fileJsonString) |
||||
|
|
||||
|
// const fileJson = JSON.parse(jsonArrayToSend[0].fileJsonString) |
||||
|
// fileJson[0].file_path = filePath |
||||
|
// fileJson[0].is_quote = null |
||||
|
// fileJson[0].last_modified = jsonArrayToSend[0].last_modified |
||||
|
this.fileList = fileName |
||||
|
this.upfilePath = filePath |
||||
|
}, |
||||
|
handleErrorResource(res) { |
||||
|
console.log('handleErrorResource', res) |
||||
|
}, |
||||
|
handleFile(event) { |
||||
|
const files = event.target.files |
||||
|
for (let i = 0; i < files.length; i++) { |
||||
|
this.fileList = [] |
||||
|
this.fileList.push(files[i]) |
||||
|
} |
||||
|
}, |
||||
|
handleComfired() { |
||||
|
if (!this.connectionType) { |
||||
|
if (this.fileList.length === 0) { |
||||
|
this.$message({ message: '请先选择相关文件!', offset: 8 }) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (this.hitchRemarkArray.length === 1 && this.hitchRemarkArray[0] === '*') { |
||||
|
this.form.fields = '' |
||||
|
} else { |
||||
|
this.form.fields = this.hitchRemarkArray.join('') |
||||
|
} |
||||
|
this.btnLoading = true |
||||
|
this.$refs['form'].validate((valid) => { |
||||
|
console.log('this.selectedCategory', this.selectedCategory) |
||||
|
if (valid) { |
||||
|
let newhitchRemark = this.hitchRemarkArray.map(item => item.replace(/^\$|\$$|\*$/g, '')) |
||||
|
newhitchRemark = newhitchRemark.filter(item => item !== '') |
||||
|
|
||||
|
if (!this.connectionType) { |
||||
|
const params = { |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'mountType': 1, // 挂接方式 1.本地挂接 2.远程挂接 3.上传目录 (目前只有1) |
||||
|
'checkRepeatType': this.form.checkRepeatType, |
||||
|
'matchingMode': this.form.matchingMode, |
||||
|
'fields': newhitchRemark, |
||||
|
'fondsId': this.selectedCategory.fondsId, |
||||
|
'upfilePath': this.upfilePath |
||||
|
} |
||||
|
console.log(params) |
||||
|
batchMountUpload(this.baseApi + '/api/collect/batchMount', |
||||
|
params |
||||
|
).then(res => { |
||||
|
if (res.data.code === 200) { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '上传附件失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCancel() |
||||
|
this.crud.refresh() |
||||
|
this.btnLoading = false |
||||
|
}) |
||||
|
} else { |
||||
|
const params = { |
||||
|
'account': this.form.filePath.account, |
||||
|
'caddress': this.form.filePath.caddress, |
||||
|
'cpassword': this.form.filePath.cpassword, |
||||
|
'cport': this.form.filePath.cport, |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'fondsId': this.selectedCategory.fondsId, |
||||
|
'checkRepeatType': this.form.checkRepeatType, |
||||
|
'matchingMode': this.form.matchingMode, |
||||
|
'fields': newhitchRemark, |
||||
|
'protocol': this.form.protocol, |
||||
|
'zipFilePath': this.form.filePath.rootDirectory + '/' + this.form.fileRemote // rootDirectory+勾选的文件名称 组成一个路径 |
||||
|
} |
||||
|
console.log(params) |
||||
|
remoteConnectiontUpload(this.baseApi + '/api/collect/remoteMount', |
||||
|
params |
||||
|
).then(res => { |
||||
|
if (res.data.code === 200) { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '操作失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCancel() |
||||
|
this.crud.refresh() |
||||
|
this.btnLoading = false |
||||
|
}) |
||||
|
} |
||||
|
} else { |
||||
|
this.btnLoading = false |
||||
|
return false |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
getFieldCommon() { |
||||
|
const params = { |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'categoryLevel': this.collectLevel |
||||
|
} |
||||
|
FetchInitCategoryInputFieldByPid(params).then(data => { |
||||
|
this.fieldOptions = data.map((item, index) => { |
||||
|
const json = {} |
||||
|
json.id = item.id |
||||
|
json.label = item.fieldCnName |
||||
|
json.value = item.fieldName |
||||
|
return json |
||||
|
}) |
||||
|
// 查找档号(archive_no)选项并设置为默认值 |
||||
|
const archiveNoOption = this.fieldOptions.find(item => item.value === 'archive_no') |
||||
|
if (archiveNoOption) { |
||||
|
this.form.hitchFiled = archiveNoOption.value |
||||
|
this.selectFiled(archiveNoOption.value) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
selectFiled(val) { |
||||
|
if (val) { |
||||
|
if (this.hitchRemarkArray && this.hitchRemarkArray.includes('$' + val + '$')) { |
||||
|
this.$message({ message: '请注意当前选择的挂接字段,在挂接详情内已存在!', offset: 8 }) |
||||
|
} else { |
||||
|
const lastItemIndex = this.hitchRemarkArray.length - 1 |
||||
|
const index = this.hitchRemarkArray.indexOf('*') |
||||
|
if (index !== -1) { |
||||
|
if (index === lastItemIndex) { |
||||
|
if (this.form.matchingMode === 2) { |
||||
|
this.hitchRemarkArray.unshift('$' + val + '$') |
||||
|
} else if (this.form.matchingMode === 3) { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} else { |
||||
|
this.hitchRemarkArray.splice(index > 0 ? index : 0, 0, '$' + val + '$') |
||||
|
} |
||||
|
} else { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} |
||||
|
} else { |
||||
|
this.hitchRemarkArray.push('$' + val + '$') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
changeMatchedType(val) { |
||||
|
const symbol = '*' |
||||
|
const index = this.hitchRemarkArray.indexOf(symbol) |
||||
|
if (index !== -1) { |
||||
|
this.hitchRemarkArray.splice(index, 1) |
||||
|
} |
||||
|
if (val === 2) { |
||||
|
this.hitchRemarkArray.push(symbol) |
||||
|
} else if (val === 3) { |
||||
|
this.hitchRemarkArray.unshift(symbol) |
||||
|
} |
||||
|
}, |
||||
|
changeProtocol(val) { |
||||
|
if (val) { |
||||
|
this.form.filePath = null |
||||
|
this.filePathOptions = [] |
||||
|
this.fileRemoteOptions = [] |
||||
|
FetchFindDetailsByProtocol({ 'protocol': val }).then(res => { |
||||
|
this.filePathOptions = res |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
changeFilePath(val) { |
||||
|
this.fileRemoteLoading = true |
||||
|
if (val) { |
||||
|
this.fileRemoteOptions = [] |
||||
|
FetchFileByRemoteConnection({ 'id': val.id }).then(res => { |
||||
|
if (res) { |
||||
|
this.fileRemoteOptions = res.map(item => { |
||||
|
const json = {} |
||||
|
json.value = item |
||||
|
json.name = item |
||||
|
return json |
||||
|
}) |
||||
|
} else { |
||||
|
this.fileRemoteOptions = [] |
||||
|
} |
||||
|
|
||||
|
this.fileRemoteLoading = false |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
removeTag(tag) { |
||||
|
const index = this.hitchRemarkArray.indexOf(tag) |
||||
|
if (index !== -1) { |
||||
|
this.hitchRemarkArray.splice(index, 1) |
||||
|
} |
||||
|
}, |
||||
|
handleCancel() { |
||||
|
this.$refs.form.resetFields() |
||||
|
this.hitchRemarkArray = [] |
||||
|
this.fileList = [] |
||||
|
this.filePathOptions = [] |
||||
|
this.fileRemoteOptions = [] |
||||
|
this.localHitchVisible = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang='scss' scoped> |
||||
|
::v-deep .el-dialog{ |
||||
|
width: 585px !important; |
||||
|
.el-dialog__body{ |
||||
|
padding: 30px 0 !important; |
||||
|
} |
||||
|
} |
||||
|
.dialog-footer{ |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
|
||||
|
.file-list{ |
||||
|
width: 400px; |
||||
|
font-size: 12px; |
||||
|
color: #A6ADB6; |
||||
|
} |
||||
|
.tag-wrapper{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
flex-wrap: wrap; |
||||
|
width: 400px; |
||||
|
min-height: 32px; |
||||
|
padding: 0 10px; |
||||
|
border-radius: 3px; |
||||
|
border: 1px solid #e6e8ed; |
||||
|
.el-tag{ |
||||
|
margin-right: 6px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.file_remote-style{ |
||||
|
::v-deep .el-form-item__content{ |
||||
|
width:400px !important; |
||||
|
.el-radio-group{ |
||||
|
display: block; |
||||
|
max-height: 60px; |
||||
|
overflow: hidden; |
||||
|
overflow-y: scroll; |
||||
|
.el-radio{ |
||||
|
display: block; |
||||
|
padding: 3px 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.upload-wm{ |
||||
|
height: 100px; |
||||
|
} |
||||
|
::v-deep .uploader-big{ |
||||
|
margin-left: 0 !important; |
||||
|
} |
||||
|
::v-deep .upload_process_box{ |
||||
|
right: 0 !important; |
||||
|
left: 120px !important; |
||||
|
top: 0 !important; |
||||
|
width: 300px !important; |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,850 @@ |
|||||
|
<template> |
||||
|
<div class="upload-minio"> |
||||
|
<el-button |
||||
|
v-if="isPreFile !== 'true' && archivesType === 'add'" |
||||
|
type="primary" |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
icon="el-icon-upload" |
||||
|
@click="uploadMinioVisible=true" |
||||
|
> |
||||
|
选择文件 |
||||
|
</el-button> |
||||
|
<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"> |
||||
|
<!-- 批量挂载模式:显示两个按钮:选择多文件 / 选择文件夹 --> |
||||
|
<template v-if="isBatchMount === 'true'"> |
||||
|
<el-button size="small" @click="triggerFileInput">选择多文件</el-button> |
||||
|
<el-button size="small" @click="triggerFolderInput">选择文件夹</el-button> |
||||
|
<p style="font-size: 12px; margin-top: 10px;">支持多选文件 或 选择文件夹</p> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<p>点击上传(可多文件上传)</p> |
||||
|
<div style="margin: 20px 0 0 0;"> |
||||
|
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<!-- 多选文件input --> |
||||
|
<input |
||||
|
ref="fileInput" |
||||
|
type="file" |
||||
|
multiple |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
:accept="fileAcceptType" |
||||
|
class="file-input" |
||||
|
style="display: none;" |
||||
|
@change="handleFileSelect" |
||||
|
> |
||||
|
<!-- 选择文件夹input webkitdirectory --> |
||||
|
<input |
||||
|
ref="folderInput" |
||||
|
type="file" |
||||
|
webkitdirectory |
||||
|
directory |
||||
|
multiple |
||||
|
:disabled="!isCaValid || isCheckingCa" |
||||
|
class="file-input" |
||||
|
style="display: none;" |
||||
|
@change="handleFolderSelect" |
||||
|
> |
||||
|
</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> |
||||
|
<!-- 文件夹来源展示相对路径 --> |
||||
|
<span v-if="fileItem.relativePath" style="color:#999;font-size:12px;margin-left:6px"> |
||||
|
{{ fileItem.relativePath }} |
||||
|
</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: '' |
||||
|
}, |
||||
|
archivesType: { |
||||
|
type: String, |
||||
|
default: 'add' |
||||
|
} |
||||
|
}, |
||||
|
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() { |
||||
|
// 批量挂载不再限制只能zip |
||||
|
return this.isBatchMount === 'true' ? '' : '' |
||||
|
}, |
||||
|
// 新增计算属性:动态获取分片接口路径 |
||||
|
chunkApiPath() { |
||||
|
return this.isBatchMount === 'true' ? '/api/collect/chunkZip' : '/api/minioUpload/chunk' |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.getCheckCaValidity() // 初始化时自动校验CA证书 |
||||
|
}, |
||||
|
methods: { |
||||
|
convertBytesToKB(bytes) { |
||||
|
// 1KB = 1024字节 |
||||
|
const kb = bytes / 1024 |
||||
|
// 四舍五入后如果小于1则返回1,否则返回四舍五入的值 |
||||
|
const result = Math.round(kb) |
||||
|
return result < 1 ? 1 : result |
||||
|
}, |
||||
|
triggerFileInput() { |
||||
|
this.$refs.fileInput.click() |
||||
|
}, |
||||
|
triggerFolderInput() { |
||||
|
this.$refs.folderInput.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 |
||||
|
// 3. 非批量挂载时,禁止上传压缩包 |
||||
|
if (this.isBatchMount !== 'true') { |
||||
|
const compressedExtensions = ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'] |
||||
|
const hasCompressedFile = selectedFiles.some(file => |
||||
|
compressedExtensions.some(ext => file.name.toLowerCase().endsWith(ext)) |
||||
|
) |
||||
|
if (hasCompressedFile) { |
||||
|
this.showMessage('不可上传压缩包文件', 'error') |
||||
|
e.target.value = '' |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
// 初始化文件上传状态(仅初始化,不上传) |
||||
|
const newFileList = selectedFiles.map(file => ({ |
||||
|
file, |
||||
|
relativePath: '', // 普通文件无相对路径 |
||||
|
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] |
||||
|
this.uploadMinioVisible = true |
||||
|
// 清空input的文件选择(避免重复选择同一文件不触发change) |
||||
|
e.target.value = '' |
||||
|
}, |
||||
|
/** |
||||
|
* 选择文件夹处理函数:读取文件夹内全部文件,带webkitRelativePath |
||||
|
* @param {Event} e |
||||
|
*/ |
||||
|
async handleFolderSelect(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 |
||||
|
|
||||
|
const newFileList = selectedFiles.map(file => ({ |
||||
|
file, |
||||
|
relativePath: file.webkitRelativePath, // 保存文件夹内相对路径,例:folder/a/b.txt |
||||
|
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] |
||||
|
this.uploadMinioVisible = true |
||||
|
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 { |
||||
|
let response |
||||
|
if (this.isBatchMount === 'true') { |
||||
|
// ==========批量挂载模式修改:不再只处理单个zip文件,处理全部validFiles数组,带上relativePath========== |
||||
|
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 = this.convertBytesToKB(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) |
||||
|
|
||||
|
json.categoryId = this.selectedCategory.id |
||||
|
|
||||
|
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
||||
|
const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) |
||||
|
if (!chunksExist) { |
||||
|
throw new Error(`【${file.name}】部分分片未上传完成,无法合并`) |
||||
|
} |
||||
|
json.archivesId = this.arcId |
||||
|
json.identifier = fileItem.md5 |
||||
|
json.filename = file.name |
||||
|
json.totalChunks = totalChunks |
||||
|
json.totalSize = this.convertBytesToKB(file.size) |
||||
|
json.fileJsonString = JSON.stringify(jsonArray) |
||||
|
// 新增:文件夹相对路径,传给后端用于重建目录 |
||||
|
json.relativePath = fileItem.relativePath || '' |
||||
|
return json |
||||
|
}) |
||||
|
const jsonArray = await Promise.all(processFiles) |
||||
|
console.log('jsonArray', jsonArray) |
||||
|
// const mergeApiUrl = `${this.baseApi}/api/collect/mergeZip` |
||||
|
// response = await axios.post(mergeApiUrl, 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) { |
||||
|
// 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}`) |
||||
|
// this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArray) |
||||
|
// this.uploadMinioVisible = false |
||||
|
// this.fileList = [] // 清空列表,避免下次打开弹框显示旧数据 |
||||
|
// } else { |
||||
|
// this.btnLoading = false |
||||
|
// throw new Error(response.data.msg || '合并失败') |
||||
|
// } |
||||
|
} 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 = this.convertBytesToKB(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) |
||||
|
if (this.isBatchMount === 'true') { |
||||
|
json.categoryId = this.selectedCategory.id |
||||
|
} else { |
||||
|
json.documentId = this.selectedDocument.id |
||||
|
} |
||||
|
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
||||
|
const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) |
||||
|
if (!chunksExist) { |
||||
|
throw new Error(`【${file.name}】部分分片未上传完成,无法合并`) |
||||
|
} |
||||
|
json.archivesId = this.arcId |
||||
|
json.identifier = fileItem.md5 |
||||
|
json.filename = file.name |
||||
|
json.totalChunks = totalChunks |
||||
|
json.totalSize = this.convertBytesToKB(file.size) |
||||
|
json.fileJsonString = JSON.stringify(jsonArray) |
||||
|
return json |
||||
|
}) |
||||
|
const jsonArray = await Promise.all(processFiles) |
||||
|
const mergeApiUrl = `${this.baseApi}/api/minioUpload/merge` |
||||
|
response = await axios.post(mergeApiUrl, 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) { |
||||
|
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}`) |
||||
|
this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArray) |
||||
|
this.uploadMinioVisible = false |
||||
|
this.fileList = [] // 清空列表,避免下次打开弹框显示旧数据 |
||||
|
} else { |
||||
|
this.btnLoading = false |
||||
|
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() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</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> |
||||
@ -0,0 +1,167 @@ |
|||||
|
<template> |
||||
|
<div class="process-container"> |
||||
|
<div v-for="item in filteredFlowList" :key="item.id" class="process-item"> |
||||
|
<span>{{ item.name }}</span> |
||||
|
<p>{{ item.flowName }}<i class="iconfont icon-shezhi" @click="toProcessSelected(item)" /></p> |
||||
|
<el-switch v-model="item.status" class="isEnable-chapter" @change="changeCategoryState(item)" /> |
||||
|
</div> |
||||
|
<eForm ref="cuform" :selected-category="selectedCategory" :edit-info-flow="editInfoFlow" @refresh="getInitCategoryFlowBase" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { FetchInitCategoryFlowBase, FetchInitFlowByCategoryId, FetchChangeCategoryState, FetchgetCategoryFlow } from '@/api/system/category/process' |
||||
|
import eForm from './module/form' |
||||
|
export default { |
||||
|
name: 'ProcessManage', |
||||
|
components: { eForm }, |
||||
|
props: { |
||||
|
selectedCategory: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
flowBaseList: [], |
||||
|
flowSetting: [], |
||||
|
editInfoFlow: null |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
filteredFlowList() { |
||||
|
if (this.selectedCategory.pid === '0' && this.selectedCategory.isType === 1) { |
||||
|
return this.flowBaseList.filter(item => item.id === '5') |
||||
|
} else { |
||||
|
return this.flowBaseList.filter(item => item.id !== '5') |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.getInitCategoryFlowBase() |
||||
|
}, |
||||
|
methods: { |
||||
|
getInitCategoryFlowBase() { |
||||
|
FetchInitCategoryFlowBase().then((res) => { |
||||
|
this.flowBaseList = res.map(item => { |
||||
|
return { |
||||
|
...item, |
||||
|
status: false |
||||
|
} |
||||
|
}) |
||||
|
this.getInitFlowByCategoryId() |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
getInitFlowByCategoryId() { |
||||
|
console.log(this.selectedCategory) |
||||
|
FetchInitFlowByCategoryId({ 'categoryId': this.selectedCategory.pid !== '0' ? this.selectedCategory.id : null }).then((res) => { |
||||
|
this.flowSetting = res |
||||
|
this.flowBaseList.forEach(item => { |
||||
|
this.flowSetting.forEach(v => { |
||||
|
if (item.id === v.flowManageId) { |
||||
|
item.status = !!v.status |
||||
|
item.flowName = v.flowName |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
toProcessSelected(item) { |
||||
|
const params = { |
||||
|
'categoryId': this.selectedCategory.pid !== '0' ? this.selectedCategory.id : null, |
||||
|
'manageId': item.id |
||||
|
} |
||||
|
FetchgetCategoryFlow(params).then((res) => { |
||||
|
if (res) { |
||||
|
// 判断所有值是否都为 null |
||||
|
if (this.allValuesNull(res)) { |
||||
|
// console.log('所有值都为 null') 新增 |
||||
|
this.editInfoFlow = null |
||||
|
} else { |
||||
|
// console.log('不是所有值都为 null') 编辑 |
||||
|
this.editInfoFlow = res |
||||
|
} |
||||
|
} else { |
||||
|
this.editInfoFlow = null |
||||
|
this.$message({ message: '数据请求失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
console.log('this.editInfoFlow', this.editInfoFlow) |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.cuform.cuDialogVisible = true |
||||
|
this.$refs.cuform.setPocessCurrent = item |
||||
|
this.$refs.cuform.getEditInfoFlow = this.editInfoFlow ? this.editInfoFlow : {} |
||||
|
this.$refs.cuform.handleEditInfoFlow(this.editInfoFlow) |
||||
|
}) |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
changeCategoryState(item) { |
||||
|
const filterItem = this.flowSetting.find(v => item.id === v.flowManageId) |
||||
|
const params = { |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'id': filterItem.id, |
||||
|
'status': item.status ? 1 : 0 |
||||
|
} |
||||
|
FetchChangeCategoryState(params).then((res) => { |
||||
|
if (res === 'SUCCESS') { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '操作失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
allValuesNull(jsonObject) { |
||||
|
for (const key in jsonObject) { |
||||
|
if (jsonObject[key] !== null) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang='scss' scoped> |
||||
|
.process-item{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
padding: 36px 40px 0 40px; |
||||
|
font-size: 14px; |
||||
|
height: 32px; |
||||
|
line-height: 32px; |
||||
|
margin-bottom: 20px; |
||||
|
span{ |
||||
|
display: block; |
||||
|
margin-right: 12px; |
||||
|
} |
||||
|
p{ |
||||
|
position: relative; |
||||
|
width: 324px; |
||||
|
height: 32px; |
||||
|
line-height: 32px; |
||||
|
padding: 0 13px; |
||||
|
margin-right: 16px; |
||||
|
color: #0C0E1E; |
||||
|
background-color: #F5F7FA; |
||||
|
i{ |
||||
|
position: absolute; |
||||
|
right: 10px; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
color: #0348F3; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,374 @@ |
|||||
|
<template> |
||||
|
<el-dialog class="process-dialog" :visible.sync="cuDialogVisible" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body title="工作流选择" :before-close="handleCloseDialog"> |
||||
|
<div class="setting-dialog"> |
||||
|
<el-form ref="form" class="process-form" inline :model="form" :rules="rules" size="small" label-width="80px"> |
||||
|
<div class="process-template"> |
||||
|
<el-form-item label="流程模板" prop="model"> |
||||
|
<el-select |
||||
|
v-model="form.model" |
||||
|
placeholder="请选择" |
||||
|
style="width: 580px;" |
||||
|
value-key="modelId" |
||||
|
@change="selectModel" |
||||
|
> |
||||
|
<el-option |
||||
|
v-for="item in modelOptions" |
||||
|
:key="item.modelId" |
||||
|
:label="item.deployName" |
||||
|
:value="item" |
||||
|
/> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<div v-loading="loadingImg" class="template-img"> |
||||
|
<img :src="srcImg" alt="" :onerror="defaultImg"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="process-task"> |
||||
|
<div class="task-tip">任务需指定执行岗位。当流程进行到该任务时,指定岗位下同一全宗的管理员可进入流程操作</div> |
||||
|
<div class="task-list"> |
||||
|
<div v-for="(task,index) in taskList" :key="index" class="task-item"> |
||||
|
<p>任务{{ index+1 }} :{{ task.nodeName }}</p> |
||||
|
<el-form-item label="执行岗位" :prop="`task${index+1}`" :rules="dynamicRules(index)"> |
||||
|
<el-select v-model="form[`task${index+1}`]" value-key="id" @change="selectJob($event,index)"> |
||||
|
<el-option v-for="item in jobOptions" :key="item.value" :label="item.label" :value="item.value" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<div v-if="form[`task${index+1}`]" class="detail-arrow" @click="task.isExpend = !task.isExpend"><i :class="['iconfont icon-xiala', task.isExpend ? 'arrow-up':'']" /></div> |
||||
|
<el-collapse-transition> |
||||
|
<el-table v-if="task.isExpend" :data="task.userTableData" border style="width: 100%"> |
||||
|
<el-table-column prop="fondsName" label="全宗" align="center" /> |
||||
|
<el-table-column prop="deptsName" label="部门" align="center" /> |
||||
|
<el-table-column prop="username" label="用户名" align="center" /> |
||||
|
</el-table> |
||||
|
</el-collapse-transition> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button @click="handleCloseDialog">取消</el-button> |
||||
|
<el-button type="primary" :loading="loading" @click="save">保存</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { FetchInitFlowAll, FetchFindFolwImgByModelId, FetchUserTaskAllByDeployId } from '@/api/system/flowable' |
||||
|
import { FetchPostAll, FetchFindUserByPost } from '@/api/system/job' |
||||
|
import { FetchFlowbinding } from '@/api/system/category/process' |
||||
|
|
||||
|
export default { |
||||
|
props: { |
||||
|
selectedCategory: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
}, |
||||
|
editInfoFlow: { |
||||
|
type: Object, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
cuDialogVisible: false, |
||||
|
loading: false, |
||||
|
loadingImg: false, |
||||
|
form: { |
||||
|
model: null |
||||
|
}, |
||||
|
modelOptions: [], |
||||
|
srcImg: '', |
||||
|
defaultImg: 'this.src="' + require('@/assets/images/system/default-img2.png') + '"', |
||||
|
jobOptions: [ |
||||
|
{ |
||||
|
value: '', |
||||
|
label: '请选择' |
||||
|
} |
||||
|
], |
||||
|
rules: { |
||||
|
model: [ |
||||
|
{ required: true, message: '流程模板不可为空', trigger: 'change' } |
||||
|
] |
||||
|
}, |
||||
|
userTableData: [], |
||||
|
taskList: [], |
||||
|
selectFlowVal: null, |
||||
|
setPocessCurrent: null, |
||||
|
getEditInfoFlow: {} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
dynamicRules() { |
||||
|
return (index) => { |
||||
|
return [{ |
||||
|
required: true, |
||||
|
message: '执行岗位不可为空', |
||||
|
trigger: 'change' |
||||
|
}] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
// editInfoFlow: { |
||||
|
// handler(newVal, oldVal) { |
||||
|
// // this.handleEditInfoFlow(newVal) |
||||
|
// }, |
||||
|
// deep: true |
||||
|
// } |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.getAllFlowable() |
||||
|
this.getAllJob() |
||||
|
}, |
||||
|
methods: { |
||||
|
// handleEditInfoFlow(newVal) { |
||||
|
// console.log('getEditInfoFlow', this.getEditInfoFlow) |
||||
|
// const findItem = this.findIdByModelId(newVal.flowId) |
||||
|
// this.form.model = findItem |
||||
|
// this.selectModel(findItem, newVal) |
||||
|
// }, |
||||
|
// findIdByModelId(modelId) { |
||||
|
// const item = this.modelOptions.find(data => data.modelId === modelId) |
||||
|
// if (item) { |
||||
|
// return item |
||||
|
// } else { |
||||
|
// return null |
||||
|
// } |
||||
|
// }, |
||||
|
handleEditInfoFlow(newVal) { |
||||
|
console.log('编辑回显数据:', newVal) |
||||
|
|
||||
|
// 确保 form 存在 model 属性 |
||||
|
if (!this.form.model) { |
||||
|
this.form.model = null |
||||
|
} |
||||
|
|
||||
|
// 无数据直接清空返回 |
||||
|
if (!newVal || !newVal.flowId) { |
||||
|
this.form.model = null |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const findItem = this.findIdByModelId(newVal.flowId) |
||||
|
this.form.model = findItem |
||||
|
|
||||
|
this.$nextTick(() => { |
||||
|
if (findItem) { |
||||
|
this.selectModel(findItem, newVal) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
findIdByModelId(modelId) { |
||||
|
return this.modelOptions.find(data => data.modelId === modelId) || null |
||||
|
}, |
||||
|
getAllFlowable() { |
||||
|
FetchInitFlowAll().then((res) => { |
||||
|
this.modelOptions = res |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
selectModel(val, parentProcessVal) { |
||||
|
if (!parentProcessVal) { |
||||
|
this.getEditInfoFlow.categoryFlowDetails = [] |
||||
|
} |
||||
|
|
||||
|
this.selectFlowVal = val |
||||
|
this.loadingImg = true |
||||
|
FetchFindFolwImgByModelId({ 'processDefinitionId': val.procdefId }).then((res) => { |
||||
|
this.srcImg = 'data:image/jpeg;base64,' + res |
||||
|
this.loadingImg = false |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
FetchUserTaskAllByDeployId({ 'deployId': val.deployId }).then((res) => { |
||||
|
this.taskList = res.map((item, index) => { |
||||
|
this.$set(this.form, `task${index + 1}`, null) |
||||
|
return { |
||||
|
...item, |
||||
|
isExpend: false, |
||||
|
userTableData: [], |
||||
|
postId: null |
||||
|
} |
||||
|
}) |
||||
|
if (parentProcessVal) { |
||||
|
parentProcessVal.categoryFlowDetails.forEach((item, index) => { |
||||
|
this.$set(this.form, `task${index + 1}`, item.postId) |
||||
|
this.selectJob(item.postId, index) |
||||
|
}) |
||||
|
} |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
getAllJob() { |
||||
|
FetchPostAll().then((res) => { |
||||
|
this.jobOptions = res.map(item => { |
||||
|
const json = {} |
||||
|
json.value = item.id |
||||
|
json.label = item.postName |
||||
|
return json |
||||
|
}) |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
selectJob(val, index) { |
||||
|
this.$forceUpdate() |
||||
|
FetchFindUserByPost({ 'postId': val }).then((res) => { |
||||
|
this.taskList.forEach((item, i) => { |
||||
|
if (index === i) { |
||||
|
item.postId = val |
||||
|
item.userTableData = res |
||||
|
} |
||||
|
}) |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
}) |
||||
|
}, |
||||
|
save() { |
||||
|
this.loading = true |
||||
|
this.$refs['form'].validate((valid) => { |
||||
|
if (valid) { |
||||
|
const detailsDTOs = this.taskList.map((item, index) => { |
||||
|
const json = {} |
||||
|
json.id = this.getEditInfoFlow && this.getEditInfoFlow.categoryFlowDetails.length !== 0 ? this.getEditInfoFlow.categoryFlowDetails[index].id : null |
||||
|
json.postId = item.postId |
||||
|
json.taskId = item.nodeId |
||||
|
return json |
||||
|
}) |
||||
|
const params = { |
||||
|
// 'categoryId': this.selectedCategory.pid !== '0' ? this.selectedCategory.id : null, |
||||
|
'categoryId': this.selectedCategory.id, |
||||
|
'detailsDTOs': detailsDTOs, |
||||
|
'flowId': this.selectFlowVal.modelId, |
||||
|
'flowName': this.selectFlowVal.deployName, |
||||
|
'flowKey': this.selectFlowVal.modelKey, |
||||
|
'flowManageId': this.setPocessCurrent.id, |
||||
|
'id': this.getEditInfoFlow && this.getEditInfoFlow.id, |
||||
|
'status': this.getEditInfoFlow && this.getEditInfoFlow.status |
||||
|
} |
||||
|
console.log('params', params) |
||||
|
FetchFlowbinding(params).then((res) => { |
||||
|
if (res === 'SUCCESS') { |
||||
|
this.$message({ message: '操作成功', type: 'success', offset: 8 }) |
||||
|
} else { |
||||
|
this.$message({ message: '操作失败', type: 'error', offset: 8 }) |
||||
|
} |
||||
|
this.handleCloseDialog() |
||||
|
this.$emit('refresh') |
||||
|
this.loading = false |
||||
|
}).catch(err => { |
||||
|
console.log(err) |
||||
|
this.loading = false |
||||
|
}) |
||||
|
} else { |
||||
|
this.loading = false |
||||
|
return false |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
handleCloseDialog() { |
||||
|
// 重置为初始状态,而不是直接赋空对象 |
||||
|
this.form = { |
||||
|
model: null |
||||
|
} |
||||
|
this.srcImg = '' |
||||
|
this.taskList = [] |
||||
|
this.selectFlowVal = null |
||||
|
this.getEditInfoFlow = {} |
||||
|
this.cuDialogVisible = false |
||||
|
|
||||
|
// 清除校验状态(必须加) |
||||
|
if (this.$refs.form) { |
||||
|
this.$refs.form.clearValidate() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
.process-dialog{ |
||||
|
::v-deep .el-dialog{ |
||||
|
width: 1100px; |
||||
|
} |
||||
|
} |
||||
|
.process-form{ |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.process-template{ |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
.template-img{ |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: 690px; |
||||
|
height: 460px; |
||||
|
border: 1px solid #eee; |
||||
|
background-color: #EDEFF3; |
||||
|
overflow: hidden; |
||||
|
img{ |
||||
|
display: block; |
||||
|
margin: 0 auto; |
||||
|
// width: 100%; |
||||
|
} |
||||
|
} |
||||
|
.task-tip{ |
||||
|
margin-bottom: 20px; |
||||
|
padding-left: 24px; |
||||
|
font-size: 14px; |
||||
|
line-height: 18px; |
||||
|
color: #0c0e1e; |
||||
|
background: url('~@/assets/images/icon/tip-icon.png') no-repeat left top; |
||||
|
background-size: 18px 18px; |
||||
|
} |
||||
|
.task-list{ |
||||
|
height: 454px; |
||||
|
overflow: hidden; |
||||
|
overflow-y: scroll; |
||||
|
} |
||||
|
.task-item{ |
||||
|
p{ |
||||
|
line-height: 32px; |
||||
|
} |
||||
|
} |
||||
|
.detail-arrow{ |
||||
|
position: relative; |
||||
|
width: 64px; |
||||
|
height: 20px; |
||||
|
margin: -8px auto 10px auto; |
||||
|
background-color:#E8F2FF; |
||||
|
border-radius: 3px; |
||||
|
cursor: pointer; |
||||
|
i{ |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
top: 50%; |
||||
|
display: block; |
||||
|
width: 14px; |
||||
|
height: 14px; |
||||
|
margin-top: -7px; |
||||
|
margin-left: -7px; |
||||
|
text-align: center; |
||||
|
font-size: 12px; |
||||
|
color: #0348F3; |
||||
|
transition: all ease-in 0.4s; |
||||
|
transform: scale(0.7); |
||||
|
&.arrow-up { |
||||
|
transform: scale(0.7) rotate(180deg); |
||||
|
transform-origin: center; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
::v-deep .el-table__body-wrapper{ |
||||
|
border-bottom: 1px solid #dfe6ec !important; |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue