smart-bid-web/src/views/common/UploadFile.vue

757 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="upload-container">
<el-upload class="upload-demo" drag action="#" multiple :show-file-list="true" :before-upload="beforeUpload"
:on-change="handleFileChange" :on-remove="handleRemove" :on-exceed="handleExceed" :file-list="files"
:accept="accept" :limit="limitUploadNum" :auto-upload="autoUpload">
<div class="upload-content">
<!-- 当只有一张图片时显示缩略图 -->
<div v-if="showImagePreview" class="image-preview">
<img :src="previewImageUrl" :alt="previewImageName" class="preview-thumbnail" />
<div class="preview-overlay">
<div class="preview-text">
<div class="main-text">点击更换图片</div>
<div class="tip-text">或拖拽新图片到此处</div>
</div>
</div>
</div>
<!-- 当只有一个文档文件时显示文件图标 -->
<div v-else-if="showFilePreview" class="file-preview">
<div class="file-icon-container">
<i :class="getFileIconClass()" class="file-icon"></i>
<div class="file-name">{{ previewFileName }}</div>
</div>
<div class="preview-overlay">
<div class="preview-text">
<div class="main-text">点击更换文件</div>
<div class="tip-text">或拖拽新文件到此处</div>
</div>
</div>
</div>
<!-- 默认上传区域 -->
<div v-else class="upload-text">
<div class="main-text"> + 点击或将文件拖拽到这里上传</div>
<div class="tip-text">
<div>单份文件大小上限 {{ maxFileTips }}</div>
<div>支持文件类型:{{ uploadType }}</div>
</div>
</div>
</div>
</el-upload>
</div>
</template>
<script>
import { uploadSmallFileByOcr, uploadLargeFileByOcr, uploadSmallFile, uploadLargeFile } from '@/api/common/uploadFile.js'
export default {
name: 'UploadFile',
props: {
fileList: {
type: Array,
default: () => []
},
maxFileTips: {
type: String,
default: '20MB'
},
uploadType: {
type: String,
default: 'png、jpg、jpeg'
},
limitUploadNum: {
type: Number,
default: 1
},
fileUploadRule: {
type: Object,
default: () => ({})
},
autoUpload: {
type: Boolean,
default: true
},
type: {
type: String,
default: ''
},
},
data() {
return {
files: [...this.fileList], // 使用展开运算符创建新数组
previewImageUrl: '',
previewImageName: '',
previewFileName: '',
previewFileType: '',
isUploading: false, // 添加上传状态标识
defaultFileSize: 1024 * 1024 * 5, // 默认文件大小5MB 超过5MB算大文件上传
}
},
methods: {
beforeUpload(file) {
alert(file.size);
// 如果正在上传中,阻止新的上传
if (this.isUploading) {
return false;
}
// 验证文件类型
const fileExtension = file.name.split('.').pop().toLowerCase();
const isAllowedType = this.allowedTypes.includes(fileExtension);
// 验证MIME类型额外验证
const isAllowedMimeType = this.mimeTypes.includes(file.type);
// 验证文件大小
const isLtMaxSize = file.size / 1024 / 1024 < this.maxSizeMB;
if (!isAllowedType || !isAllowedMimeType) {
this.$message.error(`只能上传 ${this.uploadType} 格式的文件!`);
return false;
}
if (!isLtMaxSize) {
this.$message.error(`文件大小不能超过 ${this.maxFileTips}!`);
return false;
}
return false;
},
// 文件状态改变
handleFileChange(file, fileList) {
// 如果文件状态是移除或失败,不处理
if (file.status === 'removed' || file.status === 'fail') {
return;
}
// 不使用深拷贝,直接使用 fileList但确保 files 是响应式的
this.files = this.formatFileList(fileList);
// 生成预览
if (file.raw && fileList.length === 1) {
if (this.isImageFile(file.raw)) {
// 图片预览
this.generateImagePreview(file.raw);
} else if (this.isDocumentFile(file.raw)) {
// 文档预览
this.generateDocumentPreview(file.raw);
}
} else {
// 如果不是单个文件,清除预览
this.clearPreview();
}
// 如果启用自动上传且文件验证通过,触发上传
if (this.autoUpload && file.status === 'ready' && !this.isUploading) {
alert(this.fileUploadRule.fields_json);
if (this.fileUploadRule.fields_json) {
// 文件需要ocr识别
this.uploadFile(file, '识别中');
} else {
// 文件不需要ocr识别
this.uploadFile(file, '上传中');
}
}
},
// 格式化文件列表,确保数据结构正确
formatFileList(fileList) {
return fileList.map(file => {
// 创建一个标准的文件对象
const formattedFile = {
uid: file.uid,
name: file.name,
size: file.size,
type: file.type,
status: file.status,
raw: file.raw
};
// 确保 percentage 属性存在且有效
if (file.percentage !== undefined && file.percentage !== null) {
formattedFile.percentage = Math.max(0, Math.min(100, file.percentage));
} else if (file.status === 'uploading') {
formattedFile.percentage = 0;
} else if (file.status === 'success') {
formattedFile.percentage = 100;
}
// 保留其他可能存在的属性
if (file.response) {
formattedFile.response = file.response;
formattedFile.url = file.url;
}
if (file.statusText) {
formattedFile.statusText = file.statusText;
}
return formattedFile;
});
},
// 上传文件
async uploadFile(file, text) {
// 设置上传状态
this.isUploading = true;
const formData = new FormData();
formData.append('file', file.raw);
formData.append('params', JSON.stringify(this.fileUploadRule));
// 更新文件状态为上传中,并设置初始进度为 0
this.updateFileStatus(file.uid, 'uploading', text, null, 0);
try {
this.$bus.$emit('startUpload', text);
let res = null;
if (this.defaultFileSize < file.size) {
if (this.fileUploadRule.fields_json) {
res = await uploadLargeFileByOcr(formData);
} else {
res = await uploadLargeFile(formData);
}
} else {
if (this.fileUploadRule.fields_json) {
res = await uploadSmallFileByOcr(formData);
} else {
res = await uploadSmallFile(formData);
}
}
console.log('上传成功:', res);
this.$bus.$emit('endUpload');
// 上传成功,更新文件状态,设置进度为 100
this.updateFileStatus(file.uid, 'success', '', res.data, 100);
console.log('上传成功后的文件列表:', this.files);
// 触发文件变化事件,传递当前的文件列表
this.$emit('file-change', this.getCurrentFiles(), this.type);
} catch (err) {
this.$bus.$emit('endUpload');
// 上传失败,移除文件
this.removeFailedFile(file.uid);
} finally {
// 无论成功失败,都重置上传状态
this.isUploading = false;
}
},
// 移除上传失败的文件
removeFailedFile(fileUid) {
console.log('移除上传失败的文件:', fileUid);
const fileIndex = this.files.findIndex(item => item.uid === fileUid);
if (fileIndex !== -1) {
// 从文件列表中移除
this.files.splice(fileIndex, 1);
console.log('移除失败文件后的文件列表:', this.files);
// 清除预览
this.clearPreview();
// 触发文件变化事件
this.$emit('file-change', this.getCurrentFiles(), this.type);
}
},
// 更新文件状态
updateFileStatus(fileUid, status, statusText, responseData = null, percentage = null) {
console.log('更新文件状态:', fileUid, status, statusText, '进度:', percentage);
console.log('更新前的文件列表:', this.files);
const fileIndex = this.files.findIndex(item => item.uid === fileUid);
if (fileIndex !== -1) {
const updatedFile = {
...this.files[fileIndex],
status: status
};
if (statusText) {
updatedFile.statusText = statusText;
}
// 确保 percentage 是有效的数字0-100
if (percentage !== null && percentage >= 0 && percentage <= 100) {
updatedFile.percentage = percentage;
} else if (status === 'uploading') {
// 上传中状态默认设置进度为 0
updatedFile.percentage = 0;
} else if (status === 'success') {
// 成功状态设置进度为 100
updatedFile.percentage = 100;
}
if (responseData) {
// 保存服务器返回的文件信息
updatedFile.response = responseData;
updatedFile.response.businessType = this.fileUploadRule?.fileUploadType;
}
// 使用 Vue.set 确保响应式更新
this.$set(this.files, fileIndex, updatedFile);
console.log('更新后的文件列表:', this.files);
} else {
console.warn('未找到要更新的文件:', fileUid);
}
},
// 获取当前文件列表(确保是普通数组)
getCurrentFiles() {
const currentFiles = this.files.map(file => {
const fileObj = {
uid: file.uid,
name: file.name,
size: file.size,
type: file.type,
status: file.status
};
// 确保 percentage 存在
if (file.percentage !== undefined) {
fileObj.percentage = file.percentage;
}
// 保留原始文件对象(如果存在)
if (file.raw) {
fileObj.raw = file.raw;
}
// 如果文件已上传成功,添加服务器返回的信息
if (file.response) {
fileObj.response = file.response;
fileObj.response.businessType = this.fileUploadRule?.fileUploadType;
}
return fileObj;
});
console.log('getCurrentFiles 返回:', currentFiles);
return currentFiles;
},
// 处理文件超出限制
handleExceed(files, fileList) {
console.log('文件超出限制处理', files, fileList);
// 当文件数量超出限制时,用新文件替换旧文件
if (files.length > 0) {
// 触发文件删除事件
this.$emit('del-file', fileList[0]);
// 清空原有文件列表
this.files = [];
// 手动触发新文件的上传流程
const newFile = files[0];
this.beforeUpload(newFile); // 先进行验证
// 创建新的文件对象,确保包含 percentage
const newFileObj = {
name: newFile.name,
size: newFile.size,
type: newFile.type,
raw: newFile,
uid: Date.now(), // 生成新的uid
status: 'ready',
percentage: 0 // 添加默认进度
};
// 更新文件列表
this.files = [newFileObj];
// 生成预览
if (this.isImageFile(newFile)) {
this.generateImagePreview(newFile);
} else if (this.isDocumentFile(newFile)) {
this.generateDocumentPreview(newFile);
}
console.log('handleExceed 后的文件列表:', this.files);
// 传递当前文件信息给父组件
this.$emit('file-change', this.getCurrentFiles());
// 自动上传新文件
if (this.autoUpload && !this.isUploading) {
if (this.fileUploadRule.fields_json) {
this.uploadFile(newFileObj, '识别中');
} else {
this.uploadFile(newFileObj, '上传中');
}
}
}
},
// 移除文件
handleRemove(file, fileList) {
console.log('移除文件:', file);
console.log('移除前的文件列表:', this.files);
// 如果正在上传中,阻止移除
if (this.isUploading) {
return false;
}
// 使用格式化方法而不是深拷贝
this.files = this.formatFileList(fileList);
console.log('移除后的文件列表:', this.files);
// 根据剩余文件重新生成预览
if (fileList.length === 0) {
this.clearPreview();
} else if (fileList.length === 1 && fileList[0] && fileList[0].raw) {
if (this.isImageFile(fileList[0].raw)) {
this.generateImagePreview(fileList[0].raw);
} else if (this.isDocumentFile(fileList[0].raw)) {
this.generateDocumentPreview(fileList[0].raw);
}
} else {
this.clearPreview();
}
// 触发文件删除事件
this.$emit('del-file', file);
// 传递当前文件信息给父组件
this.$emit('file-change', this.getCurrentFiles());
},
// 判断是否为图片文件
isImageFile(file) {
return (file && file.type && file.type.startsWith('image/')) || (file && file.fileType === '1');
},
// 判断是否为文档文件
isDocumentFile(file) {
if (!file || !file.name) return false;
const fileExtension = file.name.split('.').pop().toLowerCase();
return ['pdf', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtension) || (file && file.fileType === '2');
},
// 生成图片预览
generateImagePreview(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.previewImageUrl = e.target.result;
this.previewImageName = file.name;
// 清除文档预览
this.previewFileName = '';
this.previewFileType = '';
};
reader.readAsDataURL(file);
},
// 修改回显生成图片预览
generateImagePreviewFromPath(file) {
this.previewImageUrl = file.lsFilePath;
this.previewImageName = file.name;
// 清除文档预览
this.previewFileName = '';
this.previewFileType = '';
},
// 生成文档预览
generateDocumentPreview(file) {
const fileExtension = file.name.split('.').pop().toLowerCase();
this.previewFileName = file.name;
this.previewFileType = fileExtension;
// 清除图片预览
this.previewImageUrl = '';
this.previewImageName = '';
},
// 清除预览
clearPreview() {
this.previewImageUrl = '';
this.previewImageName = '';
this.previewFileName = '';
this.previewFileType = '';
},
// 获取文件图标类型
getFileIconClass() {
const iconMap = {
'pdf': 'el-icon-document',
'doc': 'el-icon-document',
'docx': 'el-icon-document',
'xls': 'el-icon-document',
'xlsx': 'el-icon-document'
};
return iconMap[this.previewFileType] || 'el-icon-document';
},
// 清空所有文件
clearFiles() {
this.files = [];
this.clearPreview();
this.$emit('file-change', []);
}
},
computed: {
// 是否显示图片预览
showImagePreview() {
return this.previewImageUrl &&
this.files.length === 1 ;
},
// 是否显示文件预览
showFilePreview() {
return this.previewFileName &&
this.previewFileType &&
this.files.length === 1;
},
accept() {
return this.uploadType.split('、').map(type => `.${type}`).join(',');
},
// 获取支持的文件类型数组
allowedTypes() {
return this.uploadType.split('、');
},
// 获取文件大小限制转换为MB数字
maxSizeMB() {
const sizeStr = this.maxFileTips.toLowerCase();
if (sizeStr.includes('mb')) {
return parseFloat(sizeStr);
} else if (sizeStr.includes('kb')) {
return parseFloat(sizeStr) / 1024;
} else if (sizeStr.includes('gb')) {
return parseFloat(sizeStr) * 1024;
}
return 20;
},
// 获取MIME类型映射
mimeTypes() {
const typeMap = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
};
return this.allowedTypes.map(type => typeMap[type] || '');
}
},
watch: {
fileList: {
handler(newVal) {
this.files = this.formatFileList(newVal);
// 如果外部传入文件列表,从 lsFilePath 生成预览
if (newVal.length > 0) {
const firstFile = newVal[0];
if (firstFile && firstFile.lsFilePath) {
// 根据文件类型生成预览
if (this.isImageFile(firstFile)) {
this.generateImagePreviewFromPath(firstFile);
} else if (this.isDocumentFile(firstFile)) {
this.generateDocumentPreview(firstFile);
}
} else {
// 如果没有 lsFilePath回退到原始逻辑
if (firstFile && firstFile.raw) {
if (this.isImageFile(firstFile.raw)) {
this.generateImagePreview(firstFile.raw);
} else if (this.isDocumentFile(firstFile.raw)) {
this.generateDocumentPreview(firstFile.raw);
}
} else {
this.clearPreview();
}
}
} else {
this.clearPreview();
}
},
immediate: true,
deep: true
}
},
}
</script>
<style scoped lang="scss">
.upload-container {
width: 100%;
.upload-demo {
width: 100%;
::v-deep .el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 240px;
border: 2px dashed #DCDFE6;
border-radius: 8px;
background-color: #FAFAFA;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
border-color: #409EFF;
background-color: #F5F7FA;
}
}
}
}
.upload-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 5px;
// 图片预览样式
.image-preview {
position: relative;
width: 100%;
height: 100%;
border-radius: 6px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.preview-thumbnail {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
.preview-text {
text-align: center;
color: white;
pointer-events: none;
.main-text {
font-size: 16px;
margin-bottom: 8px;
font-weight: 500;
}
.tip-text {
font-size: 12px;
opacity: 0.9;
}
}
}
&:hover .preview-overlay {
opacity: 1;
}
}
// 文件预览样式
.file-preview {
position: relative;
width: 100%;
height: 100%;
border-radius: 6px;
overflow: hidden;
.file-icon-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f9fafc;
.file-icon {
font-size: 64px;
color: #1F72EA;
margin-bottom: 16px;
}
.file-name {
font-size: 14px;
color: #606266;
text-align: center;
padding: 0 20px;
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
.preview-text {
text-align: center;
color: white;
pointer-events: none;
.main-text {
font-size: 16px;
margin-bottom: 8px;
font-weight: 500;
}
.tip-text {
font-size: 12px;
opacity: 0.9;
}
}
}
&:hover .preview-overlay {
opacity: 1;
}
}
// 默认上传区域样式
.upload-text {
text-align: center;
.main-text {
font-size: 18px;
color: #1F72EA;
margin-bottom: 12px;
font-weight: 500;
}
.tip-text {
font-size: 14px;
color: #909399;
line-height: 1.5;
div {
margin-bottom: 2px;
}
}
}
}
}
</style>