Vue3 + cropper 实现裁剪头像的功能(裁剪效果可实时预览、预览图可下载、预览图可上传到SpringBoot后端、附完整的示例代码和源代码)

文章目录

  • 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. 注意事项

  1. cropper 对象的 getCropBlob 方法和 getCropData 方法都是异步方法
  2. 虽然 getCropBlob 获取的的 Blob 对象在控制台打印时只有 size 和 type 属性,但是仍然可以使用window.URL.createObjectURL(blob)来生成 url ,从 Java 的角度来说,相当于重写了 Blob 类的 toString 方法
  3. 前端用 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/390290.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年8月AI内容生成技术的现状与未来:从文生文到跨模态交互的全景分析

2024年8月AI内容生成技术的现状与未来&#xff1a;从文生文到跨模态交互的全景分析 大家好&#xff0c;我是猫头虎&#xff01;&#x1f680; 随着AI在内容生成领域的爆发式发展&#xff0c;从2022年末开始&#xff0c;AI生成技术已经走过了文生文&#xff08;AIGC&#xff09…

空气净化器对去除宠物毛有效吗?小型猫毛空气净化器使用感受

作为一个养猫多年的猫奴&#xff0c;家里有两只可爱的小猫咪&#xff1a;小白和小花。虽然相处起来很开心&#xff0c;但也给生活带来了一些小麻烦。谁懂啊&#xff0c;我真的受够了&#xff0c;每天都在粘毛。猫窝的猫毛一周不清理就要堆成山&#xff0c;空气中也全是浮毛&…

使用 Kibana 和 Vega 构建高级可视化

作者&#xff1a;来自 Carly Richmond 为了解释在 Kibana 中构建 Vega 可视化的基础知识&#xff0c;我将使用此 GitHub 存储库中的 2 个示例。具体来说&#xff0c;我将介绍&#xff1a; 使用 Elasticsearch 聚合进行数据采购轴和标记事件和信号&#xff08;例如工具提示和更…

计算机基本理论与程序运行原理概述

目录 计算机的基本表示方法 计算机的组成 程序运行的原理 指令执行的流水线 编译原理 个人理解 面试题总结 计算机的基本表示方法 计算机系统使用高、低电平来表示逻辑1和0。数据在计算机中的存储、传输和处理均以二进制形式进行。数据通过总线作为电信号进行传输&…

决策树总结

决策树是什么&#xff1f;决策树(decision tree)是一种基本的分类与回归方法。举个通俗易懂的例子&#xff0c;如下图所示的流程图就是一个决策树&#xff0c;长方形代表判断模块(decision block)&#xff0c;椭圆形成代表终止模块(terminating block)&#xff0c;表示已经得出…

用于仅摄像头闭环驾驶的视觉语言模型

CarLLaVA: Vision language models for camera-only closed-loop driving 用于仅摄像头闭环驾驶的视觉语言模型 Abstract In this technical report, we present CarLLaVA, a Vision Language Model (VLM) for autonomous driving, developed for the CARLA Autonomous Driv…

kafka从浅入深

一、什么是kafka&#xff1f; kafka本质上是一个消息队列MQ&#xff08;Message Queue&#xff09;&#xff0c;用做数据流转。 1.使用消息队列的好处&#xff1f; 1.1、解耦&#xff1a;允许独立扩展或修改队列两头的处理过程&#xff1b; 1.2、可恢复性&#xff1a;即使一个…

中文网址导航模版HaoWa1.3.1/模版网站wordpress导航主题

HaoWa v1.3.1由挖主题开发的一款网址导航类主题。 HaoWA主题除主体导航列表外&#xff0c;对主题所需的小模块都进行了开放式的HTML编辑器形式的功能配置&#xff0c;同时预留出默认的代码结构&#xff0c;方便大家在现有的代码结构上进行功能调整。 同时加入了字体图标Font …

鸿蒙系统学习指南

&#x1f41f;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢编程&#x1fab4; &#x1f421;&#x1f419;个人主页&#x1f947;&#xff1a;Aic山鱼 &#x1f420;WeChat&#xff1a;z7010cyy &#x1f988;系列专栏&#xff1a;&#x1f3de;️ 前端-JS基础专栏✨前…

【全国大学生电子设计竞赛】2022年F题

&#x1f970;&#x1f970;全国大学生电子设计大赛学习资料专栏已开启&#xff0c;限时免费&#xff0c;速速收藏~

深入理解同城代驾系统源码:技术架构与实现细节

今天&#xff0c;小编将深入讲解同城代驾系统的技术架构与实现细节。 一、同城代驾系统的基本功能模块 一个完整的同城代驾系统通常包括以下核心功能模块&#xff1a; 1.用户端应用 2.司机端应用 3.后台管理系统 4.消息推送与通知 二、技术架构设计 同城代驾系统的技术架…

《Transformer Scale Gate for Semantic Segmentation》CVPR2023

摘要 论文提出了一种名为Transformer Scale Gate&#xff08;TSG&#xff09;的模块&#xff0c;用于优化在语义分割任务中多尺度上下文信息的编码。现有的基于Transformer的分割模型在组合不同尺度的特征时没有进行选择&#xff0c;这可能导致次优尺度的特征降低分割的准确性…

Linux常用命令全

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 ☁️运维工程师的职责&#xff1a;监…

吴恩达机器学习C1W2Lab05-使用Scikit-Learn进行线性回归

前言 有一个开源的、商业上可用的机器学习工具包&#xff0c;叫做scikit-learn。这个工具包包含了你将在本课程中使用的许多算法的实现。 目标 在本实验中&#xff0c;你将: 利用scikit-learn实现使用梯度下降的线性回归 工具 您将使用scikit-learn中的函数以及matplotli…

大模型在RPA领域的应用与探索-代码生成

01. 前言 随着人工智能技术的飞速发展&#xff0c;大模型在多个领域的应用日益广泛。特别是在机器人流程自动化&#xff08;RPA&#xff09;领域&#xff0c;这些技术的进步为自动化任务的执行带来了显著的效率提升。然而&#xff0c;传统RPA在任务流程编排上依赖人工编写脚本…

RabbitMQ高级特性 - 事务消息

文章目录 RabbitMQ 事务消息概述实现原理代码实现不采用事务采用事务 RabbitMQ 事务消息 概述 RabbitMQ 的 AMQP 协议实现了事务机制&#xff0c;允许开发者保证消息的发送和接收时原子性的&#xff0c;也就是说&#xff0c;要么消息全都发送成功&#xff0c;要么全都发送失败…

【秋招笔试】24-07-27-OPPO-秋招笔试题(算法岗)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 💡 第一题贪心模拟…

掌握SEO站外推广优化的五大绝招

对于网站运营者和数字营销人员来说&#xff0c;SEO站外推广是提升网站流量和排名的重要手段。以下是五个有效的SEO站外推广优化方法&#xff0c;希望对大家有所帮助。 1. 高质量的外链建设 高质量的外部链接&#xff08;Backlinks&#xff09;是搜索引擎排名的重要因素之一。…

【Docker系列】Docker 镜像管理:删除无标签镜像的技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

前端面试:八股文系列(一)

更多详情&#xff1a;爱米的前端小笔记&#xff08;csdn~xitujuejin~zhiHu~Baidu~小红shu&#xff09;同步更新&#xff0c;等你来看&#xff01;都是利用下班时间整理的&#xff0c;整理不易&#xff0c;大家多多&#x1f44d;&#x1f49b;➕&#x1f914;哦&#xff01;你们…