vue项目基于vue-cropper实现图片裁剪与图片压缩

一、应用场景

许多图片上传都需要裁剪一下或者裁剪完之后压缩一下再上传,效果如图:

因我的项目需求只有简单点裁剪和压缩,所以这里没有添置过多的操作按钮

二、实现方式

1、安装vue-cropper

bash 复制代码
npm install vue-cropper --save

bash 复制代码
yarn add vue-cropper --save

2、vue-cropper实现图片裁剪

我使用vue-cropper封装了一个图片裁剪组件,在需要的父组件中调用即可;以下是基于vue3+elementPLus写的,如果你的项目不是,可适当调整组件,另外我的需求截图框的宽高比例改为1.5:1 ,如果你的是别的比例可自由调整fixedNumber字段

javascript 复制代码
<template>
  <el-dialog title="图片裁剪" v-model="show" width="1200px" :closeOnClickModal="false" @close="handleClose" v-loading="loading">
    <el-row :gutter="20">
      <el-col :span="12" class="cropper-box">
          <div class="cropper">
            <vue-cropper
              ref="cropperRef"
              :img="option.img"
              :output-size="option.outputSize"
              :info="option.info"
              :can-scale="option.canScale"
              :auto-crop="option.autoCrop"
              :auto-crop-width="option.autoCropWidth"
              :auto-crop-height="option.autoCropHeight"
              :fixed="option.fixed"
              :fixed-number="option.fixedNumber"
              :full="option.full"
              :fixed-box="option.fixedBox"
              :can-move="option.canMove"
              :can-move-box="option.canMoveBox"
              :original="option.original"
              :center-box="option.centerBox"
              :height="option.height"
              :info-true="option.infoTrue"
              :max-img-size="option.maxImgSize"
              :enlarge="option.enlarge"
              :mode="option.mode"
              :limit-min-size="option.limitMinSize"
              @realTime="realTime"
          />
        </div>
      </el-col>
      <el-col :span="12">
        <!--预览效果图-->
        <div class="show-preview">
          <div :style="previews.div" class="preview">
            <img ref="previewsRef" :src="previews.url" :style="previews.img">
          </div>
        </div>
      </el-col>
    </el-row>
    <div>
      <el-button icon="Plus" @click="scaleBigger">放大</el-button>
      <el-button icon="Minus" @click="scaleSmaller">缩小</el-button>
      <el-button icon="Refresh" @click="reload">重置大小</el-button>
    </div>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleClose">取 消</el-button>
        <el-button type="primary" :loading="loading" @click="onSubmit">确 定</el-button>
      </span>
    </template>
  </e-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { Plus, Minus, Refresh } from '@element-plus/icons-vue';
import { compressImage } from '@/utils/biz';

const props = defineProps({
  fileSizeLimit: {
    type: Number,
    default: 20
  }
})
// 定义响应式数据
const show = ref(false)
const previews = ref({})
const cropperRef = ref()
const previewsRef = ref()
const ratioValue = ref(1.5) // 当前比例数值
const isRatioValid = ref(true) // 比例是否有效
const loading = ref(false) // 压缩加载状态

// 裁剪选项配置
const option = reactive({
  img: '', // 裁剪图片的地址
  outputSize: 1, // 裁剪生成图片的质量(可选0.1 - 1)
  outputType: 'png', // 裁剪生成图片的格式(png || png || webp),使用PNG保持质量
  info: true, // 图片大小信息
  canScale: true, // 图片是否允许滚轮缩放
  autoCrop: true, // 是否默认生成截图框
  autoCropWidth: 0, // 默认生成截图框宽度(设为0,将根据图片宽度自动计算)
  autoCropHeight: 0, // 默认生成截图框高度(设为0,将根据图片宽度和比例自动计算)
  fixed: true, // 是否开启截图框宽高固定比例
  fixedNumber: [1.5, 1], // 截图框的宽高比例改为1.5:1
  full: false, // false按原比例裁切图片,不失真
  fixedBox: false, // 允许调整截图框大小
  canMove: true, // 上传图片是否可以移动
  canMoveBox: true, // 截图框能否拖动
  original: false, // 上传图片不按照原始比例渲染,进行缩放
  centerBox: true, // 截图框是否被限制在图片里面
  height: true, // 是否按照设备的dpr 输出等比例图片
  infoTrue: true, // true为展示真实输出图片宽高,false展示看到的截图框宽高
  maxImgSize: 690, // 限制图片最大宽度和高度
  enlarge: 2, // 图片根据截图框输出比例倍数
  mode: 'contain', // 图片默认渲染方式,让图片缩放以完全显示在裁剪区域内
})

