vue2 头像上传+裁剪组件封装
背景:最近在进行公司业务开发时,遇到了头像上传限制尺寸的需求,即限制为一寸证件照(宽295像素,高413像素)。
用到的第三方库: "vue-cropper": "^0.5.5"
完整组件代码:
avatarUpload.vue
<!-- 上传图片并裁剪 -->
<template><div :style="{ marginTop: marginTop + 'px' }"><!-- 上传 --><template><div class="preview-img" v-show="previewImg" :style="{ width: uploadWidth + 'px', height: uploadHeight + 'px' }"><div class="preview-img-box" :style="{ lineHeight: uploadHeight + 'px' }"><i v-if="!disabled" class="el-icon-delete" @click="deleteFn"></i><i class="el-icon-view" @click="viewFn"></i><i v-if="!disabled" class="el-icon-refresh" @click="refreshFn"></i></div> <img :src="previewImg" :style="{ width: '100%', height: '100%', objectFit: objectFit }" /></div><el-upload v-show="!previewImg" ref="upload" class="upload-demo":style="{ width: uploadWidth + 'px', height: uploadHeight + 'px' }" :action="actionUrl":on-change="handleChangeUpload" :auto-upload="false" :show-file-list="false" :disabled="disabled"><div class="upload-demo-icon" :style="{width: uploadWidth + 'px',height: uploadHeight + 'px',lineHeight: uploadHeight + 'px',}">+</div></el-upload></template><!-- 裁剪 --><el-dialog title="图片剪裁" :visible.sync="dialogVisible" class="crop-dialog" append-to-body><div style="padding: 0 20px"><div :style="{textAlign: 'center',width: cropperWidth != 0 ? cropperWidth + 'px' : 'auto',height: cropperHeight + 'px',}"><VueCropper ref="cropper" :img="cropperImg" :output-size="outputSize" :output-type="outputType" :info="info":full="full" :can-move="canMove" :can-move-box="canMoveBox" :original="original" :auto-crop="autoCrop":can-scale="canScale" :fixed="fixed" :fixed-number="fixedNumber" :fixed-box="fixedBox":center-box="centerBox" :info-true="infoTrue" :auto-crop-width="autoCropWidth":auto-crop-height="autoCropHeight" /></div></div><!-- 这里的按钮可以根据自己的需求进行增删--><div class="action-box" v-if="actionButtonFlag"><el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleChangeUpload"style="margin-right: 15px"><el-button title="更换图片" plain circle type="primary" icon="el-icon-refresh"></el-button></el-upload><el-button title="清除图片" plain circle type="primary" icon="el-icon-close" @click="clearImgHandle"></el-button><el-button title="向左旋转" plain circle type="primary" icon="el-icon-refresh-left"@click="rotateLeftHandle"></el-button><el-button title="向右旋转" plain circle type="primary" icon="el-icon-refresh-right" @click="rotateRightHandle"></el-button><el-button title="放大" plain circle type="primary" @click="changeScaleHandle(1)" icon="el-icon-zoom-in"></el-button><el-button title="缩小" plain circle type="primary" @click="changeScaleHandle(-1)" icon="el-icon-zoom-out"></el-button><!-- <el-button type="primary" @click="fixed = !fixed">{{ fixed ? "固定比例" : "自由比例" }}</el-button> --><el-button title="下载" plain circle type="primary" icon="el-icon-download"@click="downloadHandle('blob')"></el-button></div><div slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" :loading="loading" @click="finish">确认</el-button></div></el-dialog></div>
</template><script>
import { VueCropper } from "vue-cropper";
import { insertImage } from '@/api/file';
export default {name: "Cropper",components: {VueCropper,},props: {marginTop: {type: Number,default: 0,},// 上传属性// 图片路径imgSrc: {type: String,default: "",},// 是否禁用disabled: {type: Boolean,default: false,},// 列表索引listIndex: {type: Number,default: null,},// 上传路径actionUrl: {type: String,default: "#",},// 上传宽度uploadWidth: {type: Number,default: 100,},// 上传高度uploadHeight: {type: Number,default: 100,},// 图片显示角度 传值详情参考mdn object-fitobjectFit: {type: String,default: "fill",},// 裁剪属性// 裁剪弹出框的宽度cropperWidth: {type: Number,default: 0,},// 裁剪弹出框的高度cropperHeight: {type: Number,default: 600,},// 裁剪生成图片的质量 0.1-1outputSize: {type: Number,default: 1,},// 裁剪生成图片的格式outputType: {type: String,default: "png",},// 裁剪框的大小信息info: {type: Boolean,default: true,},// 是否输出原图比例的截图full: {type: Boolean,default: false,},// 截图框能否拖动canMove: {type: Boolean,default: true,},// 截图框能否拖动canMoveBox: {type: Boolean,default: true,},// 上传图片按照原始比例渲染original: {type: Boolean,default: true,},// 是否默认生成截图框autoCrop: {type: Boolean,default: true,},// 图片是否允许滚轮缩放canScale: {type: Boolean,default: true,},// 是否开启截图框宽高固定比例fixed: {type: Boolean,default: true,},// 截图框的宽高比例 开启fixed生效fixedNumber: {type: Array,default: () => [5, 7],},// 固定截图框大小 不允许改变fixedBox: {type: Boolean,default: true,},// 截图框是否被限制在图片里面centerBox: {type: Boolean,default: true,},// true 为展示真实输出图片宽高 false 展示看到的截图框宽高infoTrue: {type: Boolean,default: true,},// 默认生成截图框宽度autoCropWidth: {type: Number,default: 295,},// 默认生成截图框高度autoCropHeight: {type: Number,default: 413,},// 是否出现操作按钮actionButtonFlag: {type: Boolean,default: false,},// 裁剪路径输出格式 base64:base64; blob:blob;cropFormat: {type: String,default: "blob",},// 图片最大宽度maxImgWidth: {type: Number,default: 648,},// 图片最大高度maxImgHeight: {type: Number,default: 1152,},// 头像扫描件idtxsmjid: {type: String,default: ""},// 是否禁用上传disabled:{type: Boolean,default: false}},data() {return {previewImg: "", // 预览图片地址dialogVisible: false, //图片裁剪弹框cropperImg: "", // 裁剪图片的地址loading: false, // 防止重复提交baseCsUrl: process.env.NODE_ENV === 'production' ? window.globalConfig.VUE_APP_BASE_API_CS : process.env.VUE_APP_BASE_API_CS, // 文件服务器地址fileName: "",};},watch: {imgSrc: {handler(newVal, oldVal) {this.previewImg = newVal;},deep: true, // 深度监听immediate: true, // 首次进入就监听},},methods: {// 上传按钮 限制图片大小和类型handleChangeUpload(file) {this.fileName = file.name;const isJPG =file.raw.type === "image/jpeg" || file.raw.type === "image/png";const isLt2M = file.size / 1024 / 1024 < 2;const min_isLt = file.size / 1024 > 20;if (!isJPG) {this.$message.error("上传头像图片只能是 JPG/PNG 格式!");return false;}if (!isLt2M) {this.$message.error("上传头像图片大小不能超过 2MB!");return false;}if (!min_isLt) {this.$modal.msgError("头像大小不能小于 20 K!");return false;}// 上传成功后将图片地址赋值给裁剪框显示图片this.$nextTick(async () => {// base64方式// this.option.img = await fileByBase64(file.raw)this.cropperImg = URL.createObjectURL(file.raw);this.loading = false;this.dialogVisible = true;});},// 放大/缩小changeScaleHandle(num) {num = num || 1;this.$refs.cropper.changeScale(num);},// 左旋转rotateLeftHandle() {this.$refs.cropper.rotateLeft();},// 右旋转rotateRightHandle() {this.$refs.cropper.rotateRight();},// 下载downloadHandle(type) {let aLink = document.createElement("a");aLink.download = "author-img";if (type === "blob") {this.$refs.cropper.getCropBlob((data) => {aLink.href = URL.createObjectURL(data);aLink.click();});} else {this.$refs.cropper.getCropData((data) => {aLink.href = data;aLink.click();});}},// 清理图片clearImgHandle() {this.cropperImg = "";},// 截图finish() {if (this.cropFormat == "base64") {// 获取截图的 base64 数据this.$refs.cropper.getCropData((data) => {this.loading = true;this.dialogVisible = false;this.getImgHeight(data, this.maxImgWidth, this.maxImgHeight).then((imgUrl) => {// console.log(imgUrl, "base64");this.previewImg = imgUrl;if (this.listIndex !== null) {this.$emit("successCheng", this.previewImg, this.listIndex);} else {this.$emit("successCheng", this.previewImg);}});});} else if (this.cropFormat == "blob") {// 获取截图的 blob 数据this.$refs.cropper.getCropBlob((blob) => {this.loading = true;// this.dialogVisible = false;this.getImgHeight(URL.createObjectURL(blob),this.maxImgWidth,this.maxImgHeight).then((imgUrl) => {// console.log(imgUrl, "blob");this.previewImg = imgUrl;if (this.listIndex !== null) {this.$emit("successCheng", this.previewImg, this.listIndex);} else {this.$emit("successCheng", this.previewImg);}});this.uploadImage(blob)});}},// 上传到后端uploadImage(blob) {let file = new File([blob], this.fileName, {type: "image/png",lastModified: Date.now(),});let fd = new FormData();fd.append("file", file);insertImage(fd, this.txsmjid).then((res) => {// console.log("图片上传", res);this.dialogVisible = false;if (res.code === 200) {this.$modal.msgSuccess("上传成功");}}).finally(() => {this.loading = false;});},// 预览图片viewFn() {let preIndex = 0;this.$viewerApi({images: [this.previewImg],options: {initialViewIndex: preIndex,},});},// 删除图片deleteFn() {this.previewImg = "";// this.$emit("deleteCheng");},// 更换图片refreshFn() {this.$refs["upload"].$refs["upload-inner"].handleClick();},// 获取图片高度并修改getImgHeight(imgSrc, scaleWidth = 648, scaleHeight = 1152) {return new Promise((resolve, reject) => {const img = new Image(); // 创建一个img对象img.src = imgSrc; // 设置图片地址let imgUrl = ""; // 接收图片地址img.onload = () => {if (img.width > scaleWidth || img.height > scaleHeight) {const canvas = document.createElement("canvas");const context = canvas.getContext("2d");canvas.width = scaleWidth;canvas.height = scaleHeight;context.drawImage(img, 0, 0, scaleWidth, scaleHeight);if (this.cropFormat == "blob") {imgUrl = this.base64toBlob(canvas.toDataURL("image/png", 1),"image/png");} else {imgUrl = canvas.toDataURL("image/png", 1);}resolve(imgUrl);} else {imgUrl = imgSrc;resolve(imgUrl);}};});},// base64转blobbase64toBlob(base64, type = "application/octet-stream") {// 去除base64头部const images = base64.replace(/^data:image\/\w+;base64,/, "");const bstr = atob(images);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return URL.createObjectURL(new Blob([u8arr], { type }));},},
};
</script><style lang="scss" scoped>
.preview-img {position: relative;cursor: pointer;&:hover {.preview-img-box {display: block;}}
}.preview-img-box {width: 100%;height: 100%;position: absolute;left: 0;top: 0;background-color: rgba(30, 28, 28, 0.5);display: none;color: #d9d9d9;font-size: 24px;text-align: center;
}.preview-img-number {width: 20px;height: 20px;overflow: hidden;position: absolute;right: 0;bottom: 0;background-color: rgba(8, 137, 53, 1);color: #d9d9d9;font-size: 20px;text-align: center;border-radius: 50%;
}.upload-demo {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;
}.upload-demo-icon {font-size: 60px;color: #8c939d;text-align: center;
}.crop-dialog {.action-box {margin: 20px;display: flex;flex-wrap: wrap;justify-content: center;button {margin-top: 15px;//width: 80px;margin-right: 15px;}}.dialog-footer {text-align: center;button {width: 100px;}}
}
</style>
使用组件:
<template><div><!-- 图片裁剪 --><avatarUpload :key="new Date().getTime()" :imgSrc="imageUrl" :uploadWidth="148" :uploadHeight="207" :actionButtonFlag="true" :objectFit="'cover'"
class="avatar-uploader" :txsmjid="txsmjid"></avatarUpload></div>
</template>
<script>
import avatarUpload from "./avatarUpload.vue";
export default {components: {avatarUpload,},data() {imageUrl:'',txsmjid:'',}
}
</script>