效果如图
图片预览我们在真实业务场景中经常会遇到。我们一般通过element-的Image组件通过设置:preview-src-list来开启预览功能。但是该组件在点击图片本身时触发,而我们自己业务中可能需要定制一些功能,比如页面中设置一些按钮点击来控制图片预览。或者下载图片,亦或者本身不需要el-image提供的旋转灯功能。所以写一个具备通用功能而且可以定制化的图片组件非常有必要。
实现思路:
(1)实现一个弹窗,宽度为100%,height:100vh;mask蒙层同理。展示的图片我们给组件传一个url, (2)在弹窗中动态设置一个canvas,通过canvas的drawImage绘制出图
(3)把canvas转化为图片,通过canvas的canvas.toDataURL("image/png")api来实现
接下来我们一步一步实现
弹窗的实现:
<template>
<el-button @click="open">预览</el-button>
<pic-viewer :url="url2" v-model="show"/>
</template>
<script>
import PicViewer from './picViewer'
export default {
components:{PicViewer },
data(){
return{
url: "//宽图片
url2:" "//长图图片
}
},
methods:{
open() {
this.show=true
}
}
}
//PicPrew组件(这一步实现基础弹窗)
<template>
<div v-if="value" class="box">
<div class="cont">
<div class="img-ele-wrap" >
<img id="thumb" :src="urlValue" alt="" />
</div>
</div>
<div class="actions">
<span class="zoomIn" @click="operate('zoomIn')">+</span>
<span class="zoomOut" @click="operate('zoomOut')">-</span>
<span class="ratateLeft" @click="operate('ratateLeft')"></span>
<span class="ratateRight" @click="operate('ratateRight')"></span>
<span class="download" @click="operate('download')">↓</span>
<span class="close" @click="operate('close')">×</span>
</div>
<div class="mask"></div>
</template>
export default {
name:"completeCav",
props:{
url:{
type:String,
default:""
},
value:{
type:Boolean,
default:false
}
},
data(){
return{
urlValue:"",
}
}
第二部,我们展示图片,这里图片展示的逻辑如下:
我们先获取到图片的原始宽高,通过宽高比得知图片到底是长图还是宽图。如果是长图,那就意味着它的高已经确定了。(我们这个图片的展示要满足是长图的时候图片高度满屏,高度是屏幕高,当然图片本身比较小的话就取图片本身的高度),我们根据高度和宽高比算出来他的长度。
<template>
<div v-if="value" class="box">
<div class="cont">
<div class="img-ele-wrap" :style="">
<img id="thumb" :src="urlValue" alt="" />
</div>
</div>
<div class="actions">
<span class="zoomIn" @click="operate('zoomIn')">+</span>
<span class="zoomOut" @click="operate('zoomOut')">-</span>
<span class="ratateLeft" @click="operate('ratateLeft')"></span>
<span class="ratateRight" @click="operate('ratateRight')"></span>
<span class="download" @click="operate('download')">↓</span>
<span class="close" @click="operate('close')">×</span>
</div>
<div class="mask"></div>
</template>
export default {
name:"completeCav",
props:{
url:{
type:String,
default:""
},
value:{
type:Boolean,
default:false
}
},
data(){
return{
urlValue:"",
transformStyle:{
ImgHeight:0,
ImgWidth:0,
}
}
},
computed:{
imgStyle(){
return{
width:this.transformStyle.ImgWidth+'px',
height:this.transformStyle.ImgHeight+'px',
}
}
},
mounted(){
this.drawCanv();
},
methods:{
drawCanv(){
let _this=this
let canvas=document.createElement("canvas");
let ctx=canvas.getContext("2d");
let w=document.documentElement.clientWidth-300||document.body.clientWidth-300;
let h=document.documentElement.clientHeight
let ImgHeight=0
let ImgWidth=0
canvas.width= w;
canvas.height= h;
let imgObj=new Image()
imgObj.src=_this.url;
imgObj.setAttribute("crossOrigin",'Anonymous')
imgObj.onload=function(e){
//获取原始宽高
let naturalWidth = (e.path)[0].naturalWidth
let naturalHeight = (e.path)[0].naturalHeight
// 图片宽高比
let imageRadio =(naturalWidth / naturalHeight);
if (imageRadio <= 1) { //长图
// 图片的高度
ImgHeight =naturalHeight>canvas.height?canvas.height:naturalHeight
// 根据原始宽高,等比例得到图片宽度
ImgWidth = ImgHeight * imageRadio;
canvas.width=ImgWidth
ctx.drawImage(imgObj,0,0,ImgWidth,ImgHeight)
var base64 = canvas.toDataURL("image/png"); //"image/png" 这里注意一下
_this.urlValue=base64
_this.transformStyle.ImgHeight=ImgHeight;
_this.transformStyle.ImgWidth=ImgWidth;
}else{
//宽图
// 图片的高度
ImgWidth =naturalWidth>canvas.width?canvas.width:naturalWidth
// 根据原始宽高,等比例得到图片宽度
ImgHeight= ImgWidth / imageRadio;
// 重新赋值canvas宽高
ctx.drawImage(imgObj,0,0,ImgWidth,ImgHeight)
var base64 = canvas.toDataURL("image/png"); //"image/png" 这里注意一下
_this.urlValue=base64
_this.transformStyle.ImgHeight=ImgHeight
_this.transformStyle.ImgWidth=ImgWidth
}
}
},
}
}
<style scoped>
.box{
width: 100%;
height: 100vh;
position: fixed;
top:0;
left:0;
z-index:100;
}
.mask{
width: 100%;
height: 100vh;
position: fixed;
top:0;
left:0;
z-index:1;
background: rgba(0,0,0,0.5);
}
.cont{
position: fixed;
z-index: 101;
width: 100%;
height: 100vh;
left:0;
top:0;
display: flex;
justify-content: center;
}
.img-ele-wrap{
transition:all 0.2s;
}
.img-ele-wrap img{
object-fit: cover;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.actions{
position:fixed;
z-index: 101;
color:#fff;
font-size: 16px;
right:20px;
top:20px
}
.ratateLeft,.download,.close,.zoomIn,.zoomOut{
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
border:1px solid #fff;
margin-left:10px
}
</style>
/*到这一步我们已经可以正常把图片展示出来了。但是这显然还没达到我们的要求,接下来我们将对图片做一些旋转,放大,缩小。下载,关闭等一些操作,图片既然有了。那么我们就可以利用css的transform:scale,rotate配合transition动画来对图片元素做一系列操作*/
methods:{
reset(){
this.transformStyle.deg=0
},
close(){
console.log("close111'")
this.$emit("input",false)
},
operate(type){
let zoomRate=0.2
switch(type){
case "close":
this.$emit("input",false)
break;
case "download":
let a=document.createElement("a")
a.href=this.url
a.target="_blank"
a.click()
break;
case "ratateLeft":
this.transformStyle.deg-=90;
break;
case "ratateRight":
this.transformStyle.deg+=90;
break;
case "zoomIn":
if(this.transformStyle.scale>=2.4){
return
}
this.transformStyle.scale = parseFloat((this.transformStyle.scale + zoomRate).toFixed(3));
console.log("this.transformStyle.scale:",this.transformStyle.scale)
break;
case "zoomOut":
if(this.transformStyle.scale>0.2){
this.transformStyle.scale = parseFloat((this.transformStyle.scale - zoomRate).toFixed(3));
}
break;
default:
break;
}
},
ratate(type){
type=='rotateLeft'?this.transformStyle.deg-=90:this.deg+=90
},
download(){
let a=document.createElement("a")
a.href=this.url
a.target="_blank"
a.click()
},
这里还有一些细节要处理:
(1):我们在动态创建canvas的时候给它设置了一个宽高,但是我们把canvas转为img的时候,canvas的宽高并没有保持和img一致导致下载图片的时候其实图片的实际宽度要大,所以我们通过宽高比得到图片实际宽高以后要把canvas的宽高重新设置一下。保持canvas的宽高和img一致,
(2)图片缩放的时候我要设置一个临界值。避免过大或者过小
(3)我们在缩放图片或者选择以后。关闭弹窗,但是此时组件并没有销毁。此时再度打开会发现图片还是保持之前的状态。所有这里我们重置一下。把图片的scale重置为1和rotate重置位0
至此,我们已经完整实现了这个效果:这里还有2个疑问:
1--我们对图片的旋转,放大缩小是利用css3的transfom来实现的,那我们是否能利用canvas的api来完整实现呢?
2--我们这个案例的实现利用了canvas做中转,是否可以不利用cavnas来实现这个效果呢?
其实答案都是可以的,后续我将利用这两种思路再实现这个预览
---------------------------------------
以下为picPreivew的完整代码:
<template>
<transition name="fade">
<div v-if="value" class="box">
<div class="cont">
<div class="img-ele-wrap" :style="imgStyle">
<img id="thumb" :src="urlValue" alt="" />
</div>
</div>
<div class="actions">
<span class="zoomIn" @click="operate('zoomIn')">+</span>
<span class="zoomOut" @click="operate('zoomOut')">-</span>
<span class="ratateLeft" @click="operate('ratateLeft')"></span>
<span class="ratateRight" @click="operate('ratateRight')"></span>
<span class="download" @click="operate('download')">↓</span>
<span class="close" @click="operate('close')">×</span>
</div>
<div class="mask"></div>
</div>
</transition>
</template>
<script>
export default {
name:"completeCav",
props:{
url:{
type:String,
default:""
},
value:{
type:Boolean,
default:false
}
},
data(){
return{
urlValue:"",
transformStyle:{
ImgHeight:0,
ImgWidth:0,
show:false,
deg:0,
scale:1
}
}
},
computed:{
imgStyle(){
const style={
width:this.transformStyle.ImgWidth+'px',
height:this.transformStyle.ImgHeight+'px',
transform:`rotate(${this.transformStyle.deg}deg) scale(${this.transformStyle.scale})`,
}
return style
}
},
mounted(){
this.drawCanv();
window.addEventListener("resize",this.throttle(this.drawCanv,200));
},
beforeDestroy(){
window.removeEventListener("resize",this.drawCanv);
},
watch:{
value(n){
if(!n){
this.reset()
}
}
},
methods:{
reset(){
this.transformStyle.deg=0
},
close(){
console.log("close111'")
this.$emit("input",false)
},
operate(type){
let zoomRate=0.2
switch(type){
case "close":
this.$emit("input",false)
break;
case "download":
let a=document.createElement("a")
a.href=this.url
a.target="_blank"
a.click()
break;
case "ratateLeft":
this.transformStyle.deg-=90;
break;
case "ratateRight":
this.transformStyle.deg+=90;
break;
case "zoomIn":
if(this.transformStyle.scale>=2.4){
return
}
this.transformStyle.scale = parseFloat((this.transformStyle.scale + zoomRate).toFixed(3));
console.log("this.transformStyle.scale:",this.transformStyle.scale)
break;
case "zoomOut":
if(this.transformStyle.scale>0.2){
this.transformStyle.scale = parseFloat((this.transformStyle.scale - zoomRate).toFixed(3));
}
break;
default:
break;
}
},
ratate(type){
type=='rotateLeft'?this.transformStyle.deg-=90:this.deg+=90
},
download(){
let a=document.createElement("a")
a.href=this.url
a.target="_blank"
a.click()
},
drawCanv(){
let _this=this
let canvas=document.createElement("canvas");
let ctx=canvas.getContext("2d");
let w=document.documentElement.clientWidth-300||document.body.clientWidth-300;
let h=document.documentElement.clientHeight
let ImgHeight=0
let ImgWidth=0
canvas.width= w;
canvas.height= h;
let imgObj=new Image()
imgObj.src=_this.url;
imgObj.setAttribute("crossOrigin",'Anonymous')
imgObj.onload=function(e){
//获取原始宽高
let naturalWidth = (e.path)[0].naturalWidth
let naturalHeight = (e.path)[0].naturalHeight
// 图片宽高比
let imageRadio =(naturalWidth / naturalHeight);
if (imageRadio <= 1) { //长图
// 图片的高度
console.log("长途")
ImgHeight =naturalHeight>canvas.height?canvas.height:naturalHeight
// 根据原始宽高,等比例得到图片宽度
ImgWidth = ImgHeight * imageRadio;
canvas.width=ImgWidth
ctx.drawImage(imgObj,0,0,ImgWidth,ImgHeight)
var base64 = canvas.toDataURL("image/png"); //"image/png" 这里注意一下
_this.urlValue=base64
_this.transformStyle.ImgHeight=ImgHeight;
_this.transformStyle.ImgWidth=ImgWidth;
}else{
console.log("宽图")
// 图片的高度
ImgWidth =naturalWidth>canvas.width?canvas.width:naturalWidth
// 根据原始宽高,等比例得到图片宽度
ImgHeight= ImgWidth / imageRadio;
// 重新赋值canvas宽高
ctx.drawImage(imgObj,0,0,ImgWidth,ImgHeight)
var base64 = canvas.toDataURL("image/png"); //"image/png" 这里注意一下
_this.urlValue=base64
_this.transformStyle.ImgHeight=ImgHeight
_this.transformStyle.ImgWidth=ImgWidth
}
}
},
throttle(fn, threshhold) {
var timeout
var start = new Date;
var threshhold = threshhold || 160
return function () {
var context = this, args = arguments, curr = new Date() - 0
clearTimeout(timeout)/
if(curr - start >= threshhold){
console.log("now", curr, curr - start)
fn.apply(context, args)
start = curr
}else{
timeout = setTimeout(function(){
fn.apply(context, args)
}, threshhold);
}
}
}
}
}
</script>
<style scoped>
.box{
width: 100%;
height: 100vh;
position: fixed;
top:0;
left:0;
z-index:100;
}
.mask{
width: 100%;
height: 100vh;
position: fixed;
top:0;
left:0;
z-index:1;
background: rgba(0,0,0,0.5);
}
.cont{
position: fixed;
z-index: 101;
width: 100%;
height: 100vh;
left:0;
top:0;
display: flex;
justify-content: center;
}
.img-ele-wrap{
transition:all 0.2s;
}
.img-ele-wrap img{
object-fit: cover;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.actions{
position:fixed;
z-index: 101;
color:#fff;
font-size: 16px;
right:20px;
top:20px
}
.ratateLeft,.download,.close,.zoomIn,.zoomOut{
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
border:1px solid #fff;
margin-left:10px
}
</style>