最近在开发一些项目,需要手机端上传图片,然后优化了一个裁剪的功能,遇到了一些神奇的BUG,然后就自己弄了一个插件。
首先采用的技术实现方案是:vue-cropper
这个是一个比较不错的图片裁剪功能,实现简单的裁剪是没有问题的,但是存在一个BUG,图片放大后,将裁剪框滑动的边缘,则无法缩小。
出现BUG的原因:该插件图片放大使用的是CSS3属性【scale】进行放大,缩小,由于限制了裁剪框在图片内,而【scale】的缩放的基准点是中心缩放,所以就会产生BUG,而且这个插件没有控制缩放基准点的配置,尝试过二次开发,放大和缩小,改变其基准点来解决BUG,功能实现了,但是效果体验不好。果断放弃该组件。
新的解决方案:使用【cropperjs】来做图片裁剪,该插件的缩放,采用的是图片拉升,更改图片的宽高来实现放大缩小,而非【scale】实现起来更加容易控制,封装代码插件如下:
安装:cropperjs
npm i cropperjs
组件:
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>
打完收工!