VUE 图片上传/剪切(vue-cropper)

1. 概述

1.1 说明

  项目中为了保证图片展示效果以及分辨率高度匹配,需对图片的尺寸、大小、类型等进行控制;最大限度保证图片在网站、小程序、app端的展示效果保持一致。

1.2 思路

  使用vue-cropper进行图片裁剪功能,使用iview组件Upload进行图片上传。

  1.2.1 功能选择

    使用下拉框进行功能选择,如图片直接上传、图片剪裁等

    

  1.2.2 图片上传与展现

    使用Upload进行图片上传,根据所需上传图片的大小、格式等进行图片验证;所有验证通过后再进行图片上传操作。

    

  1.2.3 最终组件样式

   

  1.2.4 组件使用样式

   

  1.3 vue-cropper使用

    1.  安装  npm install vue-cropper 或者 yarn add vue-cropper
    2.  main.js引用注入 
      1.   import VueCropper from 'vue-cropper'
      2.   Vue.use(VueCropper)
    3.  页面使用  <vueCropper></vueCropper>

  1.4 图片规则

2. 代码

2.1 示例代码结构:

2.2 图片上传、剪裁、展现公用组件(common)

2.2.1  图片剪裁组件代码(imageCropper.vue)

<!-- 图片上传剪裁 -->
<template>
    <div ref="refCommonImageCropper" class="image-cropper-wrapper">
        <div class="cropper-content">
            <div class="operate-content">
                <div class="scope-btn">
                    <label class="btn" for="uploads">{{ options.imgUrl ? '更换图片' : '上传图片'}}</label>
                    <input
                        type="file"
                        id="uploads"
                        style="position:absolute; clip:rect(0 0 0 0);"
                        accept="image/png, image/jpeg, image/gif, image/jpg"
                        @change="uploadImg($event)"
                    />
                    <Button @click="changeScale(1)" title="放大">+</Button>
                    <Button @click="changeScale(-1)" title="缩小">-</Button>
                    <Button @click="rotateLeft" title="左旋转"></Button>
                    <Button @click="rotateRight" title="右旋转"></Button>
                    <!-- <Button @click="imageCropperSubmit('blob')">确定</Button> -->
                  </div>
              </div>
              <div :style="{'width': options.imgWidth + 'px', 'height': options.imgHeight + 'px'}">
                  <vueCropper
                      ref="cropper"
                      :img="options.imgUrl"
                      :outputSize="options.size"
                      :outputType="option.outputType"
                      :info="true"
                      :full="option.full"
                      :canMove="option.canMove"
                      :canMoveBox="option.canMoveBox"
                      :original="option.original"
                      :autoCrop="option.autoCrop"
                      :autoCropWidth="options.cropWidth"
                      :autoCropHeight="options.cropHeight"
                      :fixedBox="option.fixedBox"
                      @realTime="realTime"
                      @imgLoad="imgLoad"
                  ></vueCropper>
            </div>
        </div>
        <div class="cropper-operate" v-if="isShowView">
            <div style="height:42px;padding:6px;">剪裁图片预览</div>
            <div
                class="show-preview"
                :style="{'width': options.cropWidth + 'px', 'height': options.cropHeight + 'px',  'overflow': 'hidden', 'margin': '5px'}"
            >
                <div :style="previews.div" class="preview">
                    <img :src="previews.url" :style="previews.img" />
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    import { uploadImage } from '../../../api/account.js'
    export default {
        name: 'image-cropper',
        props: {
            /**
             * 裁剪配置
             */
            setting: {
                type: Object,
                default () {
                    return {
                        imgUrl: '',
                        cropWidth: 200,
                        cropHeight: 200,
                        imgWidth: 350,
                        imgHeight: 300,
                        size: 1
                    }
                }
            },
            /**
             * 预览区域是否展示
             */
            isShowView: {
                type: Boolean,
                default: true
            }
        },
        data () {
            return {
                crap: false,
                previews: {},
                option: {
                    full: false,
                    outputType: 'png',
                    canMove: true,
                    original: false,
                    canMoveBox: true,
                    autoCrop: true,
                    fixedBox: true
                },
                fileName: ''
            }
        },
        computed: {
            options () {
                return this.setting
            }
        },
        watch: {
            options: {
                // eslint-disable-next-line
                handler (newVal, oldVal) {
                    this.setting = newVal
                },
                deep: true
            }
        },
        methods: {
            /**
             * 更改比例
             */
            changeScale (num) {
                num = num || 1
                this.$refs.cropper.changeScale(num)
            },
            /**
             * 左旋转
             */
            rotateLeft () {
                this.$refs.cropper.rotateLeft()
            },
            /**
             * 右旋转
             */
            rotateRight () {
                this.$refs.cropper.rotateRight()
            },
            /**
             * 实时预览函数
             */
            realTime (data) {
                this.previews = data
            },
            /**
             * 被剪裁图片的上传以及更改
             */
            uploadImg (e) {
                // 上传图片
                var file = e.target.files[0]
                if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(e.target.value)) {
                    this.$Message.warning('图片类型必须是jpeg,jpg,png中的一种')
                    return false
                }
                this.fileName = file.name
                var reader = new FileReader()
                let type = file.type
                reader.onload = (e) => {
                    let data
                    if (typeof e.target.result === 'object') {
                        // 把Array Buffer转化为blob 如果是base64不需要
                        data = URL.createObjectURL(new Blob([e.target.result], { type }))
                    } else {
                        data = e.target.result
                    }
                    this.options.imgUrl = data
                }
                // 转化为base64
                reader.readAsDataURL(file)
                // 转化为blob
                // reader.readAsArrayBuffer(file)
            },
            imgLoad (msg) {
                console.log(msg)
            },
            /**
             * 剪裁图片上传
             */
            imageCropperSubmit (type) {
                if (this.options.imgUrl) {
                    if (type === 'blob') {
                        this.$refs.cropper.getCropBlob(data => {
                            let formData = new FormData()
                            formData.append('file', data, this.fileName)
                            uploadImage(formData).then(data => {
                                this.$Message.success('上传成功')
                                let cropperData = {
                                    httpOriginalFileUri: data.httpOriginalFileUri,
                                    originalFileUri: data.originalFileUri,
                                    converFileUri: data.converFileUri,
                                    fileName: this.fileName
                                }
                                this.$emit('fileSuccess', cropperData)
                            }).catch(err => {
                                console.log(err)
                            })
                        })
                    } else {
                        // 此处得到的是base64位图片数据,暂无用
                        this.$refs.cropper.getCropData(data => {
                            console.log(data)
                        })
                    }
                } else {
                    this.$Message.warning('请上传需裁剪图片')
                }
            }
        }
    }
