vue-cropper图片裁剪、旋转、缩放、实时预览

一、ImageCropper.vue 图片裁剪组件

  1. 功能实现思路

核心设计理念 :基于 vue-cropper 库封装的独立图片裁剪组件,提供弹窗式裁剪功能,支持多种比例裁剪和图片编辑操作。

整体架构 :

  1. 组件化设计 :独立封装,通过 props 接收参数,emit 事件返回结果

  2. 弹窗式交互 :使用 Element Plus Dialog 作为容器,提供良好的用户体验

  3. 响应式设计 :支持不同屏幕尺寸,自适应裁剪框大小

  4. 实时预览 :裁剪过程中实时显示预览效果

  5. 校验机制 :内置图片尺寸、比例、大小校验

|------|-----------------|-------------------------------------|
| 模块 | 功能描述 | 实现思路 |
| 图片加载 | 加载原始图片并显示 | 使用 FileReader 将 File 对象转为 base64 格式 |
| 比例裁剪 | 支持 1:1 和 3:4 比例 | 通过 fixed-number 属性控制裁剪比例 |
| 图片编辑 | 旋转、缩放操作 | 调用 vue-cropper 提供的 API |
| 实时预览 | 裁剪过程中实时预览 | 监听 realTime 事件,更新预览图 |
| 裁剪校验 | 校验裁剪后的图片 | 检查尺寸、比例、大小是否符合要求 |
| 结果返回 | 返回裁剪后的 File 对象 | 将裁剪后的 blob 转为 File 对象,通过事件返回 |

  1. 关键代码注释

    <el-dialog v-model="visible" title="裁剪图片" width="1006" :close-on-click-modal="false" :before-close="handleClose">
    <vue-cropper ref="cropperRef" :img="imgUrl" :auto-crop="true" :auto-crop-width="cropWidth" :auto-crop-height="cropHeight" :fixed="true" :fixed-number="currentRatio" :output-type="png" @realTime="handleRealTime" />
    编辑
    旋转左
    旋转右
    放大
    缩小
    <CustomButtons @click="changeRatio([1, 1])">1:1</CustomButtons> <CustomButtons @click="changeRatio([3, 4])">3:4</CustomButtons>
    预览
    <template #footer> <CustomButtons @click="confirmCrop">确定</CustomButtons> <CustomButtons @click="handleClose">取消</CustomButtons> </template> </el-dialog>
  2. 核心功能实现

    // 加载原始文件为 base64
    const loadImage = (file: File) => {
    const reader = new FileReader()
    reader.onload = (ev) => {
    imgUrl.value = ev.target?.result as string // 将 File 转为 base64
    previewUrl.value = '' // 重置预览图
    nextTick(() => {
    if (cropperRef.value) {
    // 重置裁剪状态
    cropperRef.value.rotate(0)
    cropperRef.value.scale({ x: 1, y: 1 })
    }
    })
    }
    reader.readAsDataURL(file) // 读取文件为 DataURL
    }

比例切换功能

复制代码
// 切换裁剪比例
const changeRatio = (ratio: [number, number]) => {
  if (!cropperRef.value) return
  
  currentRatio.value = ratio  // 更新当前比例
  
  nextTick(() => {
    try {
      // 重置裁剪框
      if (cropperRef.value.reset) {
        cropperRef.value.reset()  // 调用裁剪组件的重置方法
      }
    } catch (e) {
      console.error('切换比例失败:', e)
    }
  })
}

裁剪确认逻辑

