微信小程序实现动态二维码海报生成与保存 | 高效便捷的前端方案

WEBSITE:Welcome to Jessie's World GitHub:GitHub 前言 在微信小程序开发中,经常需要实现分享海报功能,通常包含动态二维码。本文将详细介绍如何在小程序中生成带有二维码的海报,并实现保存到手机相册的功能。 实现原理 整个功能的实现主要包含以下几个步骤: 生成二维码 绘制海报背景 将二维码绘制到海报上 将画布导出为图片 保存图片到相册 核心代码实现 1. 二维码生成工具类 首先,我们需要一个二维码生成的工具类。这里使用优化后的 QRCode 工具: qrcode.js:绘制二维码 // qrcode.js 绘制二维码 !(function () { // alignment pattern var adelta = [ 0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28 ]; // version block var vpat = [ 0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69 ]; // final format bits with mask: level y) { bt = x; x = y; y = bt; } // y*y = 1+3+5... bt = y; bt *= y; bt += y; bt >>= 1; bt += x; framask[bt] = 1; } // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask) function putalign(x, y) { var j; qrframe[x + width * y] = 1; for (j = -2; j > 8) + (x & 255); } return x; } var genpoly = []; // Calculate and append ECC data to data block. Block is in strinbuf, indexes to buffers given. function appendrs(data, dlen, ecbuf, eclen) { var i, j, fb; for (i = 0; i

Mar 14, 2025 - 10:40
 0
微信小程序实现动态二维码海报生成与保存 | 高效便捷的前端方案

WEBSITE:Welcome to Jessie's World

GitHub:GitHub

前言

在微信小程序开发中,经常需要实现分享海报功能,通常包含动态二维码。本文将详细介绍如何在小程序中生成带有二维码的海报,并实现保存到手机相册的功能。

实现原理

整个功能的实现主要包含以下几个步骤:

  1. 生成二维码
  2. 绘制海报背景
  3. 将二维码绘制到海报上
  4. 将画布导出为图片
  5. 保存图片到相册

核心代码实现

1. 二维码生成工具类

首先,我们需要一个二维码生成的工具类。这里使用优化后的 QRCode 工具:

qrcode.js:绘制二维码

// qrcode.js 绘制二维码