</script>
<style lang="scss" scoped>
.image-cropper-wrapper{
  display: flex;
  flex-direction: row;
  justify-content: center;
  .cropper-content {
    display: flex;
    display: -webkit-flex;
    flex-direction: column;
      .operate-content {
        margin-bottom: 10px;
        display: flex;
        display: -webkit-flex;
        .scope-btn {
          display: flex;
          display: -webkit-flex;
          >button {
            margin-left: 6px;
          }
        }
        .btn {
          outline: none;
          display: inline-block;
          line-height: 1;
          white-space: nowrap;
          cursor: pointer;
          -webkit-appearance: none;
          text-align: center;
          -webkit-box-sizing: border-box;
          box-sizing: border-box;
          outline: 0;
          margin: 0;
          -webkit-transition: 0.1s;
          transition: 0.1s;
          font-weight: 500;
          padding: 8px 15px;
          font-size: 12px;
          border-radius: 3px;
          color: #fff;
          background-color: #67c23a;
          border-color: #67c23a;
        }
      }
  }
  .cropper-operate{
      display: flex;
      flex-direction: column;
      padding-left: 20px;
    }
    .show-preview {
      flex: 1;
      -webkit-flex: 1;
      display: flex;
      display: -webkit-flex;
      justify-content: center;
      align-items: center;
      -webkit-justify-content: center;
      .preview {
        overflow: hidden;
        border: 1px solid #cccccc;
        background: #cccccc;
      }
    }
}
</style>

2.2.1  图片上传、展现、裁剪组件代码(imageOperate.vue)

