1. 组件概述
该组件是一个图片裁剪器组件,用于在Vue 3项目中集成。它基于cropperjs
库实现,提供了丰富的图片裁剪功能,如缩放、移动、旋转、翻转等。用户可以通过该组件对图片进行裁剪,并将裁剪后的图片传递给父组件进行进一步处理。 结合了饿了吗的组件开发提供源码都有注释 方便修改
下载cropperjs
注意:我引入最新的版本时 里面没有 cropper.css文件
npm i cropperjs @1.6.1
功能图片
2. 组件功能
-
缩放:支持通过按钮对图片进行放大和缩小。
-
移动:支持通过按钮对图片进行上下左右移动。
-
旋转:支持通过按钮和滑块对图片进行任意角度的旋转。
-
翻转:支持对图片进行水平和垂直翻转。
-
裁剪框:用户可以通过拖动和调整裁剪框来选择需要裁剪的图片区域。
3. 组件属性
3.1 corImg
-
类型:
[Object, String]
-
默认值:
{}
-
描述:用于接收父组件传递的图片数据,可以是图片的URL地址或包含图片URL和其他信息的对象。
3.2 aspectRatio
-
类型:
String
-
默认值:
''
-
描述:用于设置裁剪框的宽高比,例如
16:9
。如果设置为空字符串,则表示裁剪框的宽高比自由。
3.3 imgFormat
-
类型:
String
-
默认值:
'jpeg'
-
描述:用于设置裁剪后图片的格式,支持JPEG、PNG、WEBP等多种格式。
4. 组件事件
4.1 cropImage
- 描述:当用户完成图片裁剪后触发,携带裁剪后的图片数据(包括图片名称、大小、原始Blob对象、URL地址等)。
4.2 closeImage
- 描述:当用户关闭裁剪器对话框时触发,携带原始图片数据
<template>
<div>
<el-dialog v-model="dialogVisible" title="剪裁图片" width="1000">
<!-- 裁剪框 -->
<div class="cropper">
<img ref="cropimg" :src="imgSrc" alt />
</div>
<div class="cutter-tool">
<el-button
icon="ZoomIn"
class="tool-but"
style="border-radius: 5px 0 0 5px !important; border-right: none"
@click="scale(1)" />
<el-button icon="ZoomOut" class="tool-but" @click="scale(-1)" />
<el-button icon="ArrowLeft" class="tool-but" @click="moveImg(0)" />
<el-button icon="ArrowRight" class="tool-but" @click="moveImg(1)" />
<el-button icon="ArrowUp" class="tool-but" @click="moveImg(2)" />
<el-button icon="ArrowDown" class="tool-but" @click="moveImg(3)" />
<el-button icon="Sort" class="tool-but rotate" @click="flipHorizontal" />
<el-button icon="Sort" class="tool-but" @click="flipVertically" />
<el-button icon="RefreshRight" class="tool-but" @click="turnImg(90)" />
<el-button
icon="RefreshLeft"
style="border-radius: 0 !important; border-radius: 0 5px 5px 0 !important; margin-left: 0"
@click="turnImg(-90)" />
</div>
<div style="width: calc(45px * 10); margin: 20px auto">
<el-slider
v-model="rotationAngle"
show-input
:min="-180"
:max="180"
:marks="marks"
@input="sliderInput" />
</div>
<template #footer>
<div class="dialog-footer">
<el-tag type="primary">原图 {{ quality }}</el-tag>
<div style="display: flex">
<div style="display: flex; align-items: center">
<span style="font-weight: bold">品质</span>
<div style="cursor: pointer; margin: 0 5px; display: flex; align-items: center">
<el-tooltip
class="box-item"
effect="dark"
content="调低该值可缩小图片体积 调高该值可提升清晰度"
placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</div>
<div style="width: 100px; margin: 0 20px">
<el-slider v-model="imageQuality" :min="0" :max="10" :format-tooltip="formatTooltip" />
</div>
</div>
<el-button type="info" @click="reset">重置</el-button>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="cropImage"> 确定 </el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, nextTick, watch } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
//给父组件传递方法
const emit = defineEmits(['cropImage', 'closeImage']);
const props = defineProps({
corImg: {
type: [Object, String],
default: {},
},
// 裁剪框的宽高比
aspectRatio: {
type: String,
default: '',
},
// 图片格式
imgFormat: {
type: String,
default: 'jpeg',
},
});
const imgSrc = ref('');
const aspectRatio = ref(NaN);
const imgFormat = ref('image/png');
watch(
() => props.aspectRatio,
(newVal) => {
if (newVal == '') {
aspectRatio.value = NaN;
} else {
aspectRatio.value = Number(newVal.split(':')[0]) / Number(newVal.split(':')[1]);
}
console.log(aspectRatio.value);
},
{
immediate: true,
deep: true,
},
);
const imgFormatMap = {
jpeg: 'image/jpeg',
png: 'image/png',
webp: 'image/webp',
'': 'image/jpeg', // 默认值
};
watch(
() => props.imgFormat,
(newVal) => {
imgFormat.value = imgFormatMap[newVal] || 'image/jpeg';
},
{
immediate: true,
deep: true,
},
);
// 裁剪框是否显示
const dialogVisible = ref(false);
// 图片元素引用
const cropimg = ref(null);
// 裁剪实例引用
const cropper = ref(null);
const rotationAngle = ref(0);
const quality = ref('');
// 压缩图片的品质值
const imageQuality = ref(10);
const marks = ref({
'-180': '-180°',
0: '0°',
180: '180°',
});
/**
* 将传入的值除以10并返回结果
*
* @param val 需要被除的值
* @returns 返回除以10后的结果
*/
const formatTooltip = (val) => {
return val / 10;
};
/**
* 关闭对话框
*
* @description 关闭对话框,并将对话框的显示状态设置为false,同时触发'closeImage'事件并传递null作为参数
*/
const close = () => {
dialogVisible.value = false;
emit('closeImage', props.corImg);
};
watch(
() => props.corImg,
(newVal) => {
if (newVal) {
dialogVisible.value = true;
setTimeout(() => {
// 判断newVal是字符串还是对象,如果是字符串则直接赋值给imgSrc.value
if (typeof newVal === 'string' && cropper.value) {
cropper.value.replace(newVal);
} else {
if (cropper.value && newVal.url) {
cropper.value.replace(newVal.url);
getQuality(newVal.size);
}
}
}, 500);
}
},
{
immediate: true,
},
);
watch(
() => dialogVisible.value,
(newVal) => {
if (!newVal) {
cropper.value?.destroy();
imageQuality.value = 10;
rotationAngle.value = 0;
} else {
nextTick(() => {
getCropper();
});
}
},
{
immediate: true,
},
);
/**
* 根据文件大小计算文件质量,并以合适的单位显示
*
* @param {number} size - 文件大小(以字节为单位)
* @returns {void}
*/
const getQuality = (size) => {
quality.value = size / 1024;
if (quality.value >= 1024) {
// 超过1M
quality.value = (quality.value / 1024).toFixed(2) + 'M';
} else if (quality.value >= 1) {
// 1K到1M之间
quality.value = quality.value.toFixed(2) + 'K';
} else {
// 小于1K
quality.value = (quality.value * 1024).toFixed(2) + 'B'; // 可以选择转换为字节,或者保持小数形式
}
};
/**
* 获取裁剪器实例
*
* @returns 裁剪器实例
*/
const getCropper = () => {
cropper.value = new Cropper(cropimg.value, {
/*
定义裁剪器的拖动模式
move: 移动图片
crop: 创建一个新的裁剪框
none: 什么也不做
*/
dragMode: 'move',
aspectRatio: aspectRatio.value, // 设置裁剪框的宽高比。默认值是 NaN,意味着比例自由
initialAspectRatio: NaN, // 定义裁剪框的初始宽高比。默认和图片容器的宽高比相同。只有在 aspectRatio 的值为 NaN时才可以设置
data: null, // 之前存储的裁剪后的数据,将在初始化时传递给setData方法 只有在 autoCrop 的值为 true时可用
preview: '', // 添加额外的元素(容器)以供预览
responsive: true, // 在窗口大小变化后,重新渲染裁剪器
restore: false, // 在窗口大小变化后,恢复被裁剪的区域
checkCrossOrigin: false, // 检查当前图片是否为跨域图片
checkOrientation: true, // 检查当前图片的 EXIF 信息,并根据相关信息旋转图片
modal: true, // 是否在图片和裁剪框之间显示黑色蒙版。
guides: true, // 是否显示裁剪框的辅助线
center: true, // 是否显示裁剪框中心的指示器。
highlight: true, // 是否显示裁剪框上面的白色蒙版(突出显示裁剪框)。
background: true, // 是否显示裁剪框的背景。
autoCrop: true, // 是否在初始化时自动裁剪图片。默认值为 true
autoCropArea: 1, // 设置裁剪框的初始大小。默认值为 auto,即自动调整为图片的 80%
movable: true, // 是否可以移动图片。默认为 true,即允许移动
rotatable: true, // 是否可以旋转图片。默认为 true,即允许旋转
scalable: true, // 是否可以缩放图片(以图片中心点为原点进行缩放)。
zoomable: true, // 是否可以缩放图片(以图片左上角为原点进行缩放)。
zoomOnTouch: true, // 是否允许通过拖动来缩放图片。
zoomOnWheel: true, // 是否可以通过鼠标滚轮来缩放图片。默认为 true,即允许通过鼠标滚轮进行缩放
wheelZoomRatio: 0.1, // 鼠标滚轮缩放图片的比例。默认为 0.1,即每次滚动时缩小或放大 10%
cropBoxMovable: true, // 是否可以移动裁剪框。默认为 true,即允许移动
cropBoxResizable: true, // 是否可以改变裁剪框的大小。默认为 true,即允许改变大小
toggleDragModeOnDblclick: false, // 当点击两次时可以在“crop”和“move”之间切换拖拽模式 需要浏览器支持 dblclick 事件。
/*
裁剪框的视图模式:
0 无限制
1 限制裁剪框不能超出图片的范围
2 限制裁剪框不能超出图片的范围且图片填充模式为 cover 最长边填充
3 限制裁剪框不能超出图片的范围 且图片填充模式为 contain 最短边填充
*/
viewMode: 0,
minContainerWidth: 200, // 设置裁剪器容器的最小宽度。默认为 200px
minContainerHeight: 100, // 设置裁剪器容器的最小高度。默认为 150px
minCanvasWidth: 0, // 设置图片容器的最小宽度。默认为 0,即不限制
minCanvasHeight: 0, // 设置图片容器的最小高度。默认为 0,即不限制
minCropBoxWidth: 0, // 设置裁剪框的最小宽度。默认为 0 注: 这个尺寸是相对于页面的,而不是图片。
minCropBoxHeight: 0, // 设置裁剪框的最小高度。默认为 0 注: 这个尺寸是相对于页面的,而不是图片。
ready: ready(), // ready 事件的快捷写法。 在初始化完成后触发。
cropstart: cropstart(), // cropstart 事件的快捷写法。 在开始裁剪时触发。
cropmove: null, // cropmove 事件的快捷写法。 在裁剪时触发。
cropend: null, // cropend 事件的快捷写法。 在结束裁剪时触发。
crop: null, // crop 事件的快捷写法。 在完成裁剪后触发。
zoom: null, // zoom 事件的快捷写法。 当图片缩放时触发。
});
};
const ready = (env) => {
// console.log('初始化', env);
};
const cropstart = (env) => {
// console.log('开始裁剪', env);
};
/**
* 根据传入的参数调整图片缩放比例
*
* @param env 缩放方向参数,大于0表示放大,小于等于0表示缩小
*/
const scale = (env) => {
if (env > 0) {
// 放大图片
cropper.value.zoom(0.1);
} else {
// 缩小图片
cropper.value.zoom(-0.1);
}
};
/**
* 移动图片位置
*
* @param env 环境变量,决定图片移动的方向
* 0: 向左移动
* 1: 向右移动
* 2: 向上移动
* 3: 向下移动
*/
const moveImg = (env) => {
if (env == 0) {
// 向左移动图片
cropper.value.move(-10, 0);
} else if (env == 1) {
// 向右移动图片
cropper.value.move(10, 0);
} else if (env == 2) {
// 向上移动图片
cropper.value.move(0, -10);
} else if (env == 3) {
// 向下移动图片
cropper.value.move(0, 10);
}
};
const scaleX = ref(1); // 水平翻转的缩放值
const scaleY = ref(1); // 垂直翻转的缩放值
/**
* 水平翻转图片
*/
const flipHorizontal = () => {
if (scaleX.value == 1) {
scaleX.value = -1;
} else {
scaleX.value = 1;
}
cropper.value.scale(scaleX.value, scaleY.value);
};
/**
* 垂直翻转函数
*
*/
const flipVertically = () => {
if (scaleY.value == 1) {
scaleY.value = -1;
} else {
scaleY.value = 1;
}
cropper.value.scale(scaleX.value, scaleY.value);
};
/**
* 根据环境变量旋转图片
*
* @param {Object} env - 包含旋转角度的环境变量对象
* @returns {void}
*/
const turnImg = (env) => {
cropper.value.rotate(env);
};
const sliderInput = (val) => {
cropper.value.rotateTo(Number(val));
};
const reset = () => {
cropper.value.reset();
};
const cropImage = () => {
const canvas = cropper.value.getCroppedCanvas(); // 获取裁剪后的canvas元素
// 减少图片体积,压缩图片 0-1之间,1为原始大小
canvas.toBlob(
(blob) => {
const url = URL.createObjectURL(blob); // 创建临时url地址
const file = new File([blob], props.corImg.name || 'croppedImage.jpg', {
type: blob.type,
lastModified: Date.now(),
});
const data = {
name: file.name,
size: file.size,
raw: file,
url: url,
uid: props.corImg.uid,
};
dialogVisible.value = false;
emit('cropImage', data);
},
imgFormat.value,
imageQuality.value / 10, // 动态调整压缩质量
);
};
/**
* 将Data URL转换为Blob对象
*
* @param dataurl Data URL字符串
* @returns 返回转换后的Blob对象
*/
const dataURLtoBlob = (dataurl) => {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
};
</script>
<style lang="scss" scoped>
.cropper {
width: 100%;
height: 40vh;
img {
max-height: 100%;
}
}
::v-deep .i-dialog-footer {
display: none !important;
}
.cutter-tool {
display: flex;
justify-content: center;
margin-top: 30px;
.tool-but {
margin-left: 0;
border-radius: 0 !important;
border-right: none;
}
::v-deep .rotate {
.el-icon {
transform: rotate(90deg);
}
}
}
.dialog-footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
5. 使用示例
<template>
<div>
<div>
<h2>图片剪裁</h2>
<cropperImg
:corImg="imgSrc"
:aspectRatio="aspectRatio"
:imgFormat="imgFormat"
@cropImage="cropImage"
@closeImage="closeImage" />
<el-upload
v-model:file-list="fileList"
ref="imageUpload"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
list-type="picture-card"
:auto-upload="false"
:on-change="handleChange">
<el-icon><Plus /></el-icon>
</el-upload>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import cropperImg from '../components/cropperImg/index.vue';
import { Plus } from '@element-plus/icons-vue';
const fileList = ref([]);
// 剪裁图片地址
const imgSrc = ref('');
// 裁剪框的比例
const aspectRatio = ref(''); // 裁剪框的比例
// 图片格式 支持
const imgFormat = ref('png'); // 图片格式
const imageUpload = ref(null);
const handleChange = (uploadFile) => {
if (uploadFile.percentage == 0) {
imgSrc.value = uploadFile;
}
};
const quality = ref('');
const cropImage = (res) => {
const file = fileList.value.find((file) => file.uid === res.uid);
if (file) {
file.url = res.url;
res.raw.uid = res.uid;
file.raw = res.raw;
}
// imageUpload.value.submit();
getQuality(file.size);
};
const getQuality = (size) => {
quality.value = size / 1024;
if (quality.value >= 1024) {
// 超过1M
quality.value = (quality.value / 1024).toFixed(2) + 'M';
} else if (quality.value >= 1) {
// 1K到1M之间
quality.value = quality.value.toFixed(2) + 'K';
} else {
// 小于1K
quality.value = (quality.value * 1024).toFixed(2) + 'B'; // 可以选择转换为字节,或者保持小数形式
}
console.log(quality.value, '======');
};
// 关闭剪裁组件
const closeImage = (env) => {
// 删除文件列表中对应的元素
fileList.value = fileList.value.filter((item) => item.uid !== env.uid);
};
</script>