一、ImageCropper.vue 图片裁剪组件
- 功能实现思路
核心设计理念 :基于 vue-cropper 库封装的独立图片裁剪组件,提供弹窗式裁剪功能,支持多种比例裁剪和图片编辑操作。
整体架构 :
-
组件化设计 :独立封装,通过 props 接收参数,emit 事件返回结果
-
弹窗式交互 :使用 Element Plus Dialog 作为容器,提供良好的用户体验
-
响应式设计 :支持不同屏幕尺寸,自适应裁剪框大小
-
实时预览 :裁剪过程中实时显示预览效果
-
校验机制 :内置图片尺寸、比例、大小校验
|------|-----------------|-------------------------------------|
| 模块 | 功能描述 | 实现思路 |
| 图片加载 | 加载原始图片并显示 | 使用 FileReader 将 File 对象转为 base64 格式 |
| 比例裁剪 | 支持 1:1 和 3:4 比例 | 通过 fixed-number 属性控制裁剪比例 |
| 图片编辑 | 旋转、缩放操作 | 调用 vue-cropper 提供的 API |
| 实时预览 | 裁剪过程中实时预览 | 监听 realTime 事件,更新预览图 |
| 裁剪校验 | 校验裁剪后的图片 | 检查尺寸、比例、大小是否符合要求 |
| 结果返回 | 返回裁剪后的 File 对象 | 将裁剪后的 blob 转为 File 对象,通过事件返回 |
-
关键代码注释
<el-dialog v-model="visible" title="裁剪图片" width="1006" :close-on-click-modal="false" :before-close="handleClose"><template #footer> <CustomButtons @click="confirmCrop">确定</CustomButtons> <CustomButtons @click="handleClose">取消</CustomButtons> </template> </el-dialog><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>预览 -
核心功能实现
// 加载原始文件为 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('裁剪失败,请重新操作')
}
}
-
注意事项
-
依赖要求 :
- 需安装 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 组件 |
| 文件校验 | 校验文件格式、大小、数量、时长 | 前端预校验 + 后端校验 |
-
关键代码注释
<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"></el-form-item> </el-col> </el-row>图片要求:比例为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></AdaptiveCard> </template><!-- 商品图片/视频 --> <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" />核心功能实现-封面图上传逻辑
// 封面图处理(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:1 和 3:4 比例裁剪
- 视频自动校验时长
- 上传过程中显示加载状态
-
裁剪功能 :
- 封面图强制 1:1 比例
- 媒体图强制 3:4 比例
- 裁剪弹窗支持旋转、缩放操作
- 实时预览裁剪效果
-
表单校验 :
- 前端预校验文件格式、大小、数量、时长
- 后端校验数据完整性和合法性
- 校验失败时显示友好的错误信息
-
异步处理 :
- 使用 async/await 处理异步操作
- 上传过程中显示加载状态
- 错误处理完善,显示友好的错误信息
-
性能优化 :
- 上传失败时及时清理临时文件
- 裁剪成功后及时关闭弹窗
- 使用 URL.revokeObjectURL 释放 blob URL
- 避免内存泄漏
-
用户体验 :
- 上传成功后显示成功信息
- 上传失败后显示错误信息
- 裁剪前显示友好提示
- 表单提交按钮根据状态禁用
三、总结
-
组件间关系
- ImageCropper 组件是一个独立的图片裁剪组件,被父页面引用,用于处理商品封面图和媒体文件的裁剪。
-
调用关系 :
- 父页面中的 el-upload 组件触发文件选择
- 父页面进行文件预校验
- 如果需要裁剪,调用 ImageCropper 组件
- ImageCropper 组件处理裁剪操作
- 裁剪完成后,ImageCropper 组件触发 crop-success 事件
- 父页面接收裁剪结果,继续上传流程
-
技术栈
1.|--------------|--------|
| 技术/库 | 用途 |
| Vue 3 | 前端框架 |
| TypeScript | 类型安全 |
| Element Plus | UI 组件库 |
| vue-cropper | 图片裁剪 |
| Axios | API 请求 |