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