最近在开发一些项目,需要手机端上传图片,然后优化了一个裁剪的功能,遇到了一些神奇的BUG,然后就自己弄了一个插件。

首先采用的技术实现方案是:vue-cropper

这个是一个比较不错的图片裁剪功能,实现简单的裁剪是没有问题的,但是存在一个BUG,图片放大后,将裁剪框滑动的边缘,则无法缩小。

出现BUG的原因:该插件图片放大使用的是CSS3属性【scale】进行放大,缩小,由于限制了裁剪框在图片内,而【scale】的缩放的基准点是中心缩放,所以就会产生BUG,而且这个插件没有控制缩放基准点的配置,尝试过二次开发,放大和缩小,改变其基准点来解决BUG,功能实现了,但是效果体验不好。果断放弃该组件。

新的解决方案:使用【cropperjs】来做图片裁剪,该插件的缩放,采用的是图片拉升,更改图片的宽高来实现放大缩小,而非【scale】实现起来更加容易控制,封装代码插件如下:

安装:cropperjs

npm i cropperjs

组件:

vueelementui 裁剪图片 vue 裁剪图片插件_缩放

index.vue 代码组件示例:

<template>
  <div>
    <button @click="handleCutting">裁剪</button>
    <cropperjs 
      :imgFile="imgFile"
      :ratioWidth="ratioWidth"
      :ratioHeight="ratioHeight"
      @handleCancel="handleCancel"
      @handleUpload="handleUpload"
      @handleCropData="handleCropData"
      v-if="isCutting">
    </cropperjs>
  </div>
</template>

<script>
import cropperjs from './cropperjs'
export default {
  name: 'test',
  components:{ cropperjs },
  data () {
    return {
      isCutting: true,// 是否裁剪
      imgFile: 'https://bpic.588ku.com/element_water_img/18/06/12/b2887846cb19ff36a5502401ac918809.jpg',
      ratioWidth: 4,// 裁剪比例:长 自由比例设置为: 0 即可
      ratioHeight: 3,// 裁剪比例:宽 自由比例设置为: 0 即可
    }
  },
  mounted(){
    this.init();
  },
  methods: {
    init(){},
    // 裁剪
    handleCutting(){
      this.isCutting = true;
    },
    // 取消
    handleCancel(){
      this.isCutting = false;
    },
    // 上传
    handleUpload(data){
      console.log('上传');
      console.log(data);
    },
    // 裁剪
    handleCropData(data){
      console.log('裁剪数据');
      console.log(data);
    }
  }
}
</script>

组件:cropperjs.vue 代码示例:

<template>
  <div>
    <div class="main-cropper">
      <!-- 剪裁框 -->
      <div class="cropper">
        <img class="img" ref="image" :src="imgFile" alt="">
      </div>
      <!-- 底部 -->
      <div class="cropper-footer">
        <div class="footer-handle">
          <div class="item" @click="handleCancel">取消</div>
          <div class="item" @click="handleZoom(0.05)">放大</div>
          <div class="item" @click="handleZoom(-0.05)">缩小</div>
          <!-- <div class="item" @click="handleRotate(-90)">逆时针旋转</div> -->
          <div class="item" @click="handleRotate(90)">旋转</div>
          <div class="item" @click="handleConfirm">确定</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
