背景:
基于uniapp实现微信小程序中商品详情的海报生成与保存
效果图:
![uniapp 生成海报,微信小程序,小程序,Powered by 金山文档](https://imgs.yssmx.com/Uploads/2023/07/532625-1.png)
思路:
首先把海报上需要的内容准备好,比如用户头像,商品信息,二维码等。需要注意的是,因为二维码是动态生成的,所以需要后端传给我们,前端需要把路径和参数传给后端,后端请求微信服务接口,通过配置对应的参数就可以生成一个二维码啦,再将二维码发送给前端。
图片不能是网络图片,我们需要的是本地图片,如非本地图片,那我们需要对图片进行处理。uniapp可以通过uni.getImageInfo获取图片本地路径(uni.chooseImage(OBJECT) | uni-app官网 (dcloud.net.cn))
如果通过临时路径来保存图片,canvas一倍会比较模糊,我们可以加到2或3倍,太大安卓机容易出问题,我们可以采用设备的像素作为倍率,uni.getSystemInfo获取设备信息
创建canvas绘制上下文,在绘制canvas的时候,把两个图片都切割成圆形的图片,然后中间就出现了一条线,在之后对原价进行划线删除状态的时候设置划线的颜色,会影响圆形的边线颜色。然后才知道,canvas绘图是在JS中完成,只能通过JS代码来控制,JS提供了beginPath()和closePath()两个函数来控制canvas中模块的开始和结束,这样就可以避免属性覆盖。
补充:和后端交互生成二维码,需要传参数,要注意页面路径不能再根目录前不增加/,携带的参数最大为32个可见字符,且字符有要求限制,如果不符合规范就会生成二维码失败
在页面的onLoad生命周期参数options判断options.scene是否存在,存在就是扫描二维码进入,需要对二维码返回的参数进行处理,先根据官方文档来进行解码,decodeURIComponent会将%3D解码成=,我传参的时候是通过&符号将参数之间隔开的,所以我这里使用split('&'),将解码后的字符串通过&符号分割成数组,循环数组得到键名和键值,代码如下(扫普通链接二维码打开小程序 | 微信开放文档 (qq.com))
生成、获取二维码代码
onLoad: function(options) {
if (options.scene) { //二维码进入
const scene1 = decodeURIComponent(options.scene);
let a = scene1.toString().split('&');
let obj = {}
for (let i = 0; i < a.length; i++) {
// 获取=前
let index = a[i].indexOf("=")
let name = a[i].substring(0, index);//键名
// 获取=后
let index1 = a[i].indexOf("=")
let value = a[i].substring(index1 + 1, a[i].length);
obj[name] = value //键值
}
this.goodsId = obj.id //获取商品id
this.getType = obj.type; //获取进入页面的方式
} else{//非二维码进入
}
}
//生成二维码向后端传递的参数
let data = {
page: 'page_my/pages/chengbei-goods-details/index',//路径
scene: `id=${that.goodsId}&code=${that.userCode}&type=${that.getType}`, //参数
}
HTML代码如下文章来源:https://www.toymoban.com/news/detail-532625.html
<!-- 海报 -->
<canvas :style="{height: pupopHeight + 'px',width: pupopWidth + 'px'}" canvas-id="myCanvas"></canvas>
<uni-popup ref="popup" type="center">
<view class="popup-wrap" :style="'width:' + width + 'px'">
<!-- <view class="popup-head">
生成海报
<view @click="close_popup()" class="close_icon"></view>
</view> -->
<image :src="posterImg" mode="widthFix" style="width: 100%;"></image>
<view class="popup-footer">
<view class="buttons margin_l30 marTop20" @click="saveToLocal()">保存到相册</view>
<view class="buttonNo margin_l30 marTop20" @click="closeToLocal()">取消</view>
<!-- <view class="tips">保存图片到相册,你就可以分享啦!</view> -->
</view>
</view>
</uni-popup>
<!-- 海报end -->
JS代码如下文章来源地址https://www.toymoban.com/news/detail-532625.html
data(){
return{
// 海报
pupopWidth: 590,
pupopHeight: 900,
pixelRatio: 3, //屏幕像数密度
inviteQR: '', //动态二维码
posterImg: '', //最后生成的海报
width: 100, //当前手机宽度
// 海报end
}
},
onLoad(){
let that = this
uni.getSystemInfo({
success: function(res) {
that.width = res.windowWidth * 0.8 //获取海报弹出层的宽度
that.pixelRatio = res.pixelRatio //获取像素比
}
})
this.pupopWidth = this.pupopWidth * this.pixelRatio
this.pupopHeight = this.pupopHeight * this.pixelRatio
console.log("屏幕像素密度", this.pixelRatio, this.pupopWidth, this.pupopHeight)
},
methods:{
// 生成海报
shareing() {
this.isHideSharePopor()
uni.showLoading({
title: "海报生成中...",
mask: true
})
//#ifdef MP-WEIXIN
//这里参数是前端和后端商议好,生成二维码需要前端传那些对应的值,这里我传了当前页面的路径和邀请码及当前页面的参数
this.inviteQR = this.ewmImgUrl //这里是获取后端传来的二维码,先给写死了
this.createPoster();
//#endif
},
//生成海报--微信端
createPoster() {
let _this = this
_this.headImg = uni.getStorageSync('avatarUrl') //去本地缓存获取头像
uni.getImageInfo({
src: _this.headImg,
success(image) {
const canvasId = "myCanvas"
let ctx = uni.createCanvasContext(canvasId, _this) // 自定义组件中 一定要传this
// 填充背景
ctx.setFillStyle('#FAFAFA')
ctx.fillRect(0, 0, _this.pupopWidth, _this.pupopHeight);
ctx.save()
// 头像和二维码大小都需要在规定大小的基础上放大像素比的比例后面都会*this.pixelRatio
let headerW = 102 * _this.pixelRatio
let headerX = 40 * _this.pixelRatio
let headerY = 40 * _this.pixelRatio
// 控制头像为圆形
ctx.beginPath()
ctx.setStrokeStyle('rgba(0,0,0,.2)') //设置线条颜色,如果不设置默认是黑色,头像四周会出现黑边框
ctx.arc(headerX + headerW / 2, headerY + headerW / 2, headerW / 2, 0, 2 * Math.PI)
ctx.stroke() //画出当前路径的边框
ctx.clip()//画完之后执行clip()方法,否则不会出现圆形效果
ctx.drawImage(image.path, headerX, headerY, headerW, headerW)// 将头像画到画布上
ctx.restore()
ctx.strokeStyle = '#EEEEEE';
ctx.save()
ctx.closePath()
//绘制小程序名字
const uniqueCode = "木之本樱";
let invateCode = `${uniqueCode}`
let invateCodeX = headerX + headerW + 14 * _this.pixelRatio
let invateCodeY = headerY + (40 * _this.pixelRatio)
ctx.setFontSize(26 * _this.pixelRatio);
ctx.setFillStyle('#333333');
ctx.fillText(invateCode, invateCodeX, invateCodeY);
ctx.stroke();
//绘制广告语
let invateCode1 = "广告语"
let invateCodeX1 = headerX + headerW + 14 * _this.pixelRatio
let invateCodeY1 = headerY + (84 * _this.pixelRatio)
ctx.setFontSize(26 * _this.pixelRatio);
ctx.setFillStyle('#333333');
ctx.fillText(invateCode1, invateCodeX1, invateCodeY1);
ctx.stroke();
//生成banner图
uni.getImageInfo({
src: _this.goodsThumbnailUrl, //这里的banner是展示的商品图
success(image) {
let bannerW = 510 * _this.pixelRatio
let bannerH = 510 * _this.pixelRatio
let bannerX = 40 * _this.pixelRatio
let bannerY = 40 * _this.pixelRatio + headerW + 24 * _this.pixelRatio
// 控制商品图片为圆形
ctx.beginPath()
ctx.setStrokeStyle('rgba(0,0,0,.2)') //设置线条颜色,如果不设置默认是黑色,头像四周会出现黑边框
ctx.arc(bannerX + bannerW / 2, bannerY + bannerW / 2, bannerW / 2, 0, 2 * Math.PI)
// ctx.strokeStyle = 'red'; //设置线的颜色状态
ctx.stroke()
//画完之后执行clip()方法,否则不会出现圆形效果
ctx.clip()
// 将商品主图画到画布上
ctx.drawImage(image.path, bannerX, bannerY, bannerW, bannerH)
ctx.restore()
ctx.save()
ctx.closePath()
//现价
let bannerTextX = 40 * _this.pixelRatio
let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 50 * _this.pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高
let chr = `¥${_this.oldPrice}`; //这个方法是将一个字符串分割成字符串数组
ctx.setFontSize(50 * _this.pixelRatio);
ctx.setFillStyle('#EA5506');
ctx.fillText(chr, bannerTextX, bannerTextY);
ctx.stroke();
// 测试当前现价的宽度
let metrics = ctx.measureText(`¥${_this.oldPrice}`)
// 划线价
let bannerTextX1 = 40 * _this.pixelRatio + metrics.width + 20 * _this
.pixelRatio
let bannerTextY1 = bannerY + bannerH + 20 * _this.pixelRatio + 50 * _this
.pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高
let chr1 = `¥${_this.newPrice}`; //这个方法是将一个字符串分割成字符串数组
ctx.setFontSize(26 * _this.pixelRatio);
ctx.setFillStyle('#999999');
ctx.fillText(chr1, bannerTextX1, bannerTextY1);
ctx.stroke();
// 测试原价的宽度
let metrics1 = ctx.measureText(`¥${_this.newPrice}`)
//画字体删除线
ctx.beginPath()
ctx.moveTo(bannerTextX1, bannerTextY1 - 10 * _this.pixelRatio); //移动到指定位置 X Y
//设置起点状态
ctx.lineTo(bannerTextX1 + metrics1.width, bannerTextY1 - 10 * _this
.pixelRatio);
//设置末端状态
ctx.lineWidth = 1 * _this.pixelRatio; //设置线宽状态
ctx.strokeStyle = '#999999'; //设置线的颜色状态
// ctx.setStrokeStyle('#999999')
ctx.stroke();
ctx.closePath()
//二维码
uni.getImageInfo({
src: _this.inviteQR,
success(res) {
// 画当前页面的二维码 _this.pupopWidth
const img_w = 160 * _this.pixelRatio
const img_x = _this.pupopWidth - 40 * _this.pixelRatio -
img_w
const img_y = bannerY + bannerH + 20 * _this.pixelRatio
ctx.drawImage(res.path, img_x, img_y, img_w, img_w)
// 商品名字 goodsName
//这里会处理多行显示文字,超出显示省略号的效果
let bannerTextX = 40 * _this.pixelRatio
let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 150 * _this.pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高
let chr = _this.goodsName.split(""); //这个方法是将一个字符串分割成字符串数组
let temp = "";
let row = [];
ctx.setFontSize(30 * _this.pixelRatio)
ctx.setFillStyle("#333333")
for (var a = 0; a < chr.length; a++) {
if (ctx.measureText(temp).width < 300 * _this.pixelRatio) {
temp += chr[a];
} else {
a--; //这里添加了a-- 是为了防止字符丢失,效果图中有对比
row.push(temp);
temp = "";
}
}
row.push(temp);
if (row.length > 2) {
let rowCut = row.slice(0, 1);
let rowPart = rowCut[0];
let test = "";
let empty = [];
for (var a = 0; a < rowPart.length; a++) {
if (ctx.measureText(test).width < 300 * _this.pixelRatio) {
test += rowPart[a];
} else {
break;
}
}
empty.push(test);
var group = empty[0] + "..." //这里只显示1行,超出的用...表示
rowCut.splice(0, 1, group);
row = rowCut;
}
for (var b = 0; b < row.length; b++) {
ctx.fillText(row[b], bannerTextX, bannerTextY + b *50 * _this.pixelRatio, 510 * _this.pixelRatio);
}
ctx.draw(false, () => {
uni.canvasToTempFilePath({
width: _this.pupopWidth,
height: _this.pupopHeight,
destWidth: _this.pupopWidth,
destHeight: _this.pupopHeight,
canvasId: canvasId,
fileType: 'png',
quality: 1,
success: function(res) {
_this.posterImg = res
.tempFilePath; //最终将canvas转换为图片
_this.$refs.popup.open();
uni.hideLoading()
},
fail(error) {
console.log('4', error)
// appEv.arrTips("生成海报失败,请稍后重试!")
setTimeout(() => {
uni.hideLoading()
}, 2000)
}
}, _this)
})
},
fail(error) {
console.log('获取二维码失败', error)
// appEv.arrTips("生成海报失败,获取二维码失败")
setTimeout(() => {
uni.hideLoading()
}, 2000)
}
})
},
fail(error) {
console.log('生成商品图失败', error)
// appEv.arrTips("生成海报失败,获取商品图失败")
setTimeout(() => {
uni.hideLoading()
}, 2000)
}
});
},
fail(error) {
console.log('生成头像失败', error)
// appEv.arrTips("生成海报失败,获取头像失败")
setTimeout(() => {
uni.hideLoading()
}, 2000)
}
})
},
// 取消保存
closeToLocal() {
this.$refs.popup.close()
},
//将图片保存到本地相册
saveToLocal() {
//#ifdef MP-WEIXIN
uni.saveImageToPhotosAlbum({
filePath: this.posterImg,
success: () => {
console.log('保存到相册成功')
// appEv.arrTips("保存到相册成功")
this.$refs.popup.close()
},
fail: (err) => {
console.log("保存到相册失败", err)
}
});
//#endif
},
},
到了这里,关于uniapp实现微信小程序端动态生成海报的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!