基于cropper.js封装vue在线图片裁剪组件

cropper-demo.jpg

github:demo下载

cropper.js

github:cropper.js

官网(demo)

cropper.js 安装

  • npm或bower安装
npm install cropper
# or
bower install cropper
git clone https://github.com/fengyuanchen/cropper.git

引用cropper.js

  • 主要引用cropper.js跟cropper.css两个文件
<script src="/path/to/jquery.js"></script><!-- jQuery is required -->
<link  href="/path/to/cropper.css" rel="stylesheet">
<script src="/path/to/cropper.js"></script>

注意:必须先引入jquery文件,才能使用cropper.js插件

简单使用

  • 构建截图所要用到的div容器
<!-- Wrap the image or canvas element with a block element (container) -->
<div>
  ![](picture.jpg)
</div>
  • 添加容器的样式,让img填充满整个容器(很重要)
/* Limit image width to avoid overflow the container */
img {
  max-width: 100%; /* This rule is very important, please do not ignore this! */
}
  • 调用cropper.js方法,初始化截图控件
$('#image').cropper({
  aspectRatio: 16 / 9,
  crop: function(e) {
    // Output the result data for cropping image.
    console.log(e.x);
    console.log(e.y);
    console.log(e.width);
    console.log(e.height);
    console.log(e.rotate);
    console.log(e.scaleX);
    console.log(e.scaleY);
  }
});

其他详细api请参考:github:cropper.js

封装成vue组件

封装成vue组件中需解决的问题

  • cropper.js相关
    • 模拟input框点击选择图片并对选择的图片进行格式、大小限制
    • 重新选择图片裁剪
    • 确认裁剪并获取base64格式的图片信息
  • vue相关
    • 非父子组件之间的通信问题

模拟input框点击选择图片并对选择的图片进行格式、大小限制

  • 构建一个隐藏的input标签,然后模拟点击此input,从而达到能选择图片的功能
<!-- input框 -->
<input id="myCropper-input" type="file" :accept="imgCropperData.accept" ref="inputer" @change="handleFile">
//模拟点击
document.getElementById('myCropper-input').click();
  • 给input绑定一个监听内容变化的方法,拿到上传的文件,并进行格式、大小校验
// imgCropperData: {
//   accept: 'image/gif, image/jpeg, image/png, image/bmp',
// }
handleFile (e) {
  let _this = this;
  let inputDOM = this.$refs.inputer;
  // 通过DOM取文件数据
  _this.file = inputDOM.files[0];
  // 判断文件格式
  if (_this.imgCropperData.accept.indexOf(_this.file.type) == -1) {
    _this.$Modal.error({
      title: '格式错误',
      content: '您选择的图片格式不正确!'
    });
    return;
  }
  // 判断文件大小限制
  if (_this.file.size > 5242880) {
    _this.$Modal.error({
      title: '超出限制',
      content: '您选择的图片过大,请选择5MB以内的图片!'
    });
    return;
  }
  var reader = new FileReader();
  // 将图片将转成 base64 格式
  reader.readAsDataURL(_this.file);
  reader.onload = function () {
    _this.imgCropperData.imgSrc = this.result;
    _this.initCropper();
  }
}

参考:从0开始做一个的Vue图片/ 文件选择(上传)组件[基础向]

重新选择图片裁剪

  • 当第一次选择图片之后,肯定会面临需要重选图片的问题,那么就会面临如何替换掉裁剪框中的图片,上面的步骤选择了图片后通过FileRender()方法拿到了图片的主要信息,现在就需要重新构建裁剪框就可以解决问题了,查看cropper.js给出的官方demo,发现官方是使用动态添加裁剪容器的方法,进行操作的,这里我们仿照官方进行实现。
// 初始化剪切
  initCropper () {
    let _this = this;
    // 初始化裁剪区域
    _this.imgObj = $('![](' + _this.imgCropperData.imgSrc + ')');
    let $avatarPreview = $('.avatar-preview');
    $('#myCropper-workspace').empty().html(_this.imgObj);
    _this.imgObj.cropper({
      aspectRatio: _this.proportionX / _this.proportionY,
      preview: $avatarPreview,
      crop: function(e) {
        
      }
    });
  }

