Skip to content

图片可拖拽对比轮子

作者:Yuan Tang
更新于:5 个月前
字数统计:2.5k 字
阅读时长:10 分钟

效果

具体效果如下图所示:

iptK5E.png

多张效果图中可以两两对比,效果图可拖拽移动位置,可通过鼠标滚轮放大缩小。

拖拽

思路

拖拽事件分为以下三步:

  1. 鼠标移入图片标签内,捕获所有鼠标按下的事件 setCapture
  2. 获取鼠标移动事件 onmousemove ,获取其移动的偏移量 clientXclientY
  3. 获取鼠标松开事件,取消所有事件捕获 releaseCapture

需要注意的要点:

  • 当前移动的是哪个图片(通过传参来判断)
  • 从上图也可看出,图片 dom 元素是动态创建的,不是固定写死的,因此要动态为元素绑定移动事件

代码

js
<img class='bigger_img_left' :src="$SN_DESIGN_BASE_IMG + imgs[0].imgUrl" ref="leftImg" :style="leftStyle" :width="width+'px'" :height="height+'px'">

initImgMoveFn(){
  const that = this

  this.$nextTick(() => {
    // 左侧图片元素获取
    let imgs_left = document.querySelector('.bigger_img_left')
    imgs_left && that.onMove(imgs_left, 'leftStyle')

    // 右侧图片元素获取
    let imgs_right = document.querySelector('.bigger_img_right')
    imgs_right && that.onMove(imgs_right, 'rightStyle')
  })
},

// 绑定移动事件,obj为触发事件的dom元素,data为该元素的style样式对象
onMove(obj, data) {
  const that = this
    obj.onmousedown = function(event){

      /* 设置box1捕获所有鼠标按下的事件
       * setCapture()
       *  - 只有IE支持,但是在火狐中调用时不会报错,而如果使用chrome调用,会报错
       */
      obj.setCapture && obj.setCapture();

      event = event ||window.event
      //div的偏移量,鼠标.clientX-元素.offsetLeft;鼠标.clientY-元素.offsetTop
      var ol = event.clientX -obj.offsetLeft;
      var ot = event.clientY - obj.offsetTop;
      //为document绑定一个onmousemove事件
      document.onmousemove = function(event){
        event = event ||window.event
        //获取鼠标的坐标
        var left = event.clientX-ol;
        var top = event.clientY-ot;

        //修改box1的位置
        that[data].left = left+"px";
        that[data].top = top+"px";
      };

      //为元素绑定一个鼠标松开事件
      document.onmouseup = function(){
        //当鼠标松开时,被拖拽元素固定在当前位置 onmouseup
        //取消document的onmousemove事件
        document.onmousemove = null;
        document.onmouseup = null;
        //当鼠标松开时,取消对事件的捕获
        obj.releaseCapture && obj.releaseCapture();
      };
      /*
       * 当我们拖拽一个网页的内容时,浏览器会默认去搜索引擎中搜索内容
       *   此时会导致拖拽功能的异常,这是浏览器提供的默认行为
       * 	 如果不希望发生这个行为,则可以通过return false来取消默认行为
       */
      return false;
    };
},

知识点

  • document.releaseCapture

    非标准方法,使用前请注意跨浏览器支持。

    如果该 document 中的一个元素之上当前启用了鼠标捕获,则释放鼠标捕获。通过调用

    实现在一个元素上启用鼠标捕获。

  • element.setCapture

    非标准方法,使用前请注意跨浏览器支持。

    在处理一个 mousedown 事件过程中调用这个方法来把全部的鼠标事件重新定向到这个元素,直到鼠标按钮被释放或者

    被调用。

    括号内如果被设置为 true, 所有事件被直接定向到这个元素; 如果是 false, 事件也可以在这个元素的子元素上触发。

    注意:

    该方法已弃用,请不要在新网站中使用。

  • $nextTick

    Vue 是异步执行 dom 更新的,一旦观察到数据变化,Vue 就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 Dom 操作。而在下一个事件循环时,Vue 会清空队列,并进行必要的 DOM 更新。 当设置 vm.someData = 'new value'DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的 DOM 更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

滚轮缩放

思路

事件 wheel 在滚动鼠标滚轮或操作其他类似输入设备时触发。因此可以通过它监听整个 window 的鼠标滚轮事件。触发事件后:

  1. 判断当前触发事件的鼠标 x 轴坐标,如果小于50%,则左边图片缩放,反之右边图片缩放(因为两张图片平分所有空间)
  2. 判断当前图片的宽高,如果小于等于0则停止缩小
  3. 判断当前执行的操作是放大还是缩小,触发事件后可通过 e.deltaY 参数获取到当前滚轮的方向

代码

js
mounted() {
  // 监听页面滚动事件
  window.addEventListener("wheel", this.onWhell);
},

