图片裁剪弹窗适配

图片裁剪弹窗适配

最近某个项目,有个内容发布模块,支持上传封面图,封面图需要按照比例裁剪,自然就用上了vue-cropper这个裁剪插件了。

因为是弹窗裁剪,在实现的时候,要考虑到设备低分辨率屏幕(比如1366*768这种笔记本还是有在使用的),当图片尺寸过大的时候(如高清图1920*1080),如何更好的在弹窗显示。效果如图。

使用缩放,2560*1440图片也能容得下

接下来看代码,使用了vue+element ui。

<template>
  <div>
    <el-dialog
      title="图片裁剪"
      ref="dialog"
      v-bind="$attrs"
      v-on="$listeners"
      :visible.sync="dialogVisible"
      :top="dialogTop"
      :width="dialogWidth+'px'"
      class="cropper-dialog"
      :close-on-press-escape="false"
      :before-close="closeDialog"
      :close-on-click-modal="false"
    >
      <main :style="cropperOutBoxStyle">
        <div :style="cropperWrapperStyle">
          <vue-cropper
            ref="cropper"
            v-bind="$attrs"
            v-on="$listeners"
            :img="cropperOption.img"
            :full="cropperOption.full"
            :auto-crop="cropperOption.autoCrop"
            :output-type="cropperOption.outputType"
            :fixed-box="cropperOption.fixedBox"
            :fixed="true"
            :fixed-number="photoRatio"
            :center-box="true"
            :can-move="true"
            :can-move-box="true"
          />
        </div>
      </main>
      <div
        class="cropper-operate"
        :style="{width:cropperWrapperStyle.width}"
      >
        <div class="flexItem">
          <i
            class="el-icon-refresh-right"
            @click="handleRotateRight"
          />
          <i
            class="el-icon-zoom-in"
            @click="changeScale(1)"
          />
          <i
            class="el-icon-zoom-out"
            @click="changeScale(-1)"
          />
        </div>
        <!-- <a @click="reUpload" href="javascript:;" class="cropper-operate-btn">重新上传</a> -->
      </div>
      <footer slot="footer">
        <el-button
          size="small"
          @click="doCancel"
        >
          取消
        </el-button>
        <el-button
          size="small"
          type="primary"
          @click="doCrop"
          :disabled="isUploading"
        >
          确认
        </el-button>
      </footer>
    </el-dialog>
  </div>
</template>

<script>
/**
 * 图片裁剪组件
 */
