文章目录
- 0. 前言
- 1. 裁剪效果(可实时预览)
- 2. 安装 cropper
- 3. 引入 Vue Cropper
- 3.1 局部引入(推荐使用)
- 3.2 全局引入
- 4. 在代码中使用
- 4.1 template部分
- 4.2 script部分
- 5. 注意事项
- 6. SpringBoot 后端接收图片
- 6.1 UserController.java
- 6.2 Result.java
- 7. 完整的示例代码
- 7.1 Homeview.vue
- 7.2 request.js
- 7.3 main.js
- 7.4 vite.config.js
- 8. 完整的源代码
0. 前言
裁剪头像的需求十分常见,主要目的是为了统一用户头像的尺寸,避免因为用户上传的图片尺寸大小不一致导致页面布局出现问题
高效实现需求的方法,就是避免重复造轮子,在这里推荐使用 cropper 实现头像裁剪功能 (原因是 cropper 功能强大、上手简单、文档详细)
cropper 的Gitee地址:vue-cropper
cropper Vue3在线示例:cropper Vue3在线示例
1. 裁剪效果(可实时预览)
2. 安装 cropper
# npm 安装
npm install vue-cropper@next
# yarn 安装
yarn add vue-cropper@next
3. 引入 Vue Cropper
3.1 局部引入(推荐使用)
哪个组件需要使用 Vue Cropper,就在哪个组件导入
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
3.2 全局引入
main.js 文件
import VueCropper from 'vue-cropper'
import 'vue-cropper/dist/index.css'const app = createApp(App)
app.use(VueCropper)
app.mount('#app')
4. 在代码中使用
注意事项:
要为
<vue-cropper></vue-cropper>
组件设置宽和高,并用一个外层容器包裹<vue-cropper></vue-cropper>
组件
4.1 template部分
<vue-cropperclass="crop"ref="cropper":autoCrop="option.autoCrop":autoCropHeight="option.autoCropHeight":autoCropWidth="option.autoCropWidth":canMove="option.canMove":canScale="option.canScale":centerBox="option.centerBox":fixed="option.fixed":fixedBox="option.fixedBox":fixedNumber="option.fixedNumber":img="option.img":info-true="option.infoTrue":mode="option.mode":origin="option.origin":outputSize="option.outputSize":outputType="option.outputType"@realTime="realTime"
></vue-cropper>
4.2 script部分
const option = ref({autoCrop: true, // 是否默认生成截图框autoCropHeight: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25canMove: true, // 上传图片是否可以移动canScale: true, // 图片是否允许滚轮缩放centerBox: true, // 截图框是否被限制在图片里面fixed: true, // 是否固定截图框的宽高比例fixedBox: true, // 是否固定截图框大小fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])img: 'https://img2.baidu.com/it/u=2339635883,2403687892&fm=253&fmt=auto&app=138&f=JPEG', // 裁剪图片的地址(可选值:url 地址, base64, blob)infoTrue: true, // infoTrue为 true 时显示预览图片的宽高信息,infoTrue为 false 时表示显示裁剪框的宽高信息mode: 'contain', // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)origin: false, // 上传的图片是否按照原始比例渲染outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)outputType: 'png', // 裁剪生成图片的格式(可选值:png, jpeg, webp)
})// 实时预览
const realTime = (data) => {// console.log('realTime data =', data)previews.value = data
}
5. 注意事项
- cropper 对象的 getCropBlob 方法和 getCropData 方法都是异步方法
- 虽然 getCropBlob 获取的的 Blob 对象在控制台打印时只有 size 和 type 属性,但是仍然可以使用
window.URL.createObjectURL(blob)
来生成 url ,从 Java 的角度来说,相当于重写了 Blob 类的 toString 方法 - 前端用 formData 上传文件时, key 要与后端接口中 @RequestParam(“avatar”) 指定的参数名一致
6. SpringBoot 后端接收图片
后端环境:
- JDK:17.0.7
- SpringBoot:3.0.2
6.1 UserController.java
import cn.edu.scau.controller.vo.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;@RestController
@RequestMapping("/user")
public class UserController {@PostMapping("/updateAvatar")public Result<Object> updateAvatar(@RequestParam("avatar") MultipartFile avatar) {System.err.println("文件名:" + avatar.getOriginalFilename());System.err.println("文件大小(KB):" + avatar.getSize() / 1024);try {// 拿到图片文件后,可以将图片上传到阿里云、腾讯云、minio等第三方存储服务,然后返回图片的访问地址// 这里直接保存到本地String fileName = UUID.randomUUID().toString();String suffix = Objects.requireNonNull(avatar.getOriginalFilename()).substring(avatar.getOriginalFilename().lastIndexOf("."));avatar.transferTo(new File("F:\\Blog\\crop-avatar\\" + fileName + suffix));} catch (IOException ioException) {throw new RuntimeException(ioException);}return Result.success();}}
6.2 Result.java
import java.io.Serializable;/*** 后端统一返回结果** @param <T>*/
public class Result<T> implements Serializable {private Integer code;private String message;private T data;public static <T> Result<T> success() {Result<T> result = new Result<>();result.code = 200;result.message = "success";return result;}public static <T> Result<T> success(T object) {Result<T> result = new Result<>();result.data = object;result.code = 200;result.message = "success";return result;}public static <T> Result<T> fail(String message) {Result<T> result = new Result<>();result.message = message;result.code = 500;return result;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}@Overridepublic String toString() {return "Result{" +"code=" + code +", message='" + message + '\'' +", data=" + data +'}';}}
7. 完整的示例代码
7.1 Homeview.vue
<template><div class="wrapper"><div class="blank-line"></div><div class="top"><p class="title">裁剪头像</p></div><div class="blank-line"></div><div class="main"><div class="crop-container"><vue-cropperclass="crop"ref="cropper":autoCrop="option.autoCrop":autoCropHeight="option.autoCropHeight":autoCropWidth="option.autoCropWidth":canMove="option.canMove":canScale="option.canScale":centerBox="option.centerBox":fixed="option.fixed":fixedBox="option.fixedBox":fixedNumber="option.fixedNumber":img="option.img":info="option.info":info-true="option.infoTrue":mode="option.mode":origin="option.origin":outputSize="option.outputSize":outputType="option.outputType":rounded="true"@realTime="realTime"></vue-cropper><inputid="input"ref="input"type="file"accept="image/png, image/jpeg, image/gif, image/jpg"@change="uploadAvatar($event)"v-show="false"><div class="action-buttons"><el-button :size="'default'" type="primary" @click="handleUploadAvatar">上传图片</el-button><el-button :size="'default'" type="danger" plain :icon="ZoomIn" @click="changeScale(1)">放大(向上滚动鼠标滑轮)</el-button><el-button :size="'default'" type="danger" plain :icon="ZoomOut" @click="changeScale(-1)">缩小(向下滚动鼠标滑轮)</el-button><el-button :size="'default'" type="primary" @click="rotateLeft">向左旋转</el-button><el-button :size="'default'" type="primary" @click="rotateRight">向右旋转</el-button><el-button :size="'default'" type="primary" @click="downloadPreView">下载预览图</el-button><el-button :size="'default'" type="primary" @click="updateAvatar">确定修改</el-button></div></div><div class="preview-container"><div><p class="preview-title">实时预览</p></div><div :style="getPreviewStyle"><div :style="previews.div"><img :src="previews.url" :style="previews.img" alt="" class="preview-img"></div></div></div></div></div>
</template><script setup>
import 'vue-cropper/dist/index.css'
import {VueCropper} from 'vue-cropper'
import {computed, ref} from 'vue'
import {ElMessage} from 'element-plus'
import {ZoomIn, ZoomOut} from '@element-plus/icons-vue'
import request from '@/util/request.js'const previews = ref({})
const previewBlob = ref()
const previewBase64 = ref()const cropper = ref()
const input = ref()
const option = ref({autoCrop: true, // 是否默认生成截图框autoCropHeight: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25canMove: true, // 上传图片是否可以移动canScale: true, // 图片是否允许滚轮缩放centerBox: true, // 截图框是否被限制在图片里面fixed: true, // 是否固定截图框的宽高比例fixedBox: true, // 是否固定截图框大小fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])img: 'https://img1.baidu.com/it/u=3450282427,2041051230&fm=253', // 裁剪图片的地址(可选值:url 地址, base64, blob)info: false, // 是否显示裁剪框的宽高信息infoTrue: true, // infoTrue为 true 时裁剪框显示的是预览图片的宽高信息,infoTrue为 false 时裁剪框显示的是裁剪框的宽高信息mode: 'contain', // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)origin: false, // 上传的图片是否按照原始比例渲染outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)outputType: 'png', // 裁剪生成图片的格式(可选值:png, jpeg, webp)
})// 实时预览
const realTime = (data) => {// console.log('realTime data =', data)previews.value = data
}const downloadPreView = () => {let aLink = document.createElement('a')aLink.download = '预览图.png'cropper.value.getCropBlob((blob) => {aLink.href = window.URL.createObjectURL(blob)aLink.click()})
}const uploadAvatar = (event) => {let file = event.target.files[0]// console.log('uploadAvatar file=', file)if (!/\.(gif|jpg|jpeg|png|bmp)$/i.test(event.target.value)) {ElMessage.error('图片类型必须是.gif、jpeg、jpg、png、bmp中的一种')return false}let fileReader = new FileReader()fileReader.onload = (event) => {let dataif (typeof event.target.result === 'object') {// 把 Array Buffer 转化为 blobdata = window.URL.createObjectURL(new Blob([event.target.result]))} else {// 如果是 base64 ,不需要转换data = event.target.result}option.value.img = data}// 转化为base64// fileReader.readAsDataURL(file)// 转化为blobfileReader.readAsArrayBuffer(file)
}const handleUploadAvatar = () => {input.value.click()
}const getPreviewStyle = computed(() => {return {'width': previews.value.w + 'px','height': previews.value.h + 'px','overflow': 'hidden',// 'border-radius': '50%'}
})const rotateLeft = () => {cropper.value.rotateLeft()
}const rotateRight = () => {cropper.value.rotateRight()
}const changeScale = (scaleSize) => {cropper.value.changeScale(scaleSize)
}// 注意:getCropData是一个异步方法
const getBase64 = () => {cropper.value.getCropData((base64) => {previewBase64.value = base64console.log('previewBase64 =', previewBase64.value)})
}// 注意:getCropBlob是一个异步方法
const getBlob = () => {cropper.value.getCropBlob((blob) => {previewBlob.value = blob// 虽然 getCropBlob 方法获取的的 Blob 对象在控制台打印时只有 size 和 type 属性,但是仍然可以使用 window.URL.createObjectURL(blob) 生成 url// 从 Java 的角度来说,相当于重写了 Blob 类的 toString 方法console.log('previewBlob =', previewBlob.value)})
}const updateAvatar = async () => {cropper.value.getCropBlob((blob) => {let avatar = new File([blob], 'avatar.png')let formData = new FormData()formData.append('avatar', avatar)request.post('/user/updateAvatar', formData, {headers: {'Content-Type': 'multipart/form-data'}}).then((response) => {if (response.code === 200) {ElMessage.success('修改头像成功')} else {ElMessage.error('修改头像失败')}}).catch((error) => {console.log('error =', error)ElMessage.error('修改头像失败')})})
}
</script><style scoped>
.title {font-size: 40px;text-align: center;
}.main {display: flex;justify-content: space-around;
}.crop {width: 925px;height: 500px;
}.action-buttons {display: flex;justify-content: space-between;margin-top: 10px;
}.blank-line {height: 20px;width: 100%;
}.preview-img {border: 5px solid black;
}.preview-title {font-size: 20px;margin-bottom: 10px;text-align: center;
}
</style>
7.2 request.js
import axios from 'axios'const request = axios.create({baseURL: '/api',timeout: 60000,headers: {'Content-Type': 'application/json;charset=UTF-8'}
})request.interceptors.request.use()request.interceptors.response.use(response => {if (response.data) {return response.data}return response
}, (error) => {return Promise.reject(error)
})export default request
7.3 main.js
import '@/assets/main.css'import {createApp} from 'vue'
import {createPinia} from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'import App from './App.vue'
import router from './router'
import 'default-passive-events'const app = createApp(App)app.use(createPinia())
app.use(ElementPlus, {locale: zhCn})
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}
app.use(router)app.mount('#app')
7.4 vite.config.js
import {fileURLToPath, URL} from 'node:url'import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},server: {proxy: {'/api': {target: 'http://localhost:8001',changeOrigin: true,rewrite: (path) => {return path.replace('/api', '')}}}}
})
8. 完整的源代码
前端:cropper-avatar-frontend
后端:cropper-avatar-backend