Vue 实现图片裁剪功能:基于 vue-cropper 和 Element Plus 的完整解决方案

在现代 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,通过reactiveref来管理响应式数据:

    图片上传与处理

    图片上传使用了 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字段传入的数据类型示例(也是提交事件中处理后的数据类型)

  • 实际使用需对接接口,并组件化封装拓展,本文章仅供参考,逻辑已通!
VueCropper是一个轻量级的基于Vue.js图片裁剪组件,它允许你在前端图片进行裁剪操作。虽然VueCropper本身不是一个专门用于创建圆形裁剪框的库,但它通常通过结合CSS样式JavaScript逻辑来实现自定义裁剪效果,包括圆形。 你可以通过以下步骤让VueCropper支持圆形裁剪: 1. 设置cropper容器的`viewBox`属性为"0 0 circle",使其成为一个圆形视图。 2. 使用CSS调整`border-radius`属性,使其看起来像一个圆形边框。 3. 然后在开始裁剪之前,获取canvas或画布元素,将其宽高比强制设置为1:1,并只绘制圆的部分。 下面是一个简单的例子: ```html <template> <div ref="cropper" :style="{&viewBox: '0 0 circle', width: '100%', height: '100%' }"> <img :src="imageSrc" :ref="canvasRef" /> </div> </template> <script> import Vue from 'vue'; import VueCropper from 'vue-cropper'; export default { components: { VueCropper }, data() { return { imageSrc: 'your-image-url', canvasRef: 'cropperCanvas' }; }, mounted() { this.$refs.cropper.getCropRect().then((rect) => { const ratio = rect.width / rect.height; if (ratio !== 1) { // 可能需要调整裁剪区域大小或比例,这里仅作示例 this.$refs.canvas.style.transform = `scale(${Math.min(1, 1 / ratio)})`; } }); } }; </script> ``` 请注意,这只是一个基本示例,实际应用可能需要更复杂的计算来适应圆形裁剪的需求。如果你希望得到完美的圆形裁剪功能,可能需要借助第三方库,如`vue-circular-cropper`这样的专门为圆形裁剪设计的插件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值