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~

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

相关推荐
前端大卫11 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘12 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare12 小时前
浅浅看一下设计模式
前端
Lee川12 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix12 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人12 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl12 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人13 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼13 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端