Bootstrap

vue+vant 微信小程序上传图片(上移,下移,旋转,旋转)

第一步:安装weixin-js-sdk
npm i weixin-js-sdk -S
第二步:配置信息

这里提供一个功能方法,注意 axios 引入

export function handleWxConfig() {
  const wx = require('weixin-js-sdk')
  // url 是你存储微信配置信息的接口
  axios.get(url)
    .then(res => {
      if (res.errorcode === 'ok') {
        let { signature, timestamp, noncestr } = res.result
        wx.config({
          // debug: this.$store.state.isDebugger ? true : false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
          debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
          appId: APPID, // 必填,公众号的唯一标识
          timestamp: timestamp, // 必填,生成签名的时间戳
          nonceStr: noncestr, // 必填,生成签名的随机串
          signature: signature, // 必填,签名
          jsApiList: ['chooseImage', 'updateAppMessageShareData', 'updateTimelineShareData'] // 必填,需要使用的JS接口列表
        })
      } else {
        alert('获取微信签名失败')
      }
    })
    .catch(err => {
      console.error(err)
    })
}
第三步:上传逻辑

这里使用的组件是 vant
功能包括上移,下移,旋转,旋转

<van-uploader
        v-model="fileList"
        multiple
        :max-count="maxImageCount"
        accept="image/*"
        :before-read="beforeRead"
        :after-read="afterRead"
        :before-delete="beforeDelete"
        @click-upload="chooseImage"
      >
        <template #preview-cover="{file, index}">
          <div class="upload-preview-cover" @click.stop="onAlbumChecked(file, index)">
            <div :class="['radio', albumCheckedIndex == getFileIndex(file, index) ? 'checked' : '']">
              <van-icon name="success" color="#fff" size="1em"></van-icon>
            </div>
          </div>
        </template>
      </van-uploader>

