vue elementUI + vue-cropper 图片裁剪功能(解决遇到的坑以及实现oss线上图片裁剪及跨域转换)

1.安装vue-cropper

npm install vue-cropper@0.3.6

我看网上说是要关掉mock服务,我这里倒是没有遇到这个问题
坑1:

如果直接安装不添加版本号,后台会报以下错误:

Failed to mount component: template or render function not defined

原因是vue-cropper新版本报错,解决办法是将vue-cropper降到旧版本,操作步骤为:

1).npm uninstall vue-cropper

2).npm install vue-cropper@0.3.6

2.使用

1.新建cropper.vue文件

<template>
  <div>
    <div class="cropper-content">
      <!-- 剪裁框 -->
      <div class="cropper">
        <vueCropper ref="cropper" :img="option.img" :outputSize="option.size" :outputType="option.outputType" :info="true" :full="option.full" :canMove="option.canMove" :canMoveBox="option.canMoveBox" :original="option.original" :autoCrop="option.autoCrop" :autoCropWidth="option.autoCropWidth" :autoCropHeight="option.autoCropHeight" :fixedBox="option.fixedBox" @realTime="realTime" :fixed="option.fixed" :fixedNumber="fixedNumber"></vueCropper>
      </div>
      <!-- 预览框 -->
      <div class="show-preview" :style="{'width': '300px', 'height': '300px',  'overflow': 'hidden', 'margin': '0 25px', 'display':'flex', 'align-items' : 'center'}">
        <div :style="previews.div" class="preview">
          <img :src="previews.url" :style="previews.img">
        </div>
      </div>
    </div>
    <div class="footer-btn">
      <!-- 缩放旋转按钮 -->
      <div class="scope-btn">
        <el-button size="small" type="primary" icon="el-icon-zoom-in" @click="changeScale(1)"></el-button>
        <el-button size="small" type="primary" icon="el-icon-zoom-out" @click="changeScale(-1)"></el-button>
        <el-button size="small" type="primary" @click="rotateLeft">逆时针旋转</el-button>
        <el-button size="small" type="primary" @click="rotateRight">顺时针旋转</el-button>
      </div>
      <!-- 确认上传按钮 -->
      <div class="upload-btn">
        <el-button type="primary" @click="uploadImg('blob')">上传</el-button>
      </div>
    </div>
  </div>
</template>

<script>
import VueCropper from 'vue-cropper'
export default {
  data () {
    return {
      previews: {}, // 预览数据
      option: {
        img: '', // 裁剪图片的地址  (默认:空)
        size: 1, // 裁剪生成图片的质量  (默认:1)
        full: true, // 是否输出原图比例的截图 选true生成的图片会非常大  (默认:false)
        outputType: 'png', // 裁剪生成图片的格式  (默认:jpg)
        canMove: true, // 上传图片是否可以移动  (默认:true)
        original: false, // 上传图片按照原始比例渲染  (默认:false)
        canMoveBox: true, // 截图框能否拖动  (默认:true)
        autoCrop: true, // 是否默认生成截图框  (默认:false)
        autoCropWidth: 400, // 默认生成截图框宽度  (默认:80%)
        autoCropHeight: 400, // 默认生成截图框高度  (默认:80%)
        fixedBox: false, // 固定截图框大小 不允许改变  (默认:false)
        fixed: true, // 是否开启截图框宽高固定比例  (默认:true)
        fixedNumber: [1, 1] // 截图框比例  (默认:[1:1])
      },
      downImg: '#'
    }
  },
  props: ['imgFile', 'fixedNumber', 'isNewPic'],
  methods: {
    changeScale (num) {
      // 图片缩放
      num = num || 1
      this.$refs.cropper.changeScale(num)
    },
    rotateLeft () {
      // 向左旋转
      this.$refs.cropper.rotateLeft()
    },
    rotateRight () {
      // 向右旋转
      this.$refs.cropper.rotateRight()
    },
    Update () {
      // this.file = this.imgFile
      if (this.isNewPic) { // 直接裁剪网络图片
        this.option.img = this.imgFile
      } else { // 裁剪本地图片
        this.option.img = this.imgFile.url
      }
      // 自己版本
      //
    },
    realTime (data) {
      // 实时预览
      this.previews = data
    },
    uploadImg (type) {
      // 将剪裁好的图片回传给父组件
      event.preventDefault()
      let that = this
      if (type === 'blob') {
        this.$refs.cropper.getCropBlob(data => {
          that.$emit('upload', data)
        })
      } else {
        this.$refs.cropper.getCropData(data => {
          that.$emit('upload', data)
        })
      }
    }
  },
  components: { VueCropper }
}
</script>
<style>
.cropper-content {
  display: flex;
  display: -webkit-flex;
  justify-content: flex-end;
  -webkit-justify-content: flex-end;
}
.cropper-content .cropper {
  width: 350px;
  height: 300px;
}
.cropper-content .show-preview {
  flex: 1;
  -webkit-flex: 1;
  display: flex;
  display: -webkit-flex;
  justify-content: center;
  -webkit-justify-content: center;
  overflow: hidden;
  border: 1px solid #cccccc;
  background: #cccccc;
  margin-left: 40px;
}
.preview {
  overflow: hidden;
  border: 1px solid #cccccc;
  background: #cccccc;
}
.footer-btn {
  margin-top: 30px;
  display: flex;
  display: -webkit-flex;
  justify-content: flex-end;
  -webkit-justify-content: flex-end;
}
.footer-btn .scope-btn {
  width: 45%;
  display: flex;
  display: -webkit-flex;
  justify-content: space-between;
  -webkit-justify-content: space-between;
}
.footer-btn .upload-btn {
  flex: 1;
  -webkit-flex: 1;
  display: flex;
  display: -webkit-flex;
  justify-content: center;
  -webkit-justify-content: center;
}
.footer-btn .btn {
  outline: none;
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  -webkit-appearance: none;
  text-align: center;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  outline: 0;
  margin: 0;
  -webkit-transition: 0.1s;
  transition: 0.1s;
  font-weight: 500;
  padding: 8px 15px;
  font-size: 12px;
  border-radius: 3px;
  color: #fff;
  background-color: #67c23a;
  border-color: #67c23a;
}
</style>