onWhell(e) {
  if (window.innerWidth / 2 > e.clientX) {
     // 左侧图片的宽高
    if (e.deltaY > 0) {
      this.width += 20
      this.height += 10
    } else {
      if(this.width<=30 || this.height<=30) return
      this.width -= 20
      this.height -= 10
    }
  } else {
     // 右图片的宽高
    if (e.deltaY > 0) {
      this.w += 20
      this.h += 10
    } else {
      if(this.w<=30 || this.h<=30) return
      this.w -= 20
      this.h -= 10
    }
  }
},

总体代码

vue
<script>
export default {
  props: {
    resultList: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      showPreview: false,
      width: 320,
      height: 160,
      w: 320,
      h: 160,
      leftStyle: {
        position: 'absolute',
        // transition: 'all 0.3s',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
      },
      rightStyle: {
        position: 'absolute',
        // transition: 'all 0.3s',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
      },
      imgs: [],
    }
  },
  mounted() {
    // 监听页面滚动事件
    const that = this
    window.addEventListener('wheel', that.onWhell)
  },
  methods: {
    initImgMoveFn() {
      const that = this

      this.$nextTick(() => {
        const imgs_left = document.querySelector('.bigger_img_left')
        imgs_left && that.onMove(imgs_left, 'leftStyle')

        const imgs_right = document.querySelector('.bigger_img_right')
        imgs_right && that.onMove(imgs_right, 'rightStyle')
      })
    },
    onMove(obj, data) {
      console.log(obj, data)
      const that = this
      obj.onmousedown = function (event) {

        // 设置box1捕获所有鼠标按下的事件
        /*
             * setCapture()
             *  - 只有IE支持,但是在火狐中调用时不会报错
             * 		而如果使用chrome调用,会报错
             */
        obj.setCapture && obj.setCapture()

        event = event || window.event
        // div的偏移量,鼠标.clientX-元素.offsetLeft

        // div的偏移量,鼠标.clientY-元素.offsetTop

        const ol = event.clientX - obj.offsetLeft

        const ot = event.clientY - obj.offsetTop
        // 为document绑定一个onmousemove事件

        document.onmousemove = function (event) {

          event = event || window.event
          // 当鼠标移动时被拖拽的元素跟随鼠标移动 onmousemove

          // 获取鼠标的坐标
          const left = event.clientX - ol
          const top = event.clientY - ot

          console.log(left)
          console.log(top)

          // 修改box1的位置
          that[data].left = `${left}px`
          that[data].top = `${top}px`

        }

        // 为元素绑定一个鼠标松开事件
        document.onmouseup = function () {
          // 当鼠标松开时,被拖拽元素固定在当前位置 onmouseup
          // 取消document的onmousemove事件

          document.onmousemove = null
          document.onmouseup = null
          // 当鼠标松开时,取消对事件的捕获
          obj.releaseCapture && obj.releaseCapture()
        }
        /*
             * 当我们拖拽一个网页的内容时,浏览器会默认去搜索引擎中搜索内容
             *   此时会导致拖拽功能的异常,这是浏览器提供的默认行为
             * 	 如果不希望发生这个行为,则可以通过return false来取消默认行为
             */
        return false
      }
    },
    changeImgFn(e) {
      this.$set(this.imgs, e, {})
    },
    // 关闭效果图预览
    closeFn() {
      this.showPreview = false
      this.imgs = []
      this.width = 320
      this.height = 160
      this.w = 320
      this.h = 160
      this.leftStyle = {
        position: 'absolute',
        // transition: 'all 0.3s',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
      }
      this.rightStyle = {
        position: 'absolute',
        // transition: 'all 0.3s',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
      }
      this.$emit('closeFn')
    },
    // 点击下载按钮:type左0还是右1;which完整图0还缩略图1
    handleDownLoadFn(type, which) {
      const that = this
      const image = new Image()
      // 解决跨域 Canvas 污染问题
      image.setAttribute('crossOrigin', 'anonymous')
      image.onload = function () {
        const canvas = document.createElement('canvas')
        canvas.width = image.width
        canvas.height = image.height
        const context = canvas.getContext('2d')
        context.drawImage(image, 0, 0, image.width, image.height)
        const url = canvas.toDataURL('image/png', 0.7) // 得到图片的base64编码数据
        const a = document.createElement('a') // 生成一个a元素
        const event = new MouseEvent('click') // 创建一个单击事件
        a.download = which ? `${that.imgs[type].name}裁剪图.png` : `${that.imgs[type].name}.png` // 设置图片名称
        a.href = url // 将生成的URL设置为a.href属性
        a.dispatchEvent(event) // 触发a的单击事件
      }
      const imgsrc = which ? this.imgs[type].thumbnail : this.imgs[type].imgUrl
      image.src = this.$SN_DESIGN_BASE_IMG + imgsrc
      console.log(image)
    },
    // 点击选择图片
    handlePreviewFn(e, i) {
      this.$set(this.imgs, i, e)
      this.initImgMoveFn()
    },
    onWhell(e) {
      if (window.innerWidth / 2 > e.clientX) {
        if (e.deltaY > 0) {
          this.width += 20
          this.height += 10
        }
        else {
          if (this.width <= 30 || this.height <= 30)
            return
          this.width -= 20
          this.height -= 10
        }
      }
      else {
        if (e.deltaY > 0) {
          this.w += 20
          this.h += 10
        }
        else {
          if (this.w <= 30 || this.h <= 30)
            return
          this.w -= 20
          this.h -= 10
        }
      }
    },
  }
}
</script>