<!-- 图片上传 -->
<template>
    <div class="image-upload-wrapper">
        <div class="default-upload-list" v-if="formInfo.fileUrl">
            <img :src="formInfo.fileUrl">
            <div class="default-upload-list-cover">
                <Icon type="ios-eye-outline" @click.native="avatarModal=true"></Icon>
                <Icon v-if="!isDisable" type="ios-trash-outline" @click.native="handleFileRemove"></Icon>
            </div>
        </div>
        <div v-else class="image-upload-item">
            <Select
                v-model="formInfo.uploadTypeSelected"
                placeholder="请选择图片上传方式"
                :disabled="`${imgHeight}`=='0'"
                :title="`${imgHeight}`=='0'?'尺寸不限时只能进行直接上传操作':''"
                @on-change="onChangeImageUploadType">
                <Option
                  v-for="item in imageUploadType"
                  :value="item.value"
                  :key="item.value"
                >{{ item.label }}</Option>
            </Select>
            <div class="image-upload-content">
                <div v-if="formInfo.uploadTypeSelected=='cropperUpload'" class="cut-img-wrapper">
                    <div style="width: 58px;height:58px;line-height: 58px;" @click="uploadImage">
                        <Icon
                            type="ios-cut"
                            size="20"
                            style="color: #3399ff"
                        ></Icon>
                    </div>
                </div>
                <!-- 直接上传 -->
                <Upload
                    v-else
                    ref="refWholeUpload"
                    type="drag"
                    action="/api/admin/attach/uploadAttach"
                    style="width: 58px;height:58px;"
                    :show-upload-list="false"
                    :on-success="fileSuccess"
                    :before-upload="handleBeforeUpload"
                    :on-progress="uploadProgress"
                    :on-error="uploaderror">
                    <div style="width: 58px;height:58px;line-height: 58px;">
                        <Icon
                            type="ios-cloud-upload"
                            size="20"
                            style="color: #3399ff"
                        ></Icon>
                      </div>
                </Upload>
                <Alert
                    v-if="!formInfo.fileUrl"
                    :type="imageError?'error':'info'"
                    class="image-info">
                    <div>宽高[{{(`${imgWidth}`!='0'?imgWidth:'不限')}}*{{(`${imgHeight}`!='0'?imgHeight:'不限')}}]&nbsp;&nbsp;格式[{{formInfo.uploadTypeSelected=='IconUpload'?'.ico':'.png/.jpg/.jpeg'}}]&nbsp;&nbsp;最大[{{imgSizeTip}}]</div>
                    <div v-if="imageError" style="color:red;">{{imageError}}</div>
                </Alert>
            </div>
        </div>
        <Modal
            v-model="avatarModal"
            :mask-closable="false"
            :closable='false'
            :z-index='1001'>
            <img :src="formInfo.fileUrl" style="width: 100%">
            <div slot="footer">
                <Button
                    type="primary"
                    @click="avatarModal = false"
                >关闭</Button>
            </div>
        </Modal>
        <Modal
            :width="modalCropperWidth"
            v-model="modalImageCropper"
            title='图片剪切处理'
            :mask-closable="false"
            :z-index='2001'
            class-name="vertical-center-modal"
            draggable>
            <image-cropper
                ref="refImageCropper"
                @fileSuccess="imageCropperSuccess"
                :setting="setOptions"
                :isShowView="isShowViewImg"
                v-if="modalImageCropper" />
            <div slot="footer">
                <Button @click="modalImageCropper = false">取消</Button>
                <Button type="primary" @click="submitImageCropper">确定</Button>
            </div>
        </Modal>
        <Spin
            fix
            style="z-index:9999"
            v-if='isLoading'
            class="spin"
        >图片上传中...</Spin>
    </div>
