Vue3 多文件上传
需求:上传多个文件,且保证上传的excel文件与pdf文件名一致,并按先后顺序排列
弹层页面:
<template> <div> <el-dialog :title="props.title" v-model="dialogInfo.uploadVisible" :close-on-click-modal="false" custom-class="dialogBox" :width="props.dialogWidth" top="10%" :append-to-body="true" :close-on-press-escape="false" > <el-upload v-if="dialogInfo.uploadVisible" class="upload-demo" action="#" :multiple="props.isSingle" :on-remove="handleRemove" :on-change="upPic" :auto-upload="false" :file-list="dialogInfo.fileList" :on-exceed="warningLimit" :limit="props.maxQuantity" > <el-button type="primary" @click="upload">{{ '导入' }}</el-button> <template v-slot:tip> <div class="el-upload__tip">{{ props.hint }}</div> </template> </el-upload> <ul v-if="props.isShowFileList && props.isTrueFileList.length" style="margin-top: 20px"> <li v-for="(item, index) in props.isTrueFileList" :key="index" style="margin-left: 8px; width: 100%" class="el-icon-document"> <a style="margin-left: 5px">{{ item.fileName }}</a> <a class="mc" style="position: absolute; right: 30px" title="删除" @click="delFile(item.id)"><em class="el-icon-close" /></a> </li> </ul> <template v-slot:footer> <span class="dialog-footer"> <el-button @click="(dialogInfo.uploadVisible = false), resetFileLists()"> {{ '取消' }} </el-button> <el-button type="primary" :loading="dialogInfo.btnLoading" @click="importSure">{{ '确定' }}</el-button> </span> </template> </el-dialog> </div> </template><script setup> import { defineProps, reactive } from 'vue' import { uploadUsingPost1 } from '@/api/global' import { ElMessage } from 'element-plus' const emit = defineEmits(['delFile', 'missingPersonUpload']) const props = defineProps({ // 最大上传数量 maxQuantity: { type: Number, default: 20 }, // 提示信息 hint: { type: String, default: '选择需要上传的文件' }, // 文件名限制 astrict: { type: String, default: '' }, // 是否显示已上传文件列表 isShowFileList: { type: Boolean, default: false }, // 已上传文件列表 isTrueFileList: { type: Array, default: () => [] }, // 对话框宽度 dialogWidth: { type: String, default: '35%' }, // 是否单选 isSingle: { type: Boolean, default: true }, // 文件格式 uploadFormat: { type: Array, default: () => [] }, title: { type: String, default: '上传附件' }, // 是否通过公用方法上传至服务器 isUpload: { type: Boolean, default: true }, // 上传完毕是否立即关闭对话框 isUploadVisible: { type: Boolean, default: false }, // 自定义 formData 中 文件 key 值 customKey: { type: String, default: 'files' } }) const dialogInfo = reactive({ uploadVisible: false, file: {}, fileData: {}, // 文件列表 fileList: [], // 确定loading btnLoading: false, formatErrorNotified: false }) /** * @description: * 打开对话框 * @return { * } */ const open = () => { // 当可上传多份文件时,可不置空文件列表 dialogInfo.file = {} dialogInfo.fileList = [] dialogInfo.formatErrorNotified = false dialogInfo.uploadVisible = true } /** * @description: * 删除文件的回调 * @return { * } */ const handleRemove = (file, fileList) => { dialogInfo.fileList = fileList } /** * @description: * 删除已上传文件 * @return { * } */ const delFile = async (id) => { await emit('delFile', id) } /** * @description: * 上传文件的回调 * @return { * } */ const upPic = (files, fileList) => { const getExt = (filename) => { const lastDotIndex = filename.lastIndexOf('.') return lastDotIndex === -1 ? '' : filename.slice(lastDotIndex + 1).toLowerCase() } const fileExt = getExt(files.name) if (props.uploadFormat.length && !props.uploadFormat.map((f) => f.toLowerCase()).includes(fileExt)) { // 从 fileList 中移除当前非法文件 const index = fileList.findIndex((item) => item.uid === files.uid) if (index !== -1) { fileList.splice(index, 1) } dialogInfo.file = files.raw dialogInfo.fileList = sortFileList(fileList) // 只提示一次 if (!dialogInfo.formatErrorNotified) { ElMessage.error(`上传文件只能是 ${props.uploadFormat.join('、')} 格式!`) dialogInfo.formatErrorNotified = true // 可选:延迟重置,避免同一批上传再次触发(比如 2 秒后) setTimeout(() => { dialogInfo.formatErrorNotified = false }, 2000) } return } const isValidSize = files.size / 1024 / 1024 < 100 // 限制文件大小不超过100MB if (!isValidSize) { ElMessage.error(`当前上传的文件过大,建议不超过100MB!`) for (let i = 0; i < fileList.length; i++) { const ele = fileList[i] if (files.uid === ele.uid) { fileList.splice(i, 1) break } } dialogInfo.file = files.raw // 对剩余文件排序 dialogInfo.fileList = sortFileList(fileList) return } dialogInfo.file = files.raw // 对最新 fileList 排序 dialogInfo.fileList = sortFileList(fileList) } /** * @description: 按主文件名(去扩展名 + 去空格)排序,使同名 xlsx/pdf 相邻 */ const sortFileList = (list) => { return [...list].sort((a, b) => { const baseNameA = a.name.replace(/\.[^/.]+$/, '').trim() const baseNameB = b.name.replace(/\.[^/.]+$/, '').trim() if (baseNameA < baseNameB) return -1 if (baseNameA > baseNameB) return 1 // 主文件名相同时,按扩展名排序 const extA = a.name.split('.').pop().toLowerCase() const extB = b.name.split('.').pop().toLowerCase() return extB.localeCompare(extA) }) } /** * @description: * 文件提示 * @param {} * */ const warningLimit = () => { ElMessage.warning('文件超出最大上传数量') } /** * @description: * 限制单选多选 * @param {e} 默认行为 * @return { * } */ const upload = (e) => { if (props.maxQuantity === 1 && dialogInfo.fileList.length) { ElMessage.error('只能上传单个文件') e.stopPropagation() } } /** * @description: * 重置所有文件列表 * @return { * } */ const resetFileLists = () => { dialogInfo.file = {} dialogInfo.secondFile = {} } /** * @description: * 确认上传 - 处理两个文件列表 * @return { * } */ const importSure = () => { // 首先把dialogInfo.fileList中的文件按照每一项的name值进行分类 分成xlsxList和pdfList两组 因为name包含了文件名和后缀 // 所以先把按后缀把dialogInfo.fileList分成两部分,然后判断两部分的数量是否一样,例如xlsxList.length要等于pdfLits.length // 再然后 判断xlsxList中的文件名与pdfList的文件名是否一样,如果去空和去掉后缀的名字不一样,那么进行提示 // 非空校验 - 检查两个文件列表 const hasFirstFile = dialogInfo.fileList.length > 0 // 校验两个上传区域都必须有文件 if (!hasFirstFile) { return ElMessage.warning('请上传文件!') } //按扩展名分类 const xlsxList = [] const pdfList = [] dialogInfo.fileList.forEach((file) => { const ext = file.name.split('.').pop().toLowerCase() if (ext === 'xlsx' || ext === 'xls') { xlsxList.push(file) } else if (ext === 'pdf') { pdfList.push(file) } }) // 校验数量是否相等 if (xlsxList.length !== pdfList.length) { return ElMessage.warning('EXCEL文件与PDF文件数量不相等!') } // 校验文件名是否一致(去空格+去扩展名) const getBaseName = (filename) => { return filename.replace(/\.[^/.]+$/, '').trim() } // 创建主文件名集合用于快速查找 const pdfBaseNames = new Set(pdfList.map((file) => getBaseName(file.name))) for (const xlsxFile of xlsxList) { const xlsxBaseName = getBaseName(xlsxFile.name) if (!pdfBaseNames.has(xlsxBaseName)) { return ElMessage.warning(`找不到与EXCEL文件 "${xlsxFile.name}" 对应的PDF文件!`) } } console.log('xlsxList===', xlsxList, pdfList, 'pdfList') // 触发上传事件 emit('missingPersonUpload', { firstFiles: xlsxList, // EXCEL文件 secondFiles: pdfList // PDF文件 }) dialogInfo.uploadVisible = props.isUploadVisible } // 抛出去的方法 open defineExpose({ open, delFile }) </script><style lang="scss" scoped> ::v-deep .el-upload-list { height: 150px; overflow: auto; } </style>使用方法:
//界面引入文件, uploadMoreFile是我给组件的命名 <uploadMoreFile ref="refMoreUploadFile" :is-single="true" :upload-format="moreUploadFormat" @missingPersonUpload="missingPersonMoreUpload" /><script setup> import { reactive, onMounted, ref } from 'vue' import uploadMoreFile from '@workspace/views/aaa/components/uploadMoreFile.vue' const moreUploadFormat = ref(['xlsx', 'pdf']) const refMoreUploadFile = ref(null) const refErrorText = ref(null) //处理上传逻辑的,这里写自己的逻辑 const missingPersonMoreUpload = (list) => { const fileExcels = list.firstFiles const filePdfs = list.secondFiles } </script>