LOADING

加载过慢请开启缓存 浏览器默认开启

基于Element-plus封装的文件上传组件

1 组件源码

组件源码一共分为两个部分:文件上传部分upload.ts, element-plus前端部分UploadFile.vue。

1.1 upload.ts

此文件主要提供前端上传的主要逻辑,从yudao-ui-admin项目的文件修改而来。此外,在使用S3文件存储器时,如果后端预签名地址没有指定文件的Content-type,可能会造成pdf文件或者mp4文件无法在浏览器中直接预览等情况。

import { getAccessToken, getTenantId } from '@/utils/auth'
import * as FileApi from '@/api/infra/file'
import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import { ajaxUpload } from 'element-plus/es/components/upload/src/ajax'
import axios from 'axios'

export const useUpload = () => {
  // 后端上传地址
  const uploadUrl = import.meta.env.VITE_UPLOAD_URL
  // 是否使用前端直连上传
  const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
  // 重写ElUpload上传方法
  const httpRequest = async (options: UploadRequestOptions) => {
    // 模式一:前端上传
    if (isClientUpload) {
      // 1.1 生成文件名称 默认
      const fileName = options.file.name
      // 1.2 获取文件预签名地址
      const preSignedInfo = await FileApi.getFilePreSigned(fileName)
      // 1.4 上传文件
      //(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
      return axios
        .put(preSignedInfo.uploadUrl, options.file, {
          headers: { 'Content-Type': options.file.type }
        })
        .then(() => {
          // 上传成功后,解析参数,构造FileVO写入数据库
          const fileInfo: FileApi.FileVO = {
            configId: preSignedInfo.configId,
            name: fileName,
            path: preSignedInfo.path,
            url: preSignedInfo.url,
            type: options.file.type,
            size: options.file.size
          }
          return FileApi.createFile(fileInfo).then((value) => {
            // 并且返回文件的相关信息
            return { data: fileInfo, code: 0, fileId: value }
          })
        })
    } else {
      // 模式二:后端上传
      options.headers['Authorization'] = 'Bearer ' + getAccessToken()
      options.headers['tenant-id'] = getTenantId()
      // 使用 ElUpload 的上传方法, 需要等待上传成功之后再返回,因为我要一个url,不能异步返回
      return ajaxUpload(options)
    }
  }
  return {
    uploadUrl: isClientUpload ? '' : uploadUrl, // 如果是前端直连,则返回空字符串
    httpRequest
  }
}

/**
 * 上传类型
 */
enum UPLOAD_TYPE {
  // 客户端直接上传(只支持S3服务)
  CLIENT = 'client',
  // 客户端发送到后端上传
  SERVER = 'server'
}

1.2 UploadFile.vue

<template>
  <div class="upload-file">
    <el-upload
      ref="uploadRef"
      v-model:file-list="fileList"
      :action="uploadUrlMe || uploadUrl"
      :auto-upload="autoUpload"
      :before-upload="beforeUpload"
      :drag="true"
      :limit="props.limit"
      :multiple="props.limit > 1"
      :on-error="uploadError"
      :on-exceed="handleExceed"
      :on-preview="handlePreview"
      :on-remove="handleRemove"
      :on-success="handleFileSuccess"
      :show-file-list="true"
      :http-request="httpRequest"
      :accept="'.' + fileType.join(', .')"
    >
      <i class="el-icon--upload"></i>
      <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
      <template #tip>
        <div class="el-upload__tip">
          大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
        </div>
        <div class="el-upload__tip">
          格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
        </div>
        <div class="el-upload__tip">
          最多上传的文件数量为 <b style="color: #f56c6c">{{ limit }}</b> 个
        </div>
      </template>
    </el-upload>
  </div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import { useUpload } from '@/components/UploadFile/src/upload'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
import { ElNotification } from 'element-plus'

defineOptions({ name: 'UploadFile' })

const message = useMessage() // 消息弹窗
const emit = defineEmits(['success', 'error'])