确认裁剪并获取base64格式的图片信息

let $imgData = _this.imgObj.cropper('getCroppedCanvas')
imgBase64Data = $imgData.toDataURL('image/png'); 

构造用于上传的数据

// 构造上传图片的数据
let formData = new FormData();
// 截取字符串
let photoType = imgBase64Data.substring(imgBase64Data.indexOf(",") + 1);
//进制转换
const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
  const byteCharacters = atob(b64Data);
  const byteArrays = [];
  for(let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);
    const byteNumbers = new Array(slice.length);
    for(let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }
    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }
  const blob = new Blob(byteArrays, {
    type: contentType
  });
  return blob;
}
const contentType = 'image/jepg';
const b64Data2 = photoType;
const blob = b64toBlob(b64Data2, contentType);
formData.append("file", blob, "client-camera-photo.png")
formData.append("type", _this.imgType)

非父子组件之间的通信问题

在之前的项目中,常用到父子组件之间的通信传参,一般用两种方法

  • 在router里面放置参数,然后通过调用$route.params.xxx或者$route.query.xxx进行获取
  • 通过props进行通信

这里我们使用eventBus进行组件之间的通信

步骤

1.声明一个bus组件用于B组件把参数传递给A组件

//bus.js
import Vue from 'vue';  
export default new Vue();

2.在A组件中引用bus组件,并实时监听其参数变化

// A.vue
import Bus from '../../components/bus/bus.js'

export default {
  components: { Bus },
  data () {},
  created: function () {
    Bus.$on('getTarget', imgToken => {  
      var _this = this;
      console.log(imgToken);
      ...  
    });  
  }
}

3.B组件中同样引用bus组件,来把参数传给A组件

// B.vue
// 传参
Bus.$emit('getTarget', imgToken); 

参考:

vue选图截图插件完整代码

<template>
  <div class="myCropper-container">
    <div id="myCropper-workspace">
      <div class="myCropper-words" v-show="!imgCropperData.imgSrc">请点击按钮选择图片进行裁剪</div>
    </div>
    <div class="myCropper-preview" :class="isShort ? 'myCropper-preview-short' : 'myCropper-preview-long'">
      <div class="myCropper-preview-1 avatar-preview">
        ![](!imgCropperData.imgUploadSrc ? '/images/thumbnail/thumbnail-img.jpg' : imgCropperData.imgUploadSrc)
      </div>
      <div class="myCropper-preview-2 avatar-preview">
        ![](!imgCropperData.imgUploadSrc ? '/images/thumbnail/thumbnail-img.jpg' : imgCropperData.imgUploadSrc)
      </div>
      <div class="myCropper-preview-3 avatar-preview">
        ![](!imgCropperData.imgUploadSrc ? '/images/thumbnail/thumbnail-img.jpg' : imgCropperData.imgUploadSrc)
      </div>
      <input id="myCropper-input" type="file" :accept="imgCropperData.accept" ref="inputer" @change="handleFile">
      <Button type="ghost" class="myCropper-btn" @click="btnClick">选择图片</Button>
      <Button type="primary" class="myCropper-btn" :loading="cropperLoading" @click="crop_ok">确认</Button>
    </div>
    
    
  </div>
</template>

