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>