const file = ref(null)
const form = ref({})

// 定义事件
const emit = defineEmits(['handleUploadSuccess', 'handleUploadCancel'])

// 展示裁剪弹窗
const handleOpen = (val: any, obj: any, formData: any) => {
  Object.assign(option, obj)
  file.value = val
  form.value = formData
  show.value = true
  option.img = URL.createObjectURL(val.raw)
  
  // 创建图片对象来获取图片的实际尺寸
  const img = new Image()
  img.onload = () => {
    // 获取图片的实际宽度
    const imageWidth = img.width
    
    // 根据1.5的宽高比计算高度
    const imageHeight = Math.round(imageWidth / 1.5)
    
    // 设置裁剪框的宽度为图片宽度,高度为计算出的高度
    option.autoCropWidth = imageWidth
    option.autoCropHeight = imageHeight
  }
  img.src = URL.createObjectURL(val.raw)
  
  const reader = new FileReader()
  reader.readAsDataURL(val.raw)
}

// 关闭弹窗
const handleClose = () => {
  show.value = false
  // 触发取消事件
  emit('handleUploadCancel')
}

// 实时预览函数
const realTime = (data: any) => {
  previews.value = data
  
  // 计算并显示当前比例
  if (data && data.w && data.h) {
    const ratio = parseFloat((data.w / data.h).toFixed(2))
    ratioValue.value = ratio
    
    // 检查比例是否接近1.5(允许±0.1的误差)
    isRatioValid.value = ratio >= 1.4 && ratio <= 1.6
    
    // 如果比例超出范围,显示错误提示
    if (!isRatioValid.value) {
      // 可以在这里添加错误提示,但为了避免频繁提示,只在用户操作时显示
    }
  }
}

// 缩放图片
const resizeBlob = (blob: Blob, desiredWidth: number, desiredHeight: number): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      if (!ctx) {
        reject(new Error('Canvas context not available'))
        return
      }

      // 计算缩放比例
      const scaleX = desiredWidth / img.width
      const scaleY = desiredHeight / img.height
      const scale = Math.min(scaleX, scaleY)

      // 设置 Canvas 的宽度和高度
      canvas.width = desiredWidth
      canvas.height = desiredHeight

      // 绘制图片到 Canvas 上,并进行缩放
      ctx.drawImage(img, 0, 0, img.width * scale, img.height * scale)

      // 将 Canvas 中的图像转换为 Blob 对象
      canvas.toBlob((resizedBlob) => {
        if (resizedBlob) {
          resolve(resizedBlob)
        } else {
          reject(new Error('Failed to create blob'))
        }
      }, blob.type)
    }

    img.onerror = (error) => {
      reject(error)
    }

    img.src = URL.createObjectURL(blob)
  })
}

// 确定
const onSubmit = async () => {
  if (!cropperRef.value) return

  // 显示Loading
  loading.value = true

  try {
    // 获取截图的Blob对象
    const data = await new Promise<Blob>((resolve) => {
      cropperRef.value.getCropBlob(resolve)
    })

    // 检查图片大小
    const fileSizeMB = data.size / (1024 * 1024)
    console.log(`裁剪后图片大小: ${fileSizeMB.toFixed(2)}MB`)

    let finalBlob = data
    
    // 如果图片大于指定MB,进行压缩(如不需要压缩功能,可删除这个if,不调用compressImage)
    if (fileSizeMB > props.fileSizeLimit) {
      console.log('图片大于指定MB,开始压缩...')
      finalBlob = await compressImage(data, props.fileSizeLimit)
      const compressedSizeMB = finalBlob.size / (1024 * 1024)
      console.log(`压缩后图片大小: ${compressedSizeMB.toFixed(2)}MB`)
    }

    // 创建最终文件,保持原始格式
    const originalType = data.type || 'image/png'
    const fileExtension = originalType.split('/')[1] || 'png'
    const result = new File([finalBlob], `图片${(new Date()).getTime()}.${fileExtension}`, { 
      type: originalType, 
      lastModified: Date.now() 
    })
    
    // 获取裁剪后图片的实际尺寸用于调试
    const img = new Image()
    const url = URL.createObjectURL(finalBlob)
    img.src = url
    img.onload = function() {
      console.log('最终图片尺寸:', img.width, 'x', img.height)
      URL.revokeObjectURL(url)
    }
    
    show.value = false
    emit('handleUploadSuccess', result)
  } catch (error) {
    console.error('图片处理失败:', error)
    // 可以在这里添加错误提示
  } finally {
    // 关闭Loading
    loading.value = false
  }
}

