el-upload+vue-cropper+el-image实现图片上传、裁剪、预览、删除

156 阅读4分钟

 需求:固定裁剪框宽高、传入图片只能是 JPG/PNG/webp 格式、大小不能超过 2MB

效果:

一:vue-cropper

安装

npm install vue-cropper // npm 安装

yarn add vue-cropper // yarn 安装

源文档 —— 看配置

vue-cropper仓库https://github.com/xyxiao001/vue-cropper?tab=readme-ov-file

imgCropper.vue组件

<template>
  <div class="wrap">
    <div class="cropper-box">
      <div class="cropper">
        <vueCropper
          ref="cropper"
          :img="option.img"
          :outputSize="option.size"
          :outputType="option.outputType"
          :info="option.info"
          :autoCrop="option.autoCrop"
          :autoCropWidth="option.autoCropWidth"
          :autoCropHeight="option.autoCropHeight"
          :fixedBox="option.fixedBox"
          :centerBox="option.centerBox"
        />
      </div>
    </div>
    <div class="action-box">
      <el-button type="primary" plain @click="clearImgHandle">清除图片</el-button>
      <el-button type="primary" plain @click="rotateLeftHandle">左旋转</el-button>
      <el-button type="primary" plain @click="rotateRightHandle">右旋转</el-button>
      <el-button type="primary" plain @click="changeScaleHandle(1)">放大</el-button>
      <el-button type="primary" plain @click="changeScaleHandle(-1)">缩小</el-button>
      <el-button type="primary" @click="finish" :loading="loading">确认</el-button>
    </div>
  </div>
</template>
<script setup>
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import { reactive, ref, watch, onBeforeMount } from 'vue'
const option = reactive({
  img: '',
  info: true, // 裁剪框的大小信息
  outputType: 'jpg', // 裁剪生成图片的格式
  autoCrop: true, // 是否默认生成截图框
  autoCropWidth: 345, // 默认生成截图框宽度
  autoCropHeight: 150, // 默认生成截图框高度
  fixedBox: true, // 固定截图框大小 不允许改变
  centerBox: false // 截图框是否被限制在图片里面
})
const props = defineProps({
  img: String
})

// 防止重复提交
const loading = ref(false)
// 截图后的图片地址
const previewImg = ref('')
const cropper = ref()
onBeforeMount(() => {
  option.img = props.img
})
watch(
  () => props.img,
  (newValue) => {
    option.img = newValue
  }
)

const emit = defineEmits(['finish'])

const finish = () => {
  // 获取截图的 blob 数据
  cropper.value.getCropBlob((blob) => {
    previewImg.value = blob
    console.log('blob', previewImg.value)
    emit('finish', previewImg.value)
  })
}
// 放大/缩小
const changeScaleHandle = (num) => {
  num = num || 1
  cropper.value.changeScale(num)
}
// 左旋转
const rotateLeftHandle = () => {
  cropper.value.rotateLeft()
}
// 右旋转
const rotateRightHandle = () => {
  cropper.value.rotateRight()
}
// 清理图片
const clearImgHandle = () => {
  option.img = ''
}
</script>
<style scoped lang="scss">
.wrap {
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  .cropper-box {
    display: flex;
    justify-content: center;
    width: 100%;
    .cropper {
      width: 100%;
      height: 300px;
    }
  }
  .action-box {
    margin-top: 20px;
    display: flex;
    justify-content: space-between;
  }
}
</style>

二、uploadCropper.vue组件

前置条件:安装 Element-plus、TailwindCSS

运行流程:

  • el-upload选择图片,在on-change事件中对图片格式、大小进行限制,并且将该图片传给imgCropper组件
  • imgCropper组件接收到图片、打开裁剪框、点击确认后,触发uploadCropper.vue的finishEvent事件,收到裁剪后的Blob格式的图片
  • el-upload只能接收file格式的图片,所以使用convertBlobToFile方法将blob转换成file对象,关闭裁剪框、调用httpRequest方法将图片传给后端
  • uploadCropper.vue会通过getFileData将所有图片的信息暴露给使用组件,使用组件可以通过data传入图片id

注意事项:

  • 我所使用的接口在接收图片后会返回fileId,拿图片也是通过fileId,所以.then里的内容仅供参考,自行调整
<template>
  <div class="uCPage">
    <div class="flex">
      <!-- element 上传图片按钮 -->
      <el-upload
        class="uploader"
        :limit="9"
        :auto-upload="false"
        :on-change="handleChangeUpload"
        :file-list="files"
        :on-preview="onActivatefile"
        list-type="picture-card"
        ref="elementUpload"
      >
        <el-icon class="uploader-icon"><Plus /></el-icon>
      </el-upload>
    </div>
    <div id="fullscreen">
      <el-image-viewer
        v-if="isPreview"
        :url-list="[previewImg]"
        :hide-on-click-modal="true"
        @close="isPreview = false"
        fit="fill"
      />
    </div>
    <el-dialog
      width="600px"
      title="图片剪裁"
      v-model="dialogVisible"
      class="cropDialog"
      append-to-body
      @close="beforeClose"
      destroy-on-close
      :close-on-click-modal="false"
    >
      <imgCropper :img="imageUrl" @finish="finishEvent" />
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, nextTick, computed, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import imgCropper from './imgCropper.vue'
import { getToken } from '@/utils'
import axios from 'axios'
import { getFileUrl } from '@/api/file/index'
const props = defineProps({
  data: String
})
//api请求基础路径
const PATH_URL2 = import.meta.env.VITE_UPLOADURL
// action 图片上传的地址
const uploadUrl = ref(PATH_URL2 + 'member/core/sysFile/upload')

