elementPlus_upload组件二次封装,使用 STS 临时密钥 + SDK 上传方式 实现cos上传
1.前端拿到临时密钥,使用 cos-js-sdk-v5
上传
useCosUploader.ts
js
import { GetCredential } from '@/apis/user'
import COS from 'cos-js-sdk-v5'
interface STSCredentialResponse {
TmpSecretId: string
TmpSecretKey: string
Token: string
StartTime: string
ExpiredTime: string
Bucket: string
Region: string
}
interface UploadOptions {
file: File
key: string
bucket?: string
region?: string
onProgress?: (progress: COS.ProgressInfo) => void
}
const DEFAULT_BUCKET = import.meta.env.VITE_APP_COS_BUCKET
const DEFAULT_REGION = import.meta.env.VITE_APP_COS_REGION
// 缓存的 cos 实例
let cachedCosInstance: COS | null = null
let credentialExpireTime = 0
async function getCachedCOS(): Promise<COS> {
const now = Math.floor(Date.now() / 1000)
// 如果已有 cos 实例并且没过期,直接返回
if (cachedCosInstance && credentialExpireTime - now > 60) {
return cachedCosInstance
}
// 否则重新获取 STS 凭证
const res: any = await GetCredential()
const data = res.data as STSCredentialResponse
// eslint-disable-next-line style/max-statements-per-line
if (!data) { throw new Error('获取凭证失败') }
credentialExpireTime = Number(data.ExpiredTime)
cachedCosInstance = new COS({
getAuthorization: async (_, callback) => {
callback({
TmpSecretId: data.TmpSecretId,
TmpSecretKey: data.TmpSecretKey,
XCosSecurityToken: data.Token,
StartTime: Number(data.StartTime),
ExpiredTime: Number(data.ExpiredTime),
})
},
})
return cachedCosInstance
}
export async function useCosUploader({ file, key, onProgress, bucket = DEFAULT_BUCKET, region = DEFAULT_REGION }: UploadOptions): Promise<string> {
const cos = await getCachedCOS()
return new Promise((resolve, reject) => {
cos.uploadFile(
{
Bucket: bucket,
Region: region,
Key: key,
Body: file,
// Headers: {
// 'x-cos-meta-tag': 'your-custom-tag', // 自定义业务标记(存储为对象元数据)
// 'x-custom-header': 'my-header-value', // 如果服务端做自定义鉴权识别
// },
onProgress(progress) {
// eslint-disable-next-line style/max-statements-per-line
if (onProgress) { onProgress(progress) }
},
},
(err, data) => {
if (err) {
reject(err)
} else {
resolve(`https://${data.Location}`)
}
},
)
})
}
2.upload组件二次封装
js
<script setup lang="ts">
import type { IFileList } from '@/types/index'
import type { UploadInstance, UploadProps } from 'element-plus'
import { useCosUploader } from '@/composables/useCosUploader'
import { Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { cloneDeep, debounce } from 'lodash-es'
defineOptions({
name: 'FileUpload',
})
const props = withDefaults(defineProps<{
modelValue?: IFileList[]
size?: number
limit?: number
typeLimit?: {
type: string[]
tip: string
}
}>(), {
modelValue: () => [],
size: 100,
limit: 10,
typeLimit: () => ({
type: [], // 'doc', 'docx', 'jpg', 'jpeg', 'pdf', 'png', 'ppt', 'pptx', 'txt', 'xls', 'xlsx', 'zip', 'rar'
tip: '文件类型错误,请重新选择',
}),
})
const emits = defineEmits(['update:modelValue', 'change', 'preview'])
const uploadRef = ref<UploadInstance>()
const fileList = ref<IFileList[]>(props.modelValue as IFileList[])
watchEffect(() => {
fileList.value = props.modelValue as IFileList[]
})
// 文件上传操作
const updateFileListItem = (item: any, url?: string) => {
const cloneFileList = cloneDeep(fileList.value)
const currentIndex = cloneFileList.findIndex(li => li.uid === item.file.uid)
// eslint-disable-next-line style/max-statements-per-line
if (currentIndex < 0) { return }
if (url) {
// 上传成功
cloneFileList[currentIndex] = { ...cloneFileList[currentIndex], url, status: 'success' }
} else {
// 上传失败
cloneFileList.splice(currentIndex, 1)
}
fileList.value = cloneFileList
console.log(fileList.value)
emits('update:modelValue', cloneFileList)
emits('change')
}
// 触发自定义文件上传
const httpRequest = async (item: any) => {
console.log('🚀 ~ httpRequest ~ item:', item)
try {
const file = item.file
const key = `enjoyai/${+new Date()}_${item.file.name}`
const url = await useCosUploader({ file, key, onProgress: (progress) => {
console.log(`🚀 上传进度:${(progress.percent * 100).toFixed(2)}%`)
} })
updateFileListItem(item, url)
console.log('🚀 ~ httpRequest ~ url:', url)
} catch (err) {
console.log('🚀 ~ httpRequest ~ err:', err)
updateFileListItem(item)
ElMessage.error(`"${item.file.name}"上传失败,请重试`)
}
}
// 上传前的文件校验
const validateFiles = debounce(() => {
// 当超出选择时uploading
if (fileList.value.length > props.limit) {
const index = fileList.value.findIndex(item => item.status === 'ready')
fileList.value.splice(index)
emits('update:modelValue', fileList.value)
ElMessage.warning(`最多上传${props.limit}个文件,请重新选择`)
return false
}
const index = fileList.value.findIndex(item => item.status === 'ready')
for (let i = 0; i < fileList.value.length; i++) {
// 不对以前已经上传过得文件进行校验
if (fileList.value[i].status !== 'ready') {
continue
}
// 校验文件类型是否符合要求
const fileSuffix = fileList.value[i].name.slice(fileList.value[i].name.lastIndexOf('.') + 1)
console.log('🚀 ~ validateFiles ~ fileSuffix:', fileSuffix)
if (props.typeLimit.type.length > 0 && !props.typeLimit.type.includes(fileSuffix)) {
fileList.value.splice(index)
ElMessage.error(props.typeLimit.tip)
return
}
// 校验文件大小
if (props.size * 1024 * 1024 < fileList.value[i].size!) {
fileList.value.splice(index)
ElMessage.error(`文件超过${props.size}M限制大小,请重新选择`)
return
}
}
emits('update:modelValue', fileList.value)
fileList.value.map((item) => {
if (item.status === 'ready') {
item.status = 'uploading'
}
item.status === 'uploading' && httpRequest(item)
return item
})
}, 50)
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
fileList.value.push({
url: '',
name: file.name,
type: file.name.split('.').at(-1) ?? '' as string,
status: 'ready',
uid: file.uid,
size: file.size,
sizeMb: `${(file.size / 1024 / 1024).toFixed(2)}MB`,
file,
})
validateFiles()
return false
}
// 移除文件
const removeFile = (index: number) => {
const updatedFiles = cloneDeep(fileList.value)
updatedFiles.splice(index, 1)
fileList.value = updatedFiles
emits('update:modelValue', updatedFiles)
}
// 手动触发选择文件
const choiceFile = () => {
uploadRef?.value?.$el.querySelector('.upload_button').click()
}
// 预览文件
const onPreview = (file: any, index: number) => {
if (file.status !== 'success') {
return
}
emits('preview', { fileList: fileList.value, index })
}
defineExpose({
removeFile,
choiceFile,
onPreview,
})
</script>
<template>
<ElUpload
ref="uploadRef"
class="file-upload-box"
action="#"
:multiple="props.limit > 1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:file-list="fileList"
:show-file-list="false"
v-bind="$attrs"
>
<el-button circle plain>
<el-icon><Upload /></el-icon>
</el-button>
</ElUpload>
</template>
<style lang="scss" scoped>
</style>