</template>
<script>
    import imageCropper from './imageCropper'
    export default {
        name: 'image-operate',
        props: {
            // 图片路径
            imageUrl: {
                type: String,
                default () {
                    return ''
                }
            },
            // 图片大小
            imageSize: {
                type: Number,
                default () {
                    return 500
                }
            },
            // 图片尺寸——宽
            imgWidth: {
                type: String | Number,
                default () {
                    return 100
                }
            },
            // 图片尺寸——高
            imgHeight: {
                type: String | Number,
                default () {
                    return 100
                }
            },
            // 图片大小文字内容
            imgSizeTip: {
                type: String,
                default () {
                    return '500KB'
                }
            },
            // 是否启用
            isDisable: {
                type: Boolean,
                default: false
            },
            // 是否支持icon直接上传
            isIconUpload: {
                type: Boolean,
                default: false
            }
        },
        components: {
            'image-cropper': imageCropper
        },
        data () {
            return {
                isLoading: false,
                // 图片上传类型选择
                imageUploadType: [],
                formInfo: {
                    uploadTypeSelected: 'wholeUpload',
                    fileUrl: '',
                    submitUrl: ''
                },
                // 图片预览
                avatarModal: false,
                attach: {
                    fileName: '',
                    originalFileUri: '',
                    converFileUri: ''
                },
                // 图片直接上传错误
                imageError: '',
                // 图片裁剪
                setOptions: {
                    imgUrl: '',
                    size: 1,
                    cropWidth: 200,
                    cropHeight: 200,
                    imgWidth: 350,
                    imgHeight: 300
                },
                isShowViewImg: true,
                modalCropperWidth: 700,
                modalImageCropper: false
            }
        },
        mounted () {
            if (this.isIconUpload) {
                this.imageUploadType = [
                    { label: '图片直接上传', value: 'wholeUpload' },
                    { label: '图片剪裁上传', value: 'cropperUpload' },
                    { label: 'ICON图片直接上传', value: 'IconUpload' }
                ]
            } else {
                this.imageUploadType = [
                    { label: '图片直接上传', value: 'wholeUpload' },
                    { label: '图片剪裁上传', value: 'cropperUpload' }
                ]
            }
            this.imageError = ''
            this.formInfo.fileUrl = this.imageUrl
        },
        methods: {
            /**
             * 图片上传类型选择
             */
            onChangeImageUploadType (val) {
                this.imageError = ''
                this.handleFileRemove()
            },
            /**
             * 裁剪图片
             */
            uploadImage () {
                this.setOptions.imgUrl = ''
                this.setOptions.size = 1
                this.setOptions.cropWidth = this.imgWidth
                this.setOptions.cropHeight = this.imgHeight
                this.setOptions.imgWidth = this.imgWidth + 20
                this.setOptions.imgHeight = this.imgHeight + 20
                let screenWidth = document.body.clientWidth
                let maxWidth = screenWidth / 2 - 100
                if (maxWidth >= this.imgWidth + 50) {
                    this.isShowViewImg = true
                    this.modalCropperWidth = (this.imgWidth + 40) * 2 > 500 ? (this.imgWidth + 50) * 2 : 500
                } else {
                    this.isShowViewImg = false
                    if ((this.imgWidth + 50) * 2 <= 500) {
                        this.modalCropperWidth = 500
                    } else {
                        this.modalCropperWidth = this.imgWidth + 100
                    }
                }
                this.modalImageCropper = true
            },
            /**
             * 图片确定上传
             */
            submitImageCropper () {
                this.$refs.refImageCropper.imageCropperSubmit('blob')
            },
            /**
             * 图片上传成功后返回对应图片信息
             */
            imageCropperSuccess (val) {
                this.formInfo.fileUrl = val.httpOriginalFileUri
                this.formInfo.submitUrl = val.httpOriginalFileUri
                const data = {
                    fileName: val.fileName,
                    originalFileUri: val.originalFileUri,
                    converFileUri: val.converFileUri,
                    fileUrl: val.httpOriginalFileUri,
                    submitUrl: val.originalFileUri
                }
                this.$emit('imageUploadEmit', data)
                this.modalImageCropper = false
            },
            /**
             * 删除图片
             */
            handleFileRemove () {
                this.attach = {
                    fileName: this.attach.fileName,
                    originalFileUri: '',
                    converFileUri: ''
                }
                this.formInfo.fileUrl = ''
                this.formInfo.submitUrl = ''
                const data = {
                    fileName: this.attach.fileName,
                    originalFileUri: '',
                    converFileUri: '',
                    fileUrl: '',
                    submitUrl: ''
                }
                this.$emit('imageUploadEmit', data)
            },
            /**
             * 附件上传成功返回值
             */
            fileSuccess (res, file) {
                if (res.result) {
                    const { httpOriginalFileUri, originalFileUri, converFileUri } = res.data
                    this.attach.fileName = file.name
                    this.attach.originalFileUri = originalFileUri
                    this.attach.converFileUri = converFileUri
                    this.formInfo.fileUrl = httpOriginalFileUri
                    this.formInfo.submitUrl = originalFileUri
                    const data = {
                        fileName: file.name,
                        originalFileUri: originalFileUri,
                        converFileUri: converFileUri,
                        fileUrl: httpOriginalFileUri,
                        submitUrl: originalFileUri
                    }
                    this.isLoading = false
                    this.$emit('imageUploadEmit', data)
                } else {
                    this.formInfo.fileUrl = ''
                    this.imageError = '图片上传失败,请重新上传。'
                    this.isLoading = false
                }
            },
            uploadProgress (file) {
                this.isLoading = true
            },
            /**
             * 附件上传判断
             */
            handleBeforeUpload (file) {
                this.isLoading = true
                this.imageError = ''
                let check = this.$refs.refWholeUpload.fileList.length < 1
                let isIcontype = file.type === 'image/x-icon'
                if (this.formInfo.uploadTypeSelected === 'IconUpload') {
                    if (!isIcontype) {
                        this.imageError = '请选择icon类型的图片上传。'
                        this.isLoading = false
                        return false
                    }
                }
                if (!check) {
                    this.imageError = '限制上传一张图片,请删除后重新上传。'
                    this.isLoading = false
                    return false
                } else {
                    return this.checkImageWH(file, this.imgWidth, this.imgHeight)
                }
            },
            /**
             * 判断上传文件类型
             */
            judgeFileType (type) {
                let typeList = ['image/jpeg', 'image/png', 'image/jpg', 'image/x-icon']
                let hasIndex = typeList.findIndex(item => item.indexOf(type) > -1)
                if (hasIndex > -1) {
                    return true
                } else return false
            },
            /**
             * 返回一个promise  检测通过返回resolve  失败返回reject组织图片上传
             */
            checkImageWH (file, width, height) {
                let self = this
                return new Promise(function (resolve, reject) {
                    let accordType = self.judgeFileType(file.type)
                    if (!accordType) {
                        self.imageError = '上传的文件为非图片格式,请选择图片格式文件上传'
                        self.isLoading = false
                        reject(new Error(self.imageError))
                    } else if (file.size / 1024 > self.imageSize) {
                        self.imageError = `上传图片不能超过${self.imgSizeTip}`
                        self.isLoading = false
                        reject(new Error(self.imageError))
                    } else {
                        if (`${width}` !== '0') {
                            let filereader = new FileReader()
                            filereader.onload = e => {
                                let src = e.target.result
                                const image = new Image()
                                image.onload = function () {
                                    let errorInfo = ''
                                    if ((width && this.width !== width)) {
                                        errorInfo = `宽(${this.width})、`
                                    }
                                    if (height && `${height}` !== '0' && this.height !== height) {
                                        errorInfo = `${errorInfo}高(${this.height})`
                                    }
                                    if (errorInfo) {
                                        self.imageError = `上传图片错误:${errorInfo}`
                                        self.isLoading = false
                                        reject(new Error(self.imageError))
                                    } else {
                                        self.isLoading = false
                                        resolve(true)
                                    }
                                }
                                image.onerror = reject
                                image.src = src
                            }
                            filereader.readAsDataURL(file)
                        } else {
                            self.isLoading = false
                            resolve(true)
                        }
                    }
                })
            },
            /**
             * 附件上传失败
             */
            uploaderror (file) {
                this.isLoading = false
                this.imageError = '图片上传失败,请重新上传。'
            }
        }
    }
