前端上传文件(图片,视频)至阿里oss (vue3)

1.基于 el-upload 上传 二次封装

2.有环形进度条显示

3.有图片视频的预览


1.components/UploadFiles/index.vue(子组件)

<script setup>
import { Plus } from '@element-plus/icons-vue'
import { onBeforeMount, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { generateUUID } from '@/utils/share'
import { Message } from '@/utils/message'
import OSS from 'ali-oss'
const client = ref()
onBeforeMount(async () => {
  //发请求 密钥从后端获取 使用ststoken
  client.value = new OSS({
    region: 'Bucket地点', // 根据那你的Bucket地点来填写
    accessKeyId: '自己账户的accessKeyId, // 自己账户的accessKeyId
    accessKeySecret: '自己账户的accessKeySecret', // 自己账户的accessKeySecret
    stsToken:'',
    refreshSTSToken: async () => {} //刷新 重新发请求 获取 ststoken
    bucket: 'bucket名字', // bucket名字
    // 刷新临时访问凭证的时间间隔,单位为毫秒。
    refreshSTSTokenInterval: 3600 * 1000
  })
})

const base_file_url = 'oss存储桶地址' //用于拼接

const fileList = ref([]) //选择的file 文件集合
const fileDataList = ref([]) // oss 返回的 链接 集合
const showImg = ref(false)
const imgSrc = ref()
// ----------------
const showVideo = ref(false)
const imgVideo = ref()
//按钮是否禁用
const disabledFile = ref(false)
//文件预览
const handlePreview = (file) => {
  if (props.viewfiles) {
    //回显时的预览
    file.url = file.videoS // 点击预览的时候 把图片换回来
    // 判断是图片 还是 视频 进行不同的 预览
    if (dealFileType(file.url).type === 'img') {
      // 图片
      showImg.value = true
      imgSrc.value = file.url
    } else {
      // 视频
      showVideo.value = true
      imgVideo.value = file.url
    }
  } else {
    //上传时的预览
    if (disabledFile.value) Message.error('文件正在上传中...')
    // 图片
    if (file.raw.type.startsWith('image/')) {
      showImg.value = true
      imgSrc.value = file.url
    } else {
      // 视频
      showVideo.value = true
      imgVideo.value = file.raw.videoUrl
    }
  }
}

//用户文件回显
onMounted(() => {
  if (props.viewfiles) {
    //文件回显 链接集合赋值
    fileList.value = props.viewList
    fetchVideoUrl()
  }
})
// 根据后端返回的 url地址判断 是图片 还是 视频
const dealFileType = (url) => {
  // 支持的视频格式
  const reg = /^.*\.(mp|MP)4$/
  const reg1 = /^.*\.(mov|MOV)$/
  const reg2 = /^.*\.(avi|AVI)$/
  const reg3 = /^.*\.(wmv|WMV)$/
  const isMp4 = reg.test(url)
  const isMov = reg1.test(url)
  const isAvi = reg2.test(url)
  const isWmv = reg3.test(url)
  const VideoG = isMp4 || isMov || isAvi || isWmv
  let res = {}
  if (!url) return res
  res = {
    type: VideoG ? 'video' : 'img'
    // url: VideoG ? url : this.$imgUrl + (url[0] === '/' ? url.slice(1) : url)
  }
  return res
}
//后端没返视频第一帧封面 用默认图片替代
const vidSrc = new URL('../../assets/image/lookVideo.png', import.meta.url).href
// 把视频图片 用默认图片先展示
const fetchVideoUrl = () => {
  // 假设这是从后端获取视频地址的函数
  fileList.value.forEach((item) => {
    if (dealFileType(item.url).type === 'video') {
      item.videoS = item.url // 存视频的url 先存下来 定义一个字段
      item.url = vidSrc // 用默认图片代替 视频 url
    } else {
      item.videoS = item.url // 存图片的url
    }
  })
}

//图片删除
const handleRemove = (file) => {
  disabledFile.value = false
  // 获取被删除的图片的下标
  const tempIndex = fileList.value.findIndex((item) => item === file)
  // 删除后端返回地址 对应的数据 根据下标
  fileDataList.value = fileDataList.value.filter((item, index) => tempIndex !== index)
  fileList.value = fileList.value.filter((item) => item.uid !== file.uid)
}

//截取视频的第一帧来当封面
const getVideoCover = (file) => {
  const videoUrl = URL.createObjectURL(file) //预览地址
  file.videoUrl = videoUrl // 预留一个字段 保存预览路径
  const video = document.createElement('video') // 也可以自己创建video
  video.src = videoUrl // url地址 url跟 视频流是一样的
  // file.url = videoDefCover // 设置默认封面,videoDefCover 为预制的默认封面,不需要可去除或替换成自己的
  const canvas = document.createElement('canvas') // 获取 canvas 对象
  const ctx = canvas.getContext('2d') // 绘制2d
  video.crossOrigin = 'anonymous' // 解决跨域问题,也就是提示污染资源无法转换视频
  video.currentTime = 1 // 第一帧
  video.setAttribute('preload', 'auto')
  video.oncanplay = () => {
    // console.log(video.clientWidth, video.clientHeight)
    canvas.width = video.clientWidth ? video.clientWidth : 320 // 获取视频宽度
    canvas.height = video.clientHeight ? video.clientHeight : 320 // 获取视频高度
    // 利用canvas对象方法绘图
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
    // 转换成base64形式
    const _videoFirstimgsrc = canvas.toDataURL('image/png') // 截取后的视频封面
    //_self.$set(file, 'url', _videoFirstimgsrc) // 重置文件的url为当前截取的封面,用于 el-upload展示
    fileList.value.forEach((item) => {
      if (item.uid == file.uid) {
        item.url = _videoFirstimgsrc
      }
    })
    // console.log(imageUrl.value)
    video.remove()
    canvas.remove()
  }
  return file
}

//文件自定义上传
const handleUpload = async ({ file }) => {
  //上传视频
  if (file.type.startsWith('video/')) {
    // 截取第一帧来做封面
    getVideoCover(file)
  }

  const ImgType = file.type.split('/')[1]
  // obj.file => 待上传的文件对象
  // console.log('去上传吧', file) // 需要我们自己实现请求上传图片
  const fileObj = fileList.value.find((item) => item.uid === file.uid)
  fileObj.status = 'uploading' // 开启进度条
  try {
    const res = await client.value.multipartUpload(generateUUID() + '.' + ImgType, file, {
      headers: {
        'Content-Disposition': 'inline',
        'Content-Type': file.type // 注意:根据图片或者文件的后缀来设置,我试验用的‘.png'的图片,具体为什么下文解释
      },
      progress: (p) => {
        //提升用户体验
        fileObj.percentage = Math.floor(p * 100)
      }
    })
    const temp = base_file_url + res.name
    fileDataList.value.push(temp)
    disabledFile.value = false
  } catch (e) {
    // 捕获超时异常。
    if (e.code === 'ConnectionTimeoutError') {
      // 请求超时异常处理
      ElMessage({
        message: '上传超时,请重新上传',
        type: 'error'
      })
    }
    disabledFile.value = false
  }
}

//文件改变
const handleChange = (file, List) => {
  fileList.value = [...List]
}

//上传前的校验
const beforeUpload = (file) => {
  if (disabledFile.value) return Message.error('不能同时上传多个文件')
  if (file.type.startsWith('image/')) {
    // 一般在这里对即将上传的图片做校验
    // 你希望通过校验 return true; 不通过校验  return false  意味着不会执行上传
    const flag = file.size / 1024 / 1024 < 4
    const isJPEG = file.type === 'image/jpeg'
    const isPNG = file.type === 'image/png'
    const isJPG = file.type === 'image/jpg'
    if (!isJPG && !isPNG && !isJPEG) {
      Message.error('上传图片格式只能是 .jpg、.jpeg、.png 格式!')
      return false
    }
    if (!flag) {
      Message.error('图片大小不能超过4M')
      return false
    }
  } else if (file.type.startsWith('video/')) {
    // console.log(this.fileList, '上传的文件列表')
    const res = fileList.value.filter((item) => item.raw.type.startsWith('video/'))
    if (res.length >= 2) {
      Message.error('只能上传一个视频文件')
      return false
    }
    // 一般在这里对即将上传的视频做校验
    // 限制视频大小格式
    const voideo = file.size / 1024 / 1024 < 20
    // const isMp4 = file.type === 'video/mp4'
    // const isAvi = file.type === 'video/avi'
    // const isMov = file.type === 'video/mov'
    // if (!isMp4 && !isAvi && !isMov) {
    //   this.$message.error('视频只能上传 mp4、avi、mov 格式')
    //   return false
    // }
    if (!voideo) {
      Message.error('视频大小不能超过20M')
      return false
    }
  } else {
    Message.warning('请上传图片或视频文件格式')
    return false
  }
  disabledFile.value = true
  return true
}

// 点击关闭视频预览
const closeVideo = () => {
  const video = document.getElementById('myVideo')
  //关闭视频播放
  video.pause()
  showVideo.value = false
  //回显文件状态
  if (props.viewfiles) {
    fetchVideoUrl()
  }
}
//关闭图片预览
const closeImg = () => {
  showImg.value = false
}

//暴露数据
defineExpose({
  fileList,
  fileDataList
})

//父传子
const props = defineProps({
  //上传文件的总数量
  count: {
    type: Number,
    default: 1
  },
  //文件回显 特点 viewfiles 为true
  viewfiles: {
    type: Boolean,
    default: false
  },
  //文件回显链接集合
  viewList: {
    type: Array
  }
  //编辑文件 特点(待写)
})
</script>

<template>
  <div class="UploadFile-page">
    <el-upload
      action="#"
      list-type="picture-card"
      :file-list="fileList"
      :on-preview="handlePreview"
      :limit="count"
      :class="{ disabled: fileList.length >= count, viewcss: viewfiles }"
      :on-remove="handleRemove"
      :http-request="handleUpload"
      :on-change="handleChange"
      :before-upload="beforeUpload"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
    <el-dialog :show-close="false" class="imgs" :before-close="closeImg" v-model="showImg">
      <img :src="imgSrc" alt="" style="width: 100%" />
    </el-dialog>
    <el-dialog :show-close="false" class="imgs1" :before-close="closeVideo" v-model="showVideo">
      <div class="box">
        <video id="myVideo" autoplay controls :src="imgVideo" alt="" />
      </div>
    </el-dialog>
  </div>
</template>

<style lang="scss" scoped>
.UploadFile-page {
  margin-top: 10px;
  width: 300px;
  :deep(.el-upload-list) {
    //隐藏按delete删除
    .el-icon--close-tip {
      display: none;
    }
    //隐藏按delete删除
    .el-upload-list__item.is-success:focus:not(:hover) .el-icon--close-tip {
      display: none !important;
    }
    //设置选择完显示图片上传组件的宽高
    .el-upload-list__item {
      width: 90px !important;
      height: 90px !important;
    }
    //设置点击上传的组件宽高
    .el-upload {
      width: 90px !important;
      height: 90px !important;
    }
    //设置图片的宽高
    img {
      width: 90px !important;
      height: 90px !important;
      object-fit: cover;
    }
    //设置进度条的位置
    .el-upload-list--picture-card .el-progress {
      top: 35% !important;
      left: 73% !important;
    }
    .el-upload-list--picture-card .el-progress .el-progress__text {
      top: 75% !important;
      left: -17% !important;
    }
    .el-progress-circle {
      height: 50px !important;
      width: 80px !important;
    }
    .el-progress--circle {
      width: 80px; /* 设置宽度 */
      height: 80px; /* 设置高度 */
    }
  }
  //限制数量满了之后 隐藏上传框
  :deep(.disabled) {
    .el-upload {
      display: none;
    }
  }

  :deep(.viewcss) {
    .el-upload {
      display: none;
    }
    .el-upload-list__item-delete {
      display: none;
    }
  }

  //el-dialog 弹窗提示
  .imgs {
    width: 100%;
    height: 600px;
    .el-dialog__body {
      height: 600px !important;
    }
    img {
      width: 100%;
      height: 600px;
      border-radius: 10px;
    }
  }
  .imgs1 {
    width: 100% !important;
    .box {
      border-radius: 10px;
    }
    video {
      margin: 0; /* 清除默认外边距 */
      padding: 0; /* 清除默认内边距 */
      display: block;
      width: 100%;
      height: 100%;
      border-radius: 10px;
      //object-fit: cover; //占满整个盒子
    }
  }
}
</style>

2.父组件(用于文件上传)

   <!-- 文件上传 -->
   <UploadFiles ref="UploadFileRef" :count="9"></UploadFiles>

    通过 UploadFileRef.value.fileDataList 拿到子组件上传的oss 信息集合

3.父组件(用于文件的回显)

  <!-- 文件回显-->
                <UploadFiles
                  :viewfiles="true" //用户判断是否 回显 上传
                  v-if="showTableData.files?.length"
                  :viewList="showTableData.files" //传后端返回的集合 回显
                ></UploadFiles>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值