zoukankan      html  css  js  c++  java
  • 钉钉小程序通过 Canvas 将页面生成图片并保存到本地相册

    背景

    最近公司有个账户充值业务场景需要从线下支付迁移到线上支付:

    • 线下支付场景:客户通过 POS 机付款或者扫码销售同学提供的付款二维码进行付款来完成支付,之后销售同学将相关信息录入到 CRM 后台,财务审核通过后才正式完成充值流程。
    • 线上支付场景:销售同学先在 CRM 钉钉小程序中录入充值信息后生成订单,然后系统生成支付宝或者微信付款码,销售同学将付款码页面生成的图片发送给客户,客户付款后即完成充值流程。

    整个充值流程优化上线后,大大缩短了客户账户从充值付款到充值到账的时间,明显提高了给客户账户充值的效率。

    需求分析

    本次迭代功能的小程序是使用原生钉钉语言开发的小程序,至于为什么是原生语言开发,那是历史原因了,不在本文讨论范围,原生语言开发体验明显没有使用了 uni-app、taro 等小程序框架的开发体验好,刚接手时还需要一边查文档一边开发,效率比较低。

    要实现线上支付功能,要解决的关键问题有以下两个:

    • 后端接口返回给小程序的是微信或支付宝的支付链接,小程序需要将它转成二维码显示到页面上
    • 页面上除了付款二维码,还有公司 logo,客户信息,付款金额等需要生成图片的信息,点击页面底部的保存图片按钮后,将上述信息生成图片并保存到本地相册

    综合以上两步,实现需求在技术上要解决的问题包括以下几点:

    1. 使用 Canvas 将链接转成二维码,显示到页面上,可以借助一个第三方库 weapp-qrcode 来实现,这个库是给微信小程序使用的,但钉钉小程序里也可以使用,需要改下源码
    2. 将整个页面的元素画到另一个 Canvas 上,但问题是如何将二维码 Canvas 画到另一个 Canvas 上呢,这一点开发时有遇到坑,后面会说, 本次我是采用了个小技巧,保存图片时,先使用 toTempFilePath 将二维码 Canvas 转成临时图片,然后画到另一个 Canvas 上,再使用 toTempFilePath 将另一个 Canvas 转成临时图片,最后使用 dd.saveImage 将临时图片保存到本地相册
    3. 小程序 Canvas 里面内容的自适应

    技术实现

    页面实现
    	<view class="container">
                // 省略一些代码
                <canvas canvas-id="myQrCode" id="myQrCode" class="pay-code"></canvas>
                // 省略一些代码
    	</view>
    </view>
    

    最终效果如下:

    image.png

    页面上的二维码就是使用 weapp-qrcode 实现的,由于原生小程序不能使用 npm 安装第三方库,所以我们需要将源码下载到项目目录中,官方文档也给了使用示例:

    image.png

    我下载后需要改下源码,就是将 weapp.qrcode.esm.js 文件中使用到的微信小程序api替换成钉钉小程序的api,全局搜索 wx. 并替换为 dd. 。

    第一步:在页面引入插件:

    import drawQrcode from '/utils/weapp.qrcode.esm.js'
    const app = getApp()
    page({
        data:{
        },
        onload() {
        }
    })
    

    第二步:在 onload 生命周期将二维码画到 Canvas 上:

    import drawQrcode from '/utils/weapp.qrcode.esm.js'
    const app = getApp()
    page({
        data:{
        },
        onload(query) {
            let self = this
            let { qrCodeLink } = query
            setTimeout(() => {
                drawQrcode({
                     250,
                    height: 250,
                    canvasId: 'myQrCode',
                    text: qrCodeLink,
                })
            }, 500)
    
        }
    })
    

    这一步有两个要注意的地方,一个是设置了一个倒计时,是为了保证执行 drawQrcode 的时候为了保证能获取到页面上的 canvas 了,否则二维码画不出来,另一个就是 canvas 的 id,插件上的 canvasId 对应的是页面元素上的 canvas-id 属性,而钉钉小程序的 canvasId 对应的是页面元素上的 id,这一点没注意到的话会影响下一步。

    第三步:将二维码转为临时图片文件

    import drawQrcode from '/utils/weapp.qrcode.esm.js'
    const app = getApp()
    page({
        data:{
            filePath: ''
        },
        onload() {
            let self = this
            let { qrCodeLink } = query
            setTimeout(() => {
                drawQrcode({
                     220,
                    height: 220,
                    canvasId: 'myQrCode',
                    text: qrCodeLink,
                })
                setTimeout(() => {
                    let ctx = dd.createCanvasContext('myQrCode')
                    ctx.toTempFilePath({
                        fileType: "jpg",
                        quality: 1,
                        canvasId: 'myQrCode',
                        success: function(res) {
                            self.setData({
                                filePath: res.filePath
                            })
                        },
                        fail: function(e) {
                            console.log('fail:', e)
                        }
                    })
                }, 500)
            }, 500)
        }
    })
    

    这一步使用到了 toTempFilePath 方法,仍旧了设置了一个1秒的倒计时,为什么这样做呢? 因为上一步的 drawQrcode 是个耗时的同步任务,将 canvaas 转成图片前需要保证 canvas 已经在页面上生成了。需要注意的是 dd.createCanvasContext('myQrCode') 和 toTempFilePath 方法里的 canvasId 对应的是页面元素上的 id 属性。

    第四步:使 Canvas 上的内容自适应

    在onload生命周期里已经获取到屏幕尺寸:

    dd.getSystemInfo({
        success(res){
            self.setData({
                canWidth: res.windowWidth / 750, // 750宽的设计稿
                canHeight: res.windowWidth / 750 * 1239 // 750px 宽设计稿导出图片的高度像素
            })
        }
    })
    

    设置最终要转成图片的 canvas 宽高:

    <canvas style="{{canWidth*750}}px;height:{{canHeight}}px;position:absolute;left:-1000px;top:-1000px;"  canvas-id="myCanvas" id="myCanvas" class="myCanvas"></canvas>
    

    同时我还设置了绝对定位,目的是让这个画布脱离文档流并且显示在屏幕之外。

    将元素绘制到 canvas 上:

    let rpx = res.windowWidth / 750
    const ctx = dd.createCanvasContext('myCanvas')
    ctx.setFillStyle('#fff'); // 默认白色
    
    ctx.drawImage('/static/icon/logo.png',rpx * 307, rpx * 32, rpx * 135.2, rpx * 64)
    ctx.fillRect(0, 0, rpx * 750, res.windowWidth / 750 * 1239) // fillRect(x,y,宽度,高度)
    
    ctx.setFontSize(rpx * 56)
    ctx.setFillStyle('#191F25')
    ctx.setTextAlign('center')
    ctx.fillText(self.data.shopName, rpx * 750 / 2, rpx * 176)
    
    ctx.setFontSize(rpx * 24)
    ctx.setFillStyle('#333333')
    ctx.fillText('档口ID:'+ self.data.shopId, rpx * 750 / 2, rpx * 246)
    
    ctx.setFontSize(rpx * 28)
    ctx.setFillStyle('#333333')
    ctx.fillText('支付金额', rpx * 750 / 2, rpx * 338)
    
    ctx.setFontSize(rpx * 48)
    ctx.setFillStyle('#333333')
    ctx.fillText('¥' + self.data.totalAmount, rpx * 750 / 2, rpx * 396)
    
    ctx.drawImage(self.data.bankType == 2 ? '/static/icon/wechat.png' : '/static/icon/alipay.png',rpx * 153, rpx * 478, rpx * 64, rpx * 64)
    
    ctx.setFontSize(rpx * 28)
    ctx.setFillStyle('#333333')
    ctx.setTextAlign('left')
    ctx.fillText(self.data.bankType == 2 ? '微信' : '支付宝', rpx * 236, rpx * 520)
    
    ctx.setFontSize(rpx * 28)
    ctx.setFillStyle('#3296FA')
    ctx.setTextAlign('left')
    ctx.fillText('请使用' + (self.data.bankType == 2 ? '微信' : '支付宝') + '扫一扫', rpx * 355, rpx * 500)
    
    ctx.setFontSize(rpx * 28)
    ctx.setFillStyle('#3296FA')
    ctx.fillText('扫描二维码支付', rpx * 355, rpx * 544)
    ctx.drawImage(self.data.filePath, rpx * 149, rpx * 570, rpx * 452, rpx * 458)
    
    ctx.setFontSize(rpx * 24)
    ctx.setFillStyle('#919497')
    ctx.setTextAlign('center')
    ctx.fillText('充值单号', rpx * 750 / 2, rpx * 1095)
    
    ctx.setFontSize(rpx * 24)
    ctx.setFillStyle('#919497')
    ctx.setTextAlign('center')
    ctx.fillText(self.data.applyId, rpx * 750 / 2, rpx * 1134)
    
    ctx.draw(true)
    

    上面代码中的数值都是直接在设计稿上量出来的乘以 rpx 后就能自适应显示了。

    最后一步:将 canvas 转成图片并保存到相册,这些操作在 draw 方法的回调方法里执行:

    dd.showLoading() // 点击保存图片按钮后展示 loading
    let rpx = res.windowWidth / 750
    const ctx = dd.createCanvasContext('myCanvas')
    // 省略一些代码
    ctx.draw(true, (()=>{
        setTimeout(()=>{
            ctx.toTempFilePath({
                fileType: "jpg",
                quality: 1,
                canvasId: 'myCanvas',
                success: function(res) {
                    dd.saveImage({
                        url: res.filePath,
                        showActionSheet: true,
                        success: () => {
                            dd.hideLoading()
                            dd.alert({
                                title: '保存成功',
                            });
                        },
                        fail: function() {
                            dd.hideLoading()
                                dd.alert({
                                title: '保存失败',
                            });
                        }
                    });
                },
                fail: function() {
                    dd.hideLoading()
                        dd.alert({
                        title: '保存失败',
                    });
                }
            })
        }, 1000)
    })())
    

    到这里就基本实现需求了,当然还有可以优化的地方。导出的图片效果图如下:

    微信截图_20210627113209.png

    总结

    需求是实现了,但还是有几个点是值得再思考一下的:

    • canvas 转成图片后不清晰的问题
    • 保存图片到相册时,如果用户已禁止钉钉访问相册的话,如何给予用户友好的提示

    水平有限,文中难免有不足之处,欢迎大家关注我的微信公众号。(前端民工)

    image.png

  • 相关阅读:
    转 闭包简单理解
    mac 利用 sshpass 自动登录
    阮一峰 IaaS,PaaS,SaaS 的区别
    YMP运行初始化步骤
    强烈推荐 在线接口文档管理工具 小幺鸡 小团队可以省掉测试了
    springboot JSP 404
    并发测试 JavaDemo
    JS刷新当前页面的几种方法总结
    jquery checkbox勾选/取消勾选只能操作一次的诡异问题
    微信网页 第三方登录原理详解(转)
  • 原文地址:https://www.cnblogs.com/xiaoweihuang/p/14940459.html
Copyright © 2011-2022 走看看