uniapp+vue3+ts 使用canvas实现安卓端、ios端及微信小程序端二维码生成及下载

加粗样式uniapp多端生成带二维码海报并保存至相册的实现

在微信小程序开发中,我们常常会遇到生成带有二维码的海报并保存到手机相册的需求,比如分享活动海报、产品宣传海报等。今天就来和大家分享一下如何通过代码实现这一功能。

准备工作 在开始之前,我们需要先安装 weapp-qrcode 库,使用 npm install weapp-qrcode

命令即可完成安装。这个库将帮助我们方便地生成二维码。

- 代码实现

  • 模板部分
html 复制代码
<template>
  <!-- 海报容器 -->
  <view class="poster-container" :style="{height:getClientHeight(0)}">
    <canvas id="posterCanvas" canvas-id="posterCanvas" type="2d"
      :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
    <canvas style="position:absolute;left:-999px;" :style="{width:canvasWidth*0.29+'px',height:canvasWidth*0.29+'px'}"
      canvas-id="qrcodeCanvas" id="qrcodeCanvas"></canvas>
    <!-- 统一保存按钮 -->
    <button class="save-btn" @click="handleSave" :loading="btnLoading">保存到手机相册</button>
  </view>
</template>

在模板中,我们定义了一个海报容器,包含两个 canvas

元素,一个用于绘制海报内容,另一个用于生成二维码。同时,还有一个保存按钮,点击该按钮会触发 handleSave 方法来保存海报到相册。

  • 脚本部分
