效果展示
裁剪前:
宽高:1920px * 1080px;大小:2.38MB
裁剪后:
宽高:140px * 140px;大小:18.7KB
前置条件
1.vue2 + elementUI
2.基于cropperjs二次封装,需引入cropperjs组件
npm install cropperjs --save
使用方法
<!-- 按需引入或在main.js进行全局引入,自行实现,此处不做示例 -->
<!-- 以下是完整demo -->
<template>
<div class="demo">
<div class="title">* 单图版示例</div>
<cropper ref="cropper" :width="150" :height="150" :prefix="$http.getFile" :url="url" :action="$http.uploadFile" :success="handleCropper"/>
<div style="margin-top: 40px"></div>
<div class="title">* 多图版示例</div>
<multicropper ref="multicropper" :width="150" :height="150" :prefix="$http.getFile" :url="urls" :action="$http.uploadFile"
:success="handleMultiCropper" :num="3"/>
</div>
</template>
<script>
export default {
data() {
return {
//单图版结果
url: "",
//多图版结果
urls: [],
}
},
methods: {
/**
* 单图版裁剪完成
* @param url
*/
handleCropper(url) {
this.url = url;
console.log(url)
},
/**
* 多图版裁剪完成
* @param url
*/
handleMultiCropper(url) {
this.urls = url;
console.log(url)
},
}
}
</script>
<style scoped lang="less">
.demo {
.title {
color: #d62120;
margin-bottom: 10px;
}
}
</style>
参数说明
参数名 | 类型 | 是否必传 | 默认值 | 含义 |
---|---|---|---|---|
width | 整数 | 是 | 目标尺寸宽度(即裁剪后) | |
height | 整数 | 否 | 0 | 目标尺寸高度(即裁剪后),如果不传,将根据上传的图片实际宽高按比例缩放 |
prefix | 字符串 | 否 | 空字符串 | 图片链接统一前缀,用于相对路径场景 |
url | 字符串 | 否 | 默认展示的图片链接,用于修改图片场景 | |
num | 整数 | 多图版必传 | 限制上传图片的数量 | |
name | 字符串 | 否 | file | 上传文件后端接口需要接收的参数名,需询问后端 |
size | 整数 | 否 | 5120 | 限制上传图片的尺寸大小,单位KB |
success | 函数 | 是 | 上传成功后的回调函数 | |
action | 字符串 | 是 | 上传图片的后端接口地址 |
以下为自定义组件完整代码
* 单图版
<template>
<div class="cropper">
<el-image class="upload" :src="require('./upload.png')" @click="chooseImage" v-if="isBlank(resultUrl) && isBlank(url)"></el-image>
<div v-else>
<img class="result-image" :src="prefix + (isBlank(url) ? resultUrl : url)" style="object-fit: contain;" alt="Image" @click="chooseImage"/>
</div>
<div class="tip">尺寸:{{ width }}px * {{ height > 0 ? (height + "px") : "按比例计算" }}</div>
<el-dialog title="裁剪压缩" :visible.sync="uploading" :width="(areaW + 150) + 'px'" :show-close="false" :close-on-click-modal="false"
:close-on-press-escape="false" :append-to-body="true">
<div class="area flex-row-center">
<div :style="'width: '+ (areaW + 100) +'px;max-height: '+ (areaH + 100) +'px;'">
<img class="image" v-if="imageUrl && uploading && areaW > 0 && areaW > 0" ref="image" :src="imageUrl" @load="onImageLoad" alt="Image"/>
</div>
</div>
<div slot="footer" class="flex-row-center">
<el-button class="confirm" type="primary" @click="confirm" :loading="isLoading">裁剪并上传</el-button>
<el-button class="cancel" type="plain" @click="init">取消上传</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
props: {
//图片宽度
width: {
type: Number,
required: true
},
//图片高度
height: {
type: Number,
required: false,
default: 0
},
//图片链接统一前缀
prefix: {
type: String,
required: false,
default: "",
},
//默认图片链接
url: {
type: String,
required: false
},
//上传文件后端接收的参数名
name: {
type: String,
required: false,
default: "file",
},
//图片大小限制(单位kb)
size: {
type: Number,
required: false,
default: 5120,
},
//裁剪并上传
success: {
type: Function,
required: true
},
//图片上传地址
action: {
type: String,
required: true
},
},
watch: {
url(cur) {
//初始化裁剪对象
this.init();
this.resultUrl = this.isBlank(cur) ? "" : cur;
},
width(cur) {
this.realW = cur;
},
height(cur) {
this.realH = cur;
},
},
data() {
return {
//裁剪对象
cropper: null,
//裁剪组件宽度
areaW: 0,
//裁剪组件高度
areaH: 0,
//最终图片宽度
realW: 0,
//最终图片高度
realH: 0,
//裁剪时的临时图片链接
imageUrl: "",
//裁剪后的图片链接
resultUrl: "",
//文件名
fileName: "",
//是否指定高度
isSetH: true,
//是否正在上传
uploading: false,
//是否正在裁剪
isLoading: false,
};
},
created() {
this.isSetH = this.height > 0;
this.realW = this.width;
this.realH = this.height;
//初始化裁剪对象
this.init();
},
methods: {
/**
* 初始化裁剪对象
*/
init() {
this.imageUrl = "";
this.fileName = "";
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.uploading = false;
},
/**
* 计算最大宽高
*/
getMaxSize() {
let maxW = 800;
let maxH = 400;
let areaW = 0;
let areaH = 0;
let wRate = this.realW / maxW;
let hRate = this.realH / maxH;
if (wRate > hRate) {
areaW += maxW;
areaH += Math.round(maxW / this.realW * this.realH);
} else {
areaW += Math.round(maxH / this.realH * this.realW);
areaH += maxH;
}
this.areaW = areaW;
this.areaH = areaH;
},
/**
* 选择一张图片
*/
chooseImage() {
//初始化裁剪对象
this.init();
const input = document.createElement("input");
input.type = "file";
input.accept = "image/jpeg,image/png";
input.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
if (file.size > 1024 * this.size) {
this.$message.warning("图片大小不能超过" + this.formatSize());
return;
}
// 获取文件名
this.fileName = file.name;
const reader = new FileReader();
reader.onload = (e) => {
this.imageUrl = e.target.result;
if (!this.isSetH) {
//如果未指定图片高度,将获取上传的图片宽高来计算比例
const image = new Image();
image.onload = () => {
const width = image.width;
const height = image.height;
this.realH = Math.round(this.width / width * height);
this.getMaxSize();
};
image.src = e.target.result;
}
};
reader.readAsDataURL(file);
this.getMaxSize();
this.uploading = true;
}
});
input.click();
},
/**
* 格式化尺寸
*/
formatSize() {
if (this.size < 1024) {
return `${this.size.toFixed(2)} KB`;
} else {
const sizeInMB = this.size / 1024;
return `${sizeInMB.toFixed(2)} MB`;
}
},
/**
* 当图片加载完成后,加载裁剪对象
*/
onImageLoad() {
if (this.cropper) {
this.cropper.destroy();
}
this.cropper = new this.$cropper(this.$refs.image, {
aspectRatio: this.areaW / this.areaH,// 裁剪区域的宽高比
viewMode: 1, // 视图模式,1 表示限制裁剪区域在图片内
dragMode: "move",//定义裁剪器的拖动模式
autoCropArea: 0,// 自动裁剪区域的大小,0 到 1 之间
cropBoxResizable: false, // 禁止调整裁剪区域的大小
minCanvasWidth: this.areaW, // 设置图片最小宽度
minCanvasHeight: this.areaH, // 设置图片最小高度
minContainerWidth: (this.areaW + 50), // 设置画布的最小宽度
minContainerHeight: (this.areaH + 50), // 设置画布的最小高度
wheelZoomRatio: 0.1,//缩放图片时的比例
cropBoxMovable: false, // 禁止裁剪框移动
toggleDragModeOnDblclick: false,//是否允许双击切换图片容器拖拽模式("crop"和"move")
ready: () => {
const containerData = this.cropper.getContainerData();
const cropBoxWidth = this.areaW;
const cropBoxHeight = this.areaH;
const cropBoxLeft = (containerData.width - cropBoxWidth) / 2;
const cropBoxTop = (containerData.height - cropBoxHeight) / 2;
this.cropper.setCropBoxData({
left: cropBoxLeft,
top: cropBoxTop,
width: cropBoxWidth,
height: cropBoxHeight,
});
},
});
},
/**
* 裁剪并上传
*/
confirm() {
if (!this.cropper) {
this.$message.warning("请先选择一张图片");
return;
}
this.isLoading = true;
//先压缩至指定的尺寸
const canvas = this.cropper.getCroppedCanvas({
width: this.realW,
height: this.realH,
fillColor: "transparent", // 设置导出图片的背景颜色为透明
imageSmoothingQuality: "high",
});
//检查图片类型
let mimeType = this.getImageMimeType();
if (mimeType == null) {
this.$message.warning("仅支持上传jpeg和png格式的图片");
this.isLoading = false;
return;
}
//转换为blob对象,上传至图片服务器
canvas.toBlob((blob) => {
//执行上传操作
this.upload(blob, this.fileName, result => {
//判断是否返回了上传后的图片链接
if (this.isBlank(result.message)) {
this.$message.warning("图片裁剪失败,请刷新页面后重试")
return;
}
//上传成功,初始化裁剪对象,并将图片地址传递给父页面
this.init();
this.success(result.message);
this.isLoading = false;
this.resultUrl = result.message;
});
}, mimeType, 1);
},
/**
* 获取图片类型
*/
getImageMimeType() {
// 获取文件扩展名
const fileExtension = this.fileName.split('.').pop();
let mimeType;
switch (fileExtension.toLowerCase()) {
case "jpg":
case "jpeg":
mimeType = "image/jpeg";
break;
case "png":
mimeType = "image/png";
break;
default:
mimeType = null;
}
return mimeType;
},
/**
* 判断是否为空
*/
isBlank(obj) {
if (obj instanceof Date) {
return false;
}
if (typeof obj === "number" && !isNaN(obj)) {
return false;
}
if (!obj || obj === "undefined" || obj === "null" || obj === "" || obj === {} || obj === [] || obj.length === 0) {
return true;
}
return Object.keys(obj).length < 1;
},
/**
* 上传文件
*/
upload(file, fileName = null, success) {
let formdata = new FormData();
if (fileName != null) {
formdata.append(this.name, file, fileName);
} else {
formdata.append(this.name, file);
}
axios.post(this.action, formdata, {"Content-Type": "multipart/form-data"}).then(response => {
let result = response.data;
if (result.flag !== 200) {
this.$message.warning("上传出错,原因是:" + result.message);
return;
}
if (success) {
success(result);
}
}).catch((error) => {
this.$message.warning("内部错误,原因是:" + error);
})
},
},
};
</script>
<style lang="less" scoped>
.cropper {
line-height: 12px !important;
.upload {
width: 80px;
height: 80px;
cursor: pointer;
}
.result-image {
width: 80px;
height: 80px;
border-radius: 2px;
border: 1px dashed #bbb;
}
.tip {
color: #bbb;
font-size: 12px;
margin: 5px 0;
}
.area {
width: 100%;
.image {
margin-top: 10px;
}
.buttons {
margin-top: 10px;
}
}
}
</style>
* 多图版
<template>
<div class="cropper">
<div>
<img class="result-image" v-for="(item,index) in resultUrl" :key="index" :src="prefix +item"
style="object-fit: contain;" alt="Image" @click="chooseImage(index)" @contextmenu.prevent="deleteImage(index)"/>
<el-image class="upload" :src="require('./upload.png')" @click="chooseImage(-1)" v-if="resultUrl.length < num"></el-image>
</div>
<div class="tip">可上传 {{ num }} 张图片,尺寸:{{ realW }}px * {{ isSetH ? (realH + "px") : "按比例计算" }},右键点击图片可删除</div>
<el-dialog title="裁剪压缩" :visible.sync="uploading" :width="(areaW + 150) + 'px'" :show-close="false" :close-on-click-modal="false"
:close-on-press-escape="false" :append-to-body="true">
<div class="area flex-row-center">
<div :style="'width: '+ (areaW + 100) +'px;max-height: '+ (areaH + 100) +'px;'">
<img class="image" v-if="imageUrl && uploading && areaW > 0 && areaW > 0" ref="image" :src="imageUrl" @load="onImageLoad" alt="Image"/>
</div>
</div>
<div slot="footer" class="flex-row-center">
<el-button class="confirm" type="primary" @click="confirm" :loading="isLoading">裁剪并上传</el-button>
<el-button class="cancel" type="plain" @click="init">取消上传</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
props: {
//图片宽度
width: {
type: Number,
required: true
},
//图片高度
height: {
type: Number,
required: false,
default: 0
},
//图片链接统一前缀
prefix: {
type: String,
required: false,
default: "",
},
//图片数量
num: {
type: Number,
required: true,
},
//默认图片链接数组
url: {
type: Array,
required: false
},
//上传文件后端接收的参数名
name: {
type: String,
required: false,
default: "file",
},
//图片大小限制(单位kb)
size: {
type: Number,
required: false,
default: 5120,
},
//裁剪并上传
success: {
type: Function,
required: true
},
//图片上传地址
action: {
type: String,
required: true
},
},
watch: {
url(cur) {
this.resultUrl = this.isBlank(cur) ? [] : cur.filter(item => !this.isBlank(item));
},
width(cur) {
this.realW = cur;
},
height(cur) {
this.realH = cur;
},
},
data() {
return {
//裁剪对象
cropper: null,
//裁剪组件宽度
areaW: 0,
//裁剪组件高度
areaH: 0,
//最终图片宽度
realW: 0,
//最终图片高度
realH: 0,
//裁剪时的临时图片链接
imageUrl: "",
//裁剪后的图片链接数组
resultUrl: [],
//文件名
fileName: "",
//是否指定高度
isSetH: true,
//是否正在上传
uploading: false,
//是否正在裁剪
isLoading: false,
//更改的图片下标
urlIndex: -1,
};
},
created() {
this.isSetH = this.height > 0;
this.realW = this.width;
this.realH = this.height;
//初始化裁剪对象
this.init();
if (!this.isBlank(this.url)) {
this.resultUrl = this.url.filter(item => !this.isBlank(item));
}
},
methods: {
/**
* 初始化裁剪对象
*/
init() {
this.imageUrl = "";
this.fileName = "";
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.uploading = false;
},
/**
* 计算最大宽高
*/
getMaxSize() {
let maxW = 800;
let maxH = 400;
let areaW = 0;
let areaH = 0;
let wRate = this.realW / maxW;
let hRate = this.realH / maxH;
if (wRate > hRate) {
areaW += maxW;
areaH += Math.round(maxW / this.realW * this.realH);
} else {
areaW += Math.round(maxH / this.realH * this.realW);
areaH += maxH;
}
this.areaW = areaW;
this.areaH = areaH;
},
/**
* 选择一张图片
*/
chooseImage(index) {
this.urlIndex = index;
//初始化裁剪对象
this.init();
const input = document.createElement("input");
input.type = "file";
input.accept = "image/jpeg,image/png";
input.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
if (file.size > 1024 * this.size) {
this.$message.warning("图片大小不能超过" + this.formatSize());
return;
}
// 获取文件名
this.fileName = file.name;
const reader = new FileReader();
reader.onload = (e) => {
this.imageUrl = e.target.result;
if (!this.isSetH) {
//如果未指定图片高度,将获取上传的图片宽高来计算比例
const image = new Image();
image.onload = () => {
const width = image.width;
const height = image.height;
this.realH = Math.round(this.width / width * height);
this.getMaxSize();
};
image.src = e.target.result;
}
};
reader.readAsDataURL(file);
this.getMaxSize();
this.uploading = true;
}
});
input.click();
},
/**
* 格式化尺寸
*/
formatSize() {
if (this.size < 1024) {
return `${this.size.toFixed(2)} KB`;
} else {
const sizeInMB = this.size / 1024;
return `${sizeInMB.toFixed(2)} MB`;
}
},
/**
* 当图片加载完成后,加载裁剪对象
*/
onImageLoad() {
if (this.cropper) {
this.cropper.destroy();
}
this.cropper = new this.$cropper(this.$refs.image, {
aspectRatio: this.areaW / this.areaH,// 裁剪区域的宽高比
viewMode: 1, // 视图模式,1 表示限制裁剪区域在图片内
dragMode: "move",//定义裁剪器的拖动模式
autoCropArea: 0,// 自动裁剪区域的大小,0 到 1 之间
cropBoxResizable: false, // 禁止调整裁剪区域的大小
minCanvasWidth: this.areaW, // 设置图片最小宽度
minCanvasHeight: this.areaH, // 设置图片最小高度
minContainerWidth: (this.areaW + 50), // 设置画布的最小宽度
minContainerHeight: (this.areaH + 50), // 设置画布的最小高度
wheelZoomRatio: 0.1,//缩放图片时的比例
cropBoxMovable: false, // 禁止裁剪框移动
toggleDragModeOnDblclick: false,//是否允许双击切换图片容器拖拽模式("crop"和"move")
ready: () => {
const containerData = this.cropper.getContainerData();
const cropBoxWidth = this.areaW;
const cropBoxHeight = this.areaH;
const cropBoxLeft = (containerData.width - cropBoxWidth) / 2;
const cropBoxTop = (containerData.height - cropBoxHeight) / 2;
this.cropper.setCropBoxData({
left: cropBoxLeft,
top: cropBoxTop,
width: cropBoxWidth,
height: cropBoxHeight,
});
},
});
},
/**
* 右键删除图片
*/
deleteImage(index) {
this.$confirm("将删除该图片,是否确认?", "提示", {type: "warning"}).then(() => {
this.resultUrl.splice(index, 1); // 删除指定下标的图片
});
},
/**
* 裁剪并上传
*/
confirm() {
if (!this.cropper) {
this.$message.warning("请先选择一张图片");
return;
}
this.isLoading = true;
//先压缩至指定的尺寸
const canvas = this.cropper.getCroppedCanvas({
width: this.realW,
height: this.realH,
fillColor: "transparent", // 设置导出图片的背景颜色为透明
imageSmoothingQuality: "high",
});
//检查图片类型
let mimeType = this.getImageMimeType();
if (mimeType == null) {
this.$message.warning("仅支持上传jpeg和png格式的图片");
this.isLoading = false;
return;
}
//转换为blob对象,上传至图片服务器
canvas.toBlob((blob) => {
//执行上传操作
this.upload(blob, this.fileName, result => {
//判断是否返回了上传后的图片链接
if (this.isBlank(result.message)) {
this.$message.warning("图片裁剪失败,请刷新页面后重试")
return;
}
//上传成功,初始化裁剪对象,并将图片地址传递给父页面
this.init();
this.isLoading = false;
if (this.urlIndex === -1) {
this.resultUrl.push(result.message);
} else {
this.resultUrl.splice(this.urlIndex, 1, result.message);
}
this.success(this.resultUrl);
});
}, mimeType, 1);
},
/**
* 获取图片类型
*/
getImageMimeType() {
// 获取文件扩展名
const fileExtension = this.fileName.split('.').pop();
let mimeType;
switch (fileExtension.toLowerCase()) {
case "jpg":
case "jpeg":
mimeType = "image/jpeg";
break;
case "png":
mimeType = "image/png";
break;
default:
mimeType = null;
}
return mimeType;
},
/**
* 判断是否为空
*/
isBlank(obj) {
if (obj instanceof Date) {
return false;
}
if (typeof obj === "number" && !isNaN(obj)) {
return false;
}
if (!obj || obj === "undefined" || obj === "null" || obj === "" || obj === {} || obj === [] || obj.length === 0) {
return true;
}
return Object.keys(obj).length < 1;
},
/**
* 上传文件
*/
upload(file, fileName = null, success) {
let formdata = new FormData();
if (fileName != null) {
formdata.append(this.name, file, fileName);
} else {
formdata.append(this.name, file);
}
axios.post(this.action, formdata, {"Content-Type": "multipart/form-data"}).then(response => {
let result = response.data;
if (result.flag !== 200) {
this.$message.warning("上传出错,原因是:" + result.message);
return;
}
if (success) {
success(result);
}
}).catch((error) => {
this.$message.warning("内部错误,原因是:" + error);
})
},
},
};
</script>
<style lang="less" scoped>
.cropper {
line-height: 12px !important;
.upload {
width: 80px;
height: 80px;
cursor: pointer;
margin: 6px 0;
}
.result-image {
width: 80px;
height: 80px;
border-radius: 2px;
border: 1px dashed #bbb;
margin: 5px;
&:first-child {
margin-left: 0 !important;
}
&:last-child {
margin-right: 0 !important;
}
}
.tip {
color: #bbb;
font-size: 12px;
margin: 5px 0;
}
.area {
width: 100%;
.image {
margin-top: 10px;
}
.buttons {
margin-top: 10px;
}
}
}
</style>