2.引入使用,新建index.vue(上传路径根据自己的需要进行调整)

<template>
  <div>
    <!-- 多图片上传 -->
    <el-upload v-if="multiple" action='string' list-type="picture-card" :on-preview="handlePreview" :auto-upload="false" :on-remove="handleRemove" :http-request="upload" :on-change="consoleFL" :file-list="uploadList">
      <i class="el-icon-plus"></i>
    </el-upload>
    <!-- 单图片上传 -->
    <el-upload v-else class="avatar-uploader" list-type="picture" action="'string'" :auto-upload="false" :show-file-list="false" :on-change="handleCrop" :http-request="upload">
      <img v-if="imageUrl" :src="imageUrl" class="avatar" ref="singleImg" @mouseenter="mouseEnter" @mouseleave="mouseLeave" :style="{width:width+'px',height:height+'px'}">
      <i v-else class="el-icon-plus avatar-uploader-icon" :style="{width:width+'px',height:height+'px','line-height':height+'px','font-size':height/6+'px'}"></i>
      <!-- 单图片上传状态显示 -->
      <!-- <div v-if="imageUrl" class="reupload" ref="reupload" @click.stop="handlePreviewSingle" @mouseenter="mouseEnter" @mouseleave="mouseLeave" :style="{width:reuploadWidth+'px',height:reuploadWidth+'px','line-height':reuploadWidth+'px','font-size':reuploadWidth/5+'px'}">重新上传</div> -->
      <div id="uploadIcon" v-if="imageUrl" ref="reupload" @mouseenter="mouseEnter" @mouseleave="mouseLeave" :style="{width:'100%'}">
        <i class="el-icon-zoom-in" @click.stop="handlePreviewSingle" :style="{color:'#2E2E2E',fontSize:'25px',display:'inline-block',paddingRight:'15px'}"></i>
        <i class="el-icon-edit" @click.stop="handleEditSingle" :style="{color:'#2E2E2E',fontSize:'25px',display:'inline-block',paddingRight:'15px'}"></i>
        <i class="el-icon-upload" :style="{color:'#2E2E2E',fontSize:'25px',display:'inline-block'}"></i>
      </div>
      <div class="reupload" ref="uploading" :style="{width:reuploadWidth+'px',height:reuploadWidth+'px','line-height':reuploadWidth+'px','font-size':reuploadWidth/5+'px'}">上传中..</div>
      <div class="reupload" ref="failUpload" :style="{width:reuploadWidth+'px',height:reuploadWidth+'px','line-height':reuploadWidth+'px','font-size':reuploadWidth/5+'px'}">上传失败</div>
    </el-upload>
    <!-- 多图片预览弹窗 -->
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt="">
    </el-dialog>
    <!-- 剪裁组件弹窗 -->
    <el-dialog :visible.sync="cropperModel" width="800px" :before-close="beforeClose">
      <Cropper :img-file="file" ref="vueCropper" :isNewPic = "isNewPic" :fixedNumber="fixedNumber" @upload="upload">
      </Cropper>
    </el-dialog>
  </div>