import { VueCropper } from 'vue-cropper'
import dialogMixin from './js/dialogMixin'
const defaults = {
  img: '',
  autoCropWidth: 900, // 默认生成截图框宽度
  autoCropHeight: 500, // 默认生成截图框高度
  full: false, // 是否输出原图比例的截图
  autoCrop: true, // 是否默认生成截图框
  fixedBox: false, // 固定截图框大小 不允许改变
  outputType: 'jpg' // 裁剪生成图片的格式 默认jpg  可选jpeg || png || webp
}
export default {
  components: {
    VueCropper
  },
  mixins: [dialogMixin],
  props: {
    // 图片裁剪属性配置,可参考http://docs.fed.qiweioa.cn/quickwork/qwb/index.html#/zh-CN/cropper
    cropperConfig: {
      type: Object,
      default: () => {
        return defaults
      }
    }
  },
  data() {
    return {
      isUploading: false,
      dialogWidth: 960,
      dialogHeight: 500,
      dialogTop: '10vh'
    }
  },
  computed: {
    // 截图框的宽高比例
    photoRatio() {
      const { autoCropWidth, autoCropHeight } = this.cropperOption
      return [autoCropWidth, autoCropHeight]
    },
    cropperOption() {
      return Object.assign(defaults, this.cropperConfig)
    },
    /**
     * 裁剪框外部容器样式,影响到裁剪框
     */
    cropperWrapperStyle() {
      const { width, height, zoom } = this.getSettingStyle()
      return {
        // width: (this.dialogWidth - 60) + 'px',
        // height: (this.dialogHeight) + 'px',
        width: width + 'px',
        height: height + 'px',
        transform: `scale(${zoom})`,
        'transform-origin': '0 0',
        'margin-left': 'auto',
        'margin-right': ' auto'
      }
    },
    /**
     * 设置裁剪框容器外层盒子高度是因为scale缩放不会影响原来占据的空间
     */
    cropperOutBoxStyle() {
      const { height, zoom } = this.getSettingStyle()
      return {
        height: height * zoom + 'px'
      }
    }
  },
  watch: {
    visible() {
      this.isUploading = false
      this.$nextTick(() => {
        const dialog = this.$refs.dialog.$el.querySelector('.el-dialog')
        this.dialogTop = dialog.clientHeight >= window.innerHeight ? '0' : '10vh'
      })
    }
  },
  methods: {
    /**
     * 根据配置重新计算获取裁剪框宽高缩放
     * 0.2,因为默认生成截图框宽度是外部容器的80%,所以外部容器要增加20%
     * gutter, 左右距离弹窗框边距
     * zoom,主要是为了保障大图裁剪的时候尺寸过大撑开弹窗而设置缩放
     */
    getSettingStyle() {
      const gutter = 60
      const { autoCropWidth, autoCropHeight, img } = this.cropperOption
      // 当然,还可以再优化一下,如果图片比弹窗小,不缩放。
      // const imgInstance = new Image()
      // imgInstance.src = img
      // imgInstance.onload = () => {
      //   console.log('选择的图片宽:' + imgInstance.width, '\n选择的图片高:' + imgInstance.height)
      // }
      const zoom = (this.dialogWidth - gutter) / (autoCropWidth + autoCropWidth * 0.2) < 1 ? (this.dialogWidth - gutter) / (autoCropWidth + autoCropWidth * 0.2) : 1
      const width = autoCropWidth + autoCropWidth * 0.2
      const height = autoCropHeight + autoCropHeight * 0.2
      return {
        zoom,
        width,
        height
      }
    },
    // 图片缩放
    changeScale(n) {
      this.$refs.cropper.changeScale(n)
    },
    // 图片旋转
    handleRotateRight() {
      this.$refs.cropper.rotateRight()
    },
    /**
     * blob转file对象,如果后端上传接口需要file对象,用这个方法转换
    */
    blobToFile(imgData) {
      const file = new File([imgData.blob], imgData.fileName, {
        type: imgData.fileType
      })
      // console.log('blob转file对象', file)
      return file
    },
    // 获取裁剪的图片
    async doCrop() {
      this.isUploading = true
      let base64 = null
      let blob = null
      await new Promise((resolve) => {
        this.$refs.cropper.getCropData((data) => { // 获取截取图片的base64数据
          base64 = data
          this.$emit('getCropData', data)
          return resolve(data)
        })
      })
      await new Promise((resolve) => {
        this.$refs.cropper.getCropBlob((data) => { // 获取截取图片的blob数据
          blob = data
          this.$emit('getCropBlob', data)
          return resolve(data)
        })
      })
      const { fileName, fileType, fileSize } = this.cropperOption
      const imgData = {
        blob,
        fileName,
        fileType
      }
      // console.log('base64:', base64, 'blob:', blob, 'file:', this.blobToFile(imgData))
      this.$emit('confirm', {
        base64,
        blob,
        file: this.blobToFile(imgData),
        fileName,
        fileSize,
        fileType
      })
    },
    /**
     * 重新上传
    */
    reUpload() {
      this.$emit('reUpload')
    },
    doCancel() {
      this.closeDialog()
      this.$emit('cancel')
    },
    closeDialog() {
      this.dialogVisible = false
    }
  }
}
</script>

dialogMixin.js

/**
 * 弹窗mixin
 */
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      dialogVisible: false
    }
  },
  watch: {
    visible: {
      handler(val) {
        // console.log('visible', val)
        this.dialogVisible = val
      },
      immediate: true
    },
    dialogVisible(val) {
      if (val !== this.visible) {
        this.$emit('update:visible', val)
      }
    }
  }
}
<style lang="less" scoped>
  .cropper-operate {
      display: flex;
      margin-left: auto;
      margin-right: auto;
      padding-top: 16px;
      &-btn {
          color: #3858e6;
      }
      i {
          margin-right: 5px;
          font-size: 20px;
          color: #999;
          cursor: pointer;
      }
  }
</style>

开发过程遇到的问题

刚开始的时候,使用的是zoom缩放,不过,想象很美好,现实有缺陷,zoom并不是w3c标准,caniuse.com网站显示,Firefox全系列都不支持,而我们考虑用户可能使用Firefox就只能另寻他法了

Firefox下zoom缩放不生效
zoom支持一览表

幸好,css3增加了强大的transform属性,transform的scale则是用来实现缩放的。scale的一些常见使用是图片hover缩放,带来的交互体验不错,也用于chrome不支持10px及以下像素字体大小,通过缩放实现。

zoom和scale除了兼容性的差异,zoom是整体空间缩放,而scale缩放是不影响原来的空间的,即使缩小,空间亦不会缩小。效果如下图。

上下留白很多

要想实现zoom的效果,那么可以设置缩放区域父元素的宽高即可。就是上面代码中的这一段。

/**
     * 设置裁剪框容器外层盒子高度是因为scale缩放不会影响原来占据的空间
     */
    cropperOutBoxStyle() {
      const { height, zoom } = this.getSettingStyle()
      return {
        height: height * zoom + 'px'
      }
    }

zoom和scale性能

因为zoom是实际改变物体大小的,而且这种改变会影响到其他元素,所以使用zoom的时候会引起浏览器重新渲染,性能较低。而scale是单独只改变目标元素,并不会影响到其他的元素,所以性能会好一些。

也就是说zoom会一起重排重绘,scale只会引起重绘,zoom的渲染性能会比较差。

结语

cropper.js应该是支持按照图片原尺寸比例大小输出裁剪的(不受宽高设置影响),但是试了好几个属性配置,还是不行,因此就想到了使用缩放来解决比例问题。

发表评论