const elementUpload = ref()
// el-upload文件
const files = ref([])
// 裁剪框显隐
const dialogVisible = ref(false)
// 进入裁剪的图片
const imageUrl = ref()
// 上传服务器后图片的信息
const fileInfo = reactive({
  fileId: '',
  showUrl: '',
  fileName: ''
})
// 获取token
const headers = computed(() => {
  let token = getToken()
  if (token) {
    return { Authorization: token }
  }
  return {}
})
// 将裁剪后的图片上传至服务器
const httpRequest = async (data) => {
  const formData = new FormData()
  formData.append('file', data)
  await axios({
    headers: headers.value,
    url: uploadUrl.value,
    method: 'post',
    data: formData
  }).then((res) => {
    if (res.data.code == 200) {
      fileInfo.showUrl = getFileUrl(res.data?.data[0].fileId) || ''
      fileInfo.fileId = res.data?.data[0].fileId || ''
      previewList.value.push(fileInfo.showUrl)
      files.value.push({ name: fileInfo.fileName, url: fileInfo.showUrl, fileId: fileInfo.fileId })
      console.log(fileInfo)
      console.log(files.value, previewList.value)
    }
  })
}

//选择图片
const handleChangeUpload = (file, fileList) => {
  const isJPG =
    file.raw.type === 'image/jpeg' ||
    file.raw.type === 'image/png' ||
    file.raw.type === 'image/webp'
  const isLt2M = file.size / 1024 / 1024 < 2
  if (!isJPG) {
    ElMessage.error('上传头像图片只能是 JPG/PNG/webp 格式!')
    files.value.splice(fileList.indexOf(file), 1)
    return false
  }
  if (!isLt2M) {
    ElMessage.error('上传头像图片大小不能超过 2MB!')
    files.value.splice(fileList.indexOf(file), 1)
    return false
  }
  // 在截图成功前,不显示略缩图
  files.value.splice(fileList.indexOf(file), 1)
  fileInfo.fileName = file.raw.name
  // 上传成功后将图片地址赋值给裁剪框显示图片
  nextTick(() => {
    imageUrl.value = URL.createObjectURL(file.raw)
    dialogVisible.value = true
  })
}
// 关闭弹窗
const beforeClose = () => {
  dialogVisible.value = false
}
// 预览图片
const previewImg = ref('')
const previewList = ref([])
const isPreview = ref(false)
// 点击预览
const onActivatefile = (file) => {
  isPreview.value = true
  previewImg.value = file.url
}

// blob转file
const convertBlobToFile = (blob, fileName) => {
  // 创建File对象
  const file = new File([blob], fileName, { type: blob.type })
  return file
}

const finishEvent = (blob) => {
  // blob 转file
  const fileName = fileInfo.fileName
  const fileUrl = convertBlobToFile(blob, fileName)
  console.log('file', fileUrl)
  dialogVisible.value = false
  httpRequest(fileUrl)
}
// 使用时可调用getFileData拿到所有图片
const getFileData = () => {
  return JSON.stringify(files.value)
}

defineExpose({ getFileData })
watch(
  () => props.data,
  (newVal) => {
    files.value = []
    if (!props.data) return
    console.log(newVal)
    newVal.forEach((el) => {
      fileInfo.fileId = el.fileId
      fileInfo.showUrl = getFileUrl(el.fileId)
      files.value.push({ name: el.fileName, url: fileInfo.showUrl, fileId: el.fileId })
    })
    console.log(files.value)
  },
  {
    immediate: true
  }
)
</script>
<style scoped lang="less">
.uCPage {
  .uploader {
    .uploader-icon {
      width: 100%;
      height: 100%;
    }
  }
  .cropDialog {
    display: flex;
    flex-direction: column;
  }
}
// 控制组件宽高——没图片
:deep(.el-upload--picture-card) {
  width: 223px;
  height: 100px;
}
// 控制组件宽高——有图片
:deep(.el-upload-list__item) {
  width: 223px;
  height: 100px;
}

:deep(.el-dialog) {
  width: 600px;
  min-height: 500px;
}
</style>

三、使用

<template>
    <UploadCropper ref="fileRef" :data="upf" />
</template>

<script lang="ts" setup>
    import UploadCropper from './component/UploadCropper.vue'
    //传入图片id
    const upf = ref([])
    const fileRef = ref()
    //省略获取upf
</script>