在现代 Web 应用开发中,图片裁剪功能是一个常见需求,无论是用户头像上传、商品图片处理还是社交媒体内容发布,都需要对图片进行裁剪以满足特定的展示要求。本文将介绍如何使用 Vue.js 结合 vue-cropper 插件和 Element Plus UI 库实现一个功能完善的图片裁剪组件。
功能概述
我们实现的图片裁剪组件具有以下功能:
-
支持图片上传( jpg/png 格式)
-
可视化图片裁剪界面(600x600 像素)
-
自定义裁剪框比例(可通过 props 配置)
-
图片放大 / 缩小功能
-
图片旋转功能(左右旋转)
-
裁剪后图片数据处理(转换为 File 对象)
技术栈
本组件使用了以下技术和库:
-
Vue 3.x(Composition API)
-
Element Plus(UI 组件库)
-
vue-cropper(图片裁剪插件@1.0.5版本)
组件实现解析
模板部分解析
模板部分主要包含三个核心区域:图片上传区域(因暂未对接后台接口,只测试功能,通过change事件来实现)、裁剪预览区域和操作按钮区域。
<template> <div class="w-full h-full flex-col items-center justify-center"> <!-- action="#" #在实战中替换为实际的后台接口 --> <ElUpload v-model:file-list="fileList" class="upload-demo" action="#" multiple :limit="1" accept="image/*" list-type="picture-card" :on-change="onChange" :auto-upload="false" > <el-button type="primary">Click to upload</el-button> <template #tip> <div class="el-upload__tip"> xxxxxxxxxxxxxxxxxxxxx </div> </template> </ElUpload> <div :style="{ height: '600px', width: '600px', marginBottom: '30px' }"> <VueCropper ref="cropper" :info="false" :infoTrue="options.infoTrue" :img="options.img" :autoCrop="options.autoCrop" :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox" :mode="options.cropperMode" :centerBox="options.centerBox" :enlarge="options.enlarge" :fixedNumber="options.fixedNumber" outputType="png" @realTime="realTime" /> </div> <ElRow :style="{ textAlign: 'center' }"> <ElCol :span="6"> <ElButton type="default" :icon="ZoomOut" @click="changeScale(1)"> 放大 </ElButton> </ElCol> <ElCol :span="6" ><ElButton type="default" :icon="RefreshRight" @click="rotateLeft" >左旋转</ElButton ></ElCol > <ElCol :span="6" ><ElButton type="default" :icon="RefreshLeft" @click="rotateRight" >右旋转</ElButton ></ElCol > <ElCol :span="6"> <ElButton :icon="ZoomIn" type="default" @click="changeScale(-1)"> 缩小 </ElButton> </ElCol> </ElRow> <div> <ElButton @click="handleSubmit()">提交事件</ElButton> </div> </div> </template>
逻辑部分核心功能解析
组件初始化与配置
组件使用了 Vue 3 的 Composition API,通过
reactive
和ref
来管理响应式数据:图片上传与处理
图片上传使用了 Element Plus 的
ElUpload
组件,通过onChange
事件处理图片选择,通过getImageData
函数计算裁剪框的初始尺寸:图片比例计算核心函数
getImageData
函数是整个组件的核心,它根据原图尺寸和裁剪比例计算出合适的裁剪框尺寸:裁剪结果处理
提交事件处理函数获取裁剪后的图片数据,并将其转换为 File 对象以便上传到服务器:
<script setup lang="ts"> import "vue-cropper/dist/index.css"; import { VueCropper } from "vue-cropper"; import { ref, reactive, onMounted } from "vue"; import { ElButton, ElRow, ElCol, ElUpload } from "element-plus"; import { RefreshRight, RefreshLeft, ZoomIn, ZoomOut, } from "@element-plus/icons-vue"; const previews = ref(); const cropper = ref(); const fileName = ref(); const options = reactive({ img: "", //裁剪图片的地址 autoCrop: true, //是否默认生成截图框 autoCropWidth: undefined, //默认生成截图框宽度 autoCropHeight: undefined, //默认生成截图框高度 enlarge: undefined, // 图片根据截图框输出比例倍数 fixedBox: true, //是否固定截图框大小 不允许改变 previewsCircle: false, //预览图是否是原圆形 centerBox: true, //截图框是否被限制在图片里面 fixedNumber: [1, 1], // 截图框的宽高比例 infoTrue: true, title: "修改图片", cropperMode: "contain", }); //上传图片组件数据 const fileList = ref<any>([]); interface CropperProps { action?: string; //上传接口 fixedWidthNumber?: any; // 截图框的宽高比例 fixedHeightNumber?: any; // 截图框的宽高比例 } const props = withDefaults(defineProps<CropperProps>(), { action: "后台接口", fixedWidthNumber: 1, fixedHeightNumber: 1, }); // //图片上传前回调 const onChange = (file: any, list: any) => { console.log("🚀 ~ beforeUpload ~ file:any, fileList:any:", file, list); if (file) { fileList.value[0] = file; options.img = file.url; fileName.value = file.name; const imgData: any = getImageData(600, file?.raw, [ props.fixedWidthNumber, props.fixedHeightNumber, ]); options.autoCropWidth = imgData.width; options.autoCropHeight = imgData.height; options.enlarge = imgData.ratio; options.fixedNumber = [props.fixedWidthNumber, props.fixedHeightNumber]; } }; /** * @Description: 计算原图与裁剪框显示的图片的比例,裁剪框的大小(需要传入原图的文件!!!, 裁剪弹框的高度) * 实现的功能: * 1.如果没有传截图框的比例,那么默认是1:1,那一边短那一边被当成截图框的长度。 * 2.传入截图框比例,有六种情况,处理原则为截图框比例不会改变的情况下截取原图一边。 * 三个重要核心: * 1.原图与裁剪弹框的比例。 * 2.原图宽高比例。 * 3.截图框宽高比例。 */ function getImageData(maxHeight: number, file: any, fixed: number[]) { console.log("🚀 ~ getImageData ~ file:", file); return new Promise((resolve, reject) => { // 读取文件内容 new FileReader() const reader = new FileReader(); // readAsDataURL: 方法可以将读取到的文件编码成DataURL (这里的reader.result是base64格式) reader.readAsDataURL(file); // onload:文件读取成功时触发 reader.onload = () => { // 创建一个Image对象 const image: any = new Image(); // 定义Image对象的src: image.src = reader.result; 这样做就相当于给浏览器缓存了一张图片。 image.src = reader.result; console.log("🚀 ~ returnnewPromise ~ reader.result:", reader.result); // onload:Image对象创建成功时触发 image.onload = () => { let Ratio = 1; // 获取原图的宽和高 const w = image.width; const h = image.height; // 获取原图的比例 const imgRatio = w / h; //图片长的是那一边 const imgLong = imgRatio > 1 ? "width" : "height"; // 计算出原图被缩放到裁剪框缩小的比例 const wRatio = w / maxHeight; const hRatio = h / maxHeight; // 按照功能需要,长的一边需要与裁剪弹框一样长,所以比例取长的一边 Ratio = wRatio >= hRatio ? wRatio : hRatio; // 计算截图框的比例 const fixedRatio = fixed[0] / fixed[1]; // 截图比例长的是那一边 const fixedLong = fixedRatio > 1 ? "width" : "height"; // 初始情况即没有传入裁剪弹框比例 const imgWidth = w > h ? h / Ratio : w / Ratio; let imgData = { ratio: Ratio, width: imgWidth, height: imgWidth, }; // 传入裁剪框比例的六种情况 if (imgLong === fixedLong) { if (imgLong === "width") { if (imgRatio > fixedRatio) { // 情况1 图的比例和裁剪框的比例都是宽大,但是图片比例大于截图比例。 // 情况处理:在以一边为裁剪时,图放得下裁剪框,不用换边为裁剪框的基数,以高为基数。 imgData = { ratio: Ratio, width: (h / Ratio) * fixedRatio, height: h / Ratio, }; } else { // 情况2 图的比例和裁剪框的比例都是宽大,但是图片比例小于截图比例。 // 情况处理:在以一边为裁剪时,图放不下裁剪框,换边为裁剪框的基数,以宽为基数。 imgData = { ratio: Ratio, width: w / Ratio, height: (h / Ratio) * (w / Ratio / ((h / Ratio) * fixedRatio)), }; } } else { if (imgRatio < fixedRatio) { // 情况3 图的比例和裁剪框的比例都是高大,但是图片比例小于于截图比例。(w/h 小于即大于) // 情况处理:在以一边为裁剪时,图放得下裁剪框,不用换边为裁剪框的基数,以宽为基数。 imgData = { ratio: Ratio, width: w / Ratio, height: w / Ratio / fixedRatio, }; } else { // 情况4 图的比例和裁剪框的比例都是高大,但是图片比例大于截图比例。(w/h 小于即大于) // 情况处理:在以一边为裁剪时,图放不下裁剪框,换边为裁剪框的基数,以高为基数。 imgData = { ratio: Ratio, width: (w / Ratio) * (h / Ratio / (w / Ratio / fixedRatio)), height: h / Ratio, }; } } } else { if (imgLong === "width") { // 情况5 图的比例大于1,裁剪框的比例小于1。 // 情况处理:在以一边为裁剪时,图放得下裁剪框,不用换边为裁剪框的基数,以高为基数。 imgData = { ratio: Ratio, width: (h / Ratio) * fixedRatio, height: h / Ratio, }; } else { // 情况5 图的比例小于1,裁剪框的比例大于1。 // 情况处理:在以一边为裁剪时,图放得下裁剪框,不用换边为裁剪框的基数,以宽为基数。 imgData = { ratio: Ratio, width: w / Ratio, height: w / Ratio / fixedRatio, }; } } resolve(imgData); }; image.onerror = () => { reject(new Error("Image对象创建失败")); }; }; }); } //移动框的事件 const realTime = (data: any) => { previews.value = data; }; //图片缩放 function changeScale(num: number) { num = num || 1; cropper.value.changeScale(num); } //向左旋转 function rotateLeft() { cropper.value.rotateLeft(); } //向右旋转 function rotateRight() { cropper.value.rotateRight(); } // 提交事件 async function handleSubmit() { // 提交事件 cropper.value.getCropData(async (data: any) => { //拿到裁剪后的原始数据 const file = dataURLtoFile(data, fileName.value); //调用接口--传递给后台--从后台再读取新的图片链接并赋值即可 // const result = await 后台接口({ file }); }); } // base 64 转成二进制文件流 const dataURLtoFile = (dataurl: any, filename: any) => { 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 File([u8arr], filename, { type: mime }); }; </script>
注意
getImageData中file字段传入的数据类型示例(也是提交事件中处理后的数据类型)
- 实际使用需对接接口,并组件化封装拓展,本文章仅供参考,逻辑已通!