8 changed files with 1219 additions and 16 deletions
-
19src/views/collectReorganizi/collectionLibrary/anjuan/tableList.vue
-
9src/views/collectReorganizi/collectionLibrary/file/index.vue
-
9src/views/collectReorganizi/collectionLibrary/juannei/index.vue
-
3src/views/collectReorganizi/collectionLibrary/mixins/index.js
-
463src/views/collectReorganizi/collectionLibrary/module/advancedSearchModal.vue
-
113src/views/collectReorganizi/collectionLibrary/module/collectHeader.vue
-
610src/views/collectReorganizi/collectionLibrary/module/uploadOriginal/embedUpload.vue
-
9src/views/collectReorganizi/collectionLibrary/project/index.vue
@ -0,0 +1,463 @@ |
|||
<template> |
|||
<el-dialog |
|||
:visible.sync="visible" |
|||
title="高级检索" |
|||
width="1000px" |
|||
:close-on-click-modal="false" |
|||
:modal-append-to-body="false" |
|||
append-to-body |
|||
:before-close="handleClose" |
|||
> |
|||
<div class="advanced-search-modal"> |
|||
<el-form ref="form" inline :model="form" :rules="rules" size="small" label-width="70px"> |
|||
<el-form-item label="字段名" prop="field"> |
|||
<el-select v-model="form.field" value-key="id" style="width: 200px;"> |
|||
<el-option v-for="item in fieldOptions" :key="item.id" :label="item.label" :value="item" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="运算符" prop="symbol"> |
|||
<el-select v-model="form.symbol" value-key="value" placeholder="请选择" style="width: 200px;"> |
|||
<el-option |
|||
v-for="item in symbolOptions" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="检索值" prop="keyWord" :rules="getKeywordRules"> |
|||
<el-input v-model="form.keyWord" :type="inputType" style="width: 200px;" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<div class="advanced-btn"> |
|||
<el-button size="mini" @click="addConditionData"><i class="iconfont icon-xinzeng" />新增</el-button> |
|||
<el-button class="filter-refresh" size="mini" icon="el-icon-refresh-left" @click="resetQuery">重置</el-button> |
|||
</div> |
|||
<div class="search-condition"> |
|||
<h4> 检索条件</h4> |
|||
<div class="condition-main"> |
|||
<div class="condition-left"> |
|||
<el-button size="mini" :disabled="currentIndex===null" @click="deltCurrent(currentIndex)"><i class="iconfont icon-shanchu" />删除</el-button> |
|||
<el-button size="mini" icon="el-icon-top" :disabled="currentIndex === 0" @click="moveUp(currentIndex)">上移</el-button> |
|||
<el-button size="mini" icon="el-icon-bottom" :disabled="currentIndex === conditionData.length - 1" @click="moveDown(currentIndex)">下移</el-button> |
|||
</div> |
|||
<ul id="condition-container-modal" class="condition-content"> |
|||
<li v-for="(item, index) in conditionData" :id="'modal-element-id-' + index" :key="index" :class="currentIndex===index ? 'active': ''" @click="selectCurrent(index)"> |
|||
<span style="color:#0348F3">{{ item.field }}</span> |
|||
<span style="color:#ED4A41; margin:0 4px">{{ item.symbol }}</span> |
|||
<span v-if="item.symbol && (item.symbol === '包含'|| item.symbol === '不包含')" class="keyword-style"><i>'%</i>{{ item.keyWord }}<i>%'</i></span> |
|||
<span v-else-if="item.keyWord && isNaN(parseInt(item.keyWord))" class="keyword-style"><i>'</i>{{ item.keyWord }}<i>'</i></span> |
|||
<span v-else class="keyword-style">{{ item.keyWord }}</span> |
|||
<span>{{ item.connector }}</span> |
|||
<span>{{ item.bracket }}</span> |
|||
</li> |
|||
</ul> |
|||
<div class="condition-right"> |
|||
<el-button v-for="(item,index) in connectorList" :key="index" type="primary" size="mini" @click="addConnector(item)">{{ item }}</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div slot="footer" class="dialog-footer"> |
|||
<el-button @click="handleClose">取消</el-button> |
|||
<el-button type="primary" @click="handleSearch">检索</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
import { FetchInitCategoryInputFieldByPid } from '@/api/system/category/category' |
|||
|
|||
export default { |
|||
name: 'AdvancedSearchModal', |
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
collectLevel: { |
|||
type: Number, |
|||
default: 0 |
|||
}, |
|||
initialConditions: { |
|||
type: Array, |
|||
default: () => [] |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
form: { |
|||
field: null, |
|||
symbol: null, |
|||
keyWord: null |
|||
}, |
|||
fieldOptions: [], |
|||
symbolOptions: [ |
|||
{ |
|||
label: '包含', |
|||
value: 'like' |
|||
}, |
|||
{ |
|||
label: '不包含', |
|||
value: 'not like' |
|||
}, |
|||
{ |
|||
label: '等于', |
|||
value: '=' |
|||
}, |
|||
{ |
|||
label: '不等于', |
|||
value: '!=' |
|||
}, |
|||
{ |
|||
label: '为空', |
|||
value: 'is null' |
|||
}, |
|||
{ |
|||
label: '不为空', |
|||
value: 'is not null' |
|||
}, |
|||
{ |
|||
label: '大于', |
|||
value: '>' |
|||
}, |
|||
{ |
|||
label: '大于等于', |
|||
value: '>=' |
|||
}, |
|||
{ |
|||
label: '小于', |
|||
value: '<' |
|||
}, |
|||
{ |
|||
label: '小于等于', |
|||
value: '<=' |
|||
} |
|||
], |
|||
rules: { |
|||
field: [ |
|||
{ required: true, message: '请选择字段名', trigger: 'change' } |
|||
], |
|||
symbol: [ |
|||
{ required: true, message: '请选择运算符', trigger: 'change' } |
|||
] |
|||
}, |
|||
conditionData: [], |
|||
currentIndex: null, |
|||
connectorList: ['并且', '或者', '(', ')'] |
|||
} |
|||
}, |
|||
computed: { |
|||
getKeywordRules() { |
|||
if ((this.form.symbol && this.form.symbol.label === '为空') || (this.form.symbol && this.form.symbol.label === '不为空')) { |
|||
return [] |
|||
} else { |
|||
return [{ required: true, message: '请输入检索值', trigger: 'blur' }] |
|||
} |
|||
}, |
|||
inputType() { |
|||
if ( |
|||
this.form.symbol && |
|||
(this.form.symbol.label === '大于' || |
|||
this.form.symbol.label === '大于等于' || |
|||
this.form.symbol.label === '小于' || |
|||
this.form.symbol.label === '小于等于') |
|||
) { |
|||
return 'number' |
|||
} else { |
|||
return 'text' |
|||
} |
|||
} |
|||
}, |
|||
watch: { |
|||
visible(newVal) { |
|||
if (!newVal) { |
|||
this.resetQuery() |
|||
this.conditionData = [] |
|||
this.currentIndex = null |
|||
} else if (this.initialConditions && this.initialConditions.length > 0) { |
|||
this.conditionData = JSON.parse(JSON.stringify(this.initialConditions)) |
|||
} |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.getFieldCommon() |
|||
}, |
|||
methods: { |
|||
resetQuery() { |
|||
if (this.$refs.form) { |
|||
this.$refs.form.resetFields() |
|||
} |
|||
}, |
|||
addConditionData() { |
|||
this.$refs.form.validate((valid) => { |
|||
if (valid) { |
|||
const newConditionData = {} |
|||
newConditionData.field = this.form.field.label |
|||
newConditionData.fieldName = this.form.field.value |
|||
newConditionData.symbol = this.form.symbol.label |
|||
newConditionData.symbolCode = this.form.symbol.value |
|||
newConditionData.keyWord = this.form.keyWord |
|||
this.conditionData.push(newConditionData) |
|||
this.$nextTick(() => { |
|||
const container = document.getElementById('condition-container-modal') |
|||
container.scrollTop = container.scrollHeight |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
moveUp(index) { |
|||
if (index > 0) { |
|||
const temp = this.conditionData[index] |
|||
this.conditionData[index] = this.conditionData[index - 1] |
|||
this.conditionData[index - 1] = temp |
|||
this.currentIndex = index - 1 |
|||
} |
|||
const targetElement = document.getElementById('modal-element-id-' + this.currentIndex) |
|||
if (targetElement) { |
|||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) |
|||
} |
|||
}, |
|||
moveDown(index) { |
|||
if (index < this.conditionData.length - 1) { |
|||
const temp = this.conditionData[index] |
|||
this.conditionData[index] = this.conditionData[index + 1] |
|||
this.conditionData[index + 1] = temp |
|||
this.currentIndex = index + 1 |
|||
} |
|||
const targetElement = document.getElementById('modal-element-id-' + this.currentIndex) |
|||
if (targetElement) { |
|||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) |
|||
} |
|||
}, |
|||
deltCurrent(index) { |
|||
this.conditionData.splice(index, 1) |
|||
this.currentIndex = null |
|||
}, |
|||
selectCurrent(index) { |
|||
if (this.currentIndex === index) { |
|||
this.currentIndex = null |
|||
} else { |
|||
this.currentIndex = index |
|||
} |
|||
}, |
|||
addConnector(item) { |
|||
const newConditionData = {} |
|||
if (item === '并且' || item === '或者') { |
|||
newConditionData.connector = item |
|||
} else { |
|||
newConditionData.bracket = item |
|||
} |
|||
this.conditionData.push(newConditionData) |
|||
this.$nextTick(() => { |
|||
const container = document.getElementById('condition-container-modal') |
|||
container.scrollTop = container.scrollHeight |
|||
}) |
|||
}, |
|||
getFieldCommon() { |
|||
console.log('selectedCategory', this.selectedCategory) |
|||
const params = { |
|||
'categoryId': this.selectedCategory.id, |
|||
'categoryLevel': this.collectLevel |
|||
} |
|||
console.log('params', params) |
|||
FetchInitCategoryInputFieldByPid(params).then((data) => { |
|||
if (data && data.length > 0) { |
|||
this.fieldOptions = data |
|||
.filter(item => item.fieldName !== 'fonds_no' && item.fieldName !== 'fonds_name') |
|||
.map(item => { |
|||
return { |
|||
id: item.id, |
|||
label: item.fieldCnName, |
|||
value: item.fieldName |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
checkConditions(conditionData) { |
|||
let brackets = 0 |
|||
let fields = 0 |
|||
let connectors = 0 |
|||
let previousTokenType = null |
|||
let hasValidConditionBetweenBrackets = false |
|||
|
|||
for (var i = 0; i < conditionData.length; i++) { |
|||
const condition = conditionData[i] |
|||
let currentTokenType = '' |
|||
|
|||
if (condition.hasOwnProperty('bracket')) { |
|||
currentTokenType = 'bracket' |
|||
brackets++ |
|||
} else if (condition.hasOwnProperty('field')) { |
|||
currentTokenType = 'field' |
|||
fields++ |
|||
if (brackets > 0) { |
|||
hasValidConditionBetweenBrackets = true |
|||
} |
|||
} else if (condition.hasOwnProperty('connector')) { |
|||
currentTokenType = 'connector' |
|||
connectors++ |
|||
} |
|||
|
|||
if (previousTokenType && currentTokenType) { |
|||
if ((previousTokenType === 'field' && currentTokenType === 'field') || |
|||
(previousTokenType === 'connector' && currentTokenType === 'connector')) { |
|||
this.$message({ message: '条件之间缺少或且连接符', type: 'error', offset: 8 }) |
|||
return null |
|||
} |
|||
} |
|||
previousTokenType = currentTokenType |
|||
} |
|||
|
|||
if (brackets > 0 && !hasValidConditionBetweenBrackets) { |
|||
this.$message({ message: '请输入有效条件', type: 'error', offset: 8 }) |
|||
return null |
|||
} else if (brackets > 0 && brackets % 2 !== 0) { |
|||
this.$message({ message: '括号不对称', type: 'error', offset: 8 }) |
|||
return null |
|||
} else if (fields === 0) { |
|||
this.$message({ message: '请输入有效条件', type: 'error', offset: 8 }) |
|||
return null |
|||
} else if (fields === 1 || connectors === fields - 1) { |
|||
const wheresql = this.conditionData.map(obj => { |
|||
if (obj.field) { |
|||
if (obj.symbol === '包含' || obj.symbol === '不包含') { |
|||
return obj.fieldName + ' ' + obj.symbolCode + " '%" + obj.keyWord + "%'" |
|||
} else if (obj.keyWord && isNaN(parseInt(obj.keyWord))) { |
|||
return obj.fieldName + ' ' + obj.symbolCode + " '" + obj.keyWord + "'" |
|||
} else { |
|||
return obj.fieldName + ' ' + obj.symbolCode + ' ' + obj.keyWord |
|||
} |
|||
} else if (obj.connector === '并且') { |
|||
return 'and' |
|||
} else if (obj.connector === '或者') { |
|||
return 'or' |
|||
} else { |
|||
return obj.bracket |
|||
} |
|||
}).join(' ') |
|||
return wheresql |
|||
} else { |
|||
this.$message({ message: '条件之间缺少或且连接符', type: 'error', offset: 8 }) |
|||
return null |
|||
} |
|||
}, |
|||
handleSearch() { |
|||
const wheresql = this.checkConditions(this.conditionData) |
|||
console.log('wheresql', wheresql) |
|||
if (wheresql) { |
|||
const conditions = JSON.parse(JSON.stringify(this.conditionData)) |
|||
// 保存到 localStorage |
|||
localStorage.setItem('advancedSearchConditions', JSON.stringify(conditions)) |
|||
localStorage.setItem('advancedSearchSql', wheresql) |
|||
// 发送 SQL 条件和原始条件数据(用于显示文案) |
|||
this.$emit('search', { |
|||
sql: wheresql, |
|||
conditions: conditions |
|||
}) |
|||
} |
|||
}, |
|||
handleClose() { |
|||
this.$emit('update:visible', false) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang='scss' scoped> |
|||
.advanced-search-modal { |
|||
.el-form--inline .el-form-item { |
|||
margin-right: 20px !important; |
|||
} |
|||
.advanced-btn { |
|||
display: flex; |
|||
justify-content: center; |
|||
margin-top: 15px; |
|||
.el-button { |
|||
margin-right: 10px; |
|||
} |
|||
} |
|||
.search-condition { |
|||
padding: 18px; |
|||
margin-top: 20px; |
|||
background: #F6F9FF; |
|||
border-radius: 3px; |
|||
border: 1px dashed #DCDFE6; |
|||
h4 { |
|||
margin: 0 0 17px 0; |
|||
padding-left: 25px; |
|||
color: #0C0E1E; |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
.condition-main { |
|||
display: flex; |
|||
justify-content: center; |
|||
flex-wrap: nowrap; |
|||
.condition-left { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
.el-button { |
|||
width: 76px; |
|||
margin: 5px 0; |
|||
::v-deep i.el-icon-top, |
|||
::v-deep i.el-icon-bottom { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
.condition-content { |
|||
width: 500px; |
|||
height: 160px; |
|||
margin: 0 10px; |
|||
padding: 0; |
|||
background: #E6E8ED; |
|||
overflow: hidden; |
|||
overflow-y: scroll; |
|||
li { |
|||
display: flex; |
|||
justify-content: center; |
|||
flex-wrap: nowrap; |
|||
height: 32px; |
|||
line-height: 32px; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
background-color: #fff; |
|||
border-bottom: 1px solid #E6E8ED; |
|||
cursor: default; |
|||
&:hover, |
|||
&:focus, |
|||
&.active { |
|||
background-color: #E8F2FF; |
|||
} |
|||
.keyword-style { |
|||
color: #2ECAAC; |
|||
i { |
|||
font-style: normal; |
|||
color: #545B65; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.condition-right { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
.el-button { |
|||
width: 64px; |
|||
margin: 5px 0; |
|||
background-color: #0348F3; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,610 @@ |
|||
<template> |
|||
<div class="embed-upload" :class="{ 'has-file': fileList.length > 0 }"> |
|||
<div class="upload-header"> |
|||
<span class="upload-title">原文上传</span> |
|||
<span class="upload-tip">单个文件不可超过10GB</span> |
|||
</div> |
|||
|
|||
<div class="uploader-drop" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleDrop"> |
|||
<div class="uploader-btn"> |
|||
<i class="iconfont icon-tianjiawenjian upload-icon" /> |
|||
<p>{{ fileList.length > 0 ? '点击继续添加文件' : '点击或拖拽上传文件' }}</p> |
|||
<!-- :disabled="!isCaValid || isCheckingCa" --> |
|||
<input |
|||
ref="fileInput" |
|||
type="file" |
|||
:multiple="true" |
|||
|
|||
class="file-input" |
|||
style="display: none;" |
|||
@change="handleFileSelect" |
|||
> |
|||
</div> |
|||
<div class="el-upload__tip">支持多文件上传,最大10GB/个</div> |
|||
</div> |
|||
|
|||
<div v-if="fileList.length > 0" class="file-list-container"> |
|||
<div v-for="(fileItem, index) in fileList" :key="index" class="file-item"> |
|||
<div class="file-info"> |
|||
<i :class="getFileIcon(fileItem.file.name)" /> |
|||
<span class="file-name">{{ fileItem.file.name }}</span> |
|||
<span class="file-size">{{ formatFileSize(fileItem.file.size) }}</span> |
|||
</div> |
|||
<div v-if="fileItem.uploading || fileItem.merging" class="file-status"> |
|||
<span v-if="fileItem.uploading" class="progress-text">上传中 {{ fileItem.progress }}%</span> |
|||
<span v-else class="progress-text">合并中...</span> |
|||
<div v-if="fileItem.uploading" class="progress-bar" :style="{ width: fileItem.progress + '%' }" /> |
|||
</div> |
|||
<p v-if="fileItem.errorMsg" class="error">{{ fileItem.errorMsg }}</p> |
|||
<p v-if="fileItem.successMsg" class="success">{{ fileItem.successMsg }}</p> |
|||
<i class="iconfont icon-shanchu delete-btn" @click.stop="handleDeleteFile(index)" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-if="fileList.length > 0" class="upload-footer"> |
|||
<span class="file-count">共 {{ fileList.length }} 个文件</span> |
|||
<el-button |
|||
:disabled="fileList.some(item => item.uploading || item.merging) || btnLoading" |
|||
:loading="btnLoading" |
|||
type="primary" |
|||
@click="handleUploadConfirm" |
|||
> |
|||
保存上传 |
|||
</el-button> |
|||
</div> |
|||
</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' |
|||
|
|||
export default { |
|||
name: 'EmbedUpload', |
|||
props: { |
|||
selectedCategory: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
arcId: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
fileList: [], |
|||
CHUNK_SIZE: 5 * 1024 * 1024, |
|||
isCaValid: false, |
|||
isCheckingCa: false, |
|||
btnLoading: false, |
|||
originFileData: [], |
|||
repeatFileData: [], |
|||
tempSelectedFiles: null |
|||
} |
|||
}, |
|||
computed: { |
|||
...mapGetters(['baseApi']) |
|||
}, |
|||
mounted() { |
|||
this.getCheckCaValidity() |
|||
}, |
|||
methods: { |
|||
triggerFileInput() { |
|||
this.$refs.fileInput.click() |
|||
}, |
|||
getFileIcon(fileName) { |
|||
const ext = fileName.split('.').pop().toLowerCase() |
|||
const icons = { |
|||
'jpg': 'icon-image', 'jpeg': 'icon-image', 'png': 'icon-image', 'bmp': 'icon-image', 'gif': 'icon-image', |
|||
'xlsx': 'icon-excel', 'xls': 'icon-excel', |
|||
'docx': 'icon-word', 'doc': 'icon-word', |
|||
'pdf': 'icon-pdf', |
|||
'ppt': 'icon-ppt', 'pptx': 'icon-ppt', |
|||
'zip': 'icon-zip', 'rar': 'icon-zip', |
|||
'txt': 'icon-txt' |
|||
} |
|||
return `fileIcon ${icons[ext] || 'icon-other'}` |
|||
}, |
|||
formatFileSize(bytes) { |
|||
if (bytes === 0) return '0.00MB' |
|||
const mb = bytes / (1024 * 1024) |
|||
return mb.toFixed(2) + 'MB' |
|||
}, |
|||
showMessage(message, type) { |
|||
this.$message({ message, type, offset: 8 }) |
|||
}, |
|||
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') |
|||
} 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 handleDrop(e) { |
|||
if (this.isCheckingCa) { |
|||
this.showMessage('正在校验CA证书,请稍候...', 'warning') |
|||
return |
|||
} |
|||
if (!this.isCaValid) { |
|||
this.showMessage('CA证书不在有效期内,无法上传文件', 'error') |
|||
return |
|||
} |
|||
|
|||
const selectedFiles = Array.from(e.dataTransfer.files) |
|||
if (selectedFiles.length === 0) return |
|||
|
|||
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.tempSelectedFiles = selectedFiles |
|||
this.$emit('show-repeat-modal', this.repeatFileData) |
|||
return |
|||
} |
|||
|
|||
this.addFiles(selectedFiles) |
|||
}, |
|||
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 |
|||
|
|||
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.tempSelectedFiles = selectedFiles |
|||
this.$emit('show-repeat-modal', this.repeatFileData) |
|||
e.target.value = '' |
|||
return |
|||
} |
|||
|
|||
this.addFiles(selectedFiles) |
|||
e.target.value = '' |
|||
}, |
|||
addFiles(files) { |
|||
const newFileList = files.map(file => ({ |
|||
file, |
|||
uploading: false, |
|||
merging: false, |
|||
progress: 0, |
|||
errorMsg: '', |
|||
successMsg: '', |
|||
md5: '' |
|||
})) |
|||
this.fileList = [...this.fileList, ...newFileList] |
|||
}, |
|||
handleDeleteFile(index) { |
|||
const fileItem = this.fileList[index] |
|||
if (fileItem.uploading || fileItem.merging) { |
|||
this.showMessage('当前文件正在上传/合并中,无法删除', 'warning') |
|||
return |
|||
} |
|||
this.fileList.splice(index, 1) |
|||
}, |
|||
handleRepeatFile(type) { |
|||
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') |
|||
return |
|||
} |
|||
} |
|||
|
|||
this.addFiles(filesToUpload) |
|||
this.tempSelectedFiles = null |
|||
}, |
|||
calculateFileMd5(file) { |
|||
return new Promise((resolve, reject) => { |
|||
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 { |
|||
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.md5 = fileMd5 |
|||
|
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
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.progress = 100 |
|||
} catch (err) { |
|||
fileItem.errorMsg = '上传失败: ' + (err.message || '未知错误') |
|||
} 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 |
|||
|
|||
for (const fileItem of pendingFiles) { |
|||
await this.uploadFileChunks(fileItem) |
|||
} |
|||
|
|||
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) |
|||
if (validFiles.length === 0) { |
|||
this.btnLoading = false |
|||
this.showMessage('无有效分片上传完成的文件!', 'error') |
|||
return |
|||
} |
|||
|
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = true |
|||
}) |
|||
|
|||
try { |
|||
const processFiles = validFiles.map(async(fileItem) => { |
|||
const file = fileItem.file |
|||
const jsonString = {} |
|||
|
|||
if (file.type.startsWith('image')) { |
|||
const reader = new FileReader() |
|||
const base64 = await new Promise((resolve) => { |
|||
reader.onload = (e) => resolve(e.target.result) |
|||
reader.readAsDataURL(file) |
|||
}) |
|||
const img = new Image() |
|||
const imgRes = await new Promise((resolve) => { |
|||
img.onload = () => resolve({ width: img.width, height: img.height }) |
|||
img.src = base64 |
|||
}) |
|||
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 = '' |
|||
|
|||
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) |
|||
const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) |
|||
if (!chunksExist) { |
|||
throw new Error(`部分分片未上传完成`) |
|||
} |
|||
|
|||
return { |
|||
categoryId: this.selectedCategory.id, |
|||
archivesId: this.arcId, |
|||
identifier: fileItem.md5, |
|||
filename: file.name, |
|||
totalChunks: totalChunks, |
|||
totalSize: file.size, |
|||
fileJsonString: JSON.stringify([jsonString]) |
|||
} |
|||
}) |
|||
|
|||
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' |
|||
} |
|||
}) |
|||
|
|||
if (response.data.code === 200) { |
|||
this.showMessage('所有文件上传并合并成功', 'success') |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.successMsg = '上传成功!' |
|||
fileItem.merging = false |
|||
}) |
|||
this.$emit('onUploadSuccess') |
|||
} else { |
|||
this.showMessage(`文件合并失败: ${response.data.message || '合并失败'}`, 'error') |
|||
} |
|||
} catch (err) { |
|||
this.showMessage(`文件合并失败: ${err.message}`, 'error') |
|||
validFiles.forEach(fileItem => { |
|||
fileItem.merging = false |
|||
fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` |
|||
}) |
|||
} finally { |
|||
this.btnLoading = false |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.embed-upload { |
|||
flex: 1; |
|||
margin-left: 10px; |
|||
border: 1px solid #e4e7ed; |
|||
border-radius: 4px; |
|||
background-color: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.upload-header { |
|||
padding: 12px 15px; |
|||
border-bottom: 1px solid #e4e7ed; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.upload-title { |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
color: #303133; |
|||
} |
|||
|
|||
.upload-tip { |
|||
font-size: 12px; |
|||
color: #ff4949; |
|||
} |
|||
|
|||
.uploader-drop { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 20px; |
|||
cursor: pointer; |
|||
min-height: 150px; |
|||
} |
|||
|
|||
.uploader-btn { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
color: #606266; |
|||
} |
|||
|
|||
.upload-icon { |
|||
font-size: 32px; |
|||
color: #1F55EB; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.uploader-btn p { |
|||
margin: 0; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.el-upload__tip { |
|||
font-size: 12px; |
|||
color: #A6ADB6; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.file-list-container { |
|||
max-height: 200px; |
|||
overflow-y: auto; |
|||
padding: 0 15px; |
|||
} |
|||
|
|||
.file-item { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 10px; |
|||
border: 1px dashed #409eff; |
|||
border-radius: 4px; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.file-info { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: 8px; |
|||
flex: 1; |
|||
line-height: 32px; |
|||
} |
|||
|
|||
.fileIcon { |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.file-name { |
|||
flex: 1; |
|||
font-size: 13px; |
|||
color: #303133; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.file-size { |
|||
font-size: 12px; |
|||
color: #909399; |
|||
} |
|||
|
|||
.file-status { |
|||
margin-top: 8px; |
|||
} |
|||
|
|||
.progress-text { |
|||
font-size: 12px; |
|||
color: #606266; |
|||
} |
|||
|
|||
.progress-bar { |
|||
height: 6px; |
|||
background-color: #42b983; |
|||
transition: width 0.3s ease; |
|||
border-radius: 3px; |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.delete-btn { |
|||
/* position: absolute; |
|||
right: 10px; |
|||
top: 10px; */ |
|||
font-size: 18px; |
|||
color: #909399; |
|||
cursor: pointer; |
|||
padding-left: 10px; |
|||
} |
|||
|
|||
.delete-btn:hover { |
|||
color: #ff4444; |
|||
} |
|||
|
|||
.success { |
|||
margin: 4px 0 0 0; |
|||
font-size: 12px; |
|||
color: #00C851; |
|||
} |
|||
|
|||
.error { |
|||
margin: 4px 0 0 0; |
|||
font-size: 12px; |
|||
color: #ff4444; |
|||
} |
|||
|
|||
.upload-footer { |
|||
padding: 12px 15px; |
|||
border-top: 1px solid #e4e7ed; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.file-count { |
|||
font-size: 12px; |
|||
color: #606266; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue