手把手实现一个vue图片预览组件

2,216 阅读1分钟

效果如图

5.png 图片预览我们在真实业务场景中经常会遇到。我们一般通过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>