前言
基于 uni-app 进行m端 app 开发的时候,碰上一个需求是将卡片弹窗生成图片,之前工作有用过 html2image 等插件,虽然简单便捷,但是经常会碰见一些奇奇怪怪的兼容性问题,因此此次需求采用手写 canvas 然后转成 base64 图片,杜绝一切 bug,直接无敌。
实现代码如下:
html:
页面添加一个 canvas 容器以及生成图片的 img 容器
<canvas canvas-id="myCanvas" id='cardCanvas'></canvas>
<image :src="base64" v-if="base64" class='cardImage' @longtap="saveImage()">
</image>
css:
没啥好说的,就是 canvas 和 img 的定位和宽高,隐藏 canvas,图片放置在页面中间
#cardCanvas {
width: 654rpx;
height: 400rpx;
position: absolute;
// top: -99999rpx;
// left: -99999rpx;
// z-index: 9999;
}
.cardImage {
width: 654rpx;
height: 400rpx;
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
top: 0;
left: 0;
}
js:
data 里面为一些假数据和存放 base64 图片路径的变量(base64)
在 mounted 钩子中调用函数绘制 canvas
data() {
return {
showDiaCard: true,
category: '服务类',
amount: '10,200.00',
cardNum: '0522 0522 0522 0522',
base64: null
}
},
mounted() {
// 渲染 canvas
this.renderCanvas();
},
methods 方法中共用到三个函数方法
渲染 canvas 函数
Tips: 因为是 m 端,所以考虑适配兼容问题,所有单位都乘以 w,w 为获取的设备宽度除以 750,以此达到类似于 rem, rpx 的适配兼容效果:
// canvas 单位换算为 rpx
const system = uni.getSystemInfoSync()
const w = system.windowWidth / 750console.log(w)
// 渲染 canvas
renderCanvas() {
let imgW, imgH;
const query = uni.createSelectorQuery().in(this);
// canvas 单位换算为 rpx
const system = uni.getSystemInfoSync()
const w = system.windowWidth / 750
console.log(w)
// 获取canvas
query.select('#cardCanvas').boundingClientRect(data => {
imgW = data.width;
imgH = data.height;
//绑定画布
var ctx = uni.createCanvasContext('myCanvas');
// 填充图片
ctx.drawImage('https://doc.shoufusheji.com/img/temp/20221128/qlcu3_dia-card-bg.png', 0, 0, imgW, imgH);
// 画出 icon 图标
ctx.drawImage('https://doc.shoufusheji.com/img/temp/20221205/siz7o_dia-card-icon.png', 32 * w, 48 * w, 40 * w, 40 * w);
// 菜花卡类别
ctx.font = "normal bold 34px PingFang SC-Bold"
ctx.setFillStyle('#fff');
ctx.setFontSize(34 * w);
ctx.fillText('菜花卡-' + this.category, 88 * w, 80 * w);
// 菜花卡金额
ctx.font = "normal bold 48px Inter-Bold"
ctx.setFillStyle('#fff');
ctx.setFontSize(48 * w);
ctx.fillText(this.amount, 32 * w, 224 * w);
// 画出二维码
this.drawRoundRect(ctx, 20 * w, 468 * w, 134 * w, 146 * w, 146 * w, "https://doc.shoufusheji.com/img/temp/20221128/loogd_dia-card-ewm.png")
// 卡密
ctx.font = "normal bold 28px Inter-Bold"
ctx.setFillStyle('#fff');
ctx.setFontSize(28 * w);
ctx.fillText("卡密:" + this.cardNum, 32 * w, 374 * w);
//输出到画布中
ctx.draw();
// 显示 loading
uni.showLoading({
mask: true
})
// 不加延迟的话,base64有时候会赋予undefined
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
this.base64 = res.tempFilePath
}
})
uni.hideLoading();
}, 1200)
}).exec();
},
保存 canvas 为 base64 图片函数
// 保存图片
saveImage() {
uni.saveImageToPhotosAlbum({
filePath: this.base64,
success: (res) => {
uni.showToast({
title: '保存成功',
})
}
})
},
因为二维码需要绘制圆角,所以加了个图片绘制圆角函数,在上面渲染 canvas 函数中有用到
// 二维码绘制圆角
/**
* @param {Object} ctx: canvas实例
* @param {Object} r: 圆弧的半径
* @param {Object} x: 图片 x 轴坐标
* @param {Object} y: 图片 y 轴坐标
* @param {Object} w: 图片宽度
* @param {Object} h: 图片高度
* @param {Object} img: 图片路径
*/
drawRoundRect(ctx, r, x, y, w, h, img) {
ctx.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, x, y, w, h);
ctx.restore(); // 返回上一状态
},