# 利用vue-cropper实现图片裁剪功能

# 安装

npm install vue-cropper

# 基于el-dialog实现裁剪组件

<template>
  <div class="cropper_model">
    <el-dialog title="图片剪裁" width="900px" class="cropper_model_dlg" :visible.sync="dialogVisible" append-to-body :close-on-click-modal="false" :close-on-press-escape="false">
      <div class="cropper_content">
        <div class="cropper" style="text-align: center;">
          <vueCropper ref="cropper" :img="options.img" :outputSize="options.outputSize" :outputType="options.outputType" :info="options.info" :canScale="options.canScale" :autoCrop="options.autoCrop" :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixed="options.fixed" :fixedBox="options.fixedBox" :fixedNumber="options.fixedNumber" @realTime="previewImg">
          </vueCropper>
          <div class="cropper_btns">
            <el-button type="primary" @click="goUpload" size="mini">
              重新上传
            </el-button>
            <el-button @click="rotateLeft"  icon="el-icon-refresh-left" size="mini" title="左旋转"></el-button>
            <el-button @click="rotateRight" icon="el-icon-refresh-right" size="mini" title="右旋转"></el-button>
            <el-button @click="changeScale(1)" icon="el-icon-zoom-in" size="mini" title="放大"></el-button>
            <el-button @click="changeScale(-1)" icon="el-icon-zoom-out"  size="mini" title="缩小"></el-button>
          </div>
        </div>
        <div class="cropper_right">
          <h3>预览</h3>
          <!-- 预览 -->
          <div class="cropper_preview" :style="{
              width: preview.w + 'px',
              height: preview.h + 'px',
              overflow: 'hidden',
              margin: '5px'
            }">
            <div :style="preview.div">
              <img :src="preview.url" :style="preview.img" alt="" />
            </div>
          </div>
        </div>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button size="small" @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" size="small" :loading="loading" @click="uploadImg">确定上传</el-button>
      </div>
    </el-dialog>
  </div>
</template>
 
<script>
import { VueCropper } from "vue-cropper";
export default {
  unAutoRegister: true, //不自动注册组件
  components: { VueCropper },
  props: {
    //加载状态
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      dialogVisible: false,
      options: {
        img: "", // 裁剪图片的地址
        outputSize: 1, // 裁剪生成图片的质量
        outputType: "png", // 裁剪生成图片的格式
        info: true, // 裁剪框的大小信息
        canScale: true, // 图片是否允许滚动缩放
        autoCrop: true, // 是否默认生成截图狂
        autoCropWidth: 300, // 默认生成截图框宽度
        autoCropHeight: 300, // 默认生成截图框高度
        fixed: true, // 是否开启截图框宽高固定比例
        fixedNumber: [1, 1], // 截图框的宽高比例
        full: true, // 是否输出原图比例的截图
        fixedBox: false, // 固定截图框大小 不允许改变
        canMove: true, // 上传图片是否可以移动
        canMoveBox: true, // 截图框能否拖动
        original: true, // 上传图片按照原始比例渲染
        centerBox: false, // 截图框是否被限制在图片里面
        high: false, // 是否按照设备的dpr输出等比例图片
        infoTrue: true, // true为展示真实输出图片宽高false展示看到的截图框宽高
        maximgSize: 800, // 限制图片最大宽度和高度
        enlarge: 1, // 图片根据截图框输出比例倍数
        mode: "contain" // 图片默认渲染方式(contain, cover, 100px, 100% auto)
      },
      preview: {},
      imgName: "" // 图片名字
    };
  },
  methods: {
    open(data) {
      console.log("🚀 ~ file: cropper.vue ~ line 93 ~ open ~ data", data)
      this.imgName = data.name || "photo.png"
      this.options.img = window.URL.createObjectURL(data);
      this.dialogVisible = true;
    },
    close() {
      this.dialogVisible = false;
    },
    // base64转图片文件
    dataURLtoFile(dataurl, filename) {
      let arr = dataurl.split(",");
      let mime = arr[0].match(/:(.*?);/)[1];
      let bstr = atob(arr[1]);
      let len = bstr.length;
      let u8arr = new Uint8Array(len);
      while (len--) {
        u8arr[len] = bstr.charCodeAt(len);
      }
      return new File([u8arr], filename, { type: mime });
    },
    // 左旋转
    rotateLeft() {
      this.$refs.cropper.rotateLeft();
    },
    // 右旋转
    rotateRight() {
      this.$refs.cropper.rotateRight();
    },
    // 放大缩小
    changeScale(num) {
      num = num || 1;
      this.$refs.cropper.changeScale(num);
    },
    // 实时预览
    previewImg(data) {
      this.preview = data;
    },
    goUpload() {
      this.$emit("upAgain");
    },
    // 上传图片
    uploadImg() {
      let self = this
      if (self.loading) {
        return
      }
      self.$emit("update:loading", true)
      this.$refs.cropper.getCropData(data => {
        let file = this.dataURLtoFile(data, this.imgName);
        this.$emit("getFile", file)
      });
    }
  }
};
</script>
 
<style lang="less" scoped>
.cropper_model_dlg {
  /deep/ .el-dialog__body {
    height: 500px !important;
    .cropper_content {
      width: 100%;
      margin: 0 auto;
      margin-top: 20px;
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
    }
    .cropper {
      width: 400px;
      height: 400px;
      background: yellow;
    }
    .cropper_right {
      width: 400px;
      text-align: center;
    }
    .cropper_preview {
      margin: 0 auto;
      display: inline-block;
      border: 1px solid #ddd;
    }
    .cropper_btns {
      margin-top: 20px;
    }
  }
}
</style>

