Vue3 多文件上传

需求:上传多个文件,且保证上传的excel文件与pdf文件名一致,并按先后顺序排列

弹层页面:

html 复制代码
<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>
javascript 复制代码
<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>
css 复制代码
<style lang="scss" scoped>
::v-deep .el-upload-list {
  height: 150px;
  overflow: auto;
}
</style>

使用方法:

html 复制代码
//界面引入文件, uploadMoreFile是我给组件的命名
 <uploadMoreFile ref="refMoreUploadFile" :is-single="true" :upload-format="moreUploadFormat" @missingPersonUpload="missingPersonMoreUpload" />
javascript 复制代码
<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>
相关推荐
带娃的IT创业者1 小时前
深度解析 Bun:重新定义 JavaScript 运行时的性能边界
开发语言·javascript·node.js·ecmascript·bun·运行时
文阿花1 小时前
Echarts实现3D饼状图
前端·javascript·echarts·饼状图
智码看视界1 小时前
老梁聊全栈系列:Vue2与Vue3核心区别及学习路线指南
前端·vue.js·学习
qq_363066931 小时前
react 使用web component导出静态html报告
前端·react.js·html·页面导出
weixin_457763081 小时前
展示youtube的视频
前端·javascript·html
雨翼轻尘1 小时前
03_HTML进阶标签与CSS入门
前端·css·html·入门·进阶标签
云水一下1 小时前
Vue.js从零到精通系列(六):组合式函数与逻辑复用——打造自己的 Hooks 工具箱
前端·javascript·vue.js
IT_陈寒1 小时前
Java的ArrayList扩容把我坑惨了,原来是这样搞的
前端·人工智能·后端
snow@li1 小时前
Charles:软件能力深度解析 / 跨平台 HTTP/HTTPS 代理调试工具 / 客户端与互联网之间的中间人代理 / 拦截、查看、篡改所有网络流量
前端