</template>
<script>
import Cropper from '../../common/cropper'
import $ from 'jquery'

function getBase64Image(img, width, height) {
  var canvas = document.createElement('canvas')
  canvas.width = width || img.width
  canvas.height = height || img.height
  var ctx = canvas.getContext('2d')
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
  var dataURL = canvas.toDataURL()
  return dataURL
}
function getCanvasBase64(img) {
  var image = new Image()
  // 至关重要
  image.crossOrigin = ''
  image.src = img
  // 至关重要
  var deferred = $.Deferred()
  if (img) {
    image.onload = function () {
      deferred.resolve(getBase64Image(image))// 将base64传给done上传处理
      // document.getElementById("container2").appendChild(image);
    }
    return deferred.promise()// 问题要让onload完成后再return sessionStorage['imgTest']
  }
}
export default {
  name: 'uploader',
  props: {
    targetUrl: {
      // 上传地址
      type: String,
      default: '/storage/upload'
    },
    multiple: {
      // 多图开关
      type: Boolean,
      default: false
    },
    initUrl: {
      // 初始图片链接
      default: 'http://yzwy1-app.oss-cn-shenzhen.aliyuncs.com/accessFacePic/2c9580516facf110016fad228b710000/569915364721197056_facePic_1584259914621.png'
    },
    fixedNumber: {
      // 剪裁框比例设置
      default: function () {
        return [1, 1]
      }
    },
    width: {
      // 单图剪裁框宽度
      type: Number,
      default: 178
    },
    height: {
      // 单图剪裁框高度
      type: Number,
      default: 178
    }
  },
  data () {
    return {
      file: '', // 当前被选择的图片文件
      imageUrl: '', // 单图情况框内图片链接
      dialogImageUrl: '', // 多图情况弹窗内图片链接
      uploadList: [], // 上传图片列表
      reupload: true, // 控制"重新上传"开关
      dialogVisible: false, // 展示弹窗开关
      cropperModel: false, // 剪裁组件弹窗开关
      reuploadWidth: this.height * 0.7, // 动态改变”重新上传“大小
      isNewPic: false // 是上传新的图片还是直接编辑
    }
  },
  updated () {
    if (this.$refs.vueCropper) {
      this.$refs.vueCropper.Update()
    }
  },
  watch: {
    initUrl: function (val) {
      // 监听传入初始化图片
      console.info('watch')
      console.info(val)
      if (val) {
        if (typeof this.initUrl === 'string') {
          this.imageUrl = val
        } else {
          this.uploadList = this.formatImgArr(val)
        }
      }
    }
  },
  mounted () {
    if (typeof this.initUrl === 'string') {
      this.imageUrl = this.initUrl
    } else {
      this.uploadList = this.formatImgArr(this.initUrl)
    }
  },
  methods: {
    /** **************************** multiple多图情况 **************************************/
    handlePreview (file) {
      // 点击进行图片展示
      this.dialogImageUrl = file.url
      this.dialogVisible = true
    },
    handleRemove (file, fileList) {
      // 删除图片后更新图片文件列表并通知父级变化
      this.uploadList = fileList
      this.$emit('imgupload', this.formatImgArr(this.uploadList))
    },
    consoleFL (file, fileList) {
      // 弹出剪裁框,将当前文件设置为文件
      this.cropperModel = true
      this.file = file
      this.uploadList = fileList
    },
    /************************************************************************************/

    /** **************************** single单图情况 **************************************/
    handlePreviewSingle (file) { // 点击进行图片展示
      this.dialogImageUrl = this.imageUrl
      this.dialogVisible = true
    },
    handleEditSingle() {
      let _that = this
      getCanvasBase64(_that.imageUrl).then(function (base64) {
        // 这里拿到的是转换后的base64地址,可以做其他操作
        _that.cropperModel = true
        _that.file = base64
        _that.isNewPic = true
      }, function (err) {
        console.log(err)
      })
    },
    mouseEnter () { // 鼠标划入显示“重新上传”
      this.$refs.reupload.style.display = 'block'
      if (this.$refs.failUpload.style.display === 'block') {
        this.$refs.failUpload.style.display = 'none'
      }
      this.$refs.singleImg.style.opacity = '0.6'
    },
    mouseLeave () {
      // 鼠标划出隐藏“重新上传”
      this.$refs.reupload.style.display = 'none'
      this.$refs.singleImg.style.opacity = '1'
    },
    handleCrop (file, files) {
      console.log(file)
      // 点击弹出剪裁框
      this.isNewPic = false
      this.cropperModel = true
      this.file = file
      // this.imageUrl = file.url
    },
    /************************************************************************************/

    upload (data) {
      // 自定义upload事件
      if (!this.multiple) {
        // 如果单图,则显示正在上传
        this.$refs.uploading.style.display = 'block'
      }
      let formData = new FormData()
      formData.append('attachment', data)
      this.axios.post(this.targetUrl, formData).then(res => {
        if (!this.multiple) {
          // 上传完成后隐藏正在上传
          this.$refs.uploading.style.display = 'none'
        }
        if (res.msg === 'success') {
          // 上传成功将照片传回父组件
          const currentPic = res.data.url
          if (this.multiple) {
            this.uploadList.push({
              url: currentPic,
              uid: '111'
            })
            this.uploadList.pop()
            this.$emit('imgupload', this.formatImgArr(this.uploadList))
          } else {
            this.$emit('imgupload', currentPic)
          }
        } else {
          // 上传失败则显示上传失败,如多图则从图片列表删除图片
          if (!this.multiple) {
            this.$refs.failUpload.style.display = 'block'
          } else {
            this.uploadList.pop()
          }
        }
      })
      this.cropperModel = false
    },
    formatImgArr (arr) {
      const result = arr.map((item, index) => {
        if (typeof item === 'string') {
          return {
            url: item,
            uid: `index${index}`
          }
        } else {
          return item.url
        }
      })
      return result
    },
    beforeClose (done) {
      this.uploadList.pop()
      this.cropperModel = false
    }
  },
  components: {
    Cropper
  }
}
</script>
<style>
  .avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    border-color: #409eff;
  }
  .avatar-uploader-icon {
    color: #8c939d;
    text-align: center;
  }
  .avatar {
    display: block;
  }
  .reupload {
    border-radius: 50%;
    position: absolute;
    color: #fff;
    background-color: #000000;
    opacity: 0.6;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: none;
  }
  #uploadIcon{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: none;
  }