<script>
  var ezjsUtil = Vue.ezjsUtil;
  import Bus from './bus/bus.js' 

  export default {
    components: { Bus },
    props: {
      imgType: {
        type: String
      },
      proportionX: {
        type: Number
      },
      proportionY: {
        type: Number
      }
    },
    data () {
      return {
        imgCropperData: {
          accept: 'image/gif, image/jpeg, image/png, image/bmp',
          maxSize: 5242880,
          file: null, //上传的文件
          imgSrc: '', //读取的img文件base64数据流
          imgUploadSrc: '', //裁剪之后的img文件base64数据流
        },
        imgObj: null,
        hasSelectImg: false,
        cropperLoading: false,
        isShort: false,
      }
    },
    created: function () {
      let _this = this;
    },
    mounted: function () {
      let _this = this;
      // 初始化预览区域
      let maxWidthNum = Math.floor(300 / _this.proportionX);
      let previewWidth = maxWidthNum * _this.proportionX;
      let previewHeight = maxWidthNum * _this.proportionY;
      if (previewWidth / previewHeight <= 1.7) {
        previewWidth = previewWidth / 2;
        previewHeight = previewHeight / 2;
        _this.isShort = true;
      }
      // 设置最大预览容器的宽高
      $('.myCropper-preview-1').css('width', previewWidth + 'px');
      $('.myCropper-preview-1').css('height', previewHeight + 'px');
      // 设置中等预览容器的宽高
      $('.myCropper-container .myCropper-preview .myCropper-preview-2').css('width',( previewWidth / 2) + 'px');
      $('.myCropper-container .myCropper-preview .myCropper-preview-2').css('height', (previewHeight / 2) + 'px');
      // 设置最小预览容器的宽高
      $('.myCropper-container .myCropper-preview .myCropper-preview-3').css('width',( previewWidth / 4) + 'px');
      $('.myCropper-container .myCropper-preview .myCropper-preview-3').css('height', (previewHeight / 4) + 'px');
    },
    methods: {
      // 点击选择图片
      btnClick () {
        let _this = this;
        // 模拟input点击选择文件
        document.getElementById('myCropper-input').click();
      },
      // 选择之后的回调
      handleFile (e) {
        let _this = this;
        let inputDOM = this.$refs.inputer;
        // 通过DOM取文件数据
        _this.file = inputDOM.files[0];
        // 判断文件格式
        if (_this.imgCropperData.accept.indexOf(_this.file.type) == -1) {
          _this.$Modal.error({
            title: '格式错误',
            content: '您选择的图片格式不正确!'
          });
          return;
        }
        // 判断文件大小限制
        if (_this.file.size > 5242880) {
          _this.$Modal.error({
            title: '超出限制',
            content: '您选择的图片过大,请选择5MB以内的图片!'
          });
          return;
        }
        var reader = new FileReader();
        // 将图片将转成 base64 格式
        reader.readAsDataURL(_this.file);
        reader.onload = function () {
          _this.imgCropperData.imgSrc = this.result;
          _this.initCropper();
        }
      },
      // 初始化剪切
      initCropper () {
        let _this = this;
        
        // 初始化裁剪区域
        _this.imgObj = $('![](' + _this.imgCropperData.imgSrc + ')');
        let $avatarPreview = $('.avatar-preview');
        $('#myCropper-workspace').empty().html(_this.imgObj);
        _this.imgObj.cropper({
          aspectRatio: _this.proportionX / _this.proportionY,
          preview: $avatarPreview,
          crop: function(e) {
            
          }
        });
        _this.hasSelectImg = true;
      },
      // 确认
      crop_ok () {
        let _this = this, imgToken = null, imgBase64Data = null;
        // 判断是否选择图片
        if (_this.hasSelectImg == false) {
          _this.$Modal.error({
            title: '裁剪失败',
            content: '请选择图片,然后进行裁剪操作!'
          });
          return false;
        }
        // 确认按钮不可用
        _this.cropperLoading = true;
        let $imgData = _this.imgObj.cropper('getCroppedCanvas')
        imgBase64Data = $imgData.toDataURL('image/png'); 
        // 构造上传图片的数据
        let formData = new FormData();
        // 截取字符串
        let photoType = imgBase64Data.substring(imgBase64Data.indexOf(",") + 1);
        //进制转换
                const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
                    const byteCharacters = atob(b64Data);
                    const byteArrays = [];
                    for(let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                        const slice = byteCharacters.slice(offset, offset + sliceSize);
                        const byteNumbers = new Array(slice.length);
                        for(let i = 0; i < slice.length; i++) {
                            byteNumbers[i] = slice.charCodeAt(i);
                        }
                        const byteArray = new Uint8Array(byteNumbers);
                        byteArrays.push(byteArray);
                    }
                    const blob = new Blob(byteArrays, {
                        type: contentType
                    });
                    return blob;
        }
        const contentType = 'image/jepg';
                const b64Data2 = photoType;
        const blob = b64toBlob(b64Data2, contentType);
        formData.append("file", blob, "client-camera-photo.png")
        formData.append("type", _this.imgType)
        // ajax上传
        $.ajax({
                    url: _this.$nfs.uploadUrl,
                    method: 'POST',
                    data: formData,
                    // 默认为true,设为false后直到ajax请求结束(调完回掉函数)后才会执行$.ajax(...)后面的代码
                    async: false,
                    // 下面三个,因为直接使用FormData作为数据,contentType会自动设置,也不需要jquery做进一步的数据处理(序列化)。
                    cache: false,
                    contentType: false,
          processData: false,
          type: _this.imgType,
                    success: function(res) {
            let imgToken = res.data.token;
            _this.cropperLoading = false;
            // 传参
            Bus.$emit('getTarget', imgToken);  
                    },
                    error: function(error) {
            _this.cropperLoading = false;
            _this.$Modal.error({
              title: '系统错误',
              content: '请重新裁剪图片进行上传!'
            });
                    }
                });
      },

    }
  }