export default {
  name: 'VueCropper',
  props: {
    imgFile: {
      type: String,
      default: 'https://bpic.588ku.com/element_water_img/18/06/12/b2887846cb19ff36a5502401ac918809.jpg'
    },
    ratioWidth: {
      type: Number,
      default: 0
    },
    ratioHeight: {
      type: Number,
      default: 0
    },
  },
  data() {
    return {
      myCropper: null,
      afterImg: '',
      ScaleX: 1,
      ScaleY: 1,
      fixed: false,
      fixedBox: false,
      inputRotate: 0,
      isDisabled: false
    }
  },
  computed: {},
  watch: {
    imgFile: function(file) {
      console.log(file);
      this.imgFile = file
    }
  },
  mounted() {
    this.init();
  },
  methods: {    
    // 初始化
    init() {
      this.myCropper = new Cropper(this.$refs.image, {
        // responsive: true, // 在窗口大小变化后,重新渲染裁剪器 默认 true
        // restore: true, // 在窗口大小变化后,恢复被裁剪的区域 默认 true
        // checkCrossOrigin: true, // 检查图片是否跨域
        // modal: true, // 是否在图片和裁剪框之间显示黑色蒙版 默认 true
        // guides: true, // 是否显示裁剪框的虚线 默认 true
        // center: true, // 是否显示裁剪框中心的指示器 默认 true
        // highlight: true, // 是否显示裁剪框上面的白色蒙版(突出显示裁剪框) 默认 true
        background: false, // 是否在容器内显示网格背景 默认 true
        // autoCrop: true, // 定义裁剪区域占图片的大小(百分比) 取值为 0-1 默认 0.8
        // movable: true, // 是否可以移动图片 默认 true
        // rotatable :true, // 是否可以旋转图片 默认 true
        // scalable: true, // 是否可以缩放图片 默认 true
        // zoomable: true, // 是否可以缩放图片(以图片左上角为原点进行缩放)默认 true
        // zoomOnTouch: true, // 是否允许通过拖动来缩放图片 默认 true
        zoomOnWheel: true, // 是否允许通过鼠标滚轮缩放图片 默认 true
        // wheelZoomRatio: true, // 设置通过鼠标滚轮缩放图片时的缩放比例 默认 0.1
        // cropBoxMovable: true, // 是否允许通过拖动来移动裁剪框 默认 true
        // cropBoxResizable: true, // 是否允许通过拖动来调整裁剪框的大小 默认 true
        // toggleDragModeOnDblclick: true, // 是否允许双击切换图片容器拖拽模式("crop"和"move") 默认 true
        // minContainerWidth: 200, // 设置裁剪容器的最小宽度 默认 200
        // minContainerHeight: 100, // 设置裁剪容器的最小高度 默认 100
        // minCanvasWidth: 0, // 设置图片容器的最小宽度 默认 0
        // minCropBoxWidth: 0, // 设置裁剪框的最小宽度 默认 0 这个尺寸是相对于页面的,而不是图片
        // minCropBoxHeight: 0, // 设置裁剪框的最小高度 默认 0 这个尺寸是相对于页面的,而不是图片
        // preview: '.before', // 预览样式 如果需要预览 配置预览 class 即可
        viewMode: 1, // 裁剪器配置 [0:没有限制 1:限制裁剪框不超过图片容器的范围 2:限制最片容器尺寸以在裁剪容器中展示。 如果图片容器和裁剪容器的比例不同,则图片容器以cover模式填充(图片容器保持原有比例,最长边和裁剪容器大小一致,短边等比缩放,可能会有部分区域不可见) 3:限制图片容器尺寸以在裁剪器中展示。 如果图片容器和裁剪容器的比例不同,则图片容器以contain模式填充(图片容器保持原有比例,最短边和裁剪容器大小一直,长边等比缩放,可能会有留白)]
        dragMode: 'move', // 定义裁剪器的拖动模式 [crop:创建一个新的裁剪框 move:图片容器可移动 none:什么也不做] 默认 crop
        // initialAspectRatio: 1, // 定义裁剪框的初始宽高比。默认和图片容器的宽高比相同 默认 NaN 
        autoCropArea: 1, // 定义裁剪区域占图片的大小(百分比)。取值为 0 - 1
        aspectRatio: this.ratioWidth / this.ratioHeight // 定义裁剪框的固定宽高比。默认是自由比例 NaN
      })
    },
    // 取消
    handleCancel() {
      this.$emit('handleCancel')
    },
    // 缩放
    handleZoom(val) {
      this.myCropper.zoom(val)
    },
    // 旋转
    handleRotate(val) {
      this.myCropper.rotate(val)
    },
    // 绝对角度旋转
    handleRotateTo(val) {
      this.myCropper.rotateTo(val)
    },
    // 裁剪
    uploadImgs() {
      this.afterImg = this.myCropper.getCroppedCanvas({
        imageSmoothingQuality: 'high'
      }).toDataURL('image/jpeg');
      this.$emit('handleCropData', this.afterImg)
    },
    // 确定
    handleConfirm() {
      this.afterImg = this.myCropper.getCroppedCanvas({
        imageSmoothingQuality: 'high'
      }).toDataURL('image/jpeg');
      this.$emit('handleUpload', this.base64ToBlob(this.afterImg))
    },
    base64ToBlob(code) {
      const parts = code.split(';base64,');
      const contentType = parts[0].split(':')[1];
      const raw = window.atob(parts[1]);
      const rawLength = raw.length;
      const uInt8Array = new Uint8Array(rawLength);
      for (let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
      }
      return new Blob([uInt8Array], {
        type: contentType
      })
    },    
    // 重置    
    handleReset() {
      this.myCropper.reset();
      this.ScaleX = 1;
      this.ScaleY = 1;
    },
    // 移动
    handleMove(val1, val2) {
      this.myCropper.move(val1, val2);
    },
    // X轴翻转
    handleCropperScaleX() {
      this.ScaleX = -this.ScaleX
      if (this.myCropper.getImageData().rotate === -90 || this.myCropper.getImageData().rotate === 90) {
        this.myCropper.scaleY(this.ScaleX)
      } else {
        this.myCropper.scaleX(this.ScaleX)
      }
    },
    // y轴翻转
    handleCropperScaleY() {
      this.ScaleY = -this.ScaleY
      if (this.myCropper.getImageData().rotate === -90 || this.myCropper.getImageData().rotate === 90) {
        this.myCropper.scaleX(this.ScaleY)
      } else {
        this.myCropper.scaleY(this.ScaleY)
      }
    }
  }
}
</script>
<style>
*{margin: 0; padding: 0;}
.main-cropper{position: fixed; top: 0; right: 0; bottom: 0; left: 0;}
.main-cropper .cropper{position: absolute; top: 0; right: 0; bottom: 54px; left: 0; overflow: hidden; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC);}
.main-cropper .cropper .img{width: 90%; opacity: 0;}
/**/
.main-cropper .cropper-footer{width: 100%; position: fixed; left: 0; bottom: 0;}
.main-cropper .cropper-footer .footer-handle{padding: 10px 10px; display: flex; flex-direction:row; justify-content:space-between;}
.main-cropper .cropper-footer .item{width: 60px; height: 34px; line-height: 34px; text-align: center; background: #28a745; color: #fff; cursor: pointer; border-radius: 4px; font-size: 15px;}
.main-cropper .cropper-footer .item:first-child{background: #909399;}
.main-cropper .cropper-footer .item:last-child{background: #409EFF;}
</style>

打完收工!