业务场景
需要上传图片实现图片裁剪,固定裁剪框的大小,让裁剪出来图标是正方形。但是考虑到有些图标是长方形,裁剪后图标可能裁剪不全,可以填充底色且让裁剪框可以超出图片,对与超出裁剪框的位置给他填充颜色。
点击+号可以放大,点击-号可以缩小。也可以进行图片的左旋右旋。颜色那里用的iview的颜色选择器,选择完颜色后,可以对超出裁剪框的颜色进行填充底色。点击🔍可以实现裁剪图片的预览。这里是基于我之前写的博客js实现下载本地文件_Humanideal的博客-优快云博客中图片预览的实现,有需要可以去看之前的博客。vue-cropper源码是自己写了个modal遮罩层,写了一些样式。但是对于我的业务场景,已经打开了两个遮罩层了,再使用一个遮罩层图层太过复杂,且图片不突出,效果不明显,所以通过a标签预览的时候另外打开个弹窗进行图片预览。
Blob对象格式
Blob对象是二进制数据,但它是类似文件对象的二进制数据。file继承blob。
实现
基于iview的upload上传组件和vue-cropper - npm实现的。
先下载vue-cropper
npm i vue-cropper或yarn add vue-cropper。这里用的是最新版0.6.4。
父组件
<template>
<Modal>
<Upload
v-if="!isUpload && !formData.iconUrl"
:before-upload="handleUpload"
action=""
>
<Button icon="ios-cloud-upload-outline">
上传图片
</Button>
</Upload>
<div
v-else
style="display: flex; align-items: center;"
>
<img
ref="img"
:src="formData.iconUrl"
style="width: 100px; height: 100px; margin-right: 10px;"
>
<Button
type="primary"
ghost
@click="cropperEvent"
>
裁剪图片
</Button>
</div>
<Modal
v-model="modal"
title="图片裁剪"
@on-ok="ok"
@on-cancel="cancel"
>
<cropper
v-if="cropperData.img"
:cropper-data="cropperData"
:cropper-style="cropperStyle"
@update="img=>formData.iconUrl = img"
/>
</Modal>
</Modal>
</template>
<script>
import cropper from '../../components/cropper/cropper.vue'
export default {
name: 'MediaOperatorModal',
components: {
cropper
},
data() {
return {
isUpload: false,
file: null,
modal: false,
cropperStyle: {
height: '300px'
},
cropperData: {
img: '', // 裁剪图片地址
outputSize: 1, // 裁剪生成图片质量
outputType: 'png', // 裁剪生成图片格式
canScale: true, // 图片是否允许滚轮播放
autoCrop: true, // 是否默认生成截图框 false
info: false, // 是否展示截图框信息
autoCropWidth: 200, // 生成截图框的宽度
autoCropHeight: 200, // 生成截图框的高度
canMoveBox: true, // 截图框是否可以拖动
fixedBox: true, // 固定截图框的大小
canMove: false, // 上传图片是否可拖动
centerBox: false, // 截图框限制在图片里面
}
}
},
methods: {
//将base64转file
base64ToFile(base64, fileName) {
const arr = base64.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], fileName, { type: mime })
},
handleUpload(file) {
this.file = file
const reader = new FileReader()
reader.onload = (e) => {
this.cropperData.img = e.target.result // 这就是图片的base64值
}
reader.readAsDataURL(file)
this.modal = true
this.isUpload = true
return false
},
// URL转Base64
asciiToBase64(asciiString) {
let base64String = ''
let charCode
for (let i = 0; i < asciiString.length; i++) {
charCode = asciiString.charCodeAt(i)
base64String += String.fromCharCode(charCode)
}
base64String = btoa(base64String)
return base64String
},
cropperEvent() {
this.modal = true
this.cropperData.img = this.asciiToBase64(this.formData.iconUrl)
},
}
}
</script>
cropper组件上的v-if是因为父子组件传值异步问题,子组件渲染的时候,对cropperDara.img操作赋值还未进行。使用v-if,当cropperDara.img有值后再渲染子组件。cropperStyle和cropperData都是考虑到封装之后属性的可操作性,方便用户自己传入参数和样式。同时子组件那边也对属性有默认值,如果用户没自定义这些属性,就用写好的默认值。
上传给后端文件一般的格式是file,这里上传需要把之前的base64转file格式才能上传。
注意:这里之前是使用v-model="cropperData.img",但是在后面重新上传文件进行图片裁剪的时候发现出现了问题,通过打断点发现数据传递过去后会立马被之前的数据给覆盖,就是父子组件一直在操作这个cropperData.img数据的过程中赋值出现了问题,所以这边没有通过v-model,组件监听来改变这个数据。而是在父组件使用一个副本formData.iconUrl来接收子组件,传递给子组件的cropperData.img在父组件中只有上传文件后转成base64格式进行了一下处理,其他的都交给子组件处理。
什么是base64
Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。
打印的base64格式
base64转file
base64ToFile(base64, fileName) {
const arr = base64.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], fileName, { type: mime })
}
这个函数首先将base64字符串分割为两部分:数据类型和实际的base64数据。然后,它使用atob()函数将base64数据解码为二进制字符串,然后将该字符串转换为Uint8Array(Uint8Array
数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。)。最后,它使用这个Uint8Array来创建一个新的File对象。 注意,你需要提供一个文件名和扩展名,这应该与你的base64数据的原始文件类型匹配。
arr[0].match(/:(.*?);/)[1]
这段代码是在使用正则表达式匹配字符串。arr[0]
是base64字符串的前半部分,它包含了数据类型信息,例如 "data:image/jpeg;base64"。.match(/:(.*?);/)
是在使用正则表达式匹配这个字符串。正则表达式 :(.*?);
的意思是匹配以冒号 ":" 开始,以分号 ";" 结束的任何字符。其中的 .*?
是一个非贪婪匹配,它会尽可能少的匹配字符。[1]
是在获取匹配结果的第二个元素。因为 .match()
方法返回的是一个数组,数组的第一个元素(索引为0)是完整的匹配结果,第二个元素(索引为1)是第一个括号内匹配的结果。在这个例子中,它将返回数据类型,例如 "image/jpeg"。所以,整个表达式的意思是从base64字符串的前半部分中提取出数据类型。
url转base64
asciiToBase64(asciiString) {
let base64String = ''
let charCode
for (let i = 0; i < asciiString.length; i++) {
charCode = asciiString.charCodeAt(i)
base64String += String.fromCharCode(charCode)
}
base64String = btoa(base64String)
return base64String
}
charCodeAt() 方法可返回指定位置的字符的 Unicode 编码
fromCharCode()是字符串对象的一个方法,它将Unicode转换为字符(字符串)。
btoa()
方法可以将一个二进制字符串(例如,将字符串中的每一个字节都视为一个二进制数据字节)编码为 Base64 编码的 ASCII 字符串。
子组件
<template>
<div class="cropper">
<vueCropper
ref="cropper"
v-bind="option"
:style="initCropperStyle"
/>
<div style="margin-top: 10px;">
<Button
type="primary"
icon="md-add"
@click="changeScale(1)"
/>
<Button
type="primary"
icon="md-remove"
@click="changeScale(-1)"
/>
<Button
type="primary"
icon="md-return-left"
@click="turnLeft"
/>
<Button
type="primary"
icon="md-return-right"
@click="turnRight"
/>
<Button
type="primary"
icon="md-search"
@click="preview"
/>
<ColorPicker v-model="option.fillColor" @on-change="change"/>
</div>
<a
ref="previewBox"
href=""
target="_blank"
/>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
export default {
name: 'Cropper',
components: {
VueCropper
},
props: {
cropperData: {
type: Object
},
cropperStyle: {
type: Object
}
},
data() {
return {
previews: {},
option: {
img: '', // 裁剪图片地址,这里可以本地图片或者链接,链接不用require
outputSize: 1, // 裁剪生成图片质量
outputType: 'png', // 裁剪生成图片格式
canScale: true, // 图片是否允许滚轮播放
autoCrop: true, // 是否默认生成截图框 false
info: false, // 是否展示截图框信息
autoCropWidth: 200, // 生成截图框的宽度
autoCropHeight: 200, // 生成截图框的高度
canMoveBox: true, // 截图框是否可以拖动
fixedBox: true, // 固定截图框的大小
canMove: false, // 上传图片是否可拖动
centerBox: false, // 截图框限制在图片里面
fillColor: ''
},
initCropperStyle: {
height: '300px',
width: '100%'
}
}
},
watch: {
cropperData: {
handler() {
this.option = this.cropperData ? { ...this.cropperData } : this.option
},
deep: true,
immediate: true
}
},
mounted() {
this.initCropperData()
},
methods: {
change(data) {
this.option.fillColor = data
this.cropMoving()
},
initCropperData() {
Object.assign(this.initCropperStyle, this.cropperStyle)
},
turnLeft() {
this.$refs.cropper.rotateRight()
},
turnRight() {
this.$refs.cropper.rotateLeft()
},
changeScale(num) {
num = num || 1
this.$refs.cropper.changeScale(num)
},
preview() {
const link = this.$refs.previewBox
this.$refs.cropper.getCropBlob((data) => {
const blob = data
const url = window.URL.createObjectURL(blob)
link.href = url
link.click()
link.href = '#'
})
},
cropMoving() {
this.$refs.cropper.getCropData((data) => {
this.value = data
})
}
}
}
</script>
子组件这边一上来就调用this.initCropperData(),将外部传来的参数与默认参数进行覆盖合并。如果modal模态框是写到子组件里面的,也可以监测modal的v-model属性,为true再进行调用this.initCropperData()。在子组件上绑上v-model="isShow",子组件内部的Modal上绑上v-model="isShow",@on-cancel="close",close方法内写this.$emit('input', false)。然后在watch监听isShow,为true的时候再调用this.initCropperData()
<cropper
v-if="cropperData.img"
v-model="isShow"
:data="cropperData"
:cropper-style="cropperStyle"
/>
props: {
value: {
type: Boolean,
default: false
}
}
computed: {
isShow: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
}
watch: {
isShow (val) {
val && this.initCropperData()
}
}
具体可以参考我之前的博客。利用v-model原理实现修改props_v-model绑定props-优快云博客