typescript 复制代码
```typescript
<script setup lang="ts">
  import QRCode from 'weapp-qrcode'
  import { getClientHeight } from '@/core/utils/verulia'
  const btnLoading = ref(false);
  const canvasWidth = ref(sysInfo.screenWidth)
  const canvasHeight = ref(canvasWidth.value * 1.56)
  const { proxy } = getCurrentInstance() || {}
  if (!proxy) {
    throw new Error('组件实例未找到')
  }
  //需要渲染到画布上的数据
  const xxxInfo = ref(null)
  let canvas
  // 生成二维码图片路径
  const generateQrcode = (text : string) => {
    return new Promise<string>((resolve, reject) => {
      try {
        const qrWidth = canvasWidth.value * 0.29;
        const qr = new QRCode({
          width: qrWidth,
          height: qrWidth,
          canvasId: 'qrcodeCanvas',
          text,
          correctLevel: 3
        })
        setTimeout(() => {
          uni.canvasToTempFilePath({
            canvasId: 'qrcodeCanvas',
            success: res => {
              resolve(res.tempFilePath)
            },
            fail: err => {
              uni.hideLoading()
              console.error('二维码生成失败:', err)
              reject(null)
            }
          })
        }, 500)
      } catch (e) {
        console.log(e)
        reject(e)
      }
    })
  }
  const loadWxImage = (path : string, canvas : any) => {
    return new Promise<HTMLImageElement>((resolve, reject) => {
      const img = canvas.createImage()
      img.onload = () => resolve(img)
      img.onerror = reject
      img.src = path
    })
  }
  // 微信小程序绘图处理
  const drawWeixinPoster = async () => {
    return new Promise((resolve, reject) => {
      try {
        console.log('准备获取 canvas 节点,id 为:', 'posterCanvas')
        uni.createSelectorQuery().select('#posterCanvas').fields({ node: true, size: true })
          .exec(async (res) => {
            console.log('获取 canvas 节点结果:', res)
            if (!res?.[0]?.node || JSON.stringify(res?.[0]?.node) === '{}') {
              uni.hideLoading()
              reject('获取canvas节点失败')
              return
            } else {
              canvas = res[0].node
              console.log('成功获取到 canvas 节点:', canvas)
            }
            const ctx = canvas.getContext('2d')
            if (!ctx) {
              reject('获取绘图上下文失败')
              return
            }
            //获取图片 微信小程序中需要这样转一下
            const [bgImage, orImage, deImage] = await Promise.all([
              loadWxImage("图片路径", canvas),
              loadWxImage("图片路径", canvas),
              loadWxImage("图片路径", canvas)
            ])
            let text = "二维码内容";
            let qrcodePath, qrImage
            if (text) {
              qrcodePath = await generateQrcode(String(text))
              qrImage = await new Promise<any>((resolve) => {
                const img = canvas.createImage()
                img.onload = () => resolve(img)
                img.src = qrcodePath
              })
            } else {
            //这里重新生成了一下,防止生成二维码失败
              setTimeout(async () => {
                let text1 = "二维码内容";
                if (text1) {
                  qrcodePath = await generateQrcode(String(text1))
                  qrImage = await new Promise<any>((resolve) => {
                    const img = canvas.createImage()
                    img.onload = () => resolve(img)
                    img.src = qrcodePath
                  })
                } else {
                  reject(null)
                  uni.hideLoading()
                }
              }, 1000)
            }
            const dpr = sysInfo.pixelRatio
            canvas.width = canvasWidth.value * dpr
            canvas.height = canvasHeight.value * dpr
            ctx.scale(dpr, dpr)
            //中间这里写你要在画布上要画的其他内容、根据自己需要改就可以,可以参考微信小程序/uniapp的官方文档
            //将二维码画上去
            ctx.drawImage(qrImage, canvasWidth.value * 0.29, canvasWidth.value * 0.915, canvasWidth.value * 0.42, canvasWidth.value * 0.42)
            uni.hideLoading()
            resolve(true)
          })
      } catch (e) {
        uni.hideLoading()
        console.log(e)
        reject(e)
      }
    })
  }
  //安卓端绘制方法
  const drawAppPoster = async () => {
    return new Promise(async (resolve, reject) => {
      try {
        console.log('准备获取 canvas 节点,id 为:', 'posterCanvas')
        const ctx = uni.createCanvasContext("posterCanvas",)
        if (!ctx) {
          reject('获取绘图上下文失败')
          return
        }
        let text = "二维码内容"
        let qrcodePath
        if (text) {
          qrcodePath = await generateQrcode(String(text))
        } else {
          setTimeout(async () => {
            let text1 = "二维码内容";
            if (text1) {
              qrcodePath = await generateQrcode(String(text1))
            } else {
              reject(null)
              uni.hideLoading()
            }
          }, 1000)
        }
        const dpr = sysInfo.pixelRatio
        //中间这里写你要在画布上要画的其他内容、根据自己需要改就可以,可以参考微信小程序/uniapp的官方文档
        //将二维码画上去
        await ctx.drawImage(qrcodePath, canvasWidth.value * 0.29, canvasWidth.value * 0.915, canvasWidth.value * 0.42, canvasWidth.value * 0.42)
        ctx.draw();
        uni.hideLoading()
        resolve(true)        
      } catch (e) {
        uni.hideLoading()
        console.log(e)
        reject(e)
      }
    })
  }
  //保存下载二维码
  const handleSave = async () => {
    btnLoading.value = true;
    try {
      if (uni.getSystemInfoSync().platform === 'android') {
        // #ifdef APP-PLUS
        // 安卓端使用 plus.android.requestPermissions 请求权限
        const Context = plus.android.importClass("android.content.Context");
        const PackageManager = plus.android.importClass("android.content.pm.PackageManager");
        const main = plus.android.runtimeMainActivity();
        const pm = main.getPackageManager();
        const permission = "android.permission.WRITE_EXTERNAL_STORAGE";
        const hasPermission = pm.checkPermission(permission, main.getPackageName()) === PackageManager.PERMISSION_GRANTED;
        if (!hasPermission) {
          plus.android.requestPermissions([permission], function (resultObj) {
            const result = resultObj.granted;
            if (result.indexOf(permission) !== -1) {
              console.log('已获取相册写入权限');
              // 在这里可以执行保存图片到相册的操作
              uni.canvasToTempFilePath({
                canvasId: 'posterCanvas',
                success: function (res) {
                  console.log(res.tempFilePath)
                  uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath })
                  uni.showToast({ title: '保存成功' })
                  btnLoading.value = false;
                },
                fail: function (e) {
                  console.log(e);
                  btnLoading.value = false;
                }
              })
            } else {
              console.log('用户拒绝授予相册写入权限');
              // 可以引导用户手动开启权限
              uni.showModal({
                title: '权限提示',
                content: '请在设置中开启相册写入权限,以便保存图片到相册。',
                success: (res) => {
                  if (res.confirm) {
                    // 打开设置页面
                    btnLoading.value = false;
                    uni.openSetting();
                  }
                }, fail: (err) => {
                  console.log(err);
                  btnLoading.value = false;
                }
              });
            }
          }, function (error) {
            console.error('请求权限出错:', error);
            btnLoading.value = false;
          });
        } else {
          console.log('已有相册写入权限');
          // 在这里可以执行保存图片到相册的操作
          uni.canvasToTempFilePath({
            canvasId: 'posterCanvas',
            success: function (res) {
              console.log(res.tempFilePath)
              uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath })
              uni.showToast({ title: '保存成功' });
              btnLoading.value = false;
            },
            fail: function (e) {
              console.log(e);
              btnLoading.value = false;
            }
          })
        }
        // #endif
        //#ifdef MP-WEIXIN
        // 1. 检查权限
        //安卓端微信小程序端保存下载
        const { authSetting } = await uni.getSetting();
        if (!authSetting['scope.writePhotosAlbum']) {
          btnLoading.value = false;
          await uni.authorize({ scope: 'scope.writePhotosAlbum' });
        }
        await uni.canvasToTempFilePath({
          canvas: canvas,
          success: function (res) {
            // 在H5平台下,tempFilePath 为 base64
            console.log(res.tempFilePath)
            uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath });
            uni.showToast({ title: '保存成功' });
            btnLoading.value = false;
          }, fail: function (e) {
            btnLoading.value = false;
            console.log(e);
          }
        });
        //#endif
      } else {
      //ios端及ios端微信小程序处理
        // 1. 检查权限
        const { authSetting } = await uni.getSetting();
        if (!authSetting['scope.writePhotosAlbum']) {
          btnLoading.value = false;
          await uni.authorize({ scope: 'scope.writePhotosAlbum' });
        }
        await uni.canvasToTempFilePath({
          canvas: canvas,
          success: function (res) {
            // 在H5平台下,tempFilePath 为 base64
            console.log(res.tempFilePath)
            uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath });
            uni.showToast({ title: '保存成功' });
            btnLoading.value = false;
          }, fail: function (e) {
            console.log(e);
            btnLoading.value = false;
          }
        });
      }
    } catch (e) {
      console.log(e);
      btnLoading.value = false;
    }
  };
  onReady(async () => {
    const xxxId = uni.getStorageSync("xxxId");
    uni.showLoading({
      mask: true
    })
    try {
    //这里要获取一下你的数据内容,请求接口这里只是示例
    const res = await api.getInfo(xxxId);
      xxxInfo.value = res.data
      await nextTick()
      setTimeout(async () => {
        // #ifdef MP-WEIXIN
        await drawWeixinPoster();
        // #endif
        // #ifdef APP-VUE
        await drawAppPoster()
        // #endif
      }, 1000)
    } catch (error) {
      uni.hideLoading()
      console.error('数据请求失败:', error)
    }
  })
</script>
  • 样式部分
css 复制代码
<style>
  /* 统一样式 */
  .poster-container {
    background: #fff;
  }
  .save-btn {
    margin-top: 40rpx;
    width: 80%;
    height: 80rpx;
    line-height: 80rpx;
    background: #3366FF;
    color: white;
    border-radius: 40rpx;
  }
  #posterCanvas {
    width: 100%;
    height: auto;
  }
</style>
相关推荐
Deepsleep.3 小时前
东田数码科技前端面经
前端·科技·面试
缘来的精彩4 小时前
Kotlin与Jetpack Compose的详细使用指南
android·kotlin·android studio·compose·viewmodel
涵信4 小时前
第十八节:开放性问题-Vue生态未来趋势
前端·vue.js·devops
牧杉-惊蛰5 小时前
css 数字从0开始增加的动画效果
前端·javascript·css
孤灯淡茶5 小时前
Fiori学习专题十五:Nested Views
前端·javascript·学习
green_pine_5 小时前
CSS学习笔记14——移动端相关知识(rem,媒体查询,less)
前端·css·笔记·学习·less
Monly215 小时前
Vue:el-table-tree懒加载数据
前端·javascript·vue.js
进取星辰5 小时前
16、路由守卫:设置魔法结界——React 19 React Router
前端·javascript·react.js
wx_cxc28486989185 小时前
汽车用品商城小程序源码介绍
微信小程序·小程序
守城小轩5 小时前
Chromium 134 编译指南 - Android 篇:获取源码(五)
android