复制代码
const confirmCrop = async () => {
  if (!cropperRef.value) return

  try {
    // 1. 获取裁剪后的 Blob 数据
    const blob = await new Promise<Blob>((resolve, reject) => {
      cropperRef.value.getCropBlob((blob: Blob) => {
        blob ? resolve(blob) : reject(new Error('获取裁剪数据失败'))
      })
    })

    // 2. 校验裁剪后的图片
    const { isValid, errorMsg } = await validateCroppedImage(blob)
    if (!isValid) {
      ElMessage.error(errorMsg)
      return  // 校验失败,不关闭弹窗
    }

    // 3. 生成 File 对象并触发回调
    const croppedFile = new File(
      [blob],
      props.rawFile.name,  // 保留原文件名
      { type: props.rawFile.type || 'image/png' }  // 保留原文件类型
    )
    emit('crop-success', croppedFile)  // 触发裁剪成功事件
    visible.value = false  // 关闭弹窗
    emit('update:modelValue', false)  // 更新弹窗显示状态

  } catch (error) {
    ElMessage.error('裁剪失败,请重新操作')
  }
}
  1. 注意事项

  2. 依赖要求 :

    1. 需安装 vue-cropper 库: npm install vue-cropper
    2. 引入样式文件:import 'vue-cropper/dist/index.css'
  3. 组件使用 :

    1. 必须传递 rawFile 属性(待裁剪的 File 对象)
    2. 通过 v-model 控制弹窗显示/隐藏
    3. 监听 crop-success 事件获取裁剪结果
  4. 裁剪比例 :

    1. 支持 1:1 和 3:4 两种比例
    2. 可通过 fixedNumber 属性默认指定比例
    3. 可动态切换比例
  5. 图片校验 :

    1. 内置尺寸、比例、大小校验
    2. 可通过 validateRules 属性自定义校验规则
    3. 校验失败时会显示错误信息,不关闭弹窗
  6. 性能优化 :

    1. 使用 nextTick 确保 DOM 更新后再操作裁剪组件
    2. 裁剪成功后及时关闭弹窗
    3. 重置裁剪状态,避免内存泄漏

二、父页面

整体架构 :

  1. 表单设计 :使用 Element Plus Form 组件构建商品信息表单
  2. 文件上传 :集成图片/视频上传功能,支持裁剪处理
  3. 异步处理 :使用异步函数处理文件上传和 API 调用
  4. 状态管理 :使用 ref 和 reactive 管理表单状态和上传状态

1.核心功能模块 :

|--------|-----------------------|--------------------------------|
| 模块 | 功能描述 | 实现思路 |
| 封面图上传 | 上传商品封面图,支持 1:1 比例裁剪 | 使用 el-upload + ImageCropper 组件 |
| 媒体文件上传 | 上传商品图片/视频,支持 3:4 比例裁剪 | 使用 el-upload + ImageCropper 组件 |
| 文件校验 | 校验文件格式、大小、数量、时长 | 前端预校验 + 后端校验 |

  1. 关键代码注释

    <template> <AdaptiveCard class="add_commodity">
    <el-form ref="ruleFormRef" :model="ruleForm" label-width="120rem"> <el-row :gutter="16"> <el-col :span="24"> <el-form-item label="商品封面图" prop="Businesslicense">
    图片要求:比例为1:1,1张
    <el-upload class="avatar-uploader" :show-file-list="false" :auto-upload="false" @change="handleCoverChange">
    <el-icon class="avatar-uploader-icon"><Plus /></el-icon> 点击上传
    </el-upload>
    </el-form-item> </el-col> </el-row>
    复制代码
         <!-- 商品图片/视频 -->
         <el-row :gutter="16">
           <el-col :span="24">
             <el-form-item label="商品图片/视频" prop="mediaFiles">
               <div class="images_videos">
                 <div class="tips">
                   图片要求:宽高比例为3:4, 最多上传5张(排序)
                   视频要求:时长5秒---5分钟,最多上传3个
                 </div>
                 <!-- 媒体文件上传组件 -->
                 <el-upload 
                   :auto-upload="false" 
                   v-model:file-list="localMediaFiles" 
                   list-type="picture-card"
                   @change="handleMediaChange"
                   :limit="8">
                   <el-icon><Plus /></el-icon>
                   <span>点击上传</span>
                 </el-upload>
               </div>
             </el-form-item>
           </el-col>
         </el-row>
       </el-form>
     </div>
    
     <!-- 图片裁剪组件 -->
     <ImageCropper 
       v-model="cropperVisible" 
       v-if="cropRawFile" 
       :fixed-number="fixedNumber" 
       :raw-file="cropRawFile"
       @crop-success="handleCropSuccess" 
       @crop-cancel="handleCropCancel" />
    </AdaptiveCard> </template>
  2. 核心功能实现-封面图上传逻辑

    // 封面图处理(1:1比例校验)
    const handleCoverChange = (uploadFile: UploadFile) => {
    const file = uploadFile.raw as File | undefined
    if (!file || !file.type.startsWith('image/')) {
    ElMessage.error('请上传图片格式文件')
    return
    }

    const reader = new FileReader()
    reader.onload = (e) => {
    const img = new Image()
    img.onload = () => {
    const ratio = img.width / img.height
    // 比例误差允许 ±5%
    if (Math.abs(ratio - 1) > 0.05) {
    ElMessage.warning('封面图比例需为1:1,即将为您打开裁剪工具')
    // 核心:设置裁剪参数并打开裁剪弹窗
    fixedNumber.value = [1, 1] // 强制设置为1:1比例
    cropRawFile.value = file // 传入需要裁剪的原始文件
    pendingCropUid = uploadFile.uid // 记录封面图的uid
    cropperVisible.value = true // 打开裁剪弹窗
    return
    }

    复制代码
       // 比例符合,自动上传封面图
       uploadSingleFile(fileToUploadRawFile(file, uploadFile.uid), 'cover')
     }
     img.src = e.target?.result as string

    }
    reader.readAsDataURL(file)
    }