const handleResizeBlob = (originalBlob: Blob, desiredWidth: number, desiredHeight: number) => {
  resizeBlob(originalBlob, desiredWidth, desiredHeight)
    .then((resizedBlob) => {
      // 在此处使用缩放后的 Blob 对象,保持原始格式
      const originalType = originalBlob.type || 'image/png'
      const fileExtension = originalType.split('/')[1] || 'png'
      const result = new File([resizedBlob], `图片${(new Date()).getTime()}.${fileExtension}`, { 
        type: originalType, 
        lastModified: Date.now() 
      })
      show.value = false
      emit('handleUploadSuccess', result)
    })
    .catch((error) => {
      console.error('Error resizing Blob:', error)
    })
}

// 放大
const scaleBigger = () => {
  if (cropperRef.value) {
    cropperRef.value.changeScale(1)
  }
}

// 缩小
const scaleSmaller = () => {
  if (cropperRef.value) {
    cropperRef.value.changeScale(-1)
  }
}

// 重置大小
const reload = () => {
  if (cropperRef.value) {
    cropperRef.value.reload()
  }
}

// 暴露方法给父组件
defineExpose({
  handleOpen,
  handleClose
})
</script>
<style scoped lang="scss">
  .cropper-box{
    width: 550px;
    margin-bottom: 20px;
    .cropper{
      width: 100%;
      height: 820px;
    }
  }
 
  .show-preview{
    width: 345px;
    height: 175px;
    max-width: 345px;
    max-height: 175px;
    min-width: 345px;
    min-height: 175px;
    
    .preview{
      width: 100%;
      height: 100%;
      overflow: hidden;
      border:1px solid #67c23a;
      background: #cccccc;
      img{
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
    }
  }
</style>

3、图片压缩

我上方代码里import { compressImage } from '@/utils/biz'; 引入了一个工具函数compressImage ,主要是进行压缩,返回压缩后的文件,函数入参已注释说明

javascript 复制代码
// 图片压缩函数
// blob:需要压缩的blob
// maxSizeMB: 是需要压缩到多少MB内就传几	
export const compressImage = (blob: Blob, maxSizeMB: number = 1): Promise<Blob> => {
	
  return new Promise((resolve, reject) => {
	
    const maxSizeBytes = maxSizeMB * 1024 * 1024 // 1MB
	
    // 如果图片已经小于等于1MB,直接返回
	
    if (blob.size <= maxSizeBytes) {
	
      resolve(blob)
	
      return
	
    }

    const img = new Image()
	
    const canvas = document.createElement('canvas')
	
    const ctx = canvas.getContext('2d')
	
    
	
    if (!ctx) {
	
      reject(new Error('Canvas context not available'))
	
      return
	
    }
	
    img.onload = () => {
	
      // 计算压缩比例
	
      const originalSize = blob.size
	
      const compressionRatio = maxSizeBytes / originalSize
	

      // 计算压缩后的尺寸(保持宽高比)
	
      const scale = Math.sqrt(compressionRatio)
	
      const newWidth = Math.floor(img.width * scale)
	
      const newHeight = Math.floor(img.height * scale)
	
      // 设置Canvas尺寸
	
      canvas.width = newWidth
	
      canvas.height = newHeight
	
	
      // 绘制压缩后的图片
	
      ctx.fillStyle = '#ffffff'
	
      ctx.fillRect(0, 0, newWidth, newHeight)
	
      ctx.drawImage(img, 0, 0, newWidth, newHeight)
	
      // 转换为Blob,逐步降低质量直到满足大小要求
	
      let quality = 0.9
	
      const tryCompress = () => {
	
        canvas.toBlob((compressedBlob) => {
	
          if (!compressedBlob) {
	
            reject(new Error('Failed to compress image'))
	
            return
	
          }
	
          
	
          if (compressedBlob.size <= maxSizeBytes || quality <= 0.1) {
	
            console.log(`压缩完成: ${originalSize} -> ${compressedBlob.size} bytes, 质量: ${quality}`)
	
            resolve(compressedBlob)
	
          } else {
	
            // 继续降低质量
	
            quality -= 0.1
	
            canvas.toBlob(tryCompress, 'image/jpeg', quality)
	
          }
	
        }, 'image/jpeg', quality)
	
      }
	
      
	
      tryCompress()
	
    }
	
    
	
    img.onerror = (error) => {
	
      reject(error)
	
    }
	
    
	
    img.src = URL.createObjectURL(blob)
	
  })

}

4、图片压缩组件使用

在你需要使用的父组件中引入2的子组件,以及实现相关函数,上传我用的是el-upload组件,这里只实现了beforeUpload,如果你不是改成你的上传回调即可。

javascript 复制代码
<!-- 图片裁剪框 -->
<CropperImage ref="cropperImage" :fileSizeLimit="1" @handleUploadSuccess="handleUploadSuccess" @handleUploadCancel="handleUploadCancel" />

// 图片裁剪相关
const cropperImage = ref(null)
let uploadResolve: any = null
let uploadReject: any = null

// el-upload绑定的beforeUpload函数
const pictureBeforeUpload = async (rawFile): Promise<boolean | void | File | Blob> => {
	if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
		ElMessage.error('只支持jpg和png格式的图片')
		return false
	}
	// 返回一个Promise,等待裁剪完成后再解析,否则还是给原图给el-upload组件
	return new Promise((resolve, reject) => {
		uploadResolve = resolve
		uploadReject = reject
		// 显示图片裁剪框,让用户调整图片
		cropperImage.value.handleOpen({ raw: rawFile })
	})
}

