vue3 使用cropperjs

1. 组件概述
该组件是一个图片裁剪器组件,用于在Vue 3项目中集成。它基于cropperjs库实现,提供了丰富的图片裁剪功能,如缩放、移动、旋转、翻转等。用户可以通过该组件对图片进行裁剪,并将裁剪后的图片传递给父组件进行进一步处理。 结合了饿了吗的组件开发提供源码都有注释 方便修改

下载cropperjs

npm i cropperjs @1.6.2

功能图片

 

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 class="cropperWrap">
    <el-dialog v-model="dialogVisible" title="剪裁图片" width="50%">
      <!-- 裁剪框 -->
      <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="less" scoped>
.cropperWrap {
  :deep(.el-dialog) {
    display: flex;
    flex-direction: column;
    margin: 0 !important;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-height: calc(100% - 30px);
    max-width: calc(100% - 30px);
  }
  .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 class="personInfo w1520 mt40">
    <div class="avater flex items-center">
      <div class="lable">上传头像</div>
      <img class="ml40 img" :src="data.userInfo.avter" alt="" />
      <el-upload v-model:file-list="fileList" ref="imageUpload" action="" list-type="picture" :show-file-list="false" multiple :limit="1" :auto-upload="false" :on-change="handleChange">
        <div class="upLoadBtn ml30 center">上传头像</div>
      </el-upload>
      <cropperImg :corImg="imgSrc" :aspectRatio="aspectRatio" :imgFormat="imgFormat" @cropImage="cropImage" @closeImage="closeImage" />
    </div>
    <div class="nickName flex items-center mt40 mb20">
      <div class="lable">昵称</div>
      <el-input class="w578 ml40" maxlength="10" v-model="data.userInfo.nickName" placeholder="请输入昵称"></el-input>
    </div>
    <div class="blurb flex items-center mb20">
      <div class="lable">个人介绍</div>
      <el-input class="w578 ml40" maxlength="200" type="textarea" resize="none" v-model="data.userInfo.blurb" placeholder="请输入您的个人介绍"></el-input>
    </div>

    <div class="submit pointer" @click="submit">提交</div>
  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
import { queryForUserByPage } from "@/api/home";
import { useRoute, useRouter } from "vue-router";

import cropperImg from "@/components/cropperImg/index.vue";

import productImg from "@/assets/img/product/productImg.png";
const route = useRoute();
const router = useRouter();

const data = reactive({
  userInfo: {
    avter: productImg,
    nickName: "",
    blurb: "",
  },
});

const submit = () => {
  console.log("userInfo", data.userInfo);
};


// 文件裁剪开始
const fileList = ref([]);
// 剪裁图片地址
const imgSrc = ref("");
// 裁剪框的比例
const aspectRatio = ref("1:1"); // 裁剪框的比例
// 图片格式 支持
const imgFormat = ref("png,jpeg"); // 图片格式
const imageUpload = ref(null);
const handleChange = (uploadFile) => {
  if (uploadFile.percentage == 0) {
    imgSrc.value = uploadFile;
  }
};

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();
  console.log("文件:", file);
 
  data.userInfo.avter = file.url

  // 调用删除是为了更换头像使用,否者无法更换新的
  closeImage(file)
};

// 关闭剪裁组件
const closeImage = (env) => {
  // 删除文件列表中对应的元素
  fileList.value = fileList.value.filter((item) => item.uid !== env.uid);
};
// 文件裁剪结束
</script>
<style scoped lang="less">
.personInfo {
  font-weight: bold;
  color: #000000;
  background: #ffffff;
  box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.05);
  padding: 40px 421px;
  height: 732px;
  .lable {
    width: 60px;
    font-size: 14px;
    text-align: right;
  }
  .avater {
    padding-top: 61px;
    .img {
      width: 74px;
      height: 74px;
      border-radius: 50%;
    }
    .upLoadBtn {
      width: 84px;
      height: 31px;
      line-height: 31px;
      border-radius: 4px;
      border: 1px solid #58705b;
      font-weight: bold;
      font-size: 14px;
      color: #58705b;
    }
  }
  .w578 {
    width: 578px;
  }

  .nickName {
    :deep(.el-input__inner) {
      height: 48px;
      color: #181818;
    }
    :deep(.el-input__inner::placeholder) {
      font-size: 14px;
      color: #757875;
    }
  }
  .blurb {
    align-items: baseline;
    :deep(.el-textarea__inner) {
      height: 100px;
      color: #181818;
    }
    :deep(.el-textarea__inner::placeholder) {
      font-size: 14px;
      color: #757875;
    }
  }

  .submit {
    width: 120px;
    height: 40px;
    line-height: 40px;
    background: #58705b;
    font-weight: bold;
    font-size: 14px;
    color: #ffffff;
    text-align: center;
    margin-left: 100px;
  }
}
</style>

 

<template>
  <div class="cropperWrap">
    <el-dialog v-model="dialogVisible" title="剪裁图片" width="50%">
      <!-- 裁剪框 -->
      <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="less" scoped>
.cropperWrap {
  :deep(.el-dialog) {
    display: flex;
    flex-direction: column;
    margin: 0 !important;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-height: calc(100% - 30px);
    max-width: calc(100% - 30px);
  }
  .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>
posted @ 2025-07-21 16:21  小菜波  阅读(186)  评论(0)    收藏  举报