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

文章目录

  • [0. 前言](#0. 前言)
  • [1. 裁剪效果(可实时预览)](#1. 裁剪效果(可实时预览))
  • [2. 安装 cropper](#2. 安装 cropper)
  • [3. 引入 Vue Cropper](#3. 引入 Vue Cropper)
    • [3.1 局部引入(推荐使用)](#3.1 局部引入(推荐使用))
    • [3.2 全局引入](#3.2 全局引入)
  • [4. 在代码中使用](#4. 在代码中使用)
    • [4.1 template部分](#4.1 template部分)
    • [4.2 script部分](#4.2 script部分)
  • [5. 注意事项](#5. 注意事项)
  • [6. SpringBoot 后端接收图片](#6. SpringBoot 后端接收图片)
    • [6.1 UserController.java](#6.1 UserController.java)
    • [6.2 Result.java](#6.2 Result.java)
  • [7. 完整的示例代码](#7. 完整的示例代码)
    • [7.1 Homeview.vue](#7.1 Homeview.vue)
    • [7.2 request.js](#7.2 request.js)
    • [7.3 main.js](#7.3 main.js)
    • [7.4 vite.config.js](#7.4 vite.config.js)
  • [8. 完整的源代码](#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,就在哪个组件导入

javascript 复制代码
import 'vue-cropper/dist/index.css'
import { VueCropper }  from 'vue-cropper'

3.2 全局引入

main.js 文件

javascript 复制代码
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部分

html 复制代码
<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部分

javascript 复制代码
const option = ref({
  autoCrop: true, // 是否默认生成截图框
  autoCropHeight: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25
  autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25
  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', // 裁剪图片的地址(可选值: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

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

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;
    }

    @Override
    public String toString() {
        return "Result{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }

}

7. 完整的示例代码

7.1 Homeview.vue

html 复制代码
<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', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25
  autoCropWidth: '240px', // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25
  canMove: 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 data
    if (typeof event.target.result === 'object') {
      // 把 Array Buffer 转化为 blob
      data = window.URL.createObjectURL(new Blob([event.target.result]))
    } else {
      // 如果是 base64 ,不需要转换
      data = event.target.result
    }
    option.value.img = data
  }

  // 转化为base64
  // fileReader.readAsDataURL(file)

  // 转化为blob
  fileReader.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 = base64
    console.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

javascript 复制代码
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

java 复制代码
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

javascript 复制代码
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

相关推荐
DokiDoki之父15 分钟前
Spring—注解开发
java·后端·spring
CodeCraft Studio1 小时前
【能源与流程工业案例】KBC借助TeeChart 打造工业级数据可视化平台
java·信息可视化·.net·能源·teechart·工业可视化·工业图表
lang201509281 小时前
Spring Boot缓存机制全解析
spring boot·后端·缓存
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 默认页签图标 Favicon 笔记29
java·spring boot·笔记
lang201509281 小时前
Spring Boot SQL数据库全攻略
数据库·spring boot·sql
YSRM1 小时前
Leetcode+Java+图论+最小生成树&拓扑排序
java·leetcode·图论
沐浴露z2 小时前
【JVM】详解 Class类文件的结构
java·jvm·class
桦说编程2 小时前
Java并发编程:两种控制并发度的实现方法及其比较
java·后端
杯莫停丶2 小时前
设计模式之:单例模式
java·单例模式·设计模式