需求:固定裁剪框宽高、传入图片只能是 JPG/PNG/webp 格式、大小不能超过 2MB
效果:
一:vue-cropper
安装
npm install vue-cropper // npm 安装
yarn add vue-cropper // yarn 安装
源文档 —— 看配置
vue-cropper仓库https://github.com/xyxiao001/vue-cropper?tab=readme-ov-file
imgCropper.vue组件
- option.img是父组件传入的
- 有些默认开启的功能就没重复写了(一定要看文档!!!)
- 如果需要的话,可以使用回调方法
@realTime
实现实时预览事件, - 参考文档:vue3 vue-cropper实现图片裁剪+上传功能(组件封装使用)https://juejin.cn/post/7316811315590135827?searchId=202406041026302203CEDB599689117731
<template>
<div class="wrap">
<div class="cropper-box">
<div class="cropper">
<vueCropper
ref="cropper"
:img="option.img"
:outputSize="option.size"
:outputType="option.outputType"
:info="option.info"
:autoCrop="option.autoCrop"
:autoCropWidth="option.autoCropWidth"
:autoCropHeight="option.autoCropHeight"
:fixedBox="option.fixedBox"
:centerBox="option.centerBox"
/>
</div>
</div>
<div class="action-box">
<el-button type="primary" plain @click="clearImgHandle">清除图片</el-button>
<el-button type="primary" plain @click="rotateLeftHandle">左旋转</el-button>
<el-button type="primary" plain @click="rotateRightHandle">右旋转</el-button>
<el-button type="primary" plain @click="changeScaleHandle(1)">放大</el-button>
<el-button type="primary" plain @click="changeScaleHandle(-1)">缩小</el-button>
<el-button type="primary" @click="finish" :loading="loading">确认</el-button>
</div>
</div>
</template>
<script setup>
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import { reactive, ref, watch, onBeforeMount } from 'vue'
const option = reactive({
img: '',
info: true, // 裁剪框的大小信息
outputType: 'jpg', // 裁剪生成图片的格式
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 345, // 默认生成截图框宽度
autoCropHeight: 150, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
centerBox: false // 截图框是否被限制在图片里面
})
const props = defineProps({
img: String
})
// 防止重复提交
const loading = ref(false)
// 截图后的图片地址
const previewImg = ref('')
const cropper = ref()
onBeforeMount(() => {
option.img = props.img
})
watch(
() => props.img,
(newValue) => {
option.img = newValue
}
)
const emit = defineEmits(['finish'])
const finish = () => {
// 获取截图的 blob 数据
cropper.value.getCropBlob((blob) => {
previewImg.value = blob
console.log('blob', previewImg.value)
emit('finish', previewImg.value)
})
}
// 放大/缩小
const changeScaleHandle = (num) => {
num = num || 1
cropper.value.changeScale(num)
}
// 左旋转
const rotateLeftHandle = () => {
cropper.value.rotateLeft()
}
// 右旋转
const rotateRightHandle = () => {
cropper.value.rotateRight()
}
// 清理图片
const clearImgHandle = () => {
option.img = ''
}
</script>
<style scoped lang="scss">
.wrap {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
.cropper-box {
display: flex;
justify-content: center;
width: 100%;
.cropper {
width: 100%;
height: 300px;
}
}
.action-box {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
}
</style>
二、uploadCropper.vue组件
前置条件:安装 Element-plus、TailwindCSS
运行流程:
- el-upload选择图片,在on-change事件中对图片格式、大小进行限制,并且将该图片传给imgCropper组件
- imgCropper组件接收到图片、打开裁剪框、点击确认后,触发uploadCropper.vue的finishEvent事件,收到裁剪后的Blob格式的图片
- el-upload只能接收file格式的图片,所以使用convertBlobToFile方法将blob转换成file对象,关闭裁剪框、调用httpRequest方法将图片传给后端
- uploadCropper.vue会通过getFileData将所有图片的信息暴露给使用组件,使用组件可以通过data传入图片id
注意事项:
- 我所使用的接口在接收图片后会返回fileId,拿图片也是通过fileId,所以
.then
里的内容仅供参考,自行调整
<template>
<div class="uCPage">
<div class="flex">
<!-- element 上传图片按钮 -->
<el-upload
class="uploader"
:limit="9"
:auto-upload="false"
:on-change="handleChangeUpload"
:file-list="files"
:on-preview="onActivatefile"
list-type="picture-card"
ref="elementUpload"
>
<el-icon class="uploader-icon"><Plus /></el-icon>
</el-upload>
</div>
<div id="fullscreen">
<el-image-viewer
v-if="isPreview"
:url-list="[previewImg]"
:hide-on-click-modal="true"
@close="isPreview = false"
fit="fill"
/>
</div>
<el-dialog
width="600px"
title="图片剪裁"
v-model="dialogVisible"
class="cropDialog"
append-to-body
@close="beforeClose"
destroy-on-close
:close-on-click-modal="false"
>
<imgCropper :img="imageUrl" @finish="finishEvent" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, nextTick, computed, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import imgCropper from './imgCropper.vue'
import { getToken } from '@/utils'
import axios from 'axios'
import { getFileUrl } from '@/api/file/index'
const props = defineProps({
data: String
})
//api请求基础路径
const PATH_URL2 = import.meta.env.VITE_UPLOADURL
// action 图片上传的地址
const uploadUrl = ref(PATH_URL2 + 'member/core/sysFile/upload')
const elementUpload = ref()
// el-upload文件
const files = ref([])
// 裁剪框显隐
const dialogVisible = ref(false)
// 进入裁剪的图片
const imageUrl = ref()
// 上传服务器后图片的信息
const fileInfo = reactive({
fileId: '',
showUrl: '',
fileName: ''
})
// 获取token
const headers = computed(() => {
let token = getToken()
if (token) {
return { Authorization: token }
}
return {}
})
// 将裁剪后的图片上传至服务器
const httpRequest = async (data) => {
const formData = new FormData()
formData.append('file', data)
await axios({
headers: headers.value,
url: uploadUrl.value,
method: 'post',
data: formData
}).then((res) => {
if (res.data.code == 200) {
fileInfo.showUrl = getFileUrl(res.data?.data[0].fileId) || ''
fileInfo.fileId = res.data?.data[0].fileId || ''
previewList.value.push(fileInfo.showUrl)
files.value.push({ name: fileInfo.fileName, url: fileInfo.showUrl, fileId: fileInfo.fileId })
console.log(fileInfo)
console.log(files.value, previewList.value)
}
})
}
//选择图片
const handleChangeUpload = (file, fileList) => {
const isJPG =
file.raw.type === 'image/jpeg' ||
file.raw.type === 'image/png' ||
file.raw.type === 'image/webp'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
ElMessage.error('上传头像图片只能是 JPG/PNG/webp 格式!')
files.value.splice(fileList.indexOf(file), 1)
return false
}
if (!isLt2M) {
ElMessage.error('上传头像图片大小不能超过 2MB!')
files.value.splice(fileList.indexOf(file), 1)
return false
}
// 在截图成功前,不显示略缩图
files.value.splice(fileList.indexOf(file), 1)
fileInfo.fileName = file.raw.name
// 上传成功后将图片地址赋值给裁剪框显示图片
nextTick(() => {
imageUrl.value = URL.createObjectURL(file.raw)
dialogVisible.value = true
})
}
// 关闭弹窗
const beforeClose = () => {
dialogVisible.value = false
}
// 预览图片
const previewImg = ref('')
const previewList = ref([])
const isPreview = ref(false)
// 点击预览
const onActivatefile = (file) => {
isPreview.value = true
previewImg.value = file.url
}
// blob转file
const convertBlobToFile = (blob, fileName) => {
// 创建File对象
const file = new File([blob], fileName, { type: blob.type })
return file
}
const finishEvent = (blob) => {
// blob 转file
const fileName = fileInfo.fileName
const fileUrl = convertBlobToFile(blob, fileName)
console.log('file', fileUrl)
dialogVisible.value = false
httpRequest(fileUrl)
}
// 使用时可调用getFileData拿到所有图片
const getFileData = () => {
return JSON.stringify(files.value)
}
defineExpose({ getFileData })
watch(
() => props.data,
(newVal) => {
files.value = []
if (!props.data) return
console.log(newVal)
newVal.forEach((el) => {
fileInfo.fileId = el.fileId
fileInfo.showUrl = getFileUrl(el.fileId)
files.value.push({ name: el.fileName, url: fileInfo.showUrl, fileId: el.fileId })
})
console.log(files.value)
},
{
immediate: true
}
)
</script>
<style scoped lang="less">
.uCPage {
.uploader {
.uploader-icon {
width: 100%;
height: 100%;
}
}
.cropDialog {
display: flex;
flex-direction: column;
}
}
// 控制组件宽高——没图片
:deep(.el-upload--picture-card) {
width: 223px;
height: 100px;
}
// 控制组件宽高——有图片
:deep(.el-upload-list__item) {
width: 223px;
height: 100px;
}
:deep(.el-dialog) {
width: 600px;
min-height: 500px;
}
</style>
三、使用
<template>
<UploadCropper ref="fileRef" :data="upf" />
</template>
<script lang="ts" setup>
import UploadCropper from './component/UploadCropper.vue'
//传入图片id
const upf = ref([])
const fileRef = ref()
//省略获取upf
</script>