vue实现图片裁剪与查看图片

454 阅读1分钟

1. 修改头像

1.1 图片上传预览

  1. 准备file类型输入框,并通过点击头像触发上传

    设置hidden属性让其隐藏

        <!-- 头像上传 -->
        <input
          type="file"
          hidden
          ref="file"
          @change="onFileChange"
        />
    
  2. 让头像单元格手动触发上传框点击事件

        <!-- 个人信息 -->
        <van-cell title="头像" is-link @click="$refs.file.click()">
          <van-image
            class="avatar"
            fit="cover"
            round
            :src="user.photo"
          />
        </van-cell>
    
  3. 给input定义change事件

  4. 获取上传文件信息

        onFileChange () {
          // 获取文件对象
          const file = this.$refs.file.files[0]
    ​
          // 基于文章对象获取 blob 数据
          this.img = window.URL.createObjectURL(file)
    ​
          //当连续上传两个一样的图片时,会无法触发change事件, 解决办法就是每次使用完毕,把它的 value 清空
          this.$refs.file.value = ''
        }
    

1.2 图片上传预览功能处理

  1. 定义弹出层

        <!-- 编辑头像弹层 -->
        <van-popup
          v-model="isUpdatePhotoShow"
          style="height: 100%"
          position="bottom"
        >
          hello
        </van-popup>
        <!-- 编辑头像弹层 -->
    
  2. 上传图片后展示弹出层

        onFileChange () {
          // 获取文件对象
          const file = this.$refs.file.files[0]
    ​
          // 基于文章对象获取 blob 数据
          this.img = window.URL.createObjectURL(file)
    ​
          // 展示预览图片弹出层
          this.isUpdatePhotoShow = true
    ​
          // file-input 如果选了同一个文件不会触发 change 事件
          // 解决办法就是每次使用完毕,把它的 value 清空
          this.$refs.file.value = ''
        }
    
  3. 定义更新图片组件updata-photo.vue

    <template>
        <div>
          <img :src="img" />
        </div>
    </template>
    ​
    <script>
    export default {
      props: {
        img: {
          type: String,
          default: ''
        }
      }
    }
    </script>
    ​
    <style scoped lang='less'>
    ​
    </style>
    ​
    
  4. 引用、注册、使用

    import UpdataPhoto from './components/updata-photo.vue'
    ​
      components: {
        UpdataName,
        UpdataGender,
        UpdataBirthday,
        UpdataPhoto
      },
    
        <!-- 编辑头像弹层 -->
        <van-popup
          v-model="isUpdatePhotoShow"
          style="height: 100%"
          position="bottom"
        >
          <updata-photo :img="img"></updata-photo>
        </van-popup>
        <!-- 编辑头像弹层 -->
    

1.3 图片上传预览样式处理

  1. 页面结构

    <template>
    <div class="updata-photo">
        <img :src="img" />
        <div class="toolbar">
          <span class="cancel">取消</span>
          <span class="confirm">完成</span>
        </div>
      </div>
    </template>
    
  2. CSS

    .updata-photo {
      background-color: #000;
      height: 100%;
      .toolbar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        display: flex;
        justify-content: space-between;
        .cancel, .confirm {
          width: 90px;
          height: 90px;
          font-size: 30px;
          display: flex;
          justify-content: center;
          align-items: center;
          color: #fff;
        }
      }
    }
    .img {
      display: block;
      max-width: 100%;
    }
    
  3. 点击取消

    <span class="cancel" @click="$emit('close')">取消</span>
    
    <updata-photo :img="img" @close="isUpdatePhotoShow = false"></updata-photo>
    

1.4 头像剪裁

方案一: 结合服务器的图片上传剪裁预览 (前端仅展示,其余操作交给后端)

image.png

1.5 cropperjs基本使用