</script>

<style lang="less" scoped>
  .myCropper-container {
    height: 400px;
  }
  .myCropper-container #myCropper-input {
    width: 0px;
    height: 0px;
  }
  .myCropper-container #myCropper-workspace {
    width: 500px;
    height: 400px;
    border: 1px solid #dddee1;
    float: left;
  }
  // 裁剪图片未选择图片的提示文字
  .myCropper-container #myCropper-workspace .myCropper-words{
    text-align: center;
    font-size: 18px;
    padding-top: 180px;
  }
  // 裁剪图片的预览区域
  .myCropper-container .myCropper-preview-long {
    width: 300px;
  }
  .myCropper-container .myCropper-preview-short {
    width: 200px;
  }
  .myCropper-container .myCropper-preview {
    float: left;
    height: 400px;
    margin-left: 10px;
  }
  .myCropper-container .myCropper-preview .myCropper-preview-1 {
    border-radius: 5px;
    overflow: hidden;
    border: 1px solid #dddee1;
    box-shadow: 3px 3px 3px #dddee1;
    img {
      width: 100%;
      height: 100%;
    }
  }
  .myCropper-container .myCropper-preview .myCropper-preview-2 {
    margin-top: 20px;
    border-radius: 5px;
    overflow: hidden;
    border: 1px solid #dddee1;
    box-shadow: 3px 3px 3px #dddee1;
    img {
      width: 100%;
      height: 100%;
    }
  }
  .myCropper-container .myCropper-preview .myCropper-preview-3 {
    margin-top: 20px;
    border-radius: 5px;
    overflow: hidden;
    border: 1px solid #dddee1;
    box-shadow: 3px 3px 3px #dddee1;
    img {
      width: 100%;
      height: 100%;
    }
  }
  // 按钮
  .myCropper-btn {
    float: left;
    margin-top: 20px;
    margin-right: 10px;
  }
</style>

BY-LucaLJX

github: lucaljx

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 转载 :OpenDiggawesome-github-vue 是由OpenDigg整理并维护的Vue相关开源项目库...
    果汁密码阅读 23,083评论 8 124
  • 来源:github.com Vue.js开源项目速查表:https://www.ctolib.com/cheats...
    zhangtaiwei阅读 11,593评论 1 159
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,044评论 0 29
  • Day 48 嗯,今天所有考试都结束啦,可以休息好几天,嘿嘿。我也要早早的睡觉啦(*≧ω≦) That's all.
    沙上月阅读 171评论 0 1
  • 作为一个不喜欢社交,不喜欢热闹,不喜欢跟潮流的人,也从来不会和大家一起狂热的追一部电视剧的人,这次竟然把最...
    星小阅读 188评论 0 0