js逻辑
图片旋转是通过 Cropper.js 插件进行实现的

  import { Toast } from 'vant'
  import { renameBlob, isWXEnv, isAndroid, isIOS, isWXChooseImage, handleWxConfig } from '@/utils/index'
  import {shipUpload} from '../../utils/url'
  import axios from '@/utils/request'
  import Cropper from 'cropperjs'
  import {base64toFile} from '../../utils'
 const wx = require('weixin-js-sdk')
   export default {
    name: 'index',
    data() {
      return {
        sceneId: null,
        fileList: [],
        maxImageCount: 6,
        albumCheckedIndex: -1,
        operateList: [
          {
            name: 'moveup',
            icon: 'down',
            title: '上移',
            disabled: true
          },
          {
            name: 'movedown',
            icon: 'down',
            title: '下移',
            disabled: true
          },
          {
            name: '',
            icon: '',
            title: '',
            disabled: true
          },
          {
            name: 'rotate',
            icon: 'replay',
            title: '旋转',
            disabled: true,
            loading: false
          },
          {
            name: 'delete',
            icon: 'delete-o',
            title: '删除',
            disabled: true
          }
        ],
      }
    },
    methods: {
      backPage() {
        this.$router.push({
          path: '/edit',
          query: {
            origin: 'upload',
            imgNum: this.fileList.length
          }
        })
      },
      // 返回布尔值
      beforeRead() {
        return true
      },
      chooseImage(e) {
        if (isWXChooseImage()) {
          e.preventDefault()
          wx.chooseImage({
            count: this.maxImageCount || 6,
            sourceType: ['album'], // 可以指定来源是相册还是相机,默认二者都有
            success: res => {
              let files = res.localIds.map(item => {
                return {
                  localId: item
                }
              })
              this.fileList = this.fileList.concat(files)
              this.afterRead(this.fileList)
            },
            fail: err => {
              alert(`wx.chooseImage失败:${JSON.stringify(err)}`)
              console.error(err)
            }
          })
        } else {
          console.log(e)
        }
      },
      async afterRead(lists) {
        if (!Array.isArray(lists)) {
          lists = [].concat(lists)
        }
        let fileListLen = this.fileList.length
        let listsLength = lists.length
        let startIndex = fileListLen - listsLength
        this.fileList.forEach((item, index) => {
          if (index >= startIndex) {
            this.fileList.splice(
              index,
              1,
              Object.assign(item, {
                status: 'uploading',
                message: '上传中'
              })
            )
          }
        })
        for (let i = 0; i < lists.length; i++) {
          let result
          try {
            if (isWXChooseImage() && lists[i].status == 'uploading') {
              const base64Data = await this.wxGetLocalImgData(lists[i].localId)
              this.fileList[i].url = base64Data
              this.fileList[i].content = base64Data
              const BlobData = this.base64DataToBlob(base64Data)
              this.fileList[i].file_blob = BlobData
              this.fileList[i].file = base64toFile(base64Data, '微信图片')
              this.fileList[i].blob_url = URL.createObjectURL(BlobData)
              // 更新图片显示
              this.$forceUpdate()
              await this.uploadFilePromise(this.fileList[i].file)
            } else {
              await this.uploadFilePromise(lists[i].file)
            }
          } catch (err) {
            result = 'fail'
          }
          let index = fileListLen - listsLength + i
          let item = this.fileList[index]
          this.fileList.splice(
            index,
            1,
            Object.assign(item, {
              status: 'success',
              message: '上传成功'
            })
          )
        }
      },
      uploadFilePromise(file, isFilePath = true) {
        return new Promise((resolve, reject) => {
          let formData = new FormData()
          formData.append('curuid', this.$store.getters.getCuruid)
          formData.append('file', file)
          formData.append('scene_id', this.sceneId)
          axios
            .post(shipUpload, formData, {
              headers: {
                'Content-Type': 'multipart/form-data'
              },
              hideloading: true
            })
            .then(res => {
              if (res.errorcode === 'ok') {
                resolve(res.result)
              } else {
                reject('fail')
              }
            })
            .catch(() => {
              reject('fail')
            })
        })
      },
      beforeDelete() {
        if (this.fileList.findIndex(item => item.status === 'uploading') > -1) {
          Toast('请稍后,等待所有文件上传完成后再操作')
          return false
        } else {
          return true
        }
      },
      getFileIndex(file, index) {
        let _index = this.fileList.findIndex(item => item.file_blob === file)
        return index
      },
      onAlbumChecked(file, index) {
        if (this.fileList.findIndex(item => item.status === 'uploading') > -1) {
          Toast('请稍后,等待所有文件上传完成后再操作')
          this.albumCheckedIndex = -1
          return
        }
        this.albumCheckedIndex = index
      },
      operateClick(operateItem, operateIndex) {
        const name = operateItem.name
        const index = this.albumCheckedIndex
        switch (name) {
          case 'moveup':
            if (this.operateList.find(item => item.name == 'moveup').disabled) {
              break
            }
            this.fileList[index] = this.fileList.splice(index - 1, 1, this.fileList[index])[0]
            this.albumCheckedIndex = index - 1
            break
          case 'movedown':
            if (this.operateList.find(item => item.name == 'movedown').disabled) {
              break
            }
            this.fileList[index] = this.fileList.splice(index + 1, 1, this.fileList[index])[0]
            this.albumCheckedIndex = index + 1
            break
          case 'rotate':
            if (this.operateList.find(item => item.name == 'rotate').disabled) {
              break
            }
            this.operateList.forEach(item => (item.disabled = true))
            this.operateList.find(item => item.name == 'rotate').loading = true
            this.rotateImage()
            break
          case 'delete':
            if (this.operateList.find(item => item.name == 'delete').disabled) {
              break
            }
            this.fileList.splice(index, 1)
            this.albumCheckedIndex = this.albumCheckedIndex !== 0 ? index - 1 : 0
            break
          default:
            break
        }
      },
      rotateImage() {
        let selector = `.album-upload .van-uploader__wrapper .van-uploader__preview:nth-child(${
        this.albumCheckedIndex + 1
          }) .van-image__img`
        let img = document.querySelector(selector)
        this.cropRotateImage(img, false)
      },
      cropRotateImage(imgDom, needRemove) {
        const _this = this
        const cropper = new Cropper(imgDom, {
          viewMode: 0,
          rotatable: true,
          autoCrop: false,
          autoCropArea: 1, // 将裁剪区域设置为图片的可见部分。默认值为0.8。
          checkCrossOrigin: false, //禁用检查跨域属性。默认值为true。如果您的图像是跨域的,则需要启用此选项。
          zoomable: true, // 默认值为true。如果您不需要缩放图像,则可以禁用此选项。,
          center: true, // 禁用将图像居中显示。默认值为true。如果您希望图像始终保持在裁剪区域内,可以禁用此选项
          cropBoxResizable: false,
          cropBoxMovable: false,
          crop(event) {},
          ready() {
            let imgData = this.cropper.getImageData()
            this.cropper.setCropBoxData({
              left: 0,
              top: 0,
              width: imgData.naturalHeight,
              height: imgData.naturalWidth
            })
            _this.fileList.forEach((item, index) => {
              if (index === _this.albumCheckedIndex) {
                item.status = 'uploading'
                item.message = '旋转中'
              }
            })
            // 通过 css 惊醒图片旋转
            let rotate = imgDom.dataset.rotate
            if (!rotate) {
              rotate = 90
            } else {
              rotate = +rotate + 90
            }
            imgDom.style.transform = `rotate(${rotate}deg)`
            this.cropper.moveTo(0, 0).rotate(rotate)
            imgDom.dataset.rotate = rotate
            const dataURL = this.cropper
              .getCroppedCanvas({
                width: imgData.naturalWidth,
                height: imgData.naturalHeight,
                imageSmoothingEnabled: false,
                imageSmoothingQuality: 'medium'
              })
              .toBlob(async blob => {
                let fileData = _this.fileList[_this.albumCheckedIndex]
                let _blob = renameBlob(blob, fileData.file.name)
                let file = new window.File([_blob], fileData.file.name, { type: 'image/jpeg' })
                _this.uploadFilePromise(file, false)
                _this.fileList.splice(
                  _this.albumCheckedIndex,
                  1,
                  Object.assign(fileData, {
                    status: 'success',
                    message: '旋转成功'
                  })
                )
                if (needRemove) {
                  document.body.removeChild(imgDom)
                }
                _this.operateList.find(item => item.name == 'rotate').loading = false
                const _albumCheckedIndex = _this.albumCheckedIndex
                _this.albumCheckedIndex = -1
                let timer = setTimeout(() => {
                  _this.albumCheckedIndex = _albumCheckedIndex
                  clearTimeout(timer)
                }, 100)
                this.cropper.destroy()
              }, 'image/jpeg')
          }
        })
      },
      // 通过微信sdk获取图片base64数据
      wxGetLocalImgData(wxLocalUrl) {
        return new Promise((resolve, reject) => {
          wx.getLocalImgData({
            localId: wxLocalUrl,
            success: res => {
              resolve(this.wxImgDataToBase64Data(res.localData))
            },
            fail: err => {
              alert(`文件上传失败:${JSON.stringify(err)}`)
              reject()
            }
          })
        })
      },
      // 微信localData转换成base64
      wxImgDataToBase64Data(localData) {
        let imgBase64Data
        if (isIOS()) {
          // 如果是IOS,需要去掉前缀
          imgBase64Data = localData
            .replace(/\n/g, '')
            .replace(/data:image\/(jpeg|jpg|png);base64,/gi, 'data:image/png;base64,')
        } else {
          imgBase64Data = 'data:image/png;base64,' + localData.replace(/\n/g, '')
        }
        return imgBase64Data
      },
      // base64文件转换为二进制文件
      base64DataToBlob(base64Data) {
        let arr = base64Data.split(',')
        let mime = arr[0].match(/:(.*?);/)[1]
        let bstr = atob(arr[1])
        let n = bstr.length
        let u8arr = new Uint8Array(n)
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n)
        }
        return new Blob([u8arr], { type: 'image/jpg' })
      }
    },
    created() {
      this.sceneId = this.$store.state.app.editSceneInfo.type
    },
    watch: {
      fileList: {
        handler(newVal) {
          if (newVal && !newVal.length) {
            this.albumCheckedIndex = -1
            this.operateList.forEach(item => (item.disabled = true))
          }
        },
        deep: true
      },
      albumCheckedIndex: {
        handler(n) {
          let len = this.fileList.length
          let res_operateList = this.operateList.map(item => {
            item.disabled = false
            return item
          })
          if (n > -1 && this.fileList[n].status === 'failed') {
            res_operateList.forEach(item => {
              if (item.name !== 'delete') {
                item.disabled = true
              }
            })
            this.operateList = res_operateList
            return
          }
          switch (n) {
            case -1:
              res_operateList.forEach(item => (item.disabled = true))
              break
            case 0:
              res_operateList.forEach(item => {
                if (item.name == 'moveup') {
                  item.disabled = true
                }
              })
              if (len - 1 !== 0) {
                break
              }
            case len - 1:
              res_operateList.forEach(item => {
                if (item.name == 'movedown') {
                  item.disabled = true
                }
              })
              break
            default:
              break
          }
          this.operateList = res_operateList
        },
        immediate: true
      }
    }
  }

