基于vue3 封装一个图片裁切功能组件 vue-cropper.js
使用的vue-cropper.js,官方文档
https://github.com/xyxiao001/vue-cropper
附图片各格式之间的转换
https://www.cnblogs.com/huihuihero/p/17667325.html
注意:下载vue-cropper.js时,使用
yarn add vue-cropper@next下载最新版本(当前用的1.0.9),部分版本会有报错
组件代码 img-cropper.js(使用了element的对话框组件,如有需要可自行更换)
<template>
<div class="img-cropper-container">
<div class="img-cropper-content"></div>
<div class="dialog-content">
<el-dialog
v-model="cropperDialog"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div class="every-common-dialog">
<div class="commonpop-top">
<div class="poptop-title">图片裁切</div>
<div class="poptop-cancel" @click="cancelCropper">
<span class="iconfont icon-closel"></span>
</div>
</div>
<div class="commonpop-content">
<div class="cc-cropper-content">
<vue-cropper
style="width: 100%; height: 100%"
ref="cropperRef"
:img="cropperConfig.img"
:outputSize="cropperConfig.outputSize"
:outputType="cropperConfig.outputType"
:info="cropperConfig.info"
:canScale="cropperConfig.canScale"
:autoCrop="cropperConfig.autoCrop"
:autoCropWidth="cropperConfig.autoCropWidth"
:autoCropHeight="cropperConfig.autoCropHeight"
:fixed="cropperConfig.fixed"
:fixedNumber="cropperConfig.fixedNumber"
:full="cropperConfig.full"
:fixedBox="cropperConfig.fixedBox"
:canMove="cropperConfig.canMove"
:canMoveBox="cropperConfig.canMoveBox"
:original="cropperConfig.original"
:centerBox="cropperConfig.centerBox"
:high="cropperConfig.high"
:infoTrue="cropperConfig.infoTrue"
:maxImgSize="cropperConfig.maxImgSize"
:enlarge="cropperConfig.enlarge"
:mode="cropperConfig.mode"
:limitMinSize="cropperConfig.limitMinSize"
:fillColor="cropperConfig.fillColor"
></vue-cropper>
</div>
</div>
<div class="commonpop-footer">
<div class="common-cancel-btn" v-if="cropperConfig.allowOrigin" @click="clickOriginImgBtn">上传原图</div>
<div class="common-submit-btn" @click="clickCropperImgBtn">上传裁切图</div>
</div>
</div>
</el-dialog>
</div>
</div>
</template>
<script>
import { nextTick, reactive, toRefs } from 'vue';
import { showSuccessToast, showToast, showDialog, showConfirmDialog } from 'vant';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { blobToFile, fileToBase64 } from '@/utils/file/imgformat-deal';
import { randomNum } from '@/utils/data-handle/data-number';
export default {
name: 'ImgCropper',
components: {
VueCropper,
},
props: {
/**
* 裁切配置,正常通过以下方式即可调用,更多配置参考cropperConfig里的注释
* state.cropperConfig.file = fileInfo;
* state.cropperConfig.fileName = fileInfo?.name ?? '';
* state.imgCropperRef.showCropperDialog();
*/
config: {
type: Object,
required: false,
default: () => ({}),
},
},
/**
* 方法
* emit("cancelCropper") 用户取消裁切事件。无参数返回
* emit("originImgDone") 用户确认原图事件。无参数返回
* emit("cropperImgDone") 用户确认裁切图事件。参数:裁切后的图片信息(file格式)
*/
setup(props, { emit }) {
const $route = useRoute();
const $router = useRouter();
const $store = useStore();
const state = reactive({
cropperRef: null,
cropperDialog: false, //裁切弹窗
cropperConfig: {
//自定义配置
file: null, //图片文件信息,实际应用时,若此字段有数据,则会基于此file字段转换为base64后赋值于img字段
fileName: '', //文件名称,不传则在用到时采用随机数命名
allowOrigin: true, //是否允许原图上传:true, false
//vue-cropper规定配置
img: '', //裁剪图片的地址:url 地址, base64, blob
outputSize: 1, //裁剪生成图片的质量:0.1 ~ 1
outputType: 'png', //裁剪生成图片的格式:jpeg, png, webp
info: true, //裁剪框的大小信息:true, false
canScale: true, //图片是否允许滚轮缩放:true, false
autoCrop: true, //是否默认生成截图框:true, false
autoCropWidth: '560', //默认生成截图框宽度:默认容器的80%,可选值0 ~ max
autoCropHeight: '315', //默认生成截图框高度:默认容器的80%,可选值0 ~ max
fixed: false, //是否开启截图框宽高固定比例:true, false;开启后,若配置了autoCropWidth,autoCropHeight,截图框一个边会以其中的较长边为准,另一个边根据比例自适应
fixedNumber: [1, 1], //截图框的宽高比例:[ 宽度 , 高度 ]
full: false, //是否输出原图比例的截图:true, false
fixedBox: false, //固定截图框大小: true, false
canMove: true, //上传图片是否可以移动:true, false
canMoveBox: true, //截图框能否拖动:true, false
original: false, //上传图片按照原始比例渲染:true, false
centerBox: true, //截图框是否被限制在图片里面:true, false
high: true, //是否按照设备的dpr 输出等比例图片:true, false
infoTrue: false, //true为展示真实输出图片宽高 false为展示看到的截图框宽高:true, false
maxImgSize: 2000, //限制图片最大宽度和高度:0 ~ max
enlarge: 1, //图片根据截图框输出比例倍数:0 ~ max(不要太大不然会卡死)
mode: 'contain', //图片默认渲染方式:contain , cover, 100px, 100% auto
limitMinSize: 10, //裁剪框限制最小区域:Number, Array, String
fillColor: '', //导出时背景颜色填充:#ffffff, white
},
});
//弹出裁切弹窗
function showCropperDialog() {
Object.assign(state.cropperConfig, props.config);
if (state.cropperConfig.file) {
fileToBase64(state.cropperConfig.file).then((res) => {
state.cropperConfig.img = res.data;
nextTick(() => {
state.cropperDialog = true;
});
});
} else if (state.cropperConfig.img) {
nextTick(() => {
state.cropperDialog = true;
});
} else {
showToast('图片地址项 cropperConfig.img 未配置!');
}
}
//关闭裁切弹窗
function hideCropperDialog() {
state.cropperDialog = false;
}
//用户主动取消裁切
function cancelCropper() {
emit('cancelCropper');
hideCropperDialog();
}
//用户确认原图
function clickOriginImgBtn() {
emit('originImgDone');
hideCropperDialog();
}
//用户确认裁切图
function clickCropperImgBtn() {
let name = state.cropperConfig?.fileName ?? randomNum(10000, 1000000);
state.cropperRef.getCropBlob((data) => {
blobToFile(data, name).then((res) => {
emit('cropperImgDone', res.data);
hideCropperDialog();
});
});
}
return {
showCropperDialog,
cancelCropper,
clickOriginImgBtn,
clickCropperImgBtn,
...toRefs(state),
};
},
};
</script>
<style scoped lang="less">
.img-cropper-container {
.img-cropper-content {
}
.dialog-content {
:deep(.el-dialog) {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
}
background-color: transparent;
}
.every-common-dialog {
background-color: #fff;
border-radius: 4px;
overflow: hidden;
.commonpop-top {
display: flex;
border-bottom: 1px solid #eee;
.poptop-title {
flex: 1;
height: 50px;
line-height: 50px;
text-align: center;
margin-left: 50px;
font-size: 18px;
color: #333;
}
.poptop-cancel {
flex-shrink: 0;
width: 50px;
height: 50px;
line-height: 50px;
text-align: center;
color: #999;
cursor: pointer;
transition: all 0.2s;
.iconfont {
font-size: 20px;
}
&:hover {
background-color: #f5f5f5;
color: @tc-main;
}
}
}
.commonpop-content {
padding: 20px 20px;
box-sizing: border-box;
.cc-cropper-content {
width: 100%;
height: 315px;
}
}
.commonpop-footer {
display: flex;
padding: 0 20px 20px;
box-sizing: border-box;
.common-cancel-btn,
.common-submit-btn {
flex: 1;
height: 50px;
line-height: 50px;
text-align: center;
font-size: 16px;
border-radius: 4px;
&:nth-child(n + 2) {
margin-left: 20px;
}
}
.common-cancel-btn {
background-color: #e7efff;
color: @tc-main;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
}
}
.common-submit-btn {
color: #fff;
background-color: @tc-main;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
}
}
}
}
}
}
</style>
如何调用裁切组件
<img-cropper
ref="imgCropperRef"
:config="cropperConfig"
@originImgDone="originImgDone"
@cropperImgDone="cropperImgDone"
@cancelCropper="cancelCropper"
/>
const state = reactive({
nowFileInfo: null, //当前选中的图片文件信息
//图片裁切相关
imgCropperRef: null, //图片裁切实例
cropperConfig: { //图片裁切配置
file: '',
fileName: '',
fixed: true,
fixedNumber: [16, 9],
},
})
//调用裁切组件
function handleCropper(fileInfo){
state.nowFileInfo = fileInfo;
state.cropperConfig.file = fileInfo; //将读取到的file信息传递给裁切组件
state.cropperConfig.fileName = fileInfo?.name ?? ''; //为裁切组件图片命名
state.imgCropperRef.showCropperDialog(); //父组件中获取裁切组件实例,并调用裁切组件的弹窗方法
}
//用户确认原图上传事件
function originImgDone() {
state.isUploading = true;
uploadFile(state.nowFileInfo); //调用接口将图片上传至服务器
}
//用户确认裁切图上传事件
function cropperImgDone(fileInfo) {
state.nowFileInfo = fileInfo;
state.isUploading = true;
uploadFile(state.nowFileInfo); //调用接口将图片上传至服务器
}
//用户取消裁切事件
function cancelCropper() {
state.isUploading = false;
}