</script>
<style lang="scss" scoped>
.image-upload-wrapper .image-upload-item{
  display:flex;
  flex-direction: column;
}
.image-upload-wrapper .image-upload-content{
  margin-top: 10px;
  display: flex;
  .image-info {
    margin-left:10px;
    text-align: left;
    display:flex;
    flex-direction:column;
    justify-content:center;
    padding: 0 10px;
    font-size: 13px;
    height: 60px;
  }
}
.cut-img-wrapper{
    background: #fff;
    border: 1px dashed #dcdee2;
    display: felx;
    width: 58px;
    height: 58px;
    border-radius: 4px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.2s;
}
.cut-img-wrapper:hover{
    border: 1px dashed #2d8cf0;
}
.default-upload-list{
  display: inline-block;
  width: 60px;
  height: 60px;
  text-align: center;
  line-height: 60px;
  border: 1px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  background: #fff;
  position: relative;
  box-shadow: 0 1px 1px rgba(0,0,0,.2);
  margin-right: 4px;
}
.default-upload-list img{
  width: 100%;
  height: 100%;
}
.default-upload-list-cover{
  display: none;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0,0,0,.6);
}
.default-upload-list:hover .default-upload-list-cover{
  display: block;
}
.default-upload-list-cover i{
  color: #fff;
  font-size: 20px;
  cursor: pointer;
  margin: 0 2px;
}
</style>

