vue 封装图片预览组件 图片放大、缩小、旋转

封装步骤

插入body,制作全局遮罩层、禁止背景滚动

创建一个组件插入body中,使用固定定位生成一个全局遮罩层,再把图片放进去

创建组件/components/PreviewImage/index.vue

搭建组件结构,传入url数组,以及打开的当前图片索引下标,以便进行翻页查看

如果遮罩层后面页面有滚动条时在组件打开时,需要禁止背景内容随鼠标滚轮滚动,每次打开关闭时给body动态增加样式 overflow: hidden属性即可

html 复制代码
<template>
  <div v-if="show" class="previewImage_wrapper">
      <div class="previewImage_image">
        <img :src="previewImgList[currentIndex] || ''">
      </div>
      <div class="previewImage_close previewImage_btn" @click="closePreviewImage">&times;</div>
      <div class="previewImage_navigation">
        <span class="previewImage_navigation_left previewImage_btn" @click="prevImage">&lt;</span>
        <span class="previewImage_navigation_right previewImage_btn" @click="nextImage">&gt;</span>
      </div>
    </div>
</template>
<script>
export default {
  props: {
    visible: { // 显示控制
      type: Boolean,
      default: false
    },
    previewImgList: { // url数组
      type: Array,
      default: () => []
    },
    currentIndex: { // 当前图片索引
      type: Number,
      default: 0
    }
  },
  computed: {
    // 双向绑定
    show: {
      get() {
        return this.visible
      },
      set(newVal) {
        this.$emit('update:visible', newVal)
      }
    }
  },
  watch: {
    visible: { // 给body动态增加style属性,禁止背景内容的鼠标滚轮滚动
      handler(newVal) {
        if(newVal) {
          document.body.style.overflow = "hidden";
        } else {
          document.body.style.overflow = "";
        }
      }
    },
  },
  methods: {
    // 上一张图片
    prevImage() {
      if (this.currentIndex === 0) {
        this.currentIndex = this.previewImgList.length - 1
      } else {
        this.currentIndex--
      }
    },
    // 下一张图片
    nextImage() {
      if (this.currentIndex === this.previewImgList.length - 1) {
        this.currentIndex = 0
      } else {
        this.currentIndex++
      }
    },
    // 关闭预览图片组件
    closePreviewImage() {
      this.show = false
    }
  },
  mounted() { // 插入body
    document.body.appendChild(this.$el);
  },
  destroyed() { // 组件销毁后同步清除元素
    this.$el.parentNode.removeChild(this.$el);
  }
}

定义组件过渡动画

在打开遮罩层时加上过渡效果,让组件体验更好

定义过渡动画,使用vue推荐使用自带的 组件,这样切换显示隐藏都会触发过渡效果,如果以class类名的形式定义的过渡动画,在使用指令时v-if或者v-show 隐藏关闭时不会触发结束的过渡效果

html 复制代码
<transition name="zoom">
    <!-- 组件 -->
</transition>
css 复制代码
.zoom-enter, .zoom-leave-to { // 元素进入和离开时的动作
  transform: scale(0);
}
.zoom-enter-active, .zoom-leave-active { // 元素进入和离开时的过渡动画定义
  transition: transform 0.3s;
}

效果如下

效果实现了,接下来还可以加入更多的功能

结合 transform 实现图片控制

在图片底部加一个控制工具栏,例如对预览图片的控制,放大、缩小、翻转等。在封装组件实现功能的时候我们应当先实现基础功能,再深入开发细节功能

想要实现元素的放大缩小,翻转,可以直接利用css3中transform 属性中的 scale rotate,然后使用 js 进行动态控制

