概述
本文档介绍如何在vue3+ts的芋道源码后台管理项目中使用基于 cropperjs 的图片裁剪能力,封装为以下组件,满足头像裁剪、图片裁剪预览、弹窗裁剪、配合上传等场景。
图片要压缩质量,必须使用image/jpeg才有效,使用image/jpeg后,图片透明色会变成黑色,所以需要变白处理。
- 导出组件
- CropperImage: 基础裁剪核心组件
- CropperAvatar: 头像裁剪 + 弹窗选择上传的完整流程
- CropperImg: 简易图片占位/预览卡片,触发外部裁剪逻辑
- CopperModal: 内部使用的裁剪弹窗(可独立使用)
从 @/components/Cropper 引入:
import { CropperImage, CropperAvatar, CropperImg } from '@/components/Cropper'
快速开始
场景一:用户头像裁剪(推荐)
内置完整流程:选择图片 → 弹窗裁剪 → 确认后返回裁剪结果(二进制 Blob 与 base64)。
<template>
<CropperAvatar
:value="avatarUrl"
:showBtn="true"
btnText="更换头像"
@change="onChange"
ref="avatarRef"
/>
<!-- 也可配合 v-model:value 使用 -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { CropperAvatar } from '@/components/Cropper'
const avatarRef = ref()
const avatarUrl = ref('')
// { source: base64, data: Blob, filename: string }
const onChange = async ({ data, source, filename }) => {
// 1) 将 Blob 上传到后端
// 2) 服务端返回可访问 URL,更新本地头像
// 3) 可调用 avatarRef.value.close() 关闭弹窗
}
</script>
参考项目实际上传逻辑可看 src/views/Profile/components/UserAvatar.vue 中的 handelUpload:使用 useUpload().httpRequest 将 data 作为文件上传,上传成功后更新用户资料并关闭弹窗。
场景二:手动控制裁剪弹窗(更灵活)
直接使用裁剪弹窗 CopperModal,适用于需要自定义触发/自定义外观的页面。
<template>
<el-button type="primary" @click="open">上传并裁剪</el-button>
<CopperModal ref="modalRef" :circled="true" @upload-success="onUploadSuccess" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CopperModal from '@/components/Cropper/src/CopperModal.vue'
const modalRef = ref()
const open = () => {
modalRef.value.openModal()
}
const onUploadSuccess = ({ source, data, filename }) => {
// source: 裁剪后 base64
// data: 裁剪后 Blob,可直接上传
// filename: 原始文件名
}
</script>
-
弹窗内置能力:选择图片、预览、重置、旋转、翻转、缩放、圆形/方形裁剪等。
-
重要事件:
uploadSuccess返回裁剪结果,业务方仅需上传data到后端
场景三:页面内嵌裁剪器(无弹窗)
在页面内直接使用 CropperImage,监听 cropend 实时获取裁剪结果。
<template>
<CropperImage
:src="imgSrc"
:circled="false"
height="360px"
:size="500"
:options="options"
@ready="onReady"
@cropend="onCropEnd"
@cropendError="onError"
/>
<!-- size>100 输出 jpeg 且质量为 size/1000;否则为 png -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type Cropper from 'cropperjs'
import { CropperImage } from '@/components/Cropper'
const imgSrc = ref('https://example.com/demo.jpg')
const options: Cropper.Options = { aspectRatio: 16 / 9 }
const onReady = (cropper: Cropper) => { /* 可保存实例以便手动调用 API */ }
const onCropEnd = ({ imgBase64, imgInfo }) => {
// imgBase64: 裁剪结果
// imgInfo: 裁剪数据(x,y,width,height,rotate,scaleX,scaleY...)
}
const onError = () => { /* 处理错误 */ }
</script>
场景四:占位/预览卡片触发裁剪
使用 CropperImg 显示占位/预览,并通过事件让外部打开裁剪弹窗或路由到裁剪页。
<template>
<CropperImg
:modelValue="img"
:height="'150px'"
:width="'150px'"
:showDelete="true"
@cropperImg="openCropModal"
@deleteImg="onDelete"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { CropperImg } from '@/components/Cropper'
const img = ref('')
const openCropModal = () => { /* 打开 CopperModal 或跳转到裁剪页面 */ }
const onDelete = () => { img.value = '' }
</script>
组件 API
CropperImage
- props
- src: string 图片地址
- alt: string
- circled: boolean 是否圆形裁剪,默认 false
- realTimePreview: boolean 是否实时回调 cropend,默认 true
- height: string 组件高度,默认 360px
- crossorigin: '' | 'anonymous' | 'use-credentials' | undefined
- imageStyle: CSSProperties 外层 img 的样式
- options: Cropper.Options 透传 cropperjs 配置
- size: number 输出质量控制;>100 使用 image/jpeg 且质量为 size/1000
- emits
- ready(cropper: Cropper) 裁剪器实例已就绪
- cropend({ imgBase64, imgInfo }) 裁剪结束/变更回调
- cropendError 生成 base64 失败
- 说明
- 当 circled=true,组件内部会将透明背景转白(当 size>100),并以圆形遮罩输出。
CroppperAvatar
- props
- width: string 显示宽度,默认 200px
- value: string 头像地址
- showBtn: boolean 是否显示按钮,默认 true
- btnText: string 按钮文案
- emits
- update:value 用于 v-model:value
- change({ source, data, filename }) 裁剪完成,返回 base64、Blob、文件名
- expose
- open() 打开弹窗
- close() 关闭弹窗
CopperModal(裁剪弹窗)
- props
- srcValue: string 进入弹窗时的默认图片
- circled: boolean 是否圆形裁剪,默认 true
- title: string 弹窗标题
- fileSize: number 选择图片大小限制(MB),默认 5
- fileType: string[] 允许类型,默认 ['image/jpeg','image/png','image/gif']
- emits
- uploadSuccess({ source, data, filename }) 点击“确定”时回调
- expose
- openModal()、closeModal()
CropperImg(占位/预览卡片)
- props
- modelValue: string 图片地址
- disabled: boolean 是否禁用,默认 false
- height: string 默认 150px
- width: string 默认 150px
- borderradius: string 默认 8px
- showDelete: boolean 默认 true
- showBtnText: boolean 默认 true
- emits
- cropperImg() 点击添加/编辑时触发
- deleteImg() 删除时触发
上传接口对接(示例)
- 在 @change 回调中,拿到 data: Blob,通过自有上传方法 useUpload().httpRequest 发送到后端;
- 成功后返回 URL,更新图片资料并关闭裁剪弹窗。
简化示例:
const onChange = async ({ data }) => {
// 1) 通过接口上传 Blob
// const { data: url } = await httpRequest({ file: data, filename: 'avatar.png' })
// 2) 保存到图片资料
// await updateUserProfile({ avatar: url })
// 3) 关闭弹窗
}
常见问题与技巧
-
图片跨域:远程图片需要支持跨域访问,可设置
crossorigin或确保图片服务器正确的 CORS。 -
输出格式与质量:通过文件Size控制。
文件Size > 100→image/jpeg且质量为文件Size/1000;否则输出image/png。
图片要压缩质量,必须使用image/jpeg才有效,当上传的图片为jpeg格式,只能用image/jpeg,否则将会变大
function croppered() {
if (!cropper.value) {
return
}
let imgInfo = cropper.value.getData()
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
canvas.toBlob(
(blob) => {
if (!blob) {
return
}
let fileReader: FileReader = new FileReader()
fileReader.readAsDataURL(blob)
fileReader.onloadend = (e) => {
emit('cropend', {
imgBase64: e.target?.result ?? '',
imgInfo
})
}
fileReader.onerror = () => {
emit('cropendError')
}
},
// 文件Size由父组件传过来
pprops.size > 100 ? 'image/jpeg' : props.type === 'image/jpeg' ? 'image/jpeg' : 'image/png',
props.size > 100 ? props.size / 1000 : 0.5
)
}
使用image/jpeg后,图片透明色会变成黑色,所以需要变白处理
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas()
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')!
const width = sourceCanvas.width
const height = sourceCanvas.height
canvas.width = width
canvas.height = height
context.imageSmoothingEnabled = true
context.drawImage(sourceCanvas, 0, 0, width, height)
context.globalCompositeOperation = 'destination-in'
context.beginPath()
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
context.fill()
if (props.size > 100 || props.type === 'image/jpeg') {
// 将canvas的透明背景设置成白色
var imageData = context.getImageData(0, 0, canvas.width, canvas.height)
for (var i = 0; i < imageData.data.length; i += 4) {
// 当该像素是透明的,则设置成白色
if (imageData.data[i + 3] == 0) {
imageData.data[i] = 255
imageData.data[i + 1] = 255
imageData.data[i + 2] = 255
imageData.data[i + 3] = 255
}
}
context.putImageData(imageData, 0, 0)
}
return canvas
}
-
圆形裁剪:
circled=true时自动生成圆形输出,透明背景会处理为白色(在 jpeg 场景)。 -
高级控制:通过
CropperImage的@ready获取cropper实例,调用cropper.rotate/zoom/scaleX/scaleY/reset等方法。