官网

  1. 安装cropperjs

    npm install cropperjs
    
  2. updata-photo.vue引入css、js

    import 'cropperjs/dist/cropper.css'
    import Cropper from 'cropperjs'
    
  3. 在mounted中初始化

    <template>
    <div class="updata-photo">
        //设置ref属性获取DOM   
        <img ref="img" :src="image" />
        <div class="toolbar">
          <span class="cancel" @click="$emit('close')">取消</span>
          <span class="confirm" @click="onConfirm">完成</span>
        </div>
      </div>
    </template>
    
    data () {
      return {
        cropper: null
      }
    },
    mounted () {
        // 获取图片页面元素
        const image = this.$refs.img
        this.cropper = new Cropper(image, {
          viewMode: 1, // 定义裁剪模式,0:裁剪框可以扩展到画布外, 1,2,3会将裁剪框限制在画布内
          dragMode: 'move', // 拖动模式 move: 移动画布
          aspectRatio: 1, // 截图比例, 1:1可以简写为1
          autoCropArea: 1, // 自动截图区
          cropBoxMovable: false, // 截图区域是否可以移动
          cropBoxResizable: false, // 截图区的缩放功能
          background: false, // 是否显示默认背景
          movable: true // 背景图片是否可以移动,默认true
          // crop (event) { // 当移动图片是会返回的各种数据(今天的案例中不需要使用)
          //   console.log(event.detail.x)
          //   console.log(event.detail.y)
          //   console.log(event.detail.width)
          //   console.log(event.detail.height)
          //   console.log(event.detail.rotate)
          //   console.log(event.detail.scaleX)
          //   console.log(event.detail.scaleY)
          // }
        })
    }
    

1.6 裁剪结果的两种方式

1.6.1 服务端处理数据

如果基于服务端裁剪,则使用getData方法,该方法可以得到裁剪区域的各参数

  1. 获取cropper实例

    this.cropper = new Cropper(image, {
      viewMode: 1, // 定义裁剪模式,0:裁剪框可以扩展到画布外, 1,2,3会将裁剪框限制在画布内
      dragMode: 'move', // 拖动模式 move: 移动画布
      aspectRatio: 1, // 截图比例, 1:1可以简写为1
      autoCropArea: 1, // 自动截图区
      cropBoxMovable: false, // 截图区域是否可以移动
      cropBoxResizable: false, // 截图区的缩放功能
      background: false, // 是否显示默认背景
      movable: true // 背景图片是否可以移动,默认true
    })
    
  2. 给确定注册点击事件,在事件函数中获取参数

    <span class="confirm" @click="onConfirm()">完成</span>
    
  3. 处理数据(把数据传递给后端,后端全权处理)

    onConfirm () {
      console.log(this.cropper.getData())
    }
    

    image-20220704162318286

1.6.2 前端处理数据

confirm () {
      // console.log(this.cropper.getData())
      this.cropper.getCroppedCanvas().toBlob(blob => {
        console.log(blob)
      })
    }

1.7 实现裁剪图片提交

  1. api/user.js封装更新头像api

    /**
     * 更新用户照片资料
     */
    export const updateUserPhoto = data => {
      return request({
        method: 'PATCH',
        url: '/v1_0/user/photo',
        data
      })
    }
    
  2. api/index.js中输出

    import { updateUserPhoto } from './user.js'export const updateUserPhotoAPI = updateUserPhoto
    
  3. 引入方法并调用

    import { updateUserPhotoAPI } from '../../../api/index.js'
    
        onConfirm () {
          // 基于服务端的裁切使用 getData 方法获取裁切参数
          // console.log(this.cropper.getData())
    ​
          // 纯客户端的裁切使用 getCroppedCanvas 获取裁切的文件对象
          this.cropper.getCroppedCanvas().toBlob(blob => {
            this.updateUserPhoto(blob)
          })
        },
    
        // 处理裁剪图片并上传
        async updateUserPhoto (blob) {
          try {
            // 如果接口要求 Content-Type 是 multipart/form-data
            // 则你必须传递 FormData 对象
            // 创建formData数据
            const formData = new FormData()
            formData.append('photo', blob)
    ​
            const { data: res } = await updateUserPhotoAPI(formData)
    ​
            // 提示成功
            this.$toast.success('更新成功')
          } catch (error) {
            this.$toast.fail('更新失败')
          }
        }
    
  4. 关闭弹层、更新视图

        // 处理裁剪图片并上传
        async updateUserPhoto (blob) {
          try {
                    ...
            // 关闭弹出层
            this.$emit('close')
            // 更新视图
            this.$emit('update-photo', res.data.photo)
    ​
            // 提示成功
            this.$toast.success('更新成功')
          } catch (error) {
            this.$toast.fail('更新失败')
          }
        }
    
          <updata-photo
            :img="img"
            @close="isUpdatePhotoShow = false"
            @update-photo="user.photo = $event"
          ></updata-photo>
    
  5. 利用v-if重置数据

          <updata-photo
            v-if="isUpdatePhotoShow"
            :img="img"
            @close="isUpdatePhotoShow = false"
            @update-photo="user.photo = $event"
          ></updata-photo>
    
  6. 优化loading效果

    // 处理裁剪图片并上传
        async updateUserPhoto (blob) {
          this.$toast.loading({
            message: '保存中...',
            forbidClick: true, // 禁止背景点击
            duration: 0 // 持续展示
          })
          ...
        }
    

