概述
本文档介绍如何在vue3+ts的芋道源码后台管理项目中使用基于 cropperjs
的图片裁剪能力,封装为以下组件,满足头像裁剪、图片裁剪预览、弹窗裁剪、配合上传等场景。
图片要压缩质量,必须使用image/jpeg
才有效,使用image/jpeg
后,图片透明色会变成黑色,所以需要变白处理。
- 导出组件
-
CropperImage
: 基础裁剪核心组件 -
CropperAvatar
: 头像裁剪 + 弹窗选择上传的完整流程 -
CropperImg
: 简易图片占位/预览卡片,触发外部裁剪逻辑 -
CopperModal
: 内部使用的裁剪弹窗(可独立使用)
从 @/components/Cropper
引入:
javascript
import { CropperImage, CropperAvatar, CropperImg } from '@/components/Cropper'
快速开始
场景一:用户头像裁剪(推荐)
内置完整流程:选择图片 → 弹窗裁剪 → 确认后返回裁剪结果(二进制 Blob
与 base64)。
xml
<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
,适用于需要自定义触发/自定义外观的页面。
xml
<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
实时获取裁剪结果。
xml
<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
显示占位/预览,并通过事件让外部打开裁剪弹窗或路由到裁剪页。
ini
<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,更新图片资料并关闭裁剪弹窗。
简化示例:
javascript
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
才有效
javascript
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有父组件传过来
props.size > 100 ? 'image/jpeg' : 'image/png',
props.size / 1000
)
}
使用image/jpeg
后,图片透明色会变成黑色,所以需要变白处理
ini
// 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)
if (props.size > 100) {
// 将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)
}
context.globalCompositeOperation = 'destination-in'
context.beginPath()
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
context.fill()
return canvas
}
-
圆形裁剪 :
circled=true
时自动生成圆形输出,透明背景会处理为白色(在 jpeg 场景)。 -
高级控制 :通过
CropperImage
的@ready
获取cropper
实例,调用cropper.rotate/zoom/scaleX/scaleY/reset
等方法。