共计 9632 个字符,预计需要花费 25 分钟才能阅读完成。
文章目录
- 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. 在代码中使用
注意事项:
要为
组件设置宽和高,并用一个外层容器包裹
组件
4.1 template 部分
vue-cropper
class="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',
autoCropWidth: '240px',
canMove: 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',
infoTrue: true,
mode: 'contain',
origin: false,
outputSize: 1,
outputType: 'png',
})
const realTime = (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 ResultObject> updateAvatar(@RequestParam("avatar") MultipartFile avatar) {
System.err.println("文件名:" + avatar.getOriginalFilename());
System.err.println("文件大小(KB):" + avatar.getSize() / 1024);
try {
String fileName = UUID.randomUUID().toString();
String suffix = Objects.requireNonNull(avatar.getOriginalFilename()).substring(avatar.getOriginalFilename().lastIndexOf("."));
avatar.transferTo(new File("F:Blogcrop-avatar" + fileName + suffix));
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
return Result.success();
}
}
6.2 Result.java
import java.io.Serializable;
public class ResultT> implements Serializable {
private Integer code;
private String message;
private T data;
public static T> ResultT> success() {
ResultT> result = new Result>();
result.code = 200;
result.message = "success";
return result;
}
public static T> ResultT> success(T object) {
ResultT> result = new Result>();
result.data = object;
result.code = 200;
result.message = "success";
return result;
}
public static T> ResultT> fail(String message) {
ResultT> 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;
}
@Override
public 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-cropper
class="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>
input
id="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',
autoCropWidth: '240px',
canMove: true,
canScale: true,
centerBox: true,
fixed: true,
fixedBox: true,
fixedNumber: [1, 1],
img: 'https://img1.baidu.com/it/u=3450282427,2041051230&fm=253',
info: false,
infoTrue: true,
mode: 'contain',
origin: false,
outputSize: 1,
outputType: 'png',
})
const realTime = (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]
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 data
if (typeof event.target.result === 'object') {
data = window.URL.createObjectURL(new Blob([event.target.result]))
} else {
data = event.target.result
}
option.value.img = data
}
fileReader.readAsArrayBuffer(file)
}
const handleUploadAvatar = () => {
input.value.click()
}
const getPreviewStyle = computed(() => {
return {
'width': previews.value.w + 'px',
'height': previews.value.h + 'px',
'overflow': 'hidden',
}
})
const rotateLeft = () => {
cropper.value.rotateLeft()
}
const rotateRight = () => {
cropper.value.rotateRight()
}
const changeScale = (scaleSize) => {
cropper.value.changeScale(scaleSize)
}
const getBase64 = () => {
cropper.value.getCropData((base64) => {
previewBase64.value = base64
console.log('previewBase64 =', previewBase64.value)
})
}
const getBlob = () => {
cropper.value.getCropBlob((blob) => {
previewBlob.value = blob
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'
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
原文地址: Vue3 cropper 实现裁剪头像的功能(裁剪效果可实时预览、预览图可下载、预览图可上传到 SpringBoot 后端、附完整的示例代码和源代码)
正文完