html 复制代码
<script>
export default {
    data() {
      return {
        imgHandle: { // 图片控制
          scale: 1,
          rotate: 0
        }
      }
    },
    methods: {
    // 初始化还原图片缩放旋转控制
    async initImgHandle() {
      this.imgHandle = {
        scale: 1,
        rotate: 0
      }
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 放大图片
    async largeHandle() {
      console.log(this.imgHandle.scale, 'scale')
      this.imgHandle.scale = Number((this.imgHandle.scale + 0.2).toFixed(2)) // 使用toFixed防止小数点精度不准
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 缩小图片
    async shrinkHandle() {
      console.log(this.imgHandle.scale, 'scale')
      if (this.imgHandle.scale === 0.2) { // 最低缩放到0.2倍
        return
      }
      this.imgHandle.scale = Number((this.imgHandle.scale - 0.2).toFixed(2)) // 使用toFixed防止小数点精度不准
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    }
}
</script>

在进行小数点计算的时候,要注意小数点的精度问题,不然可能会导致计算出错产生bug,这里使用 toFixed 来解决下

在 JavaScript 中处理小数计算时,会遇到舍入误差导致计算结果不准确的情况。这是由于 JavaScript 中采用的是双精度浮点数格式(IEEE 754 标准)来表示数字,而这种格式无法准确地表示某些十进制小数

接下来写旋转的方法,然后给元素绑定点击事件就ok了

html 复制代码
<script>
export default {
    methods: {
        // 向左翻转
    async turnLeftHandle() {
      this.imgHandle.rotate = this.imgHandle.rotate - 90
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 向右翻转
    async turnRightHandle() {
      this.imgHandle.rotate = this.imgHandle.rotate + 90
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    }
}
</script>

最后记得给img图片加上过渡效果 transition: transform 0.3s ease; ,当控制图片操作的时候更平滑

最后的效果如下

组件代码

组件完整代码/components/PreviewImage/index.vue

html 复制代码
<template>
  <transition name="zoom">
    <div v-if="show" class="previewImage_wrapper" @wheel="handleScroll">
      <div class="previewImage_image">
        <img ref="previewImage_img" :src="previewImgList[currentIndex] || ''">
      </div>
      <div class="previewImage_close previewImage_btn" @click="closePreviewImage">&times;</div>
      <div class="previewImage_navigation">
        <span class="previewImage_navigation_left previewImage_btn" @click="prevImage">&lt;</span>
        <span class="previewImage_navigation_right previewImage_btn" @click="nextImage">&gt;</span>
      </div>
      <div class="previewImage_toolbar">
        <span class="previewImage_btn" @click="shrinkHandle">-</span>
        <span class="previewImage_btn" @click="largeHandle">+</span>
        <span class="previewImage_btn" @click="turnLeftHandle">↺</span>
        <span class="previewImage_btn" @click="initImgHandle">▣</span>
        <span class="previewImage_btn" @click="turnRightHandle">↻</span>
      </div>
    </div>
  </transition>
</template>
<script>
export default {
  props: {
    visible: { // 显示控制
      type: Boolean,
      default: false
    },
    previewImgList: { // url数组
      type: Array,
      default: () => []
    },
    currentIndex: { // 当前图片索引
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      imgHandle: { // 图片控制
        scale: 1,
        rotate: 0
      }
    }
  },
  computed: {
    // 双向绑定
    show: {
      get() {
        return this.visible
      },
      set(newVal) {
        this.$emit('update:visible', newVal)
      }
    }
  },
  watch: {
    visible: { // 给body动态增加style属性,禁止背景内容的鼠标滚轮滚动
      handler(newVal) {
        if(newVal) {
          document.body.style.overflow = "hidden";
          this.initImgHandle() // 每次打开图片初始化
        } else {
          document.body.style.overflow = "";
        }
      }
    },
  },
  methods: {
    // 鼠标滚轮
    handleScroll(event) {
      if (event.deltaY > 0) {
        // 向下滚动事件
        // console.log('向下滚动');
        this.shrinkHandle()
      } else {
        // 向上滚动事件
        // console.log('向上滚动');
        this.largeHandle()
      }
    },
    // 向左翻转
    async turnLeftHandle() {
      this.imgHandle.rotate = this.imgHandle.rotate - 90
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 向右翻转
    async turnRightHandle() {
      this.imgHandle.rotate = this.imgHandle.rotate + 90
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 初始化还原图片缩放旋转控制
    async initImgHandle() {
      this.imgHandle = {
        scale: 1,
        rotate: 0
      }
      await this.$nextTick()
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 放大图片
    async largeHandle() {
      console.log(this.imgHandle.scale, 'scale')
      this.imgHandle.scale = Number((this.imgHandle.scale + 0.2).toFixed(2)) // 使用toFixed防止小数点精度不准
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 缩小图片
    async shrinkHandle() {
      console.log(this.imgHandle.scale, 'scale')
      if (this.imgHandle.scale === 0.2) { // 最低缩放到0.2倍
        return
      }
      this.imgHandle.scale = Number((this.imgHandle.scale - 0.2).toFixed(2)) // 使用toFixed防止小数点精度不准
      const element = this.$refs.previewImage_img
      element.style.transform = `scale(${this.imgHandle.scale}) rotate(${this.imgHandle.rotate}deg)`
    },
    // 上一张图片
    prevImage() {
      if (this.currentIndex === 0) {
        this.currentIndex = this.previewImgList.length - 1
      } else {
        this.currentIndex--
      }
      this.initImgHandle()
    },
    // 下一张图片
    nextImage() {
      if (this.currentIndex === this.previewImgList.length - 1) {
        this.currentIndex = 0
      } else {
        this.currentIndex++
      }
      this.initImgHandle()
    },
    // 关闭预览图片组件
    closePreviewImage() {
      this.show = false
    }
  },
  mounted() { // 插入body
    document.body.appendChild(this.$el);
  },
  destroyed() { // 组件销毁后同步清除元素
    this.$el.parentNode.removeChild(this.$el);
  }
}
</script>
<style lang="less" scoped>
.previewImage_wrapper{
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: rgba(0, 0, 0, .5);
  z-index: 9999;
  .previewImage_image{
    display: flex;
    align-items: center;
    justify-content: center;
    img {
      width: 100vw;
      height: 100vh;
      object-fit: scale-down;
      transition: transform 0.3s ease; 
    }
  }
  .previewImage_close{
    position: absolute;
    right: 20px;
    top: 20px;
    transition: transform 0.2s ease-out;
    &:hover{
      transform: scale(1.2);
    }
  }
  .previewImage_navigation{
    &_left{
      position: absolute;
      left: 15px;
      top: 50%;
      transform: translate(0, -50%);
      transition: transform 0.2s ease-out;
    }
    &_right{
      position: absolute;
      right: 15px;
      top: 50%;
      transform: translate(0, -50%);
      transition: transform 0.2s ease-out;
    }
    &_left:hover,&_right:hover{
      transform: translate(0, -50%) scale(1.2);
    }
  }
  .previewImage_toolbar{
    position: absolute;
    bottom: 10px;
    left: 50%;
    transform: translate(-50%, 0);
    display: flex;
    align-items: center;
    span{
      margin-right: 10px;
      transition: transform 0.2s ease-out;
      &:hover{
        transform: scale(1.1) ;
      }
    }
    span:last-child{
      margin-right: 0;
    }
  }
  .previewImage_btn{
    width: 50px;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    color: #fff;
    background-color: #606266;
    border-radius: 50%;
    cursor: pointer;
  }
}
.zoom-enter, .zoom-leave-to { // 元素进入和离开时的动作
  transform: scale(0);
}
.zoom-enter-active, .zoom-leave-active { // 元素进入和离开时的过渡动画定义
  transition: transform 0.3s;
}

.slide-enter, .slide-leave-to { // 元素进入和离开时的动作
  transform: translateX(100%);
}
.slide-enter-active, .slide-leave-active { // 元素进入和离开时的过渡动画定义
  transition: transform 0.3s ease-in-out;
}
</style>  

使用组件代码

html 复制代码
<template>
  <div class="home">
    <div class="home-box">
      <img v-for="(item, index) in previewImg.imgUrlList" :key="item" :src="item" @click="openPreviewImg(index)">
    </div>
    <PreviewImage
      :visible.sync="previewImg.visible"
      :currentIndex="previewImg.currentIndex"
      :previewImgList="previewImg.imgUrlList"
    />
  </div>
</template>

<script>
import PreviewImage from '../components/PreviewImage/index.vue'
export default {
  name: 'Home',
  components: { PreviewImage },
  data() {
    return {
      previewImg: {
        visible: false,
        currentIndex: -1,
        imgUrlList: ['http://files.jiangtao.ltd/10000.jpg', 'http://files.jiangtao.ltd/10055.png', 'http://files.jiangtao.ltd/10054.png', 'http://files.jiangtao.ltd/10053.png'],
      }
    };
  },
  methods: {
    // 打开预览图片遮罩层
    openPreviewImg(index) {
      this.previewImg.currentIndex = index
      this.previewImg.visible = true
    }
  }
}
</script>
<style lang="less">
.home{
  height: 3000px;
  &-box{
    display: flex;
    justify-items: center;
    img{
      width: 150px;
      height: 150px;
      cursor: pointer;
    }
  }
}
</style>
相关推荐
customer0812 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙2 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
小远yyds3 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果3 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长3 小时前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
guai_guai_guai4 小时前
uniapp
前端·javascript·vue.js·uni-app
王哲晓5 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js