媒体文件上传逻辑

复制代码
// 文件选择变化:自动触发校验
const handleMediaChange = async (file: UploadFile, uploadFiles: UploadUserFile[]) => {
  if (file.status !== 'ready' || !file.raw) {
    return
  }

  // 预处理:确保uploadFiles中所有文件的name/uid非空
  const safeUploadFiles: ExtendedUploadFile[] = uploadFiles.map(item => ({
    ...item,
    name: item.name || `未命名文件_${Date.now()}`,
    uid: typeof item.uid === 'number' ? item.uid : generateUid(),
    thumbUrl: item.url,
    status: item.status || 'ready' as UploadStatus,
  }))

  localMediaFiles.value = safeUploadFiles

  await validateAndUpload(file.raw as UploadRawFile)
}

裁剪成功回调

复制代码
// 裁剪成功回调:替换文件并上传
const handleCropSuccess = async (croppedFile: File) => {
  if (!pendingCropUid) return

  // 生成UploadRawFile对象
  const newFile = Object.assign(croppedFile, {
    uid: pendingCropUid
  }) as UploadRawFile

  // 区分封面图/媒体图上传
  if (fixedNumber.value[0] === 1 && fixedNumber.value[1] === 1) {
    uploadSingleFile(newFile, 'cover')
  } else {
    // 临时显示缩略图
    const tempThumbUrl = URL.createObjectURL(croppedFile)
    const index = localMediaFiles.value.findIndex(f => f.uid === pendingCropUid)
    if (index > -1) {
      localMediaFiles.value[index] = {
        ...localMediaFiles.value[index]!,
        thumbUrl: tempThumbUrl,
        status: 'uploading'
      }
    }
    uploadSingleFile(newFile)
  }

  // 重置状态
  pendingCropUid = null
  cropperVisible.value = false
  ElMessage.success(isCoverCrop ? '封面图裁剪成功' : '媒体图裁剪成功')
}

文件校验逻辑

