cropperjs vue3.2 +ts +elment-plus实现图片裁剪上传功能 (复制可用)
特别鸣谢
插件github地址:https://github.com/fengyuanchen/cropper/blob/master/README.md
插件在线演示:https://fengyuanchen.github.io/cropperjs/
我是基于这个作者代码改的:https://blog.csdn.net/weixin_41897680/article/details/123606376
这位哥文章有中文文档:https://blog.csdn.net/weixin_46212682/article/details/122339262
cropperjs上传文件时文件名称问题:https://blog.csdn.net/weixin_43856603/article/details/103993434
最近产品提了一个需求,就是实现图片裁剪上传。去找了几个裁剪插件
最多用的是vue-cropper@next 但是发现有点bug 图片裁剪的时候只能拖动一边
只有这个角落能够拖动 太鸡肋了。
遂换了一个 能够满足需求 cropper.js
接下来看一下我的实现效果
elmentui-plus有公司统一特别定制,所以你们复制代码后弹窗样式可能会不太一样,不过也大差不差
公司上传的文件是二进制文件 如果是其他格式 转码一下即可
npm install cropperjs
<template>
<div>
<el-image class="upload" v-if="imgUrl" :src="imgUrl" fit="contain" @click="openUpload"></el-image>
<el-icon v-else class="avatar-uploader-icon" @click="openUpload"><Plus /></el-icon>
<!-- 上传弹窗 -->
<el-dialog v-model="uploadDialog" :title="title" width="800px" :before-close="handleClose">
<div class="container">
<!-- 左侧裁剪区 -->
<div class="left">
<!-- 大图显示区 -->
<!-- :style="{ 'background-image': 'url(' + imgUrl + ')' }" -->
<div class="big-image-preview">
<img v-if="imgUrl" :src="imgUrl" class="big-image" ref="imageRef" />
<div v-else class="big-image" @click="chooseImage" />
</div>
<div class="tool">
<p>{{ tips }}</p>
<el-button @click="chooseImage">上传图片</el-button>
<el-button @click="chooseImage">更换图片</el-button>
<el-button @click="zoomImage(0.2)">放大</el-button>
<el-button @click="zoomImage(-0.2)">缩小</el-button>
<el-button @click="rotateImage(90)">左转90°</el-button>
<el-button @click="rotateImage(90)">右转90°</el-button>
</div>
</div>
<!-- 右侧预览区 -->
<div class="right">
<!-- 小图预览区域 -->
<div class="right-top">
<div class="image-view"></div>
<div class="view-info">
<div style="margin-bottom: 20px">
<span>比例:</span>
<el-input-number
:controls="false"
v-model="heightScale"
style="width: 50px"
@change="proportionFn"
placeholder="长"
></el-input-number>
-
<el-input-number
:controls="false"
v-model="widthScale"
style="width: 50px; margin-right: 20px"
@change="proportionFn"
placeholder="宽"
></el-input-number>
<el-button @click="proportionCloseFn">X</el-button>
</div>
<div style="margin-bottom: 20px">
<span>宽度:</span>
<el-input-number
v-model="cropperWidth"
:min="1"
@change="setcropperHW"
></el-input-number>
px
</div>
<div>
<span>高度:</span>
<el-input-number
v-model="cropperHeight"
:min="1"
@change="setcropperHW"
></el-input-number>
px
</div>
</div>
</div>
</div>
</div>
<!-- 只用input来实现上传,但是不显示input -->
<input v-show="false" ref="fileRef" type="file" accept="image/png, image/jpeg" @change="getImageInfo" />
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitImage">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { nextTick, ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const props = defineProps({
imgUrl: { // 回显需求
type: String,
default: ''
},
title: { //弹框标题
type: String,
default: '上传图片'
},
tips: { //图片下的提示语
type: String,
default: ''
},
api: { //上传的api
type: String,
default: '/api/apps/common/Index/file'
},
size: { //允许上传最大图片MB
type: Number,
default: 10000
}
})
// 默认显示的图片
const imgUrl = ref(props.imgUrl)
const tips = ref(props.tips)
const isCreate = ref(false)
// 裁剪对象
const cropper = ref(null)
const title = ref(props.title)
const api = ref(props.api)
const size = ref(props.size)
const http = axios.create()
const env = import.meta.env
const info = ref()
const imageRef = ref(null)
const cropperHeight = ref()
const cropperWidth = ref()
const widthScale = ref()
const heightScale = ref()
const uploadDialog = ref(false)
const fileRef = ref(null)
const fileName = ref()
const emit = defineEmits(['upload'])
// 打开弹窗方法
const openUpload = () => {
uploadDialog.value = true
if (imgUrl.value) {
cropper.value.setData(info.value)
}
}
// 关闭弹窗
const handleClose = () => {
uploadDialog.value = false
}
// 选择图片
const chooseImage = () => {
// 当input的type属性值为file时,点击方法可以进行选取文件
fileRef.value.click()
}
// 上传头像
const submitImage = () => {
const cas = cropper.value.getCroppedCanvas()
// const base64url = cas.toDataURL('image/jpeg')
cas.toBlob(function (e) {
imgUrl.value = window.URL.createObjectURL(e)
})
info.value = cropper.value.getData()
uploadDialog.value = false
}
// 上传
const uploadImage = () => {
const ca = cropper.value.getCroppedCanvas()
const formData = new FormData()
ca.toBlob(function (e) {
formData.append('file', e ,fileName.vallue)
http.post(api.value, formData, {
baseURL: env.VITE_BASE_URL, // 基础路径
headers: {
'Content-Type': 'multipart/form-data', // 关键
token: sessionStorage.getItem('token')
}
})
.then((res) => {
if (res.status == 200) {
emit('upload', res.data)
}
})
.catch((error) => {
console.log(error)
})
})
}
// 获取文件信息
const getImageInfo = (e) => {
// 上传的文件
const file = e.target.files[0]
const fileSize = (file.size / 1024).toFixed(2)
if (fileSize > size.value * 1024) {
ElMessage.warning(`'图片大小必须在${size.value}MB以内!'`)
return false
}
fileName.vallue = file.name
// 获取 window 的 URL 工具
const URL = window.URL || window.webkitURL
// 通过 file 生成目标 url
imgUrl.value = URL.createObjectURL(file)
// console.log('图片:', imgUrl);
nextTick(() => {
// 判定裁剪对象是否存在
// 存在销毁重新创建(这里不替换图片,图片不一样大时会变形),不存在创建对象
if (cropper.value) {
cropper.value.destroy()
cropImage()
} else {
cropImage()
}
isCreate.value = true
})
}
// 裁剪图片
const cropImage = () => {
if (imageRef.value) {
cropper.value = new Cropper(imageRef.value, {
// 宽高比
aspectRatio: widthScale.value / heightScale.value, //设置裁剪框为固定的宽高比
// initialAspectRatio: [100, 200],
viewMode: 1,
// 预览
preview: '.image-view',
// cropBoxResizable: false,
background: false,
crop(event) {
cropperHeight.value = parseInt(event.detail.height)
cropperWidth.value = parseInt(event.detail.width)
}
})
isCreate.value = true
}
}
// 旋转
const rotateImage = (angle) => {
if (isCreate.value) {
cropper.value.rotate(angle)
}
}
// 缩放
const zoomImage = (num) => {
if (isCreate.value) {
cropper.value.zoom(num)
}
}
const setcropperHW = () => {
if (isCreate.value) {
cropper.value.setData({
height: cropperHeight.value ? +cropperHeight.value : 0,
width: cropperWidth.value ? +cropperWidth.value : 0
})
}
}
const proportionFn = () => {
if (isCreate.value) {
if (widthScale.value && heightScale.value) {
cropper.value.destroy()
cropImage()
} else if (!widthScale.value && !heightScale.value) {
cropper.value.destroy()
cropImage()
}
}
}
const proportionCloseFn = () => {
if (isCreate.value) {
widthScale.value = null
heightScale.value = null
cropper.value.destroy()
cropImage()
}
}
defineExpose({
uploadImage
})
</script>
<style scoped lang="scss">
//上传的基本样式
.upload {
width: 142px;
height: 142px;
// border: 5px solid #eeeeee;
box-sizing: border-box;
cursor: pointer;
background-position: center center;
background-size: 100%;
border-radius: 6px;
background: #eee;
}
//hover的基本样式
.base-hover {
position: absolute;
width: 100%;
height: 100%;
content: '更换头像';
background: black;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.6;
}
.container {
width: 800px;
height: 500px;
display: flex;
margin: 20px 20px 0px;
.left {
width: 65%;
height: 100%;
.big-image-preview {
width: 100%;
height: 85%;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center center;
border: 1px solid #999;
}
.tool {
width: 100%;
height: 15%;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0px 10px;
cursor: pointer;
}
}
.big-image {
width: 100%;
height: 100%;
display: block;
max-width: 100%;
}
}
.right {
width: 240px;
height: 100%;
font-size: 14px;
.right-top {
width: 100%;
height: 70%;
display: flex;
flex-direction: column;
align-items: center;
.image-view {
// border: 1px solid gray;
overflow: hidden;
min-width: 200px;
height: 200px;
}
.view-info {
position: absolute;
top: 340px;
}
}
.right-bottom {
width: 100%;
height: 30%;
display: flex;
flex-direction: column-reverse;
align-items: center;
}
}
}
:deep(.el-icon.avatar-uploader-icon) {
font-size: 28px;
color: #8c939d;
width: 142px;
height: 142px;
text-align: center;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
}
:deep(.cropper-point.point-se) {
height: 5px;
width: 5px;
}
</style>
引用
<template>
<image-cropper ref="imageCropperRef" @upload="uploadBack"></image-cropper>
<el-button @click="upload">上传</el-button>
</template>
<script setup>
import { ref } from 'vue'
import imageCropper from 'base/src/components/imageCropper.vue'
const imageCropperRef = ref(null)
const upload = () => {
imageCropperRef.value.uploadImage() //上传钩子
}
const uploadBack = (val) => {
console.log(val) //上传完成回传
}
</script>
<style scoped lang="scss"></style>