</style>

成功如图所示:(这里只展示多图的,具体可根据自己的需求运用)
在这里插入图片you 有预览裁剪上传三个操作

坑2:
点击裁剪的时候裁剪框和裁剪的图片出不来,解决办法
在el-upload上加上 list-type=“picture”
3.## 线上图片和本地图片区分处理
我这里是直接从后台获取到oss的图片链接,跟从处理方式本地的不一样,我这里做了具体区分(再加一个子元素来区分是线上还是本地图片)
1)线上图片,直接将线上图片路径变为文件路径在这里插入图片描述
2)本地图片

在这里插入图片描述
子组件cropper处理
在这里插入图片描述
坑3:
以上操作在本地运行流畅,但是放到线上,会出现跨域问题,后台报如下错:

Access to image at
‘https://yzwy1-app.oss-cn-shenzhen.aliyuncs.com/accessFacePic/2c9981bb6fbc5254016fc0f74c270002/689490869266317312_facePic_1584433027711.png’
from origin ‘https://test.kcsmkj.cn’ has been blocked by CORS policy:
No ‘Access-Control-Allow-Origin’ header is present on the requested
resource. 689490869266317312_facePic_1584433027711.png:1 Failed to
load resource: net::ERR_FAILED

解决办法:将线上的图片先转为base64再进行裁剪:
方法如下:(用的是jquery,也可以自己再找方式)

function getBase64Image(img, width, height) {
  var canvas = document.createElement('canvas')
  canvas.width = width || img.width
  canvas.height = height || img.height
  var ctx = canvas.getContext('2d')
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
  var dataURL = canvas.toDataURL()
  return dataURL
}
function getCanvasBase64(img) {
  var image = new Image()
  // 至关重要
  image.crossOrigin = ''
  image.src = img
  // 至关重要
  var deferred = $.Deferred()
  if (img) {
    image.onload = function () {
      deferred.resolve(getBase64Image(image))// 将base64传给done上传处理
      // document.getElementById("container2").appendChild(image);
    }
    return deferred.promise()// 问题要让onload完成后再return sessionStorage['imgTest']
  }
}

拿到图片后做如下引用处理即可:

 getCanvasBase64(imgUrl2).then(function (base64) {
          // 这里拿到的是转换后的base64地址,可以做其他操作
          _that.imageUrl = base64
        }, function (err) {
          console.log(err)
        })

以上就全部完成了:

参考:https://juejin.im/post/5b3f14c2f265da0f5405080f

Logo

前往低代码交流专区

更多推荐