css样式

<style lang="scss" scoped>
  .upload-frame {
    position: relative;
    overflow: hidden;
  }
  .upload-head {
    position: relative;
    width: 100%;
    height: 70px;
    text-align: center;
    line-height: 70px;
    border: none;
    font-size: 30px;
    box-shadow: 0 0 4px 2px #bdbdbd;
    background: #ffffff;
    img {
      width: 40px;
      height: 40px;
      position: absolute;
      left: 20px;
      top: 20px;
    }
  }
  .upload-body {
    width: 100%;
    margin-top: 5px;
    margin-bottom: 5px;
    height: calc(100vh - 260px);
    overflow-x: hidden;
    overflow-y: auto;
    background: #fff;
    padding: 15px;
    box-sizing: border-box;

  }
  .upload-bottom {
    width: 100%;
    /*height: 120px;*/
    background: #ffffff;
  }
  .upload-preview-cover {
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    position: relative;
    .radio {
      position: absolute;
      right: 16px;
      bottom: 16px;
      border-radius: 50%;
      border: 2px solid #eee;
      width: 32px;
      height: 32px;
      font-size: 28px;
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(0, 0, 0, 0.4);
      &.checked {
        background: #1565c0;
        .van-icon {
          display: block;
        }
      }
      .van-icon {
        display: none;
        margin-top: 2px;
        margin-left: 0px;
      }
    }
  }

  .van-uploader {
    ::v-deep {
      .van-uploader__preview-image {
        .van-image__img {
          object-fit: contain !important;
          background: #f4f4f6;
        }
      }
    }
  }
  .operate-wrap {
    .operate-item {
      font-size: 24px;
      color: #1565c0;
      .van-icon {
        color: #1565c0;
      }
      .van-loading {
        font-size: 48px;
        margin-top: -8px;
      }
      &.operate-item-moveup {
        .van-icon {
          transform: rotate(-180deg);
        }
      }

      &.operate-item-delete {
        color: #fa3534;
        .van-icon {
          color: #fa3534;
        }
      }

      &.disabled {
        color: #d3d4d6;

        .van-icon {
          color: #d3d4d6;
        }
      }
      .operate-title {
        margin-top: 12px;
      }
    }
  }
  .submit {
    width: 100%;
    height: 80px;
    line-height: 80px;
    background: #ffffff;
    border-top: 1px solid #979797;
    z-index: 100 !important;
    font-size: 20px;
    color: red;
    text-align: center;
    display: flex;
    justify-content: space-between;
    .tips {
      margin-left: 140px;
    }
    .submit-btn {
      margin-top: -10px;
      margin-right: 20px;
    }
  }
  ::v-deep {
    .van-grid-item__content {
      padding: 0;
    }
    .van-cell {
      padding: 10px 0 0 0;
    }
  }