!(function () {
  // alignment pattern
  var adelta = [
    0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, 26, 26, 28, 28, 24, 24, 26, 26,
    26, 28, 28, 24, 26, 26, 26, 28, 28
  ];

  // version block
  var vpat = [
    0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4,
    0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69
  ];

  // final format bits with mask: level << 3 | mask
  var fmtword = [
    0x77c4,
    0x72f3,
    0x7daa,
    0x789d,
    0x662f,
    0x6318,
    0x6c41,
    0x6976, //L
    0x5412,
    0x5125,
    0x5e7c,
    0x5b4b,
    0x45f9,
    0x40ce,
    0x4f97,
    0x4aa0, //M
    0x355f,
    0x3068,
    0x3f31,
    0x3a06,
    0x24b4,
    0x2183,
    0x2eda,
    0x2bed, //Q
    0x1689,
    0x13be,
    0x1ce7,
    0x19d0,
    0x0762,
    0x0255,
    0x0d0c,
    0x083b //H
  ];

  // 4 per version: number of blocks 1,2; data width; ecc width
  var eccblocks = [
    1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28, 1, 0, 55, 15, 1,
    0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2,
    2, 15, 18, 2, 2, 11, 22, 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,
    1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24, 2,
    2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6,
    2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,
    11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19,
    24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2,
    19, 14, 28, 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10,
    15, 28, 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13,
    24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
    8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8,
    4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30, 7, 7,
    116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3,
    115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1,
    115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30, 12, 7,
    121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4,
    122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20,
    4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
  ];

  // Galois field log table
  var glog = [
    0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, 0x04, 0x64, 0xe0, 0x0e, 0x34,
    0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93,
    0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72,
    0xa6, 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, 0x36, 0xd0, 0x94, 0xce,
    0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b,
    0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3,
    0xa7, 0x57, 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, 0xe3, 0xa5, 0x99,
    0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd,
    0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47,
    0x6d, 0x41, 0xa2, 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, 0x6c, 0xa1,
    0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0,
    0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea,
    0xa8, 0x50, 0x58, 0xaf
  ];

  // Galios field exponent table
  var gexp = [
    0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, 0x4c, 0x98, 0x2d, 0x5a, 0xb4,
    0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4,
    0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde,
    0xa1, 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, 0xfd, 0xe7, 0xd3, 0xbb,
    0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d,
    0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33,
    0x66, 0xcc, 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, 0xa8, 0x4d, 0x9a,
    0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e,
    0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5,
    0x57, 0xae, 0x41, 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, 0x51, 0xa2,
    0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4,
    0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8,
    0xad, 0x47, 0x8e, 0x00
  ];

  // Working buffers:
  // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
  var strinbuf = [],
    eccbuf = [],
    qrframe = [],
    framask = [],
    rlens = [];
  // Control values - width is based on version, last 4 are from table.
  var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
  var ecclevel = 2;
  // set bit to indicate cell in qrframe is immutable.  symmetric around diagonal
  function setmask(x, y) {
    var bt;
    if (x > y) {
      bt = x;
      x = y;
      y = bt;
    }
    // y*y = 1+3+5...
    bt = y;
    bt *= y;
    bt += y;
    bt >>= 1;
    bt += x;
    framask[bt] = 1;
  }

  // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
  function putalign(x, y) {
    var j;

    qrframe[x + width * y] = 1;
    for (j = -2; j < 2; j++) {
      qrframe[x + j + width * (y - 2)] = 1;
      qrframe[x - 2 + width * (y + j + 1)] = 1;
      qrframe[x + 2 + width * (y + j)] = 1;
      qrframe[x + j + 1 + width * (y + 2)] = 1;
    }
    for (j = 0; j < 2; j++) {
      setmask(x - 1, y + j);
      setmask(x + 1, y - j);
      setmask(x - j, y - 1);
      setmask(x + j, y + 1);
    }
  }

  //========================================================================
  // Reed Solomon error correction
  // exponentiation mod N
  function modnn(x) {
    while (x >= 255) {
      x -= 255;
      x = (x >> 8) + (x & 255);
    }
    return x;
  }

  var genpoly = [];

  // Calculate and append ECC data to data block.  Block is in strinbuf, indexes to buffers given.
  function appendrs(data, dlen, ecbuf, eclen) {
    var i, j, fb;

    for (i = 0; i < eclen; i++) strinbuf[ecbuf + i] = 0;
    for (i = 0; i < dlen; i++) {
      fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
      if (fb != 255)
        /* fb term is non-zero */
        for (j = 1; j < eclen; j++) strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
      else for (j = ecbuf; j < ecbuf + eclen; j++) strinbuf[j] = strinbuf[j + 1];
      strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
    }
  }

  //========================================================================
  // Frame data insert following the path rules

  // check mask - since symmetrical use half.
  function ismasked(x, y) {
    var bt;
    if (x > y) {
      bt = x;
      x = y;
      y = bt;
    }
    bt = y;
    bt += y * y;
    bt >>= 1;
    bt += x;
    return framask[bt];
  }

  //========================================================================
  //  Apply the selected mask out of the 8.
  function applymask(m) {
    var x, y, r3x, r3y;

    switch (m) {
      case 0:
        for (y = 0; y < width; y++)
          for (x = 0; x < width; x++) if (!((x + y) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
        break;
      case 1:
        for (y = 0; y < width; y++) for (x = 0; x < width; x++) if (!(y & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
        break;
      case 2:
        for (y = 0; y < width; y++)
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        break;
      case 3:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = r3y, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 4:
        for (y = 0; y < width; y++)
          for (r3x = 0, r3y = (y >> 1) & 1, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) {
              r3x = 0;
              r3y = !r3y;
            }
            if (!r3y && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        break;
      case 5:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 6:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!(((x & y & 1) + (r3x && r3x == r3y)) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 7:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!(((r3x && r3x == r3y) + ((x + y) & 1)) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
    }
    return;
  }

  // Badness coefficients.
  var N1 = 3,
    N2 = 3,
    N3 = 40,
    N4 = 10;

  // Using the table of the length of each run, calculate the amount of bad image
  // - long runs or those that look like finders; called twice, once each for X and Y
  function badruns(length) {
    var i;
    var runsbad = 0;
    for (i = 0; i <= length; i++) if (rlens[i] >= 5) runsbad += N1 + rlens[i] - 5;
    // BwBBBwB as in finder
    for (i = 3; i < length - 1; i += 2)
      if (
        rlens[i - 2] == rlens[i + 2] &&
        rlens[i + 2] == rlens[i - 1] &&
        rlens[i - 1] == rlens[i + 1] &&
        rlens[i - 1] * 3 == rlens[i] &&
        // white around the black pattern? Not part of spec
        (rlens[i - 3] == 0 || // beginning
          i + 3 > length || // end
          rlens[i - 3] * 3 >= rlens[i] * 4 ||
          rlens[i + 3] * 3 >= rlens[i] * 4)
      )
        runsbad += N3;
    return runsbad;
  }

  // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
  function badcheck() {
    var x, y, h, b, b1;
    var thisbad = 0;
    var bw = 0;

    // blocks of same color.
    for (y = 0; y < width - 1; y++)
      for (x = 0; x < width - 1; x++)
        if (
          (qrframe[x + width * y] &&
            qrframe[x + 1 + width * y] &&
            qrframe[x + width * (y + 1)] &&
            qrframe[x + 1 + width * (y + 1)]) || // all black
          !(
            qrframe[x + width * y] ||
            qrframe[x + 1 + width * y] ||
            qrframe[x + width * (y + 1)] ||
            qrframe[x + 1 + width * (y + 1)]
          )
        )
          // all white
          thisbad += N2;

    // X runs
    for (y = 0; y < width; y++) {
      rlens[0] = 0;
      for (h = b = x = 0; x < width; x++) {
        if ((b1 = qrframe[x + width * y]) == b) rlens[h]++;
        else rlens[++h] = 1;
        b = b1;
        bw += b ? 1 : -1;
      }
      thisbad += badruns(h);
    }

    // black/white imbalance
    if (bw < 0) bw = -bw;

    var big = bw;
    var count = 0;
    big += big << 2;
    big <<= 1;
    while (big > width * width) (big -= width * width), count++;
    thisbad += count * N4;

    // Y runs
    for (x = 0; x < width; x++) {
      rlens[0] = 0;
      for (h = b = y = 0; y < width; y++) {
        if ((b1 = qrframe[x + width * y]) == b) rlens[h]++;
        else rlens[++h] = 1;
        b = b1;
      }
      thisbad += badruns(h);
    }
    return thisbad;
  }

  function genframe(instring) {
    var x, y, k, t, v, i, j, m;

    // find the smallest version that fits the string
    t = instring.length;
    version = 0;
    do {
      version++;
      k = (ecclevel - 1) * 4 + (version - 1) * 16;
      neccblk1 = eccblocks[k++];
      neccblk2 = eccblocks[k++];
      datablkw = eccblocks[k++];
      eccblkwid = eccblocks[k];
      k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
      if (t <= k) break;
    } while (version < 40);

    // FIXME - insure that it fits insted of being truncated
    width = 17 + 4 * version;

    // allocate, clear and setup data structures
    v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
    for (t = 0; t < v; t++) eccbuf[t] = 0;
    strinbuf = instring.slice(0);

    for (t = 0; t < width * width; t++) qrframe[t] = 0;

    for (t = 0; t < (width * (width + 1) + 1) / 2; t++) framask[t] = 0;

    // insert finders - black to frame, white to mask
    for (t = 0; t < 3; t++) {
      k = 0;
      y = 0;
      if (t == 1) k = width - 7;
      if (t == 2) y = width - 7;
      qrframe[y + 3 + width * (k + 3)] = 1;
      for (x = 0; x < 6; x++) {
        qrframe[y + x + width * k] = 1;
        qrframe[y + width * (k + x + 1)] = 1;
        qrframe[y + 6 + width * (k + x)] = 1;
        qrframe[y + x + 1 + width * (k + 6)] = 1;
      }
      for (x = 1; x < 5; x++) {
        setmask(y + x, k + 1);
        setmask(y + 1, k + x + 1);
        setmask(y + 5, k + x);
        setmask(y + x + 1, k + 5);
      }
      for (x = 2; x < 4; x++) {
        qrframe[y + x + width * (k + 2)] = 1;
        qrframe[y + 2 + width * (k + x + 1)] = 1;
        qrframe[y + 4 + width * (k + x)] = 1;
        qrframe[y + x + 1 + width * (k + 4)] = 1;
      }
    }

    // alignment blocks
    if (version > 1) {
      t = adelta[version];
      y = width - 7;
      for (;;) {
        x = width - 7;
        while (x > t - 3) {
          putalign(x, y);
          if (x < t) break;
          x -= t;
        }
        if (y <= t + 9) break;
        y -= t;
        putalign(6, y);
        putalign(y, 6);
      }
    }

    // single black
    qrframe[8 + width * (width - 8)] = 1;

    // timing gap - mask only
    for (y = 0; y < 7; y++) {
      setmask(7, y);
      setmask(width - 8, y);
      setmask(7, y + width - 7);
    }
    for (x = 0; x < 8; x++) {
      setmask(x, 7);
      setmask(x + width - 8, 7);
      setmask(x, width - 8);
    }

    // reserve mask-format area
    for (x = 0; x < 9; x++) setmask(x, 8);
    for (x = 0; x < 8; x++) {
      setmask(x + width - 8, 8);
      setmask(8, x);
    }
    for (y = 0; y < 7; y++) setmask(8, y + width - 7);

    // timing row/col
    for (x = 0; x < width - 14; x++)
      if (x & 1) {
        setmask(8 + x, 6);
        setmask(6, 8 + x);
      } else {
        qrframe[8 + x + width * 6] = 1;
        qrframe[6 + width * (8 + x)] = 1;
      }

    // version block
    if (version > 6) {
      t = vpat[version - 7];
      k = 17;
      for (x = 0; x < 6; x++)
        for (y = 0; y < 3; y++, k--)
          if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
            qrframe[5 - x + width * (2 - y + width - 11)] = 1;
            qrframe[2 - y + width - 11 + width * (5 - x)] = 1;
          } else {
            setmask(5 - x, 2 - y + width - 11);
            setmask(2 - y + width - 11, 5 - x);
          }
    }

    // sync mask bits - only set above for white spaces, so add in black bits
    for (y = 0; y < width; y++) for (x = 0; x <= y; x++) if (qrframe[x + width * y]) setmask(x, y);

    // convert string to bitstream
    // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
    v = strinbuf.length;

    // string to array
    for (i = 0; i < v; i++) eccbuf[i] = strinbuf.charCodeAt(i);
    strinbuf = eccbuf.slice(0);

    // calculate max string length
    x = datablkw * (neccblk1 + neccblk2) + neccblk2;
    if (v >= x - 2) {
      v = x - 2;
      if (version > 9) v--;
    }

    // shift and repack to insert length prefix
    i = v;
    if (version > 9) {
      strinbuf[i + 2] = 0;
      strinbuf[i + 3] = 0;
      while (i--) {
        t = strinbuf[i];
        strinbuf[i + 3] |= 255 & (t << 4);
        strinbuf[i + 2] = t >> 4;
      }
      strinbuf[2] |= 255 & (v << 4);
      strinbuf[1] = v >> 4;
      strinbuf[0] = 0x40 | (v >> 12);
    } else {
      strinbuf[i + 1] = 0;
      strinbuf[i + 2] = 0;
      while (i--) {
        t = strinbuf[i];
        strinbuf[i + 2] |= 255 & (t << 4);
        strinbuf[i + 1] = t >> 4;
      }
      strinbuf[1] |= 255 & (v << 4);
      strinbuf[0] = 0x40 | (v >> 4);
    }
    // fill to end with pad pattern
    i = v + 3 - (version < 10);
    while (i < x) {
      strinbuf[i++] = 0xec;
      // buffer has room    if (i == x)      break;
      strinbuf[i++] = 0x11;
    }

    // calculate and append ECC

    // calculate generator polynomial
    genpoly[0] = 1;
    for (i = 0; i < eccblkwid; i++) {
      genpoly[i + 1] = 1;
      for (j = i; j > 0; j--) genpoly[j] = genpoly[j] ? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
      genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
    }
    for (i = 0; i <= eccblkwid; i++) genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step

    // append ecc to data buffer
    k = x;
    y = 0;
    for (i = 0; i < neccblk1; i++) {
      appendrs(y, datablkw, k, eccblkwid);
      y += datablkw;
      k += eccblkwid;
    }
    for (i = 0; i < neccblk2; i++) {
      appendrs(y, datablkw + 1, k, eccblkwid);
      y += datablkw + 1;
      k += eccblkwid;
    }
    // interleave blocks
    y = 0;
    for (i = 0; i < datablkw; i++) {
      for (j = 0; j < neccblk1; j++) eccbuf[y++] = strinbuf[i + j * datablkw];
      for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[neccblk1 * datablkw + i + j * (datablkw + 1)];
    }
    for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[neccblk1 * datablkw + i + j * (datablkw + 1)];
    for (i = 0; i < eccblkwid; i++) for (j = 0; j < neccblk1 + neccblk2; j++) eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
    strinbuf = eccbuf;

    // pack bits into frame avoiding masked area.
    x = y = width - 1;
    k = v = 1; // up, minus
    /* inteleaved data and ecc codes */
    m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
    for (i = 0; i < m; i++) {
      t = strinbuf[i];
      for (j = 0; j < 8; j++, t <<= 1) {
        if (0x80 & t) qrframe[x + width * y] = 1;
        do {
          // find next fill position
          if (v) x--;
          else {
            x++;
            if (k) {
              if (y != 0) y--;
              else {
                x -= 2;
                k = !k;
                if (x == 6) {
                  x--;
                  y = 9;
                }
              }
            } else {
              if (y != width - 1) y++;
              else {
                x -= 2;
                k = !k;
                if (x == 6) {
                  x--;
                  y -= 8;
                }
              }
            }
          }
          v = !v;
        } while (ismasked(x, y));
      }
    }

    // save pre-mask copy of frame
    strinbuf = qrframe.slice(0);
    t = 0; // best
    y = 30000; // demerit
    // for instead of while since in original arduino code
    // if an early mask was "good enough" it wouldn't try for a better one
    // since they get more complex and take longer.
    for (k = 0; k < 8; k++) {
      applymask(k); // returns black-white imbalance
      x = badcheck();
      if (x < y) {
        // current mask better than previous best?
        y = x;
        t = k;
      }
      if (t == 7) break; // don't increment i to a void redoing mask
      qrframe = strinbuf.slice(0); // reset for next pass
    }
    if (t != k)
      // redo best mask - none good enough, last wasn't t
      applymask(t);

    // add in final mask/ecclevel bytes
    y = fmtword[t + ((ecclevel - 1) << 3)];
    // low byte
    for (k = 0; k < 8; k++, y >>= 1)
      if (y & 1) {
        qrframe[width - 1 - k + width * 8] = 1;
        if (k < 6) qrframe[8 + width * k] = 1;
        else qrframe[8 + width * (k + 1)] = 1;
      }
    // high byte
    for (k = 0; k < 7; k++, y >>= 1)
      if (y & 1) {
        qrframe[8 + width * (width - 7 + k)] = 1;
        if (k) qrframe[6 - k + width * 8] = 1;
        else qrframe[7 + width * 8] = 1;
      }
    return qrframe;
  }

  var _canvas = null;

  var api = {
    get ecclevel() {
      return ecclevel;
    },

    set ecclevel(val) {
      ecclevel = val;
    },

    get size() {
      return _size;
    },

    set size(val) {
      _size = val;
    },

    get canvas() {
      return _canvas;
    },

    set canvas(el) {
      _canvas = el;
    },

    drawRoundRectPath: function (cxt, width, height, radius) {
      cxt.beginPath(0);
      //从右下角顺时针绘制,弧度从0到1/2PI
      cxt.arc(width - radius, height - radius, radius, 0, Math.PI / 2);

      //矩形下边线
      cxt.lineTo(radius, height);

      //左下角圆弧,弧度从1/2PI到PI
      cxt.arc(radius, height - radius, radius, Math.PI / 2, Math.PI);

      //矩形左边线
      cxt.lineTo(0, radius);

      //左上角圆弧,弧度从PI到3/2PI
      cxt.arc(radius, radius, radius, Math.PI, (Math.PI * 3) / 2);

      //上边线
      cxt.lineTo(width - radius, 0);

      //右上角圆弧
      cxt.arc(width - radius, radius, radius, (Math.PI * 3) / 2, Math.PI * 2);

      //右边线
      cxt.lineTo(width, height - radius);
      cxt.closePath();
    },
    /**该方法用来绘制一个有填充色的圆角矩形
     *@param cxt:canvas的上下文环境
     *@param x:左上角x轴坐标
     *@param y:左上角y轴坐标
     *@param width:矩形的宽度
     *@param height:矩形的高度
     *@param radius:圆的半径
     *@param fillColor:填充颜色
     **/
    fillRoundRect: function (cxt, x, y, width, height, radius, /*optional*/ fillColor) {
      //圆的直径必然要小于矩形的宽高
      if (2 * radius > width || 2 * radius > height) {
        return false;
      }

      cxt.save();
      cxt.translate(x, y);
      //绘制圆角矩形的各个边
      this.drawRoundRectPath(cxt, width, height, radius);
      cxt.fillStyle = fillColor || '#000'; //若是给定了值就用给定的值否则给予默认值
      cxt.fill();
      cxt.restore();
    },
    getFrame: function (string) {
      return genframe(string);
    },
    //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
    utf16to8: function (str) {
      var out, i, len, c;

      out = '';
      len = str && str.length;
      for (i = 0; i < len; i++) {
        c = str.charCodeAt(i);
        if (c >= 0x0001 && c <= 0x007f) {
          out += str.charAt(i);
        } else if (c > 0x07ff) {
          out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));
          out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
        } else {
          out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
        }
      }
      return out;
    },

    /**
     * 新的绘制方法,用于在指定的画布上绘制二维码。
     * @param {Function} createSelectorQuery - 用于创建选择器查询的函数。
     * @param {string} str - 要编码为二维码的字符串。
     * @param {string} _canvasId - 画布的ID(可选)。
     * @param {number} cavW - 画布的宽度。
     * @param {number} cavH - 画布的高度。
     * @param {Object} $this - 组件的上下文,允许在组件中生成二维码。
     * @param {number} ecc - 错误纠正级别(可选)。
     * @param {Function} callBack - 绘制完成后的回调函数。
     * @param {HTMLCanvasElement} _Canvas - 画布元素。
     * @param {number} dpr - 设备像素比,用于高分辨率屏幕。
     */
    newDraw: async function (
      createSelectorQuery,
      str,
      _canvasId,
      cavW,
      cavH,
      $this,
      ecc,
      callBack,
      _Canvas,
      dpr,
      fillStyle,
      fillBgStyle = '#ffffff'
    ) {
      var that = this; // 保存当前上下文
      ecclevel = ecc || ecclevel; // 设置错误纠正级别,如果未提供则使用默认值
      const canvasId = _canvasId || _canvas; // 获取画布ID,如果未提供则使用默认画布

      // 检查是否提供了画布
      if (!canvasId) {
        console.warn('No canvas provided to draw QR code in!'); // 如果没有画布,发出警告
        return; // 退出函数
      }

      var size = Math.min(cavW, cavH); // 计算画布的最小尺寸
      str = that.utf16to8(str); // 将字符串转换为UTF-8格式,以支持中文

      var frame = that.getFrame(str), // 获取二维码的帧数据
        px = Math.round(size / (width + 8)); // 计算每个二维码单元的像素大小
      var ctx = _Canvas.getContext('2d'); // 获取2D绘图上下文
      _Canvas.width = cavW * dpr; // 设置画布宽度,考虑设备像素比
      _Canvas.height = cavH * dpr; // 设置画布高度,考虑设备像素比
      ctx.scale(dpr, dpr); // 缩放上下文以适应高分辨率

      var roundedSize = px * (width + 8), // 计算绘制的二维码的总大小
        offset = Math.floor((size - roundedSize) / 2); // 计算偏移量以居中二维码
      size = roundedSize; // 更新大小变量
      ctx.fillStyle = '#FEF5E0'; // 设置填充颜色为白色
      this.fillRoundRect(ctx, 0, 0, cavW, cavW, 10, /*optional*/ fillBgStyle); // 绘制圆角矩形背景
      ctx.fillStyle = '#FF7D00'; // 设置填充颜色为黑色

      // 绘制二维码的每个模块
      for (var i = 0; i < width; i++) {
        for (var j = 0; j < width; j++) {
          if (frame[j * width + i]) {
            // 如果当前模块是黑色
            ctx.fillRect(px * (4 + i) + offset, px * (4 + j) + offset, px, px); // 绘制黑色矩形
          }
        }
      }

      callBack && callBack(); // 如果提供了回调函数,则执行它
    }
  };
  module.exports = { api };
  // exports.draw = api;
})();

qrcode-helper.ts:创建二维码的增强辅助函数,支持自定义样式和高清显示

// qrcode-helper.ts
/**
 * 创建二维码的增强辅助函数,支持自定义样式和高清显示
 * 
 * @param {Object} _this - 组件实例的this引用,用于上下文绑定
 * @param {string} qrCode - 需要生成二维码的字符串内容
 * @param {string} canvasId - canvas元素的ID标识符
 * @param {number} cavW - canvas的宽度(单位:px)
 * @param {number} cavH - canvas的高度(单位:px)
 * @param {Function} success - 成功回调函数,参数为生成的临时文件路径
 * @param {Function} fail - 失败回调函数,参数为错误信息对象
 * @param {Function} complete - 完成回调函数,无论成功失败都会执行
 * @param {HTMLCanvasElement} _Canvas - canvas DOM元素实例
 * @param {number} dpr - 设备像素比,用于处理高清屏幕显示
 * @param {string} [fillStyle] - 二维码前景色,可选参数,默认为黑色
 * @param {string} [fillBgStyle] - 二维码背景色,可选参数,默认为白色
 * 
 * @example
 *
 * newCreateQrCodeHelper(
 *   this,
 *   'https://example.com',
 *   'qrCanvas',
 *   200,
 *   200,
 *   (tempFilePath) => console.log('成功:', tempFilePath),
 *   (error) => console.error('失败:', error),
 *   () => console.log('完成'),
 *   canvasElement,
 *   2,
 *   '#000000',
 *   '#ffffff'
 * );
 * 
 * 
 * @description
 * 该函数主要用于在小程序环境中生成二维码,具有以下特点:
 * 1. 支持高清显示,通过dpr参数适配不同设备
 * 2. 可自定义二维码颜色样式
 * 3. 支持成功/失败/完成三种回调
 * 4. 仅支持微信小程序环境(WEAPP)
 * 
 * @throws {Error} 当运行环境不是微信小程序时,会通过fail回调返回错误
 * 
 * @returns {void}
 */
export function newCreateQrCodeHelper(
  _this,
  qrCode,
  canvasId,
  cavW,
  cavH,
  success,
  fail,
  complete,
  _Canvas,
  dpr,
  fillStyle,
  fillBgStyle
) {
  // 获取当前运行环境
  const env = Taro.getEnv();

  // 判断是否在微信小程序环境中
  if (env === 'WEAPP') {
    // 调用QR.api.newDraw方法绘制二维码
    QR.api.newDraw(
      Taro.createSelectorQuery,  // 创建选择器查询对象的函数
      qrCode,                    // 二维码内容
      canvasId,                  // Canvas ID
      cavW,                      // 画布宽度
      cavH,                      // 画布高度
      _this,                     // 组件实例引用
      null,                      // 二维码配置项,这里使用默认值
      // 绘制完成后的回调,将画布内容转换为图片
      () => newCanvasToTempFilePath(
        _Canvas,    // Canvas实例
        cavW,       // 宽度
        cavH,       // 高度
        success,    // 成功回调
        fail,       // 失败回调
        complete,   // 完成回调
        _this       // 组件实例引用
      ),
      _Canvas,      // Canvas实例
      dpr,          // 设备像素比
      fillStyle,    // 二维码前景色
      fillBgStyle   // 二维码背景色
    );
  } else {
    // 如果不是在微信小程序环境中,调用失败回调
    fail({ errorMessage: `不支持的平台:${env}` });
  }
}

2. 海报组件实现

export default class InvitePoster extends Component {
  state = {
    isLoading: false,
    posterImage: '',
  }

  /**
   * 创建动态二维码并处理相关逻辑
   * @param str 需要生成二维码的字符串
   * @param canvasId canvas元素的ID
   * @param cavW canvas宽度
   * @param cavH canvas高度
   * @param retryCount 重试次数,默认3次
   */
  async createQrCode(str: string, canvasId: string, cavW: number, cavH: number, retryCount = 3) {
    // 检查重试次数
    if (retryCount <= 0) {
      console.error('二维码生成重试次数已用完');
      Taro.showToast({
        title: '生成失败,请重试',
        icon: 'none'
      });
      return;
    }

    let isDrawing = false;

    try {
      // 获取canvas实例
      const _canvas = await this.getCanvas(canvasId);
      const dpr = Taro.getSystemInfoSync().pixelRatio;

      // 生成二维码
      newCreateQrCodeHelper(
        this,
        str,
        canvasId,
        cavW,
        cavH,
        (tempFilePath: string) => {
          isDrawing = true;
          this.createPoster(tempFilePath);
        },
        (error: Error) => {
          this.handleQrCodeError(error, str, canvasId, cavW, cavH, retryCount);
        },
        () => {
          if (!isDrawing) {
            console.warn('二维码绘制可能未完成,请检查');
          }
        },
        _canvas,
        dpr,
        null,
        '#FEF5E0'
      );
    } catch (error) {
      this.handleCanvasError();
    }
  }

  /**
 * 创建分享海报
 * 该函数负责将二维码和背景图片合成为一张完整的分享海报
 * 
 * @param {string} qrcode - 已生成的二维码图片的临时路径
 * 
 * @description
 * 海报生成流程:
 * 1. 创建画布并设置尺寸(适配不同设备)
 * 2. 绘制背景图片
 * 3. 绘制二维码背景色
 * 4. 绘制二维码
 * 5. 将画布导出为图片
 * 
 * @example
 * 
 * this.createPoster('tempFilePath/qrcode.png');
 *
 */
createPoster(qrcode) {
  // 检查二维码参数
  console.log('qrcodeqrcodeqrcodeqrcode', qrcode);
  var _this = this;

  // 参数验证:确保二维码路径存在
  if (!qrcode) {
    console.error('二维码图片路径为空');
    Taro.showToast({
      title: '海报生成失败',
      icon: 'none'
    });
    return;
  }

  // 获取画布尺寸配置
  let size = this.setCanvasSize();

  // 获取设备像素比,用于高清适配
  const dpr = Taro.getSystemInfoSync().pixelRatio;

  // 获取画布上下文
  Taro.createSelectorQuery()
    .select('#poster') // 选择海报画布节点
    .node(({ node: canvas }) => {
      const context = canvas.getContext('2d');
      // 清空画布
      context.clearRect(0, 0, canvas.width, canvas.height);

      // 设置画布尺寸,考虑设备像素比
      canvas.width = windowWidth * dpr; // 画布宽度
      canvas.height = ((812 * windowWidth) / 375) * dpr; // 画布高度,保持宽高比
      context.scale(dpr, dpr); // 缩放上下文以适应高分辨率

      // 创建并加载背景图片
      const image1 = canvas.createImage();
      image1.onload = () => {
        // 绘制背景图片,适配屏幕宽度
        context.drawImage(image1, 0, 0, windowWidth, (812 * windowWidth) / 375);

        // 绘制二维码背景
        context.fillStyle = '#FEF5E0'; // 设置二维码背景色
        context.fillRect(size.qrX, size.qrY, size.qw, size.qh);

        // 保存当前绘图状态
        context.save();

        // 创建并加载二维码图片
        const image2 = canvas.createImage();
        image2.onload = () => {
          // 绘制二维码到指定位置
          context.drawImage(image2, size.qrX, size.qrY, size.qw, size.qh);

          // 延迟导出图片,确保绘制完成
          setTimeout(() => {
            // 将画布内容导出为图片
            wx.canvasToTempFilePath(
              {
                width: 750, // 输出图片宽度
                height: 1624, // 输出图片高度
                destWidth: 750 * 2, // 输出图片实际宽度(乘2用于高清显示)
                destHeight: 1624 * 2, // 输出图片实际高度
                canvas: canvas, // canvas实例,2D接口需要传入
                x: 0, // 裁剪起点x坐标
                y: 0, // 裁剪起点y坐标
                canvasId: 'poster', // 画布标识符
                success: function (res) {
                  // 导出成功,更新状态
                  console.log('海报成功:', res.tempFilePath);
                  console.log({ posterImage: res });
                  _this.setState({
                    posterImage: res.tempFilePath,
                    isLoading: false
                  });
                },
                fail: err => {
                  // 导出失败,提示重试
                  Taro.showModal({
                    title: '生成海报失败,请重试',
                    showCancel: false,
                    success: () => {
                      _this.createPoster(qrcode); // 失败后重试
                    }
                  });
                }
              },
              canvas
            );
          }, 200); // 给予200ms延迟确保绘制完成
        };
        // 设置二维码图片源
        image2.src = qrcode;
      };
      // 设置背景图片源,本地图片
      image1.src = require('./images/29.jpg');
    })
    .exec();
}

  /**
   * 保存到相册
   */
  saveToPhone = () => {
    Taro.getSetting({
      success: res => {
        if (!res.authSetting['scope.writePhotosAlbum']) {
          this.requestAlbumAuthorization();
        } else {
          this.downloadImage();
        }
      }
    });
  };
}

3. 页面模板

const Index = () => {
    useEffect(()=>{
        let shareUrl = getGlobalData('europeanShareUrl');
    console.log('海报中的shareUrl', shareUrl);

    let size = this.setCanvasSize();

        // 开始绘制二维码
    setTimeout(() => this.createQrCode(shareUrl, 'mycanvas', size.w, size.h), 300);
    },[])

    return  <View className="invite-poster-page">
      {isLoading ? (
        <Loading type="modal" show={true} />
      ) : (
        <View className="container">
          <Image className="poster-image" src={posterImage} mode="aspectFill" />
          <View className="save-btn" onClick={this.saveToPhone}>
            保存到手机
          </View>
        </View>
      )}
      <Canvas className="qr-canvas" type="2d" id="mycanvas" />
      <Canvas className="poster-canvas" type="2d" id="poster" />
    </View>
}

关键技术点说明

1. 画布适配

为了确保在不同设备上显示正常,我们需要处理好画布的尺寸适配:

setCanvasSize() {
  const systemInfo = Taro.getSystemInfoSync();
  const windowWidth = systemInfo.windowWidth;
  const designWidth = 750;
  const designQrSize = 200;

  const scale = windowWidth / designWidth;
  const qrSize = Math.round(designQrSize * scale);

  return {
    w: qrSize,
    h: qrSize,
    qw: qrSize,
    qh: qrSize,
    qrX: Math.round(275 * scale),
    qrY: Math.round(1280 * scale)
  };
}

2. 权限处理

在保存图片到相册时,需要处理权限问题:

requestAlbumAuthorization() {
  Taro.authorize({
    scope: 'scope.writePhotosAlbum',
    success: () => {
      this.downloadImage();
    },
    fail: () => {
      Taro.showModal({
        content: '未授权,无法保存图片,请前往授权',
        success: res => {
          if (res.confirm) {
            Taro.openSetting();
          }
        }
      });
    }
  });
}

3. 错误处理

为了提高用户体验,我们需要做好错误处理和重试机制:

handleQrCodeError(error: Error, str: string, canvasId: string, cavW: number, cavH: number, retryCount: number) {
  console.error('二维码生成失败:', error);
  Taro.showToast({
    title: '二维码生成失败,正在重试',
    icon: 'none',
    duration: 2000
  });

  setTimeout(() => {
    this.createQrCode(str, canvasId, cavW, cavH, retryCount - 1);
  }, 1000);
}

注意事项

  1. Canvas 必须使用 type="2d" 属性
  2. 需要处理设备像素比(dpr)以支持高清屏幕
  3. 图片绘制时需要给予足够的延时确保加载完成
  4. 要合理处理授权失败的情况
  5. 建议实现重试机制提高成功率

总结

通过以上实现,我们可以在小程序中生成美观的分享海报。关键在于:

  1. 使用 Canvas 2D API 进行绘制
  2. 处理好设备适配问题
  3. 实现完善的错误处理机制
  4. 注意权限和授权处理