el-upload实现可替换、删除、预览的图片上传
- 组件使用:
<template>
<div>
<UploadImage sendUrl='' :limit="1" :size="size" :gifSize="gifSize" v-model="images"></UploadImage>
</div>
</template>
<script>
import Vue from 'vue'
import UploadImage from './UploadImage.vue'
export default {
components: {
UploadImage
},
data(){
retun {
images:'',
size: 400 * 1024,
ifSize: 1 * 1024 * 1024,
// images:[]
}
}
}
</script>
UploadImage.vue
组件:
<template>
<div class="uploadFileMain" :style="{'--height':`${height}px`,'--width':`${width}px`}">
<div v-for="(item,index) in fileList" :key="index" class="upload-list" :class="[limit==1?'upload-lis1':'']">
<template v-if="switchIndex == index && showProgress" >
<div class="percentMain" v-if="limit == 1 && getAcceptArray.includes('video')">
<el-progress type="circle" v-if="!percentNumLoading" :format="format" :width="Math.min(width,height)*0.8" v-bind="percentNum==100?{status:'success'}:{}" :percentage="percentNum" ></el-progress>
<div v-else v-loading="true" :style="{ 'height': `${height}px`, 'width': `${width}px` }" class="loadingView" element-loading-background="rgba(0, 0, 0, 0.8)" />
</div>
<div v-else v-loading="true" :style="{ 'height': `${height}px`, 'width': `${width}px` }" class="loadingView" element-loading-background="rgba(0, 0, 0, 0.8)" />
</template>
<template v-if="getAcceptArray.includes('audio')">
<div class="txtMp"><span>{{ getName(item.url).nametxt1 }}</span>{{ getName(item.url).nametxt2 }}.{{ getName(item.url).ext }}</div>
</template>
<img v-else :id="`${idName}_image_${index}`" class="el-upload-listImg" :src="getShowImage(item.url, `${idName}_image_${index}`)" alt="">
<span class="el-actions" :class="[!showProgress && 'actionsHover']">
<span
v-if="getAcceptArray.includes('video')"
class="el-upload-icon"
@click="handlePlayVideo(index)"
>
<i v-if="audioObject[index] && audioObject[index].play" class="el-icon-video-pause" />
<i v-else class="el-icon-video-play" />
</span>
<span
v-else-if="getAcceptArray.includes('audio')"
class="el-upload-icon"
@click="handlePlayMp(index)"
>
<i v-if="audioObject[index] && audioObject[index].play" class="el-icon-video-pause" />
<i v-else class="el-icon-video-play" />
</span>
<span
v-else
class="el-upload-icon"
@click="handlePictureCardPreview(index,item.url)"
>
<i class="el-icon-zoom-in" />
</span>
<span
class="el-upload-icon"
@click="switchFn(index)"
>
<i class="el-icon-sort" style="transform: rotate(90deg);" />
</span>
<span
class="el-upload-icon"
@click="delRemove(index)"
>
<i class="el-icon-delete" />
</span>
</span>
</div>
<el-upload
:class="[showProgress && 'noClick']"
v-show="!limit || fileList.length<limit"
:ref="`${idName}_upload`"
:show-file-list="false"
:multiple="multiple"
:limit="limit?limit+1:limit"
:action="sendUrl"
list-type="picture-card"
:headers="{Authorization: $utils.getToken()}"
:accept="acceptArray.length>0?acceptArray.map(n=>this.acceptType[n]).join(',') :'*'"
:file-list="fileList"
:before-upload="beforeUpload"
:on-progress="progressFn"
:on-success="uploadSuccess"
>
<template v-if="switchIndex == -1 && showProgress" >
<div class="percentMain" v-if="limit == 1 && getAcceptArray.includes('video')">
<el-progress type="circle" v-if="!percentNumLoading" :format="format" :width="Math.min(width,height)*0.8" v-bind="percentNum==100?{status:'success'}:{}" :percentage="percentNum" ></el-progress>
<div v-else v-loading="true" :style="{ 'height': `${height}px`, 'width': `${width}px` }" class="loadingView" element-loading-background="rgba(0, 0, 0, 0.8)" />
</div>
<div v-else v-loading="true" :style="{ 'height': `${height}px`, 'width': `${width}px` }" class="loadingView" element-loading-background="rgba(0, 0, 0, 0.8)" />
</template>
<div v-else class="uploadClick" :id="`${idName}_uploadClick`" slot="trigger" @click="changeIndex(-1)">
<i class="el-icon-plus" />
</div>
</el-upload>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
export default {
props: {
// 上传地址
sendUrl: {
type: String,
default: ''
},
value: {
type: [Array, String],
default: ''
},
width: {
type: [Number, String],
default: 80
},
height: {
type: [Number, String],
default: 80
},
multiple: {
type: Boolean,
default: true
},
limit: {
type: Number,
default: null
// default: 4
},
// 大小限制:10 * 1024 * 1024 = 10MB
size: {
type: Number,
default: -1
},
// 限制类型,按照acceptType数组里面来
acceptArray: {
type: Array,
default: () => {
return ['png', 'jpg', 'jpeg', 'gif']
}
},
gifSize: {
type: Number,
default: -1
},
// 分片上传
isSharding: {
type: Boolean,
default: false
},
// 切片大小
chunkSize: {
type: Number,
default: 5 * 1024 * 1024
},
// 合并地址
mergeUrl: {
type: String,
default: ``
}
},
data() {
return {
audioObject: {
},
acceptType: {
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pdf': 'application/pdf',
'csv': '.csv',
'txt': 'text/plain',
'image': 'image/*',
'png': 'image/png',
'gif': 'image/gif',
'jpg': 'image/jpg',
'jpeg': 'image/jpeg',
'audio': 'audio/*',
'mp3': 'audio/mpeg',
'video': 'video/*'
},
idName: `${Math.random().toString(36).slice(-8)}_${new Date().getTime()}`,
switchIndex: -1,
fileList: [],
dataArray: {
1: [],
2: []
},
showProgress: false,
percentNum: 0,
percentNumLoading: false
}
},
computed: {
getAcceptArray() {
const dataArray = this.acceptArray.map(n => this.acceptType[n]).filter(n => n)
if (this.acceptArray.indexOf('video') != -1) {
dataArray.push('video/*')
}
return dataArray.join(',')
}
},
watch: {
value: {
handler(value) {
if (Array.isArray(value)) {
this.fileList = value.filter(n => n != '').map(n => {
return {
url: n
}
})
} else {
this.fileList = value.split(',').filter(n => n != '').map(n => {
return {
url: n
}
})
}
},
deep: true,
immediate: true
},
fileList: {
handler(value) {
if (value.length > 0) {
this.toFileImg(value)
}
},
deep: true,
immediate: true
},
dataArray: {
handler(value) {
// 1 和 2 相等表示这次上传成功的数量相同,会添加到数组里面
if (value[1].length != 0 && value[2].length != 0 && value[1].length == value[2].length) {
value[2].forEach(e => {
//成功的加进去
if (e.code == 0) {
// 有更换就变化
if (this.switchIndex != -1) {
this.fileList.splice(this.switchIndex, 1, {
status: 'success',
url: e.data
})
} else {
// 添加时判断是否超出校址
if ((this.limit && this.fileList.length < this.limit) || !this.limit) {
// 没有更好追加
this.fileList.push({
status: 'success',
url: e.data
})
}
}
} else {
this.$message.error(e.message || '上传失败')
}
})
if (this.switchIndex != -1) {
setTimeout(() => {
this.switchIndex = -1
this.showProgress = false
}, 300)
} else {
this.showProgress = false
setTimeout(() => {
this.switchIndex = -1
}, 300)
}
this.dataArray = {
1: [],
2: []
}
// 清除组件上传类别
this.$refs[`${this.idName}_upload`].clearFiles()
}
},
deep: true
}
},
methods: {
async lookImageViewer(url, list) {
let listImg = list
const thisIndex = list.indexOf(url)
const firstArray = list.slice(thisIndex, list.length)
const twoArray = list.slice(0, thisIndex)
listImg = [...firstArray, ...twoArray]
// this.$viewerApi({ images: listImg })//v-viewer组件
const id = 'MyElImageViewer_' + new Date().getTime() + '_' + parseInt(Math.random() * 1000)
// 引用组件(找到Elementui中image-viewer的位置)
const ElImageViewer = (await import('element-ui/packages/image/src/image-viewer')).default
const MyElImageViewer = Vue.component('MyElImageViewer', ElImageViewer)
const MyElImageViewerNew = new MyElImageViewer({
propsData: {
urlList: listImg,
onClose: () => {
// 删除组件
compDOm.$destroy()
document.body.removeChild(document.getElementById(id))
}
}
})
const DOM = document.createElement('div')
DOM.setAttribute('id', id)
DOM.setAttribute('class', 'imageSwipeViewer_Show')
document.body.appendChild(DOM)
// 挂载组件
const compDOm = MyElImageViewerNew.$mount(DOM.appendChild(document.createElement('template')))
compDOm.$nextTick(() => {
const showDom = document.getElementById(id)
showDom.querySelector('.el-icon-circle-close').style = 'font-size:38px;color:#fff'
})
},
getName(url) {
let name = url.split('/').at(-1)
name = name.split('?')[0]
const nametxt = name.split('.')[0]
const ext = name.split('.')[1]
return {
nametxt1: nametxt.slice(0, nametxt.length - 3),
nametxt2: nametxt.slice(nametxt.length - 3, nametxt.length),
ext
}
},
filterSize(size) {
const pow1024 = (num) => {
return Math.pow(1024, num)
}
if (!size) return ''
if (size < pow1024(1)) return size + ' B'
if (size < pow1024(2)) return (size / pow1024(1)).toFixed(0) + ' KB'
if (size < pow1024(3)) return (size / pow1024(2)).toFixed(0) + ' MB'
if (size < pow1024(4)) return (size / pow1024(3)).toFixed(0) + ' GB'
return (size / pow1024(4)).toFixed(2) + ' TB'
},
// 上传之前放到1
beforeUpload(e) {
const fileSize = e.size
if (this.gifSize > 0) {
if (e.type.indexOf('gif') != -1) {
if (fileSize > this.gifSize) {
this.$message.error(`gif最大上传${this.filterSize(this.gifSize)}`)
return false
}
} else {
if (this.size > 0 && fileSize > this.size) {
this.$message.error(`最大上传${this.filterSize(this.size)}`)
return false
}
}
} else {
if (this.size > 0 && fileSize > this.size) {
this.$message.error(`最大上传${this.filterSize(this.size)}`)
return false
}
}
this.dataArray[1].push({
status: 'uploading',
...e
})
return true
},
// 通过 slot="trigger" ,区分模拟点击,表示这次时人为点击的
changeIndex(index) {
if (index == -1) {
this.switchIndex = -1
}
},
progressFn(e) {
if (this.limit == 1 && this.getAcceptArray.includes('video')) {
this.percentNum = e.percent
if (this.percentNum < 100) {
this.percentNumLoading = false
} else {
setTimeout(() => {
this.percentNumLoading = true
}, 500)
}
}
this.showProgress = true
},
format(percentage) {
return Math.ceil(percentage) + '%'
},
// 更换图片,模拟点击
switchFn(index) {
document.getElementById(`${this.idName}_uploadClick`).click(this.switchIndex)
setTimeout(() => {
this.switchIndex = index
}, 0)
},
// 查看图片
handlePictureCardPreview(index, url) {
this.lookImageViewer(url, [url])
},
// 进入全屏
fullScreen(ele) {
if (ele.requestFullscreen) {
ele.requestFullscreen()
} else if (ele.mozRequestFullScreen) {
ele.mozRequestFullScreen()
} else if (ele.webkitRequestFullScreen) {
ele.webkitRequestFullScreen()
}
},
// 退出全屏
exitFullscreen() {
const de = document
if (de.exitFullscreen) {
de.exitFullscreen()
} else if (de.mozCancelFullScreen) {
de.mozCancelFullScreen()
} else if (de.webkitCancelFullScreen) {
de.webkitCancelFullScreen()
}
},
// 视频播放
handlePlayVideo(index) {
const videoDom = document.createElement('video')
videoDom.setAttribute('src', this.fileList[index].url)
videoDom.setAttribute('style', 'position: fixed;top: -200vh;')
document.body.appendChild(videoDom)
// 获取全屏相关方法
const getChangeFullscreen = () => {
const checkIsFullScreen = () => {
var isFullScreen = document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen
return isFullScreen == undefined ? false : isFullScreen
}
const fullscreenEvents = [
['fullscreenchange', 'requestFullscreen', 'exitFullscreen'],
['webkitfullscreenchange', 'webkitRequestFullScreen', 'webkitCancelFullScreen'],
['mozfullscreenchange', 'mozRequestFullScreen', 'mozCancelFullScreen']
]
for (let x = 0; x < fullscreenEvents.length; x++) {
if (document[fullscreenEvents[x][2]]) {
return { fullscreenEventsName: fullscreenEvents[x][0], fullScreenName: fullscreenEvents[x][1], exitFullscreenName: fullscreenEvents[x][2], checkIsFullScreen }
}
}
}
const { fullscreenEventsName, fullScreenName, exitFullscreenName, checkIsFullScreen } = getChangeFullscreen()
setTimeout(() => {
videoDom[fullScreenName]()
}, 0)
document.addEventListener(fullscreenEventsName, () => {
if (checkIsFullScreen()) {
videoDom.play()
} else {
document[exitFullscreenName]()
videoDom.pause()
setTimeout(() => {
videoDom.parentNode.removeChild(videoDom)
}, 0)
}
})
},
// 音频播放
handlePlayMp(index) {
const _this = this
if (!this.audioObject[index]) {
const thisObje = {}
thisObje[index] = {
audio: new Audio(this.fileList[index].url),
play: false
}
this.audioObject = { ...this.audioObject, ...thisObje }
const play = () => {
_this.$set(_this.audioObject[index], 'play', true)
}
const stop = () => {
_this.$set(_this.audioObject[index], 'play', false)
}
this.audioObject[index].audio.addEventListener('play', play, true)
this.audioObject[index].audio.addEventListener('pause', stop, true)
this.audioObject[index].audio.addEventListener('ended', stop, true)
this.audioObject[index].audio.play()
} else {
if (this.audioObject[index].play) {
this.audioObject[index].audio.pause()
} else {
this.audioObject[index].audio.play()
}
}
},
// 成功后放到2
uploadSuccess(e) {
this.dataArray[2].push({
...e
})
},
// 传递图片
toFileImg(value) {
if (Array.isArray(this.value)) {
this.$emit('input', value.map(n => n.url))
} else {
this.$emit('input', value.map(n => n.url).join(','))
}
},
delRemove(index) {
this.fileList.splice(index, 1)
if (this.fileList.length == 0) {
this.toFileImg(this.fileList)
}
},
getVideoImg(url, time = 0) {
return new Promise((r, j) => {
const video = document.createElement('video') // 创建video对象
video.src = url // url地址
const canvas = document.createElement('canvas') // 创建 canvas 对象
const ctx = canvas.getContext('2d') // 绘制2d
video.crossOrigin = 'anonymous' // 解决跨域问题,也就是提示污染资源无法转换视频
video.currentTime = 1 // 第一秒帧
video.oncanplay = () => {
canvas.width = video.videoWidth
canvas.height = video.videoHeight
// 利用canvas对象方法绘图
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
r(canvas.toDataURL('image/png'))
}
})
},
getShowImage(url, id) {
const ext = url.split('.').at(-1)
if (ext.indexOf('mp4') != -1 || ext.indexOf('avi') != -1) {
this.getVideoImg(url).then(res => {
if (document.getElementById(id)) {
document.getElementById(id).src = res
}
})
if (document.getElementById(id)) {
return document.getElementById(id).src
}
} else {
return url
}
},
// 上传图片
uploadFile(e) {
// 整个上传
// const uploadAll = (e) => {
// const formData = new FormData()
// for (const key in e.data) {
// formData.append(key, e.data[key])
// }
// formData.append(e.filename, e.file)
// return new Promise((resolve) => {
// axios({
// method: 'post',
// headers: {
// 'Content-Type': 'multipart/form-data',
// ...e.headers
// },
// url: e.action,
// data: formData,
// onUploadProgress: progressEvent => {
// e.onProgress({ percent: Number((progressEvent.loaded / progressEvent.total * 100).toFixed(1)) })
// }
// }).then((res) => {
// if (res.data.code == 200) {
// e.onSuccess(res.data, e.file, this.fileList)
// } else {
// e.onError({ message: '上传失败' }, e.file, this.fileList)
// }
// }).catch(() => {
// e.onError({ message: '上传失败' }, e.file, this.fileList)
// })
// })
// }
// uploadAll(e)
// 切片上传
const uploadLarge = async(e) => {
// 分片上传
let percent = 0
const uploadChunk = (formData) => {
// 分片上传方法
return axios({
method: 'post',
headers: {
'Content-Type': 'multipart/form-data',
...e.headers
},
url: e.action,
data: formData,
onUploadProgress: progressEvent => {
// 上传进度
const largeBili = 1 / chunkCount
const thisprogress = Number((progressEvent.loaded / progressEvent.total * 100))
if (thisprogress >= 100) {
percent = percent + thisprogress * largeBili
}
const allprogress = Number((percent + thisprogress * largeBili).toFixed(1))
e.onProgress({ percent: allprogress >= 100 ? allprogress : allprogress })
}
})
}
// 上传过程中用到的变量
const largeChunkSize = this.chunkSize
const chunkCount = Math.ceil(e.file.size / largeChunkSize) // 总片数
// 获取当前切片数据
const getChunkInfo = (file, index) => {
const start = index * largeChunkSize
const end = Math.min(file.size, start + largeChunkSize)
const chunkFile = file.slice(start, end)
const chunkSize = chunkSize
return { start, end, chunkFile }
}
// 针对单个切片上传封装,进行chunk上传
const readChunk = (index, uploadId = null, objectKey = null) => {
const { chunkFile } = getChunkInfo(e.file, index)
const formData = new FormData()
uploadId && formData.append('uploadId', uploadId)
objectKey && formData.append('objectKey', objectKey)
formData.append('partNumber', index + 1)
formData.append('sliceFile', chunkFile)
formData.append('filename', e.file.name)
for (const key in e.data) {
formData.append(key, e.data[key])
}
return uploadChunk(formData, index)
}
// 开始上传加整合
let uploadId = null
let objectKey = null
const promiseList = []
// 示例时需要先请求第一张切片获取,uploadId 、objectKey (可自行修改)
const { data: firstData } = await readChunk(0)
console.log(firstData)
const parts = []
parts.push({
partNumber: firstData.data.partNumber,
etag: firstData.data.etag
})
if (firstData.code === 200) {
uploadId = firstData.data.uploadId
objectKey = firstData.data.objectKey
// 整体监听
for (let index = 1; index < chunkCount; ++index) {
promiseList.push(await readChunk(index, uploadId, objectKey))
}
const allRes = await Promise.all(promiseList)
allRes.forEach((item, index) => {
parts.push({
partNumber: item.data.data.partNumber,
etag: item.data.data.etag
})
})
const params = {
uploadId: uploadId,
objectKey: objectKey,
parts
}
// 全部上传完,合并切片
axios.post(this.mergeUrl, params)
.then(res => {
e.onSuccess(res.data, e.file, this.fileList)
})
.catch(() => {
e.onError({ message: '上传失败' }, e.file, this.fileList)
})
} else {
e.onError({ message: '上传失败' }, e.file, this.fileList)
}
}
uploadLarge(e)
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-upload--picture-card{
width: var(--width);
display:flex;
justify-content:center;
align-items:center;
height: var(--height);
}
.uploadClick{
width: var(--width);
display:flex;
justify-content:center;
align-items:center;
height: var(--height);
}
::v-deep .el-upload-list--picture-card .el-upload-list__item{
width: var(--width);
display:flex;
align-items:center;
height: var(--height);
transition: none !important;
}
::v-deep .el-upload-list__item .el-icon-check{
position: absolute;
margin-top: 0px;
top: 10px;
right: 14px;
}
::v-deep .el-loading-spinner{
width: 100%;
height: 100%;
top: 0;
margin-top: 0;
display: flex;
align-items: center;
justify-content: center;
}
::v-deep .el-upload-list,::v-deep .el-upload-list--picture-card{
//display: none;
}
.uploadFileMain{
display: flex;
flex-wrap: wrap;
.upload-list{
flex-shrink:0;
width: var(--width);
border:1px solid #0000005d;
box-sizing: border-box;
height: var(--height);
margin-right: 20px;
margin-bottom: 10px;
&.upload-lis1{
margin-bottom: 0px;
}
overflow: hidden;
border-radius: 8px;
position: relative;
.el-actions{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
display: none;
.el-upload-icon{
margin: 5px;
i{
color: #ffffff;
cursor: pointer;
}
}
}
&:hover{
.actionsHover{
display: flex;
}
}
}
.el-upload-listImg{
width: 100%;
height: 100%;
object-fit: contain;
}
}
.loadingView{
width: 100%;
height: 100%;
}
.percentMain{
width: 100%;
height: 100%;
position: relative;
z-index:99;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
}
.txtMp{
width: 100%;
display: flex;
justify-content: center;
span{
width: 50%;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.noClick{
pointer-events: none;
}
</style>
往返缓存(BFCache)
BFCache是一种浏览器优化,可实现即时前进和后退载入页面。它改善了用户的浏览体验,尤其是那些网络或设备速度较慢的用户。
*我们可以通过这个方法判断当前页面是不是返回的页面*
在APP站内嵌套h5页面,判断进入拨号页返回情况:
-
我们需要通过
visibilitychange
-
通过在点击时修改一个状态值,回来时和上面的方法进行判断
const isClick=false // 是否点击了离开页面按钮 const isShowPop=false // 是否显示弹窗 document.addEventListener('visibilitychange', () => { if (document.visibilityState == "visible") { if (isClick) { isShowPop = true } isClick = false } else {} })
如果是在浏览器里面,判断进入拨号页(三方客服链接)返回情况:
-
在浏览器里面如果是离开了页面还是可以通过
visibilitychange
判断,但是跳转的是三方客服链接,那我我们回到页面,页面是会重新刷新的,我们需要知道是否返回了,就需要通过performance.getEntriesByType('navigation')[0].type
-
同时通过在点击时修改一个状态值,回来时和上面的方法进行判断
const isClick=false // 是否点击了离开页面按钮 const isShowPop=false // 是否显示弹窗 const pageshowFn=(event)=> { const navigationType = performance.getEntriesByType('navigation')[0].type const {persisted = null} = event if (persisted || navigationType == 'back_forward') { isShowPop = true isClick = false } } window.removeEventListener('pageshow', pageshowFn) window.addEventListener('pageshow', pageshowFn) // 页面初次进入需要调用 pageshowFn({})