<template>
  <div>
    <div v-if="showPreview" class="box">
      <div class="close" @click="closeFn">
        ×
      </div>
      <div class="top">
        <div class="left">
          <template v-if="imgs[0] && imgs[0].id">
            <div class="name" @click="changeImgFn(0)">
              {{ imgs[0].name }}(点我切换)效果图
            </div>
            <div class="btns" @click="handleDownLoadFn(0, 0)">
              下载完整图
            </div>
            <div class="btns" @click="handleDownLoadFn(0, 1)">
              下载裁剪图
            </div>
          </template>
        </div>
        <div class="right">
          <template v-if="imgs[1] && imgs[1].id">
            <div class="name" @click="changeImgFn(1)">
              {{ imgs[1].name }}(点我切换)效果图
            </div>
            <div class="btns" @click="handleDownLoadFn(1, 0)">
              下载完整图
            </div>
            <div class="btns" @click="handleDownLoadFn(1, 1)">
              下载裁剪图
            </div>
          </template>
        </div>
      </div>

      <div class="bottom">
        <div ref="left_box">
          <div v-if="!imgs[0] || !imgs[0].id" class="list_box">
            <div v-for="item in resultList" :key="item.id" class="list" @click="handlePreviewFn(item, 0)">
              {{ item.name }}
            </div>
          </div>
          <template v-else>
            <img
              ref="leftImg" class="bigger_img_left" :src="$SN_DESIGN_BASE_IMG + imgs[0].imgUrl" :style="leftStyle"
              :width="`${width}px`" :height="`${height}px`"
            >
            <div class="thumbnail">
              <img :src="$SN_DESIGN_BASE_IMG + imgs[0].thumbnail" width="100px" height="100px">
              <div>裁剪效果</div>
            </div>
          </template>
        </div>
        <div>
          <div v-if="!imgs[1] || !imgs[1].id" class="list_box">
            <div v-for="item in resultList" :key="item.id" class="list" @click="handlePreviewFn(item, 1)">
              {{ item.name }}
            </div>
          </div>
          <template v-else>
            <img class="bigger_img_right" :src="$SN_DESIGN_BASE_IMG + imgs[1].imgUrl" alt="" :width="`${w}px`" :height="`${h}px`" :style="rightStyle">
            <div class="thumbnail">
              <img :src="$SN_DESIGN_BASE_IMG + imgs[1].thumbnail" width="100px" height="100px">
              <div>裁剪效果</div>
            </div>
          </template>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang='less' scoped>
.box {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;height: 100vh;
  background-color: rgba(0,0,0,.5);
  z-index:9999;

  .bigger_img_left,
  .bigger_img_right {
    cursor: move;
  }

  .close {
    position: absolute;
    top: 10px;
    right: 20px;
    font-size: 25px;
    cursor: pointer;
  }

  .top {
    height: 60px;
    line-height: 60px;
    background-color: #fff;

    > div {
      display: inline-block;
      height: 100%;
      width: 50%;
      overflow: hidden;
      padding-left: 25px;

      > div {
        display: inline-block;
      }

      .name {
        cursor: pointer;

        &:hover {
          color: #1890ff;
        }
      }

      .btns {
        height: 50px;
        line-height: 50px;
        margin-top: 5px;
        padding: 0 20px;
        border: 1px solid #ccc;
        border-radius: 10px;
        cursor: pointer;
        margin-left: 20px;
        box-sizing: border-box;

        &:hover {
          background-color: #1890ff;
          color: #fff;
          border: 1px solid #1890ff;
        }
      }

      .name {
        font-size: 20px;
        font-weight: 600;
      }
    }
  }

  .bottom {
    height: calc(100vh - 60px);
    > div {
      display: inline-block;
      position: relative;
      height: 100%;
      width: 50%;border: 1px solid red;
      overflow: hidden;

      .list_box {
        height: 500px;
        margin-top: 200px;
        overflow-y: auto;
      }
      .list {
        background-color: #fff;
        width: 50%;
        height: 60px;
        line-height: 60px;
        text-align: center;
        margin: 10px auto;
        cursor: pointer;
        &:hover {
          color: #fff;
          background-color: #1890ff;
        }
        &:first-child {
          margin-top: 200px;
        }
      }

      .thumbnail {
        position: absolute;
        top: 0;
        right: 0;
        width: 120px;
        text-align: center;
        padding-top: 10px;
        height: 140px;
        background-color: #fff;
        box-sizing: border-box;
      }
    }
  }
}
</style>

Contributors

Yuan Tang