klren0312 / daliy_knowledge

知识积累,正确使用方式是watch
22 stars 4 forks source link

elementui upload 使用阿里oss断点续传 #805

Open klren0312 opened 5 months ago

klren0312 commented 5 months ago
<template>
  <div class="upload-block">
    <template v-if="!modelValue">
      <el-upload
        v-if="uploadStatus === 0"
        class="upload-resource"
        action="#"
        :multiple="false"
        :limit="1"
        :auto-upload="true"
        :show-file-list="false"
        :file-list="uploadFileList"
        :http-request="uploadFile"
        :before-upload="handleBeforeUpload"
        accept=".jpg,.png,.mp4,.jpeg"
      >
        <el-button link type="primary">上传</el-button>
      </el-upload>
      <div class="upload-process flex" v-else-if="uploadStatus === 1">
        <el-progress style="width: 100%;" type="line" :percentage="uploadProcess"></el-progress>
        <el-icon @click="doPauseOrPlay(true)" v-if="!isUploadPlay"><VideoPlay /></el-icon>
        <el-icon @click="doPauseOrPlay(false)" v-if="isUploadPlay"><VideoPause /></el-icon>
      </div>
    </template>
    <div v-else>{{ fileName }}</div>
  </div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { createClient } from '/@/utils/oss'
import { ElMessage, UploadInstance, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import OSS, { Checkpoint } from 'ali-oss'

defineOptions({
  name: 'ResourceUpload'
})

const props = defineProps<{
  modelValue: string
  fileName: string
  downloadUrl: string | undefined
}>()
const $emit = defineEmits(['update:modelValue', 'update:fileName', 'fileChange'])
const uploadStatus = ref(0) // 0-未上传 1-上传中 2-上传成功
const uploadProcess = ref(0)
const uploadCheckPoint = ref<Checkpoint>()
const isUploadPlay = ref(false) // 是否正在上传

const uploadPath = ref('') // 上传的路径
const theUploadFile = ref<UploadRawFile>()

const uploadRef = ref<UploadInstance>()
const uploadFileList = ref<UploadUserFile[]>([])

const client = ref<OSS | null | undefined>()

watch(() => props.modelValue, (nv) => {
  if (!nv) {
    uploadStatus.value = 0
  }
})

onMounted(() => {
  window.addEventListener('offline', offlineHandle)
})

onUnmounted(() => {
  window.removeEventListener('offline', offlineHandle)
})

/**
 * 断网时,暂停上传
 */
const offlineHandle = () => {
  if (client.value && uploadProcess.value) {
    (client.value as any).cancel()
    isUploadPlay.value = false
  }
}

/**
 * 上传开始/暂停
 * @param isPlay true-开始 false-暂停
 */
const doPauseOrPlay = (isPlay: boolean) => {
  if (isPlay) {
    resumeUploadFile()
    isUploadPlay.value = true
  } else {
    (client.value as any).cancel()
    isUploadPlay.value = false
  }
}

const uploadFile = async (options: UploadRequestOptions) => {
  client.value = await createClient()
  if (client.value) {
    isUploadPlay.value = true
    uploadStatus.value = 1
    const theFile = options.file
    const suffix = options.file.name.split('.').pop()
    const uuid = new Date().getTime() + Math.random().toString(36).substr(2)
    const path = 'materials/resource/' + uuid + '.' + suffix
    uploadPath.value = path
    theUploadFile.value = theFile
    try {
      const result = await client.value.multipartUpload(path, theFile, {
        // 设置并发上传的分片数量。
        parallel: 4,
        // 设置分片大小。默认值为1 MB,最小值为100 KB。
        partSize: 1024 * 1024,
        headers: {
          'Cache-Control': 'no-cache',
          'Content-Encoding': 'utf-8',
          'x-oss-forbid-overwrite': 'true'
        },
        progress: (p: number, checkpoint: Checkpoint) => {
          const progress = Math.floor(p * 100)
          if (progress === 100) {
            uploadProcess.value = progress
            uploadStatus.value = 2
          } else {
            uploadCheckPoint.value = checkpoint
            uploadProcess.value = progress
          }
        }
      })
      if (result.res.status === 200) {
        $emit('update:fileName', options.file.name)
        $emit('update:modelValue', path)
        $emit('fileChange')
        clearCache()
        isUploadPlay.value = false
      }
    } catch (error) {
      console.error(error)
      if (uploadProcess.value === 0) {
        doUploadError()
      }
    }
  } else {
    doUploadError()
  }
}

/**
 * 清空缓存
 */
const clearCache = () => {
  uploadCheckPoint.value = undefined
  client.value = undefined
  uploadPath.value = ''
  theUploadFile.value = undefined
}

/**
 * 上传失败操作
 */
const doUploadError = () => {
  isUploadPlay.value = false
  uploadStatus.value = 0
  ElMessage.error('上传失败, 请重新上传')
  uploadRef.value?.clearFiles()
  clearCache()
}

/**
 * 上传前校验文件
 */
const handleBeforeUpload = (theFile: UploadRawFile) => {
  const suffix = theFile.name.split('.').pop() || ''
  const fileName = theFile.name.split('.').shift() || ''
  const extArr = [
    'jpg', 'png', 'mp4', 'jpeg'
  ]
  if (!extArr.includes(suffix)) {
    ElMessage.error('资源格式错误')
    return false
  } else if (!/^[\u4E00-\u9FA5A-Za-z0-9]+$/.test(fileName)) {
    // 文件名称支持中英文和数字
    ElMessage.error('封面图名称只支持中英文和数字')
    return false
  } else {
    return true
  }
}

/**
 * @description 恢复上传
 * @param {*} item 文件信息
 * @param {*} checkpoint 分片信息
 */
const resumeUploadFile = async () => {
  if (uploadCheckPoint.value) {
    try {
      if (uploadProcess.value < 100 && client.value && theUploadFile.value) {
        try {
          isUploadPlay.value = true
          const result = await client.value.multipartUpload(uploadPath.value, theUploadFile.value, {
            // 设置并发上传的分片数量。
            parallel: 4,
            // 设置分片大小。默认值为1 MB,最小值为100 KB。
            partSize: 1024 * 1024,
            headers: {
              'Cache-Control': 'no-cache',
              'Content-Encoding': 'utf-8',
              'x-oss-forbid-overwrite': 'true'
            },
            checkpoint: uploadCheckPoint.value,
            progress: (p: number, checkpoint: Checkpoint) => {
              const progress = Math.floor(p * 100)
              if (progress === 100) {
                uploadProcess.value = progress
                uploadStatus.value = 2
              } else {
                uploadCheckPoint.value = checkpoint
                uploadProcess.value = progress
              }
            }
          })
          if (result.res.status === 200) {
            $emit('update:fileName', theUploadFile.value.name)
            $emit('update:modelValue', uploadPath.value)
            $emit('fileChange')
            clearCache()
            isUploadPlay.value = false
          }
        } catch (error) {
          await resetUpload(error)
        }
      }
    } catch {
      doUploadError()
    }
  } else {
    doUploadError()
  }
}

/**
 * 报错后重置上传
 */
const resetUpload = async (err: any) => {
  const msg = JSON.stringify(err)
  if (msg.indexOf('Error') !== -1) {
    if (client.value) {
     ( client.value as any).cancel()
    }
    client.value = await createClient()
    await resumeUploadFile()
  }
}
</script>

<style lang="scss" scoped>
.upload-block,
.upload-process {
  width: 100%;
}
</style>