背景:最近在进行公司业务开发时,遇到了头像上传限制尺寸的需求,即限制为一寸证件照(宽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-fit
objectFit: {
type: String,
default: "fill",
},
// 裁剪属性
// 裁剪弹出框的宽度
cropperWidth: {
type: Number,
default: 0,
},
// 裁剪弹出框的高度
cropperHeight: {
type: Number,
default: 600,
},
// 裁剪生成图片的质量 0.1-1
outputSize: {
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,
},
// 头像扫描件id
txsmjid: {
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转blob
base64toBlob(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>