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>