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>
相关推荐
San30.11 分钟前
从零构建坚固的前端堡垒:TypeScript 与 React 实战深度指南
前端·react.js·typescript
东东5161 小时前
果园预售系统的设计与实现spingboot+vue
前端·javascript·vue.js·spring boot·个人开发
怪兽毕设2 小时前
基于SpringBoot的选课调查系统
java·vue.js·spring boot·后端·node.js·选课调查系统
Amumu121382 小时前
Vue Router(一)
前端·javascript·vue.js
VT.馒头2 小时前
【力扣】2694. 事件发射器
前端·javascript·算法·leetcode·职场和发展·typescript
切糕师学AI2 小时前
VSCode 下如何检查 Vue 项目中未使用的依赖?
vue.js·vscode
我是伪码农3 小时前
Vue 1.30
前端·javascript·vue.js
利刃大大3 小时前
【Vue】默认插槽 && 具名插槽 && 作用域插槽
前端·javascript·vue.js
风之舞_yjf3 小时前
Vue基础(27)_脚手架安装
vue.js
BYSJMG3 小时前
计算机毕设选题推荐:基于大数据的癌症数据分析与可视化系统
大数据·vue.js·python·数据挖掘·数据分析·课程设计