const props = defineProps({
  title: propTypes.string.def('文件上传'),
  fileType: propTypes.array.def(['pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
  fileSize: propTypes.number.def(5), // 大小限制(MB)
  limit: propTypes.number.def(5), // 数量限制
  autoUpload: propTypes.bool.def(true), // 自动上传
  isShowTip: propTypes.bool.def(true), // 是否显示提示
  uploadUrlMe: propTypes.string.def(""), // 自定义文件上传url
})

// ========== 上传相关 ==========
const uploadRef = ref<UploadInstance>() // 上传表单的ref
const uploadList = ref<UploadUserFile[]>([]) // 上传成功的文件数量
const fileList = ref<UploadUserFile[]>([]) //文件列表
const uploadNumber = ref<number>(0) // 上传成功的数量

const { uploadUrl, httpRequest } = useUpload()

/**
 * 文件上传之前判断
 */
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
  if (fileList.value.length > props.limit) {
    ElNotification({
      title: '错误',
      message: `上传文件数量不能超过${props.limit}个!`,
      type: 'error'
    })
    return false
  }
  let fileExtension = ''
  if (file.name.lastIndexOf('.') > -1) {
    fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
  }
  const isImg = props.fileType.some((type: string) => {
    if (file.type.indexOf(type) > -1) return true
    return !!(fileExtension && fileExtension.indexOf(type) > -1)
  })
  const isLimit = file.size < props.fileSize * 1024 * 1024
  if (!isImg) {
    ElNotification({
      title: '错误',
      message: `文件格式不正确, 请上传${props.fileType.join('/')}格式!`,
      type: 'error'
    })
    return false
  }
  if (!isLimit) {
    ElNotification({
      title: '错误',
      message: `存在文件大小超过${props.fileSize}MB的文件, 将会跳过此文件!`,
      type: 'error'
    })
    return false
  }
  uploadNumber.value++
}

/** 
 * 提交表单
 */
const submitFiles = () => {
  if (fileList.value.length == 0) {
    message.error('请上传文件')
    return
  }
  unref(uploadRef)?.submit()
}

/**
 * 清空文件列表
 */
const clearFiles = () => {
  unref(uploadRef)?.clearFiles()
}
defineExpose({ submitFiles, clearFiles })


/**
 * 文件上传成功
 * @param res 上传成功的回调
 */
const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
  // 删除自身
  const index = fileList.value.findIndex(
    (item) =>
      (res.code === 0 && item.response?.fileId === res.fileId)
  )
  fileList.value.splice(index, 1) // 删除该元素
  uploadList.value.push({
    name: res.data.name,
    url: res.data.url,
    size: res.data.size,
    path: res.data.path,
    fileId: res.fileId,
    configId: res.data.configId,
    type: res.data.type
  }) // 拼接上传结果
  if (uploadList.value.length == uploadNumber.value) {
    fileList.value.push(...uploadList.value)
    uploadList.value = []
    uploadNumber.value = 0
    emit('success', fileList)
  }
}

/**
 * 文件数超出提示
 */
const handleExceed: UploadProps['onExceed'] = (): void => {
  message.error(`上传文件数量不能超过${props.limit}个!`)
}

/**
 * 上传错误提示
 */
const uploadError: UploadProps['onError'] = (): void => {
  emit('error')
}

/**
 * 删除上传文件
 * @param file 要删除的文件
 */
const handleRemove = (file: UploadFile) => {
  const index = fileList.value.map((f) => f.name).indexOf(file.name)
  if (index > -1) {
    fileList.value.splice(index, 1)
  }
}

/**
 * 预览
 * @param uploadFile 要预览的文件
 */
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
  // TODO RG 增加针对每个文件预览的操作
  console.log(uploadFile)
}
</script>

2 使用示例

将上面的两个文件放在components文件夹下,按照正确的结构组织起来,然后在项目中直接导入即可。
例如在yudao-ui-admin项目中,其组织结构如下:

|--src
|   |--upload.ts
|   |--UploadFile.vue
|--index.ts

2.1 FileForm.vue

此文件是其中一个使用实例

<template>
  <Dialog
    v-model="dialogVisible"
    title="上传文件"
  >
    <UploadFile
      ref="uploadRef"
      :file-type="['pdf', 'doc', 'docx']"
      :file-size="10"
      :limit="5"
      :auto-upload="false"
      :is-show-tip="true"
      @success="submitFormSuccess"
    />
    <template #footer>
      <el-button
        :disabled="formLoading"
        type="primary"
        @click="submitFileForm"
      >确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>
<script lang="ts" setup>
import { UploadFile } from '@/components/UploadFile';
defineOptions({ name: 'InfraFileForm' })

const message = useMessage() // 消息弹窗

const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const uploadRef = ref()

/** 打开弹窗 */
const open = async () => {
  dialogVisible.value = true
  resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗

/** 提交表单 */
const submitFileForm = () => {
  uploadRef.value?.submitFiles()
}

/** 文件上传成功处理 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = (fileList) => {
  // 清理
  message.success("共上传成功" + fileList.value.length + "个文件")
  uploadRef.value?.clearFiles()
  dialogVisible.value = false
  emit('success')
}

/** 重置表单 */
const resetForm = () => {
  // 重置上传状态和文件
  formLoading.value = false
  uploadRef.value?.clearFiles()
}
</script>