欢迎光临
我们一直在努力

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 对象,通过事件返回

2. 关键代码注释

<!– 裁剪弹窗 –>
<el-dialog v-model="visible" title="裁剪图片" width="1006" :close-on-click-modal="false" :before-close="handleClose">
<div class="cropper-box">
<!– 左侧裁剪区域 –>
<div class="cropper_left">
<!– vue-cropper 核心组件 –>
<vue-cropper
ref="cropperRef"
:img="imgUrl" <!– 待裁剪图片的 base64 –>
:auto-crop="true" <!– 自动裁剪 –>
:auto-crop-width="cropWidth" <!– 裁剪宽度 –>
:auto-crop-height="cropHeight" <!– 裁剪高度 –>
:fixed="true" <!– 固定比例 –>
:fixed-number="currentRatio" <!– 当前裁剪比例 –>
:output-type="png" <!– 输出格式 –>
@realTime="handleRealTime" <!– 实时裁剪回调 –>
/>
</div>
<!– 右侧编辑和预览区域 –>
<div class="right">
<!– 编辑操作区 –>
<div class="top">
<div class="title">编辑</div>
<div class="button">
<!– 旋转、缩放、比例切换按钮 –>
<div class="icon" @click="rotateImage('left')">旋转左</div>
<div class="icon" @click="rotateImage('right')">旋转右</div>
<div class="icon" @click="scaleImage(0.1)">放大</div>
<div class="icon" @click="scaleImage(-0.1)">缩小</div>
<!– 比例切换按钮 –>
<CustomButtons @click="changeRatio([1, 1])">1:1</CustomButtons>
<CustomButtons @click="changeRatio([3, 4])">3:4</CustomButtons>
</div>
</div>
<!– 预览区 –>
<div class="bottom">
<div class="title">预览</div>
<div class="Preview" :style="{
backgroundImage: previewUrl ? `url(${previewUrl})` : 'none',
backgroundSize: 'cover'
}"></div>
</div>
</div>
</div>
<!– 底部操作按钮 –>
<template #footer>
<CustomButtons @click="confirmCrop">确定</CustomButtons>
<CustomButtons @click="handleClose">取消</CustomButtons>
</template>
</el-dialog>

3. 核心功能实现

// 加载原始文件为 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('裁剪失败,请重新操作')
}
}

4. 注意事项

  •  依赖要求 :
  • 需安装 vue-cropper 库: npm install vue-cropper
  • 引入样式文件: import 'vue-cropper/dist/index.css'
  • 组件使用 :
  • 必须传递 rawFile 属性(待裁剪的 File 对象)
  • 通过 v-model 控制弹窗显示/隐藏
  • 监听 crop-success 事件获取裁剪结果
  • 裁剪比例 :
  • 支持 1:1 和 3:4 两种比例
  • 可通过 fixedNumber 属性默认指定比例
  • 可动态切换比例
  • 图片校验 :
  •  内置尺寸、比例、大小校验
  • 可通过 validateRules 属性自定义校验规则
  • 校验失败时会显示错误信息,不关闭弹窗
  • 性能优化 :
  • 使用 nextTick 确保 DOM 更新后再操作裁剪组件
  • 裁剪成功后及时关闭弹窗
  • 重置裁剪状态,避免内存泄漏
  • 二、父页面

    整体架构 :

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

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

    2. 关键代码注释

    <template>
    <AdaptiveCard class="add_commodity">
    <div class="form-content">
    <el-form ref="ruleFormRef" :model="ruleForm" label-width="120rem">
    <!– 商品封面图 –>
    <el-row :gutter="16">
    <el-col :span="24">
    <el-form-item label="商品封面图" prop="Businesslicense">
    <div class="cover_image">
    <div class="tips">图片要求:比例为1:1,1张</div>
    <!– 封面图上传组件 –>
    <el-upload
    class="avatar-uploader"
    :show-file-list="false"
    :auto-upload="false"
    @change="handleCoverChange">
    <!– 已上传图片显示 –>
    <img v-if="ruleForm.mainImage" :src="ruleForm.mainImage" class="avatar" />
    <!– 未上传时的占位符 –>
    <div v-else class="avatar-uploader-icon-wrapper">
    <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
    <span>点击上传</span>
    </div>
    </el-upload>
    </div>
    </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>

    3. 核心功能实现-封面图上传逻辑

    // 封面图处理(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
    }

    4. 注意事项

  • 文件上传 :
  • 支持图片和视频上传
  • 图片支持 1:1 和 3:4 比例裁剪
  • 视频自动校验时长
  • 上传过程中显示加载状态
  • 裁剪功能 :
  • 封面图强制 1:1 比例
  • 媒体图强制 3:4 比例
  • 裁剪弹窗支持旋转、缩放操作
  • 实时预览裁剪效果
  • 表单校验 :
  • 前端预校验文件格式、大小、数量、时长
  • 后端校验数据完整性和合法性
  • 校验失败时显示友好的错误信息
  • 异步处理 :
  • 使用 async/await 处理异步操作
  • 上传过程中显示加载状态
  • 错误处理完善,显示友好的错误信息
  • 性能优化 :
  • 上传失败时及时清理临时文件
  • 裁剪成功后及时关闭弹窗
  • 使用 URL.revokeObjectURL 释放 blob URL
  • 避免内存泄漏
  • 用户体验 :
  • 上传成功后显示成功信息
  • 上传失败后显示错误信息
  • 裁剪前显示友好提示
  • 表单提交按钮根据状态禁用
  • 三、总结

  • 组件间关系
  • ImageCropper 组件是一个独立的图片裁剪组件,被父页面引用,用于处理商品封面图和媒体文件的裁剪。
  • 调用关系 :
  • 父页面中的 el-upload 组件触发文件选择
  • 父页面进行文件预校验
  • 如果需要裁剪,调用 ImageCropper 组件
  • ImageCropper 组件处理裁剪操作
  • 裁剪完成后,ImageCropper 组件触发 crop-success 事件
  • 父页面接收裁剪结果,继续上传流程
  • 技术栈
  • 技术/库 用途
    Vue 3 前端框架
    TypeScript 类型安全
    Element Plus UI 组件库
    vue-cropper 图片裁剪
    Axios API 请求
  • 赞(0)
    未经允许不得转载:171主机测评 » vue-cropper图片裁剪、旋转、缩放、实时预览
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址