</style>

以下是用到的公共方法 index.js,直接复制使用

/**
 * @param {string} url
 * @returns {Object}
 */
export function param2Obj(url) {
  const search = url.split('?')[1]
  if (!search) {
    return {}
  }
  return JSON.parse(
    '{"' +
      decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"').replace(/\+/g, ' ') +
      '"}'
  )
}

// 文件重命名
export function renameBlob(blob, newFilename) {
  // 获取原始 Blob 对象的 MIME 类型
  let type = blob.type

  // 创建一个新的 Blob 对象,其中包含原始 Blob 对象的数据
  let newBlob = new Blob([blob], {
    type: type
  })

  // 为新的 Blob 对象设置新的文件名
  newBlob.name = newFilename

  return newBlob
}

// 判断微信环境
export function isWXEnv() {
  const ua = window.navigator.userAgent.toLowerCase()
  if (ua.match(/MicroMessenger/i) && ua.match(/MicroMessenger/i)[0] === 'micromessenger') {
    return true
  } else {
    return false
  }
}

// 判断是否使用wx.chooseImage功能
export function isWXChooseImage() {
  return isWXEnv() && (isAndroid() || isIOS())
}

// 判断是否是安卓
export function isAndroid() {
  return /Android/i.test(navigator.userAgent)
}

// 判断是否是IOS
export function isIOS() {
  return /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)
}

// 手机号隐藏
export function geTel(tel) {
  return tel.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
}

// base64 转文件流
export function base64toFile(data, fileName) {
  const dataArr = data.split(',')
  const byteString = atob(dataArr[1])
  const options = {
    type: 'image/jpeg',
    endings: 'native',
  }
  const u8Arr = new Uint8Array(byteString.length)
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i)
  }
  return new File([u8Arr], `${fileName}.jpg`, options) // 返回文件流
}

最终效果如下在这里插入图片描述

;