vue3 利用vue-cropper裁剪图片并上传
1、安装vue-cropper
npm install vue-cropper
2、引入vue-cropper
import { VueCropper } from "vue-cropper";
3、使用vue-cropper
3.1 该实例将vue-cropper插件和element-ui的el-upload组件结合使用,实现裁剪图片上传
<VueCropper
v-show="imgUrl !== ''" // 判断是否显示剪裁box
ref="cropper"
v-loading="loading" // loading效果
:img="imgUrl" // 剪裁图片url
:output-size="1" // 裁剪生成图片的质量
output-type="png" // 裁剪生成图片的格式
:auto-crop-width="120" // 默认生成截图框宽度
:auto-crop-height="120" // 默认生成截图框高度
:auto-crop="true" // 是否默认生成截图框
:can-move="false" // 上传图片是否可以移动
:centerBox="true" // 截图框是否被限制在图片里面
:fixed="false" // 是否开启截图框宽高固定比例
:fixedNumber="[1, 1]" // 截图框的宽高比例 生效前提--fixed为true
:can-scale="false" // 图片是否允许滚轮缩放
:full="true" // 是否输出原图比例的截图
style="width: 240px; height: 240px" // 剪裁区域展示图片本身的宽高
class="animate__animated animate__fadeIn" // 给组件的动画效果
@realTime="onRealTime" // 实时预览事件
/>
3.2 预览图片区域(实时预览)
<div class="modify-avatar__cropper__preview">
<div class="all">
<div
v-show="previews['url']"
style="
width: 80px;
height: 80px;
overflow: hidden;
border-radius: 6px;
"
>
<div :style="cropperBox">
<img :src="previews['url']" :style="previews['img']" />
</div>
</div>
<div v-show="!previews['url']" />
</div>
<p>图片预览</p>
</div>
3.3 页面预览
「重新上传」即再次调用上传图片方法
4、基于el-upload组件二次封装的上传裁剪图片组件code(可根据具体需求进行改动)
<template>
<el-dialog
:model-value="dialogVisibleShow"
title="修改图片"
width="408px"
center
:before-close="handleClose"
align-center
>
<div class="modify-avatar__cropper">
<el-upload
v-show="imgUrl === ''"
v-loading="loading"
class="upload-demo"
drag
:action="上传图片接口地址"
:headers="{ Authorization: token }"
multiple
:show-file-list="false"
:before-upload="onBeforeUpload"
:on-success="onSuccessUpload"
>
<img
class="modify-avatar__icon"
src="上传图片前裁剪图片区域的占位图片"
/>
<div class="el-upload__text">
<p>点击上传图片</p>
支持 3MB 以内的 JPG 或 PNG 图片
</div>
</el-upload>
<VueCropper
v-show="imgUrl !== ''"
ref="cropper"
v-loading="loading"
:img="imgUrl"
:output-size="1"
output-type="png"
:auto-crop-width="120"
:auto-crop-height="120"
:auto-crop="true"
:can-move="false"
:centerBox="true"
:fixed="false"
:fixedNumber="[1, 1]"
:can-scale="false"
:full="true"
style="width: 240px; height: 240px"
class="animate__animated animate__fadeIn"
@realTime="onRealTime"
/>
<div class="modify-avatar__cropper__preview">
<div class="all">
<div
v-show="previews['url']"
style="
width: 80px;
height: 80px;
overflow: hidden;
border-radius: 6px;
"
>
<div :style="cropperBox">
<img :src="previews['url']" :style="previews['img']" />
</div>
</div>
<div v-show="!previews['url']" />
</div>
<p>图片预览</p>
</div>
</div>
<template #footer>
<span class="dialog-footer_div">
<el-upload
v-if="imgUrl !== ''"
class="upload-demo__reUpload"
:action="上传图片接口地址"
:headers="{ Authorization: token }"
multiple
:show-file-list="false"
:before-upload="onBeforeUpload"
:on-success="onSuccessUpload"
>重新上传</el-upload
>
<div v-else />
<div class="modify-avatar__foot">
<div class="common-follow" @click="handleClose">取 消</div>
<div
:class="[
imgUrl === '' ? 'filed-upload' : 'complete-upload common-button'
]"
@click="onSuccess"
>
确 定
</div>
</div>
</span>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, onMounted } from "vue";
import "vue-cropper/dist/index.css"; // 引入vue-cropper的样式
import { VueCropper } from "vue-cropper";
import { getToken } from "@/utils/auth";
import axios from "axios";
import { ElMessage } from "element-plus";
export default defineComponent({
name: "ModifyAvatar",
components: {
VueCropper
},
props: {
dialogVisibleShow: {
type: Boolean,
default: false
}
},
setup() {
const fileUrl = ''; // 图片地址前缀
const imgWidth = ref(null);
const imgHeight = ref(null);
const cropperWidth = ref(null);
const cropperHeight = ref(null);
const previewStyle = reactive({ value: {} });
const previews = ref({});
const cropperBox = ref("");
const imgUrl = ref("");
const loading = ref(false);
onMounted(() => {});
return {
imgWidth, // 图片宽度
imgHeight, // 图片高度
cropperWidth, // 裁剪框宽度
cropperHeight, // 裁剪框高度
previewStyle, // 预览样式
previews, // 裁剪图片属性
loading, // 上传loading
cropperBox, // 裁剪框样式
imgUrl, // 上传完图片链接
fileUrl
};
},
methods: {
handleClose() {
this.$emit("onClose");
},
// 图片上传之前
onBeforeUpload(file) {
const imgType: any = ["png", "jpg", "jpeg"];
if (
!imgType.find(item => {
return file.type.includes(item);
})
) {
ElMessage.error("仅支持png,jpg格式");
return false;
} else if (file.size / 1024 / 1024 > 3) {
ElMessage.error("单个图片不超过 3MB!");
return false;
}
this.loading = true;
const blob = new Blob([file], { type: "image/png" });
this.imgUrl = window.URL.createObjectURL(blob);
// 获取上传图片宽高
const reader = new FileReader();
reader.onload = event => {
const txt = event.target.result;
const img = document.createElement("img");
img.src = txt;
img.onload = () => {
// 设置截取框背景图片宽高
if (img.width >= img.height) {
this.imgWidth = 240;
this.imgHeight = (240 * img.height) / img.width;
} else {
this.imgHeight = 240;
this.imgWidth = (240 * img.width) / img.height;
}
};
};
reader.readAsDataURL(file);
return true;
},
// 图片上传成功
onSuccessUpload() {
this.loading = false;
},
// 确认上传图片
onSuccess() {
if (this.imgUrl) {
this.$refs.cropper.getCropBlob(data => {
const formData = new FormData();
formData.append("file", data, "avatar.png"); //添加图片信息的参数
axios({
method: "post",
headers: {
Authorization: token
},
url: `上传图片接口地址`,
data: formData
})
.then(data => {
if (data.data.resp_code === 0) {
this.imgUrl = data.data.datas;
this.$emit("onSuccessAvatar", this.imgUrl);
}
})
.catch(() => {
this.$message.warning("网络似乎不通畅,请检查后再试");
});
});
}
},
// 实时裁剪
onRealTime(data) {
const previews = data;
this.cropperWidth = previews.w;
this.cropperHeight = previews.h;
this.previewStyle1 = {
width: previews.w + "px",
height: previews.h + "px",
overflow: "hidden",
margin: "0"
};
// 获取截取框的偏移量
const cropperImg = document.getElementsByClassName("cropper-crop-box")[0];
const transformX = cropperImg["style"].transform
.split("(")[1]
.split(",")[0]
.split("px")[0];
const transformY = cropperImg["style"].transform
.split("(")[1]
.split(",")[1]
.split("px")[0];
// 设置预览框背景图宽高
data.img.width = (this.imgWidth * 80) / data.w + "px";
data.img.height = (this.imgHeight * 80) / data.h + "px";
this.cropperBox = `width: ${
(240 * data.img.width.split("px")[0]) / this.imgWidth
}px;height: ${
(240 * data.img.height.split("px")[0]) / this.imgHeight
}px;display:flex;align-items: center;justify-content: center;`;
// 计算偏移量
const previewWidth = (240 * 80) / data.w;
const previewHeight = (240 * 80) / data.h;
const X = (transformX * previewWidth) / 240;
const Y = (transformY * previewHeight) / 240;
data.img.transform = `translateX(-${X}px) translateY(-${Y}px)`;
this.$nextTick(() => {
this.previews = data;
});
}
}
});
</script>
<style lang="scss">
.el-dialog {
border-radius: 8px;
.el-dialog__header {
text-align: left;
}
.el-dialog__title {
color: #1c232f;
font-weight: 600;
font-size: 18px;
}
.el-upload {
border: none;
background: #fff;
}
}
.modify-avatar__cropper {
display: flex;
.el-upload {
width: 240px;
height: 240px;
.el-upload-dragger {
width: 100%;
height: 100%;
background: #f4f5f7;
border: 1px solid transparent;
}
}
}
.upload-demo__reUpload {
color: #143f98;
.el-upload--picture-card:hover,
.el-upload:focus {
color: #143f98;
}
.el-upload {
width: 56px;
height: 22px;
}
.el-upload--picture-card:hover,
.el-upload {
color: #143f98;
}
}
.el-dialog__body {
padding-top: 0;
padding-bottom: 0;
}
.vue-cropper {
width: 480px;
height: auto;
}
.el-upload__text {
font-size: 12px !important;
color: #5e6982;
margin-top: 24px;
p {
font-size: 14px;
color: #143f98;
}
}
.dialog-footer_div {
display: flex;
align-items: center;
justify-content: space-between;
text-align: center;
line-height: 32px;
font-size: 14px;
}
.modify-avatar__icon {
width: 80px;
height: 80px;
margin: 0 auto;
}
.modify-avatar__reUpload {
color: #143f98;
cursor: pointer;
}
.modify-avatar__foot {
display: flex;
cursor: pointer;
div {
width: 64px;
height: 32px;
background: #f3f4f7;
border-radius: 4px;
}
div:nth-of-type(1) {
color: #5b6985;
margin-right: 8px;
}
.complete-upload {
background: #143f98;
color: #ffffff;
}
.filed-upload {
background: #94bfff;
color: #ffffff;
cursor: not-allowed;
}
.common-follow {
margin-left: 24px;
}
}
.modify-avatar__cropper__preview {
margin-left: 24px;
.all {
width: 80px;
height: 80px;
display: flex;
align-items: center;
background: #fff;
}
img {
width: 80px;
height: 36px;
background: #f4f5f7;
object-fit: contain;
}
div {
width: 80px;
height: 80px;
background: #f4f5f7;
}
p {
font-size: 14px;
color: #5b6985;
text-align: center;
margin-top: 8px;
}
}
</style>
补充
vue-cropper插件常用相关属性和方法
属性
名称 | 功能 | 默认值 | 可选值 |
---|---|---|---|
img | 裁剪图片的地址 | 空 | url 地址, base64, blob |
outputSize | 裁剪生成图片的质量 | 1 | 0.1 ~ 1 |
outputType | 裁剪生成图片的格式 | jpg (jpg 需要传入jpeg) | jpeg, png, webp |
info | 裁剪框的大小信息 | true | true, false |
fixed | 是否开启截图框宽高固定比例 | false | true, false |
fixedNumber | 截图框的宽高比例, 开启fixed生效 | [1, 1] | [ 宽度 , 高度 ] |
… | … | … | … |
可用回调方法
方法名 | 功能 |
---|---|
realTime | 实时预览事件 |
imgMoving | 图片移动回调函数 |
cropMoving | 截图框移动回调函数 |
imgLoad | 图片加载的回调, 返回结果 success, error |
内置方法和属性
属性 | 说明 |
---|---|
this.$refs.cropper.cropW | 截图框宽度 |
this.$refs.cropper.cropH | 截图框高度 |
具体方法和属性请查看插件官网
https://www.npmjs.com/package/vue-cropper