2.3 使用示例(formOperate.vue/index.vue)

2.3.1  表单示例代码(formOperate.vue)

<template>
  <Form ref="refForm" :model="formData" :rules="rules">
      <FormItem label="图片" prop="fileUrl" label-for="fileUrl">
        <Input v-show="false" v-model="formData.fileUrl" />
        <image-upload
          :imageUrl="formData.fileUrl"
          :isIconUpload="true"
          @imageUploadEmit="imageUploadFunction"
          :imageSize="settingImageUpload.maxSize"
          :imgWidth="settingImageUpload.width"
          :imgHeight="settingImageUpload.height"
          :imgSizeTip="settingImageUpload.sizeTip"
        />
      </FormItem>
    </Form>
</template>
<script>
import ImageUpload from "./common/imageOperate";
export default {
  components: {
    ImageUpload,
  },
  data() {
    return {
      formData: {
        fileUrl: "", // 图片路径
      },
      settingImageUpload: {
        width: 100,
        height: 100,
        maxSize: 100,
        sizeTip: "100kb",
      },
      rules: {
        fileUrl: { required: true, message: '请上传图片' }
      }
    };
  },
  methods: {
    /**
     * 图片上传成功
     */
    imageUploadFunction(val) {
      this.formData.originalFileUri = val.originalFileUri;
      this.formData.obtainIconUrl = val.fileUrl;
    }
  },
};
</script>

2.3.2  入口示例代码(index.vue)

<template>
  <div class="medo-wrapper">
    <Button type="primary" @click="handleAdd">新建</Button>
    <Drawer
      :title="`${propOperateData.id ? '修改' : '新增'}表单`"
      v-model="showInfoOperate"
      :width="drawerStyles.width"
      :styles="drawerStyles.style"
      :mask-closable="false"
      @on-close="showInfoOperate = false"
    >
      <form-operate
        v-if="showInfoOperate"
        :data="propOperateData"
        ref="refFormOperate"
      />
      <div class="default-drawer-footer">
        <Button type="text" @click="showInfoOperate = false"> 关闭 </Button>
        <Button type="primary" @click="handleSubmit"> 确定 </Button>
      </div>
    </Drawer>
  </div>
</template>
<script>
import formOperate from './formOperate'
export default {
  components: {
    formOperate
  },
  data() {
    return {
      showInfoOperate: false,
      propOperateData: {
        id: "",
        fileUrl: "",
      },
      drawerStyles: {
        // 内容样式
        styles: {
            height: 'calc(100% - 55px)',
            overflow: 'auto',
            paddingBottom: '53px',
            position: 'static'
        },
        width: 720
      },
    };
  },
  methods: {
    handleAdd () {
      this.showInfoOperate = true
    },
    /**
     * 提交按钮
     */
    handleSubmit() {
      this.$refs.refFormOperate.$refs.refForm.validate(validate => {
        if (validate) {
          console.log('验证成功调用接口')
        }
      })
    }
  }
};
</script>
<style lang="scss" scoped>
.default-drawer-footer{
  width: 100%;
  position: absolute;
  bottom: 0;
  left: 0;
  border-top: 1px solid #e8e8e8;
  padding: 10px 16px;
  text-align: right;
  background: #fff;
}
</style>

2.3 上传代码接口返回:

 

posted @ 2021-03-31 09:55  ajuan  阅读(3385)  评论(0编辑  收藏  举报