我使用的是 mpvue 开发小程序

遇到的坑

  1. canvas.drawImage`image 资源必须是本地,所以需要把背景图片,先wx.downloadFile`本地
  2. canvas.drawImage`image 资源不能是base64,所以需要把后台返回的base64`二维码转成图片并存在本地
  3. canvas 在小程序里必须加canvas-id="myCanvas"属性,否则会display:none
  4. 如果保存海报到相册授权取消,再次调用wx.saveImageToPhotosAlbum是不弹出授权框,只能使用button open-type="openType"跳到授权设置页面。

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<template>
<div v-if="value" class="poster">
<div class="poster-wrap">
<image :src="bg" alt="" mode="widthFix" class="poster-bg" />
<image :src="codeImg" class="poster-code" />
</div>
<canvas class="canvas" canvas-id="myCanvas" style="width: 300px;height: 495px"> </canvas>

<button class="btn" :open-type="openType" @opensetting="openSetting" @click="btnTap">{{btnText}}</button>
</div>
</template>

<style lang="scss" scoped>
.poster {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 10;
}
.canvas {
z-index: -1;
position: absolute;
left: 50%;
top: 45rpx;
transform: translateX(-50%);
}
.poster-wrap {
position: relative;
margin: 45rpx 75rpx;
width: 600rpx;
height: 989rpx;
}
.poster-code {
position: absolute;
left: 50%;
bottom: 229rpx;
width: 200rpx;
height: 200rpx;
transform: translateX(-50%);
}
.btn {
position: fixed;
right: 0;
bottom: 0;
left: 0;
height: 104rpx;
font-size: 32rpx;
font-weight: 400;
text-align: center;
color: rgba(255, 255, 255, 1);
line-height: 104rpx;
background: rgba(26, 138, 250, 1);
}
</style>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<script>
import {base64src} from '../util'

export default {
name: 'Poster',
props: {
value: {
type: Boolean,
default: false
}
},
data () {
return {
bg: 'https://img09.zhaopin.cn/2012/other/mobile/standout/renmai-speed/bg_wechat_post@2x.png',
codeImg: '', // 二维码转成本地图片地址
localBg: '', // 下载背景到本地图片
posterTemp: '' // 海报临时地址
}
},
computed:{
btnText () {
return this.authSavePoster ? '保存图片' : '去授权'
},
openType () {
return this.authSavePoster ? '' : 'openSetting'
}
},
watch: {
value (val) { // 由于mpvue 在组件中使用created,会在app启动后就调用,只能监听 value来调用生成海报的功能
val && (this.posterTemp ? this.getAuth() : this.generatorPoster())
}
},

methods: {
downloadFile () { // 下载背景图片到本地
return new Promise((resolve, reject) => wx.downloadFile({
url: this.bg,
success: (res) => {
console.log('TCL: downloadFile -> res', res)
this.localBg = res.tempFilePath
resolve(res)
},
fail (err) {
reject(err)
}
}))
},
// 从接口中获取二维码
getCode () {
return new Promise(async (resolve, reject) => {
const res = await getWXacode({
scene: `share=1234`,
page: ''// TODO 获取分享二维码 页面路径
})
console.log('TCL: getCode -> res', res)

base64src(`data:image/png;base64,${res.data}`).then(res => { // 二维码转图片,并返回本地路径
this.codeImg = res
resolve()
}).catch(err => {
console.log('TCL: getCode -> err', err)
})
})
},
// 合成海报
generatorPoster () {
wx.showLoading({
title: '下载海报模版中...',
mask: true
})
Promise.all([
this.downloadFile(),
this.getCode()
]).then(res => {
wx.showLoading({
title: '合成海报中...',
mask: true
})
const ctx = wx.createCanvasContext('myCanvas', this)
ctx.drawImage(this.localBg, 0, 0, 300, 495)
ctx.drawImage(this.codeImg, 100, 280, 100, 100)
ctx.draw(false, (e) => {
console.log('TCL: generatorPoster -> e', e)
this.canvasToTemp()
})
}).catch(() => {
wx.hideLoading()
wx.showToast({
title: '海报模版下载失败',
icon: 'none'
})
})
},
// canvas合成的海报,临时保存到本地
canvasToTemp () {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 300,
height: 495,
canvasId: 'myCanvas',
success: (res) => {
console.log('TCL: success -> res', res)
this.posterTemp = res.tempFilePath
this.getAuth()
},
fail (err) {
console.log('TCL: fail -> err', err)
wx.showToast({
title: err.errMsg,
icon: 'none'
})
}
})
},

// 海报保存到相册
savePoster () {
wx.saveImageToPhotosAlbum({
filePath: this.posterTemp,
success: res => {
wx.showToast({ title: '海报已成功保存到相册', icon: 'none' })
this.$emit('input', false)
},
fail (err) {
this.authSavePoster = false
wx.showToast({
title: '海报保存失败',
icon: 'none'
})
}
})
},
// 获取设置权限
getAuth () {
wx.getSetting({
success: (res) => {
console.log('TCL: getAuth -> res', res)
const { authSetting } = res
if (authSetting['scope.writePhotosAlbum'] === false) {
console.log('去授权')
this.authSavePoster = false
wx.hideLoading()
} else {
this.savePoster()
}
}
})
},
// 设置权限页面后,回调
openSetting ({mp: {detail}}) {
console.log('TCL: openSetting -> detail', detail)
detail.authSetting['scope.writePhotosAlbum'] && (this.authSavePoster = true)
},
btnTap () {
this.openType || this.savePoster()
}
}
}
</script>

util.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function base64src(base64data) {
const fsm = wx.getFileSystemManager()
const FILE_BASE_NAME = 'tmp_base64src'
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || []
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`
const buffer = wx.base64ToArrayBuffer(bodyData)
fsm.writeFile({
filePath,
data: buffer,
encoding: 'binary',
success() {
resolve(filePath)
},
fail() {
reject(new Error('ERROR_BASE64SRC_WRITE'))
},
})
})
}