uniapp —— 生成带参的小程序码海报分享保存相册并调试(包含后端PHP代码)
uniapp —— 生成小程序二维码海报保存相册并分享(包含后端代码)
uniapp —— 生成带参的小程序码海报分享保存相册并调试(包含后端PHP代码)
生成小程序推广二维码海报,我们在日常的工作当中经常需要用到,因此今天总结了一下开发的过程步骤以及,踩过的一些坑。
温馨提示:完整的代码放在最后面,文章段落分析截取的不是完全的代码。
零、实现效果以及思路
- 获取微信
access_token
; - 根据
access_token
调用接口生成特定的小程序码; - 获取小程序码并绘画成
canvas
; - 利用
canvas
生成海报图片; - 分享或保存到相册。
一、后端获取Token并生成带参的小程序码
为什么需要在后端生成?前端生成不可以吗?
其实,前端也可以自己生成二维码,只是鉴于密钥的安全性问题,这个步骤最好还是交给后端处理,避免泄露;
另外,uni.request
或wx.request
官方指定不可以直接请求获取access_token
接口;
最后,前端请求回来access_token
使用时,会经常性提示 token 过期,用户体验不好。
//把请求发送到微信服务器换取二维码
function httpRequest($url, $data='', $method='GET'){
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
if($method=='POST'){
curl_setopt($curl, CURLOPT_POST, 1);
if ($data != ''){
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
}
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
//生成二维码
function get_qrcode($id){
$APPID = "xxxx";
$APPSECRET = "xxxxxxxxx";
//获取access_token
$access_token = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$APPID&secret=$APPSECRET";
//缓存access_token
session_start();
$_SESSION['access_token'] = "";
$_SESSION['expires_in'] = 0;
$ACCESS_TOKEN = "";
if(!isset($_SESSION['access_token']) || (isset($_SESSION['expires_in']) && time() > $_SESSION['expires_in'])){
$json = httpRequest( $access_token );
$json = json_decode($json,true);
$_SESSION['access_token'] = $json['access_token'];
$_SESSION['expires_in'] = time()+7200;
$ACCESS_TOKEN = $json["access_token"];
}else{
$ACCESS_TOKEN = $_SESSION["access_token"];
}
//构建请求二维码参数
//path是扫描二维码跳转的小程序路径,可以带参数?id=xxx
//width是二维码宽度
$qcode ="https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=$ACCESS_TOKEN";
$param = json_encode(array("path"=>"pages/weixiurenyuan/weixiurenyuan?id=$id","width"=>150));
//POST参数
$result = httpRequest( $qcode, $param,"POST");
//生成二维码
// file_put_contents("qrcode.png", $result);
$base64_image = "data:image/jpeg;base64,".base64_encode( $result );
return $base64_image; //返回base64图片数据
}
二、绘画canvas
这里有必要说一下,我们判断是否开始绘画
canvas
的节点,就是判断用户是否点击保存或分享,以此来规避频繁请求接口和频繁绘画canvas
。所以,这里选择的处理方式是,当用户点击推广按钮时候:
- 请求后端获取第一次小程序码,先用
HTML
渲染海报,供用户选择保存或分享;- 生成小程序码之后,多次点击推广按钮,不会再次请求后端获取小程序码;
- 当用户选择保存或分享时,再开始绘画
canvas
。
逻辑代码,我们需要先分析一下绘画的步骤:
- 背景图片绘画,该背景图片是网络图片,需将其先转换为本地临时图片,再绘画,否则真机上
canvas
不显示; - 小程序码是
base64
格式,因为drawImage
方法不支持base64
图片,因此,需先转换为本地临时图片再绘画,否则真机上canvas
不显示; - 有了思路之后我们开始执行。
// 引入请求后端二维码接口
import { user_invited } from '@/utils/request/api.js'
export default {
data() {
return {
qrCode: '', // 存储请求回来的base64图片路径
realShow: false, // 控制poster显示隐藏
cacheImage: '' // 存储canvas转换而来的图片
}
},
methods: {
/**
* 控制显示海报
* 在当前页面中,如果base64图片路径为空,则请求二维码
* 否则,只控制HTML显隐,以减少HTTP请求
*/
async show() {
if (this.qrCode == '') {
await user_invited().then(res => {
this.qrCode = res.data
})
}
this.realShow = true
},
// 关闭海报
close() {
this.realShow = false
},
// 将网络图片转换成本地临时图片
handleNetworkImgaeTransferTempImage(url) {
return new Promise(resolve => {
if(url.indexOf('http') || url.indexOf('https') === 0) {
uni.downloadFile({
url,
success: res => {
resolve(res.tempFilePath);
}
});
}
else {
resolve(url);
}
});
},
// 删除本地同名base64图片文件
removeSave() {
return new Promise((resolve, reject) => {
const fsm = uni.getFileSystemManager();
const FILE_BASE_NAME = 'tmp_base64src';
const format = 'jpeg';
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
fsm.unlink({
filePath: filePath,
success(res) {
resolve(true);
},
fail(err) {
resolve(true);
}
})
})
},
// 将base64图片存储到本地并转换成临时图片
handleBase64ImageTransferTempImage(base64Image, FILE_BASE_NAME = 'tmp_base64src') {
const fsm = uni.getFileSystemManager();
return new Promise((resolve, reject) => {
//format这个跟base64数据的开头对应
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64Image) || [];
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'));
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
fsm.writeFile({
filePath,
data: bodyData,
encoding: 'base64',
success() {
resolve(filePath);
},
fail() {
reject(new Error('ERROR_BASE64SRC_WRITE'));
},
});
})
},
// 生成 canvas 并生成微信缓存图片
async htmlToCanvas() {
let that = this
uni.showLoading({title: '正在生成海报'})
const ctx = uni.createCanvasContext('poster', this)
// 像素转换
const system = uni.getSystemInfoSync(), wpx = system.windowWidth / 750
// 图片尺寸
// const imageWidth = 610, imageHeight = 1050
// 绘画过程,需先将网络图片转为本地临时图片
const image = 'http://nq34.gzfloat.com/public/user/invite_bg@2x.png'
// 绘制网络图片 canvas
ctx.drawImage(await this.handleNetworkImgaeTransferTempImage(image),0,0,610*wpx,950*wpx)
// 绘画过程,需将base64图片转为本地临时图片,否则canvas不支持
await this.removeSave()
const base64CacheImage = await this.handleBase64ImageTransferTempImage(this.qrCode)
if (base64CacheImage) {
ctx.drawImage(base64CacheImage,218*wpx,705*wpx,185*wpx,195*wpx)
}
}
}
}
三、生成海报
为什么需要单独拎出来讲呢?因为这里踩过坑(呜呜呜)
使用 draw()
这个方法的时候,记得一定要添加一个定时器,记得一定要添加一个定时器,记得一定要添加一个定时器!否则,canvas
绘画出来永远都是空白的,保存到相册的图片也都是空白的。
setTimeout(() => {
ctx.draw(true, () => {
uni.canvasToTempFilePath({
canvasId: 'poster',
success(res) {
that.cacheImage = res.tempFilePath
},
fail(error) {
throw new Error(error)
}
}, that)
})
uni.hideLoading()
}, 100)
四、分享或保存本地
分享好友就直接调用button
里面的open-type="share"
,因为小程序不支持图片直接转发,因此你可以将生成的海报作为分享的imgUrl
封面,然后path
携带参数即可(对实现分享不理解的可以阅读我的另一篇文章《传送地址》)。
// 授权相册并保存
authToSave() {
let that = this
uni.authorize({
scope: 'scope.writePhotosAlbum',
success() {
uni.showLoading({title: '正在保存海报'})
if (that.cacheImage != '') {
uni.getImageInfo({
src: that.cacheImage,
success: function(image) {
/* 保存图片到手机相册 */
uni.saveImageToPhotosAlbum({
filePath: that.cacheImage,
success: function() {
that.realShow = false
uni.$u.toast('图片已成功保存到相册')
}
});
},
fail: function(image){
console.log(image);
}
});
} else {
that.authToSave()
}
},
complete(res) {
uni.hideLoading()
/* 判断如果没有授权就打开设置选项让用户重新授权 */
uni.getSetting({
success(res) {
if (!res.authSetting['scope.writePhotosAlbum']) {
/* 打开设置的方法 */
uni.showModal({
content: '没有授权保存图片到相册,点击确定去允许授权',
success: function(res) {
if (res.confirm) {
/* 打开设置的API*/
uni.openSetting({
success(res) {}
});
} else if (res.cancel) {
uni.showModal({
cancelText: '取消',
confirmText: '重新授权',
content: '你点击了取消,将无法进行保存操作',
success: function(res) {
if (res.confirm) {
uni.openSetting({
success(res) {/* 授权成功 */}
});
} else if (res.cancel) {
console.log('用户不授权');
}
}
});
}
}
});
}
}
});
}
})
},
// 监控canvas错误函数
canvasIdErrorCallback(e) {
console.log(e);
},
// 保存本地
savePoster() {
this.htmlToCanvas()
this.authToSave()
}
五、调试
- 微信开发者工具上传解析你生成的二维码
- 微信开发者工具模拟场景和自定义参数
- 页面中获取
onLoad(options) {
if (options) {
console.log('tui_id:' + options.tui_id)
}
}
获取成功。
六、最后,完整的父子组件丢出来
父组件HTML:
<template>
<view>
<!-- 生成海报 -->
<poster ref="poster"></poster>
</view>
</template>
父组件JavaScript:
this.$refs.poster.show();
子组件完整代码:
子组件HTML:
<template>
<view :class="[{'poster_container_active': realShow},'poster_container']">
<view class="poster">
<view @click="close" class="poster_close">
<u-icon name="close-circle" color="#fff" size="24"></u-icon>
</view>
<view class="poster_qrcode">
<image :src="qrCode" mode="widthFix"></image>
</view>
<canvas v-if="realShow" style="width: 100%;height: 100%;opacity: 0;" canvas-id="poster" @error="canvasIdErrorCallback"></canvas>
</view>
<view class="poster_tool">
<view class="poster_tool_item">
<button open-type="share">
<u-icon name="weixin-circle-fill" color="green" size="38"></u-icon>
<text>分享到微信</text>
</button>
</view>
<view class="poster_tool_item" @click="savePoster">
<u-icon name="download" color="#00aaff" size="38"></u-icon>
<text style="margin-top: 10rpx;">保存到本地</text>
</view>
</view>
</view>
</template>
子组件JavaScript:
<script>
import { user_invited } from '@/utils/request/api.js'
export default {
data() {
return {
qrCode: '',
realShow: false,
cacheImage: ''
}
},
methods: {
// 显示海报
async show() {
if (this.qrCode == '') {
await user_invited().then(res => {
this.qrCode = res.data
})
}
this.realShow = true
},
// 关闭海报
close() {
this.realShow = false
},
// 将网络图片变成临时图片
handleNetworkImgaeTransferTempImage(url) {
return new Promise(resolve => {
if(url.indexOf('http') === 0) {
uni.downloadFile({
url,
success: res => {
resolve(res.tempFilePath);
}
});
}
else {
resolve(url);
}
});
},
// 删除本地同名base64图片文件
removeSave() {
return new Promise((resolve, reject) => {
const fsm = uni.getFileSystemManager();
const FILE_BASE_NAME = 'tmp_base64src';
const format = 'jpeg';
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
fsm.unlink({
filePath: filePath,
success(res) {
resolve(true);
},
fail(err) {
resolve(true);
}
})
})
},
// 将base64图片存储到本地并转换成临时图片
handleBase64ImageTransferTempImage(base64Image, FILE_BASE_NAME = 'tmp_base64src') {
const fsm = uni.getFileSystemManager();
return new Promise((resolve, reject) => {
//format这个跟base64数据的开头对应
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64Image) || [];
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'));
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
fsm.writeFile({
filePath,
data: bodyData,
encoding: 'base64',
success() {
resolve(filePath);
},
fail() {
reject(new Error('ERROR_BASE64SRC_WRITE'));
},
});
})
},
// 生成 canvas 并生成微信缓存图片
async htmlToCanvas() {
let that = this
uni.showLoading({title: '正在生成海报'})
const ctx = uni.createCanvasContext('poster', this)
// 像素转换
const system = uni.getSystemInfoSync(), wpx = system.windowWidth / 750
// 图片尺寸
// const imageWidth = 610, imageHeight = 1050
// 绘画过程,需先将网络图片转为本地临时图片
const image = 'http://nq34.gzfloat.com/public/user/invite_bg@2x.png'
// 绘制网络图片 canvas
ctx.drawImage(await this.handleNetworkImgaeTransferTempImage(image),0,0,610*wpx,950*wpx)
// 绘画过程,需将base64图片转为本地临时图片,否则canvas不支持
await this.removeSave()
const base64CacheImage = await this.handleBase64ImageTransferTempImage(this.qrCode)
if (base64CacheImage) {
ctx.drawImage(base64CacheImage,218*wpx,705*wpx,185*wpx,195*wpx)
}
setTimeout(() => {
ctx.draw(true, () => {
uni.canvasToTempFilePath({
canvasId: 'poster',
success(res) {
that.cacheImage = res.tempFilePath
},
fail(error) {
throw new Error(error)
}
}, that)
})
uni.hideLoading()
}, 100)
},
// 授权相册并保存
authToSave() {
let that = this
uni.authorize({
scope: 'scope.writePhotosAlbum',
success() {
uni.showLoading({title: '正在保存海报'})
if (that.cacheImage != '') {
uni.getImageInfo({
src: that.cacheImage,
success: function(image) {
/* 保存图片到手机相册 */
uni.saveImageToPhotosAlbum({
filePath: that.cacheImage,
success: function() {
that.realShow = false
uni.$u.toast('图片已成功保存到相册')
}
});
},
fail: function(image){
console.log(image);
}
});
} else {
that.authToSave()
}
},
complete(res) {
uni.hideLoading()
/* 判断如果没有授权就打开设置选项让用户重新授权 */
uni.getSetting({
success(res) {
if (!res.authSetting['scope.writePhotosAlbum']) {
/* 打开设置的方法 */
uni.showModal({
content: '没有授权保存图片到相册,点击确定去允许授权',
success: function(res) {
if (res.confirm) {
/* 打开设置的API*/
uni.openSetting({
success(res) {}
});
} else if (res.cancel) {
uni.showModal({
cancelText: '取消',
confirmText: '重新授权',
content: '你点击了取消,将无法进行保存操作',
success: function(res) {
if (res.confirm) {
uni.openSetting({
success(res) {/* 授权成功 */}
});
} else if (res.cancel) {
console.log('用户不授权');
}
}
});
}
}
});
}
}
});
}
})
},
// 监控canvas错误函数
canvasIdErrorCallback(e) {
console.log(e);
},
// 保存本地
savePoster() {
this.htmlToCanvas()
this.authToSave()
}
}
}
</script>
子组件完整CSS:
<style lang="scss" scoped>
.poster_container {
opacity: 0;
display: block;
position: fixed;
z-index: -99;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
transition: opacity .5s;
.poster {
background: url('http://nq34.gzfloat.com/public/user/invite_bg@2x.png');
background-size: 100% 100%;
background-repeat: no-repeat;
position: absolute;
width: 610rpx;
height: 950rpx;
top: 50%;
left: 50%;
transform: translate3d(-50%,-575rpx,0);
.poster_close {
position: absolute;
top: 2%;
right: 2%;
z-index: 9999999999999;
}
.poster_qrcode {
position: absolute;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
image {
width: 175rpx;
}
}
}
.poster_tool {
position: absolute;
left: 0;
bottom: 0;
z-index: 99999999999;
width: 100vw;
height: 210rpx;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
background-color: #fff;
display: flex;
justify-content: space-around;
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
.poster_tool_item {
width: 150rpx;
height: 150rpx;
background-color: #e6e6e652;
border-radius: 15rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 23rpx;
color: #666666b8;
button {
width: 150rpx;
height: 150rpx;
background-color: #f5f5f552;
border-radius: 15rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 23rpx;
color: #666666b8;
padding: 0;
}
button::after{
border: none;
outline: none;
}
}
.poster_tool_item:hover {
background-color: #dfdfdf52;
}
}
}
.poster_container_active {
opacity: 1;
z-index: 99999999;
background-color: rgba(0, 0, 0, .7);
}
</style>
更多推荐
所有评论(0)