web技术分享| 图片上传与图片裁剪结合 vue3

时间:2022-12-07 19:01:39

需求:

  • 上传的图片限制长宽相同;

  • 只能上传图片;

  • 图片大小限制 500k

  • 当前项目仅需要上传的图片信息

项目组件使用

裁剪:vue-cropper

import "vue-cropper/dist/index.css";
import { VueCropper } from "vue-cropper";

上传: Upload 上传(element plus 组件)

实现理论:

  1. 通过组件 Upload 进行图片上传前校验方法判断是否需要裁剪、是否是图片、是否超过大小限制;
  2. 裁剪后的图片限制大小;
  3. 上传的图片信息暴漏出组件;

组件调用

img-url 默认预览的图片 @img-upload 上传所需的最终图片

代码实现

页面

裁剪时弹窗显示

<!-- 上传 -->
  <el-upload
    class="uploader_cutsom"
    action=""
    :show-file-list="false"
    :accept="imgUpload.imgBmp"
    :http-request="updataImg"
    :before-upload="beforUpload"
  >
    <div v-if="imgUpload.imageUrl" class="w-full h-full relative">
      <img class="w-full h-full" :src="imgUpload.imageUrl" />
      <!-- 删除 -->
      <div
        @click="clearImg"
        class="absolute top-0 right-0 w-8 h-8 rounded flex items-center justify-center bg-black bg-opacity-30 text-white text-base z-20 cursor-pointer"
      >
        <i class="iconfont icon-shanchu"></i>
      </div>
    </div>

    <div v-else class="uploader_cutsom_default">
      <p>从你的计算机中</p>
      <p class="uploader_cutsom_default_button">选择文件</p>
    </div>
  </el-upload>
  <!-- 裁剪 -->
  <el-dialog
    v-model="imgUpload.dialogCropping"
    custom-class="dialog_custom"
    title="裁剪图片"
    append-to-body
    :close-on-press-escape="false"
    :show-close="false"
    :close-on-click-modal="false"
  >
    <div>
      <div class="h-96 w-full">
        <vueCropper
          ref="cropper"
          class="vue_cropper_custom"
          :img="imgCropping.imageUrl"
          :outputType="imgCropping.outputType"
          :autoCrop="imgCropping.autoCrop"
          :autoCropWidth="imgCropping.autoCropWidth"
          :autoCropHeight="imgCropping.autoCropHeight"
          :fixed="imgCropping.fixed"
          :fixedNumber="imgCropping.fixedNumber"
          :centerBox="imgCropping.centerBox"
          :full="imgCropping.full"
        ></vueCropper>
      </div>
    </div>
    <template #footer>
      <el-button v-focus @click="handleCropping($refs.cropper, false)">
        取消
      </el-button>
      <el-button
        v-focus
        type="primary"
        @click="handleCropping($refs.cropper, true)"
      >
        确定
      </el-button>
    </template>
  </el-dialog>

逻辑

/** 图片地址传递 */
const prop = defineProps({
  imgUrl: {
    type: String,
    default: "",
  },
});
/** 事件通知 */
const emit = defineEmits(["img-upload"]);

/** 图片上传 */
const imgUpload = reactive({
  // 是否展示裁剪
  dialogCropping: false,
  isCropping: false, // 判断是否已经截图
  // 图片
  imageUrl: "",
  // 图片格式
  imgBmp: "image/*",
  // 图片名称
  imgName: "",
});
// 图片上传前校验
const beforUpload: UploadProps["beforeUpload"] = async (rawFile) => {
  // 判断是否需要进行裁剪
  imgUpload.isCropping = (await imgSize(rawFile)) as boolean;
  // 图片名称
  imgUpload.imgName = rawFile.name;
  if (imgUpload.isCropping) {
    if (rawFile.type.indexOf("image/") === -1) {
      ElMessage.error("请上传正确的图片格式");
      return false;
    } else if (rawFile.size / 1024 / 1024 > 0.5) {
      ElMessage.error("图片大小不能超过 500k");
      return false;
    }
  } else {
    // 进入裁剪
    imgCropping.imageUrl = URL.createObjectURL(rawFile);
    imgUpload.dialogCropping = true;
    return false;
  }
};
// 上传
const updataImg = async (data: any) => {
  // 如果未截图,打开裁剪
  if (imgUpload.isCropping) {
    // 图片预览
    imgUpload.imageUrl = URL.createObjectURL(data.file);
    imgUpload.dialogCropping = false;
    imgUpload.isCropping = false;
    // 图片信息
    emit("img-upload", data.file);
  }
};

/** 图片裁剪 */
const imgCropping = reactive({
  imageUrl: "",
  // 裁剪生成图片的格式
  outputType: "png",
  // 是否默认生成截图框
  autoCrop: true,
  // 上传图片按照原始比例渲染
  //   original: true,
  // 是否输出原图比例的截图
  full: false,
  // 默认生成截图框宽度
  autoCropWidth: 160,
  // 默认生成截图框高度
  autoCropHeight: 160,
  // 是否开启截图框宽高固定比例
  fixed: true,
  // 截图框的宽高比例
  fixedNumber: [1, 1],
  // 截图框是否被限制在图片里面
  centerBox: true,
});

// 生成裁剪图片
const handleCropping = (roleRefs: any, type: boolean) => {
  if (type) {
    roleRefs.getCropBlob((data: any) => {
      // 判断裁剪图片大小
      if (data.size / 1024 / 1024 > 0.5) {
        ElMessage.error("裁剪图片大小不能超过 500k");
      } else {
        // 图片预览
        imgUpload.imageUrl = URL.createObjectURL(data);
        imgUpload.dialogCropping = false; 
        // 图片信息
        emit("img-upload", blobToFile(data, imgUpload.imgName));
      }
    });
  } else {
    imgUpload.imageUrl = "";
    imgUpload.dialogCropping = false;
  }
};

// 清除图片
const clearImg = () => {
  imgUpload.imageUrl = "";
  emit("img-upload");
};

onMounted(() => {
  // 图片地址传递
  imgUpload.imageUrl = prop.imgUrl;
});

图片相关方法封装

/** 查询图片大小 */
export const imgSize = (file: any) => {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      // 让页面中的img标签的src指向读取的路径
      let img = new Image();
      img.src = reader.result as string;
      if (img.complete) {
        // 如果存在浏览器缓存中
        if (img.width / img.height !== 1) {
          resolve(false);
        } else {
          resolve(true);
        }
      } else {
        img.onload = () => {
          if (img.width / img.height !== 1) {
            resolve(false);
          } else {
            resolve(true);
          }
        };
      }
    };
  });
};

/**
 * 文件格式转换
 * @description blobToFile
 * @param {Blob} blob
 * @param {String} fileName
 */
export const blobToFile = (blob: any, fileName: string) => {
  return new window.File([blob], fileName, {
    type: blob.type,
  });
};