效果图:
参考文档:
Vue-Cropper 文档Vue-Cropper 文档
安装VueCropper
//npm安装
npm install vue-cropper@next -d --save//yarn安装
yarn add vue-cropper@next
引入组件
在main.ts中全局注册:
import VueCropper from 'vue-cropper';
import 'vue-cropper/dist/index.css';const app = createApp(App);
app.use(VueCropper);
app.mount('#app');
局部:
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
代码层:
页面(父组件)
<template><div class="p-4"><imgv-if="photo":src="photo"class="avatar"@click="handleClick"/><el-icon v-else class="avatar-uploader-icon" @click="handleClick"><Plus/></el-icon><ImageCropperv-model="visible"@onSuccess="onSuccess":image="photo"></ImageCropper></div>
</template><script setup lang="ts">
import ImageCropper from "@/components/imageCropper.vue";//裁剪图片组件
import { uploadPicturesApi } from "@/api/common/index";
import { ElMessage } from "element-plus";
import { ref } from "vue";
const visible = ref<boolean>(false); //上传图片弹框
const photo=ref<string>('')
// 打开弹框
const handleClick = (img: string) => {console.log(img);visible.value = true;
};const handleClickImgUrl = () => {console.log(formInfo.value);
};/*** 上传图片成功* @param data File*/
const onSuccess = async (data: File) => {console.log(data);try {
//接口上传为formDataconst res = await uploadPicturesApi(data);if (res.code == 200) {formInfo.value.photo = res.data;//接口返回图片地址ElMessage.success("上传成功");visible.value = false;}console.log(res);} catch (error) {ElMessage.error("上传失败");}
};
</script><style lang="scss" scoped>
.avatar {width: 160px;height: 160px;display: block;/* border: 1px dashed #8c939d; */border-radius: 6px;
}.el-icon.avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 160px;height: 160px;text-align: center;border: 1px dashed #8c939d;border-radius: 6px;
}
</style>
封装剪切图片组件(子组件):
<template><el-dialogappend-to-body:close-on-click-modal="false"v-model="visible"title="图片裁剪"width="800px"@close="handleClose"><div class="cropper-cut"><div class="picUpload-wraper"><div class="pic-left-wraper"><VueCropperref="cropperRef":img="option.img":outputType="option.outputType":info="true":full="option.full":canMove="option.canMove":canScale="option.canScale":canMoveBox="option.canMoveBox":fixed="option.fixed":fixedBox="option.fixedBox":original="option.original":autoCrop="option.autoCrop":autoCropWidth="cropWith || option.autoCropWidth":autoCropHeight="cropHeight || option.autoCropHeight":centerBox="option.centerBox":high="option.high":infoTrue="option.infoTrue":enlarge="option.enlarge"@realTime="realTime"></VueCropper></div><div class="pic-right-wraper"><div class="title">图片预览</div><div :style="previewStyle" class="previewImg" v-show="option.img"><div :style="previews.div"><img :src="previews.url" :style="previews.img" /></div></div></div></div><div class="pic-operate"><inputstyle="display: none"@change="handleChange"ref="fileRef"type="file"name=""id=""/><el-button @click="fileRef?.click()">选择图片</el-button><el-button @click="changeScale(1)" :icon="Plus"> </el-button><el-button @click="changeScale(-1)" :icon="Minus"> </el-button><el-button @click="rotateRight()" :icon="RefreshRight"> </el-button></div></div><template #footer><div class="dialog-footer"><el-button @click="$emit('update:modelValue', false)">取消</el-button><el-button @click="saveHeadPic">保存</el-button></div></template></el-dialog>
</template><script setup lang="ts">
import { reactive, ref, watch } from "vue";
import { VueCropper } from "vue-cropper";
import "vue-cropper/dist/index.css";
import { Plus, Minus, RefreshRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";const props = defineProps({// 窗口modelValue: {type: Boolean,default: false,},// 大小limitSize: {type: Number,default: 100,},// 宽cropWith: {type: String,default: "350px",},// 高cropHeight: {type: String,default: "350px",},accept: {type: Array<String>,default: () => [],},// 校验regExp: {type: RegExp,default: /\.(jpg|jpeg|png)$/i,},
});const emit = defineEmits(["update:modelValue", "onSuccess"]);const visible = ref<boolean>(false);const option = reactive({img: "",outputSize: 1, // 裁剪生成图片的质量 1 0.1 - 1outputType: "png", // 裁剪生成图片的格式 jpg (jpg 需要传入jpeg) jpeg || png || webpinfo: true, // 裁剪框的大小信息 true true || falsecanScale: true, // 图片是否允许滚轮缩放 true true || falseautoCrop: true, //是否默认生成截图框 false true || falseautoCropWidth: 240, //默认生成截图框宽度 容器的80% 0~maxautoCropHeight: 300, //默认生成截图框高度 容器的80% 0~maxfixed: false, //是否开启截图框宽高固定比例 true true | false// fixedNumber: [1, 1], //截图框的宽高比例 [1, 1] [宽度, 高度]full: false, //是否输出原图比例的截图 false true | falsefixedBox: true, //固定截图框大小 不允许改变 false true | falsecanMove: false, //上传图片是否可以移动 true true | falsecanMoveBox: true, //截图框能否拖动 true true | falseoriginal: false, //上传图片按照原始比例渲染 false true | falsecenterBox: false, //截图框是否被限制在图片里面 false true | falsehigh: false, //是否按照设备的dpr 输出等比例图片 true true | falseinfoTrue: false, //true 为展示真实输出图片宽高 false 展示看到的截图框宽高 false true | falsemaxImgSize: 2000, //限制图片最大宽度和高度 2000 0-maxenlarge: 1, //图片根据截图框输出比例倍数 1 0-max(建议不要太大不然会卡死的呢)mode: "contain", //图片默认渲染方式 contain contain , cover, 100px, 100% auto
});const previewStyle = ref<any>({});
const previews = ref<any>({});const fileRef = ref<HTMLInputElement>();
const cropperRef = ref<typeof VueCropper>();
let _file: File | null = null;watch(() => props.modelValue,(val) => {visible.value = val;}
);/*** 选择图片* @param e*/
const handleChange = (e: Event) => {const _target = e.target as HTMLInputElement;// console.log(_target)if (_target.files && _target.files.length > 0) {_file = _target.files[0];if (_file.size / 1024 / 1024 > props.limitSize) {ElMessage.warning(`图片大小不能超过${props.limitSize}MB`);return;}// const type = _file.name.split('.')[1]if (!props.regExp.test(_file.name)) {ElMessage.warning(`请上传jpg、jpeg、png格式图片`);return;}const reader = new FileReader();reader.readAsDataURL(_file);reader.onload = (e) => {option.img = e.target?.result as string;};}
};
/*** 实时预览事件* @param data*/
const realTime = (data: any) => {previews.value = data;previewStyle.value = {width: data.w + "px",height: data.h + "px",overflow: "hidden",margin: "0",zoom: 160 / data.w,};
};
/*** 放大缩小* @param num*/
const changeScale = (num: number) => {num = num || 1;cropperRef.value?.changeScale(num);
};
/*** 旋转图片*/
const rotateRight = () => {cropperRef.value?.rotateRight();
};/*** 取消清空*/
const handleClose = () => {// console.log('close')option.img = "";fileRef.value && (fileRef.value.value = "");previews.value = {};previewStyle.value = {};cropperRef.value?.clearCrop();_file = null;emit("update:modelValue", false);
};/***上传保存图片*/
const saveHeadPic = () => {if (!option.img) return ElMessage.warning("请先上传图片");cropperRef.value?.getCropBlob((blob: Blob) => {// blob 转 fileif (_file) {const file = new File([blob], _file.name, { type: _file.type });emit("onSuccess", file);} else {ElMessage.error("获取图片失败");}});
};
</script><style lang="scss" scoped>
.picUpload-wraper {height: fit-content;display: flex;justify-content: space-between;.pic-left-wraper {width: 540px;height: 360px;border: 1px solid #ddd;}.pic-right-wraper {padding: 20px;width: 200px;max-height: 360px;border: 1px solid #ddd;background: #fafafa;.title {margin-bottom: 10px;font-size: 14px;font-weight: 600;}.previewImg {max-height: 360px !important;overflow: hidden;}}
}
.pic-operate {margin-top: 10px;
}
</style>