此代码并非最优,可根据实际情况更改及优化

# 实战

代码中有模拟分页接口,对于后端无数据又需要测试分页功能时,可使用。

<template>
  <div class="el-cropper">
    <div class="el-upload-list__item" v-for="(item, index) in imgList" :key="index">
      <img :src="item.url" class="el-upload-list__item-thumbnail" />
      <span class="el-upload-list__item-actions">
        <span class="el-upload-list__item-do" @click="handlePictureCardPreview(item.url)">
          <i class="el-icon-zoom-in"></i>
        </span>
        <span class="el-upload-list__item-do" @click="handleDownload(item)">
          <i class="el-icon-download"></i>
        </span>
        <span class="el-upload-list__item-do" @click="handleRemove(item, index)">
          <i class="el-icon-delete"></i>
        </span>
      </span>
    </div>
    <el-upload ref="upload" :show-file-list="false" action="#" :file-list="imgList" list-type="picture-card" :auto-upload="false" :on-change="changePhotoFile">
      <i class="el-icon-plus"></i>
    </el-upload>
    <el-dialog title="图片预览" :visible.sync="dialogVisible">
      <img class="preview-img" width="100%" :src="dialogImageUrl" alt="">
    </el-dialog>
    <cropper ref="myCropper" :loading.sync="loading" @getFile="getFile" @upAgain="upAgain"></cropper>
  </div>
</template>

<script>
import cropper from "./components/cropper.vue"
export default {
  name: "el-cropper",
  components: {
    cropper
  },
  data() {
    return {
      loading: false, //上传图片按钮加载状态
      dialogImageUrl: "", //图片预览地址
      dialogVisible: false, //图片预览弹框
      imgList: [] //图片列表
    }
  },
  methods: {
    /**
     * @description: 下载图片方法
     * @param {*} imgsrc
     * @param {*} name
     * @return {*}
     * @author: syx
     */    
    downloadIamge(imgsrc, name) {//下载图片地址和图片名
      let image = new Image();
      // 解决跨域 Canvas 污染问题
      image.setAttribute("crossOrigin", "anonymous");
      image.onload = function() {
        let canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        let context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, image.width, image.height);
        let url = canvas.toDataURL("image/png"); //得到图片的base64编码数据
        let a = document.createElement("a"); // 生成一个a元素
        let event = new MouseEvent("click"); // 创建一个单击事件
        a.download = name || "photo"; // 设置图片名称
        a.href = url; // 将生成的URL设置为a.href属性
        a.dispatchEvent(event); // 触发a的单击事件
      };
      image.src = imgsrc;
    },
    /**
     * @description: 通过url下载图片
     * @param {*}
     * @return {*}
     * @author: syx
     */
    handleDownload(file) {
      this.downloadIamge(file.url, file.name)
    },
    /**
     * @description: 图片预览
     * @param {*}
     * @return {*}
     * @author: syx
     */
    handlePictureCardPreview(url) {
      this.dialogImageUrl = url;
      this.dialogVisible = true;
    },
    /**
     * @description: 图片改变事件
     * @param {*}
     * @return {*}
     * @author: syx
     */
    changePhotoFile(file, fileList) {
      if (fileList.length > 0) {
        this.photoList = [fileList[fileList.length - 1]]
      }
      this.handleCrop(file);
    },

    /**
     * @description: 触发图片裁减
     * @param {*} file
     * @return {*}
     * @author: syx
     */
    handleCrop(file) {
      this.$nextTick(() => {
        this.$refs.myCropper.open(file.raw || file)
      })
    },
    /**
     * @description: 确定上传
     * @param {*} file
     * @return {*}
     * @author: syx
     */
    getFile(file) {
      const url = window.URL.createObjectURL(file);
      this.imgList.push({
        name: file.name,
        url
      })
      this.loading = false
      //上传成功后,关闭弹框组件
      this.$refs.myCropper.close()
    },
    // 点击弹框重新上传时触发
    upAgain() {
      this.$refs['upload'].$refs["upload-inner"].handleClick();
    },
    handleRemove(file, index) {
      this.imgList.splice(index, 1)
    },
  }
}
</script>

<style lang="less" scoped>
.el-cropper {
  .el-upload-list__item {
    width: 148px;
    height: 148px;
    float: left;
    margin: 0;
    margin-right: 10px;
    margin-bottom: 10px;
    position: relative;
    border-radius: 4px;
    overflow: hidden;
    .el-upload-list__item-thumbnail {
      width: 100%;
      height: 100%;
      vertical-align: middle;
    }
    .el-upload-list__item-actions {
      position: absolute;
      display: none;
      opacity: 0;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      background: rgba(0, 0, 0, 0.5);
      transition: all 0.36s;
      justify-content: center;
      align-items: center;
      .el-upload-list__item-do {
        color: #fff;
        cursor: pointer;
        .el-icon-zoom-in, .el-icon-download, .el-icon-delete{
          font-size: 24px;
        }
        & + .el-upload-list__item-do {
          margin-left: 20px;
        }
      }
    }

    &:hover {
      .el-upload-list__item-actions {
        display: flex;
        opacity: 1;
      }
    }
  }
  /deep/ .el-dialog {
    .el-dialog__body {
      height: 60vh;
      .preview-img {
        height: 100%;
        object-fit: contain;
        margin: 0 auto;
      }
    }
  }
}
</style>
显示代码