复制代码
// 核心:文件校验函数(分流处理图片/视频)
const validateAndUpload = async (rawFile: UploadRawFile) => {
  const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(rawFile.type)
  const isVideo = rawFile.type.startsWith('video/')

  // 1. 基础格式校验
  if (!isImage && !isVideo) {
    ElMessage.error('仅支持图片/视频格式!')
    removeFileByUid(rawFile.uid)
    return false
  }

  // 2. 大小校验
  const maxImgSize = 10 * 1024 * 1024 // 10MB
  const maxVideoSize = 300 * 1024 * 1024 // 300MB
  if ((isImage && rawFile.size > maxImgSize) || (isVideo && rawFile.size > maxVideoSize)) {
    ElMessage.error(isImage ? '图片不能超过10MB!' : '视频不能超过300MB!')
    removeFileByUid(rawFile.uid)
    return false
  }

  // 3. 数量校验
  const imgCount = ruleForm.mediaFiles.filter(f => f.raw?.type.startsWith('image')).length
  const videoCount = ruleForm.mediaFiles.filter(f => f.raw?.type.startsWith('video')).length
  if ((isImage && imgCount >= 5) || (isVideo && videoCount >= 3)) {
    ElMessage.error(isImage ? '最多上传5张图片!' : '最多上传3个视频!')
    removeFileByUid(rawFile.uid)
    return false
  }

  // 4. 视频专属:时长校验
  if (isVideo) {
    return new Promise<boolean>((resolve) => {
      const video = document.createElement('video')
      video.preload = 'metadata'
      video.src = URL.createObjectURL(rawFile)

      video.onloadedmetadata = () => {
        URL.revokeObjectURL(video.src)
        const duration = video.duration
        if (duration < 5 || duration > 300) {
          ElMessage.error('视频要求:时长5秒---5分钟!')
          removeFileByUid(rawFile.uid)
          resolve(false)
        } else {
          uploadSingleFile(rawFile)
          resolve(true)
        }
      }
    })
  }

  // 5. 图片专属:比例校验
  if (isImage) {
    return new Promise<boolean>((resolve) => {
      const reader = new FileReader()
      reader.onload = (e) => {
        const img = new Image()
        img.onload = () => {
          const ratio = img.width / img.height
          const expectedRatio = 3 / 4
          const tolerance = 0.05

          if (Math.abs(ratio - expectedRatio) > tolerance) {
            // 比例不符 → 打开裁剪弹窗
            fixedNumber.value = [3, 4]
            cropRawFile.value = rawFile
            pendingCropUid = rawFile.uid
            cropperVisible.value = true
            resolve(false)
          } else {
            // 比例符合 → 自动上传
            uploadSingleFile(rawFile)
            resolve(true)
          }
        }
        img.src = e.target?.result as string
      }
      reader.readAsDataURL(rawFile)
    })
  }

  return false
}
  1. 注意事项

  2. 文件上传 :

    1. 支持图片和视频上传
    2. 图片支持 1:1 和 3:4 比例裁剪
    3. 视频自动校验时长
    4. 上传过程中显示加载状态
  3. 裁剪功能 :

    1. 封面图强制 1:1 比例
    2. 媒体图强制 3:4 比例
    3. 裁剪弹窗支持旋转、缩放操作
    4. 实时预览裁剪效果
  4. 表单校验 :

    1. 前端预校验文件格式、大小、数量、时长
    2. 后端校验数据完整性和合法性
    3. 校验失败时显示友好的错误信息
  5. 异步处理 :

    1. 使用 async/await 处理异步操作
    2. 上传过程中显示加载状态
    3. 错误处理完善,显示友好的错误信息
  6. 性能优化 :

    1. 上传失败时及时清理临时文件
    2. 裁剪成功后及时关闭弹窗
    3. 使用 URL.revokeObjectURL 释放 blob URL
    4. 避免内存泄漏
  7. 用户体验 :

    1. 上传成功后显示成功信息
    2. 上传失败后显示错误信息
    3. 裁剪前显示友好提示
    4. 表单提交按钮根据状态禁用

三、总结

  1. 组件间关系

    1. ImageCropper 组件是一个独立的图片裁剪组件,被父页面引用,用于处理商品封面图和媒体文件的裁剪。
  2. 调用关系 :

    1. 父页面中的 el-upload 组件触发文件选择
    2. 父页面进行文件预校验
    3. 如果需要裁剪,调用 ImageCropper 组件
    4. ImageCropper 组件处理裁剪操作
    5. 裁剪完成后,ImageCropper 组件触发 crop-success 事件
    6. 父页面接收裁剪结果,继续上传流程
  3. 技术栈
    1.

    |--------------|--------|
    | 技术/库 | 用途 |
    | Vue 3 | 前端框架 |
    | TypeScript | 类型安全 |
    | Element Plus | UI 组件库 |
    | vue-cropper | 图片裁剪 |
    | Axios | API 请求 |

相关推荐
梦6503 小时前
Vue 单页面应用 (SPA) 与 多页面应用 (MPA) 对比
前端·javascript·vue.js
清铎3 小时前
大模型训练_week3_day15_Llama概念_《穷途末路》
前端·javascript·人工智能·深度学习·自然语言处理·easyui
岛泪3 小时前
把 el-cascader 的 options 平铺为一维数组(只要叶子节点)
前端·javascript·vue.js
Kiyra4 小时前
阅读 Netty 源码关于 NioEventLoop 和 Channel 初始化部分的思考
运维·服务器·前端
冰暮流星4 小时前
javascript的switch语句介绍
java·前端·javascript
做科研的周师兄4 小时前
【MATLAB 实战】|多波段栅格数据提取部分波段均值——批量处理(NoData 修正 + 地理信息保真)_后附完整代码
前端·算法·机器学习·matlab·均值算法·分类·数据挖掘
da_vinci_x4 小时前
图标量产:从“手绘地狱”到“风格克隆”?Style Reference 的工业化实战
前端·游戏·ui·prompt·aigc·设计师·游戏美术
利刃大大5 小时前
【ES6】变量与常量 && 模板字符串 && 对象 && 解构赋值 && 箭头函数 && 数组 && 扩展运算符 && Promise/Await/Async
开发语言·前端·javascript·es6
天若有情6735 小时前
ES6 模块与 CommonJS 的区别详解
前端·javascript·es6