第一步:安装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) // 返回文件流
}
最终效果如下