// 裁剪成功的回调
const handleUploadSuccess = (val) => {
	// 将裁剪后的图片传递给el-upload组件
	if (val && uploadResolve) {
		// 解析Promise
		uploadResolve(val)
		uploadResolve = null
		uploadReject = null
	}
}

// 裁剪取消的回调
const handleUploadCancel = () => {
	// 如果用户取消裁剪,拒绝Promise
	if (uploadReject) {
		uploadReject(new Error('用户取消了图片裁剪'))
		uploadResolve = null
		uploadReject = null
	}
}

end~

希望记录的问题能帮助到你!

相关推荐
用户47949283569152 小时前
记住这张时间线图,你再也不会乱用 useEffect / useLayoutEffect
前端·react.js
汝生淮南吾在北2 小时前
SpringBoot+Vue养老院管理系统
vue.js·spring boot·后端·毕业设计·毕设
咬人喵喵2 小时前
14 类圣诞核心 SVG 交互方案拆解(附案例 + 资源)
开发语言·前端·javascript
问君能有几多愁~2 小时前
C++ 日志实现
java·前端·c++
咬人喵喵2 小时前
CSS 盒子模型:万物皆是盒子
前端·css
2401_860319522 小时前
DevUI组件库实战:从入门到企业级应用的深度探索,如何快速应用各种组件
前端·前端框架
韩曙亮3 小时前
【Web APIs】元素滚动 scroll 系列属性 ② ( 右侧固定侧边栏 )
前端·javascript·bom·window·web apis·pageyoffset
珑墨3 小时前
【浏览器】页面加载原理详解
前端·javascript·c++·node.js·edge浏览器
FreeBuf_3 小时前
Next.js 发布扫描工具:检测并修复受 React2Shell 漏洞(CVE-2025-66478)影响的应用
开发语言·javascript·ecmascript