2.完整代码

<template>
<div class="updata-photo">
    <img ref="img" :src="image" />
    <div class="toolbar">
      <span class="cancel" @click="$emit('close')">取消</span>
      <span class="confirm" @click="onConfirm">完成</span>
    </div>
  </div>
</template>
​
<script>
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { updateUserPhotoAPI } from '../../../api/index.js'
export default {
  props: {
    image: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      Cropper: null
    }
  },
  mounted () {
    // 初始化裁剪功能Cropper
    this.initCropper()
  },
  methods: {
    initCropper () {
      // 获取图片页面元素
      const image = this.$refs.img
      this.cropper = new Cropper(image, {
        viewMode: 1, // 定义裁剪模式,0:裁剪框可以扩展到画布外, 1,2,3会将裁剪框限制在画布内
        dragMode: 'move', // 拖动模式 move: 移动画布
        aspectRatio: 1, // 截图比例, 1:1可以简写为1
        autoCropArea: 1, // 自动截图区
        cropBoxMovable: false, // 截图区域是否可以移动
        cropBoxResizable: false, // 截图区的缩放功能
        background: false, // 是否显示默认背景
        movable: true // 背景图片是否可以移动,默认true
      // crop (event) { // 当移动图片是会返回的各种数据(今天的案例中不需要使用)
      //   console.log(event.detail.x)
      //   console.log(event.detail.y)
      //   console.log(event.detail.width)
      //   console.log(event.detail.height)
      //   console.log(event.detail.rotate)
      //   console.log(event.detail.scaleX)
      //   console.log(event.detail.scaleY)
      // }
      })
    },
    onConfirm () {
      // 基于服务端的裁切使用 getData 方法获取裁切参数
      // console.log(this.cropper.getData())
​
      // 纯客户端的裁切使用 getCroppedCanvas 获取裁切的文件对象
      this.cropper.getCroppedCanvas().toBlob(blob => {
        this.updateUserPhoto(blob)
      })
    },
    async updateUserPhoto (blob) {
      try {
        // 如果接口要求 Content-Type 是 multipart/form-data
        // 则你必须传递 FormData 对象
        // 创建formData数据
        const formData = new FormData()
        formData.append('photo', blob)
        const res = await updateUserPhotoAPI(formData)
        console.log(res)
        // 关闭弹出层
        this.$emit('close')
        // 父组件修改数据
        this.$emit('update-photo', res.data.data.photo)
        // 提示成功
        this.$toast.success('更新成功')
      } catch (error) {
        this.$toast.fail('更新失败')
      }
    }
  }
}
</script>
​
<style scoped lang='less'>
.updata-photo {
  background-color: #000;
  height: 100%;
  .toolbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    justify-content: space-between;
    .cancel, .confirm {
      width: 90px;
      height: 90px;
      font-size: 30px;
      display: flex;
      justify-content: center;
      align-items: center;
      color: #fff;
    }
  }
}
.img {
  display: block;
  max-width: 100%;
}
</style>
​