elementPlus_upload组件二次封装,cos上传

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>
相关推荐
枫super13 分钟前
Day-03 前端 Web-Vue & Axios 基础
前端·javascript·vue.js
计算机学姐34 分钟前
基于SSM的校园美食交流系统
java·vue.js·mysql·spring·tomcat·mybatis·美食
程序猿chen1 小时前
Vue.js组件安全工程化演进:从防御体系构建到安全性能融合
前端·vue.js·安全·面试·前端框架·跳槽·安全架构
huige99991 小时前
Vue 与 ORM 的对比理解:抽象的力量
vue.js·orm
爱的叹息1 小时前
容器初始化Spring Boot项目原理,即web项目(war)包涉及相关类对比详解
前端·vue.js·typescript
pink大呲花2 小时前
Vue.js 中 v-if 的使用及其原理
前端·javascript·vue.js
阿豪啊2 小时前
TypeScript 中any 和 unknown的区别
typescript
天官赐福_2 小时前
vue2的scale方式适配大屏
前端·vue.js
季禮祥2 小时前
Vite+Vue报错TypeError: Failed to fetch dynamically imported module,阁下该如何应对?
vue.js·vite
skyWang4162 小时前
基于 Vue3 和 Tiptap的在线多人协同编辑文本编辑器
vue.js