微信小程序使用canvas自定义富文本内容做图片分享

最终实际效果

可进行富文本内容文字和图片的显示,并调用微信小程序的canvasToTempFilePathapi来进行生成图片并分享,具体代码如下(可直接复制套用,某些数据及图片需要根据自己实际情况修改)

javascript 复制代码
 //在小程序wxml里,item.sayId是我自己的循环出来的id,根据自己实际情况来
  <view>
      <button size="mini" class="goldenSaying-btn" data-id="{{item.sayId}}" bind:tap="handleShare">分享</button>
        <canvas 
    type="2d" 
    id="shareCanvas" 
    canvas-id="shareCanvas" 
    style="width:400px;height:600px;position:fixed;top:-9999px;left:-9999px;"
  ></canvas>
  </view>
javascript 复制代码
  //微信小程序的js里
  //全局定义的线上地址
  const base_URL = getApp().globalData.base_URL;
  const baseDomain = getApp().globalData.baseDomain;
  handleShare(e) {
    wx.showLoading({
      title: '图片生成中...'
    })
    const id = e.currentTarget.dataset.id
    //这里我调用的是自己的接口,来获取数据,根据自己实际情况来
    wx.request({
      url: base_URL + '/system/saying/' + id,
      method: 'GET',
      success: (response) => {
        console.log(response)
        const data = response.data.data
        const query = this.createSelectorQuery()
        query.select('#shareCanvas')
          .fields({
            node: true,
            size: true
          })
          .exec(async (res) => {
            const canvas = res[0].node
            const ctx = canvas.getContext('2d')
            // 背景图
            const dpr = wx.getSystemInfoSync().pixelRatio
            const canvasWidth = 400
            let canvasHeight = 600
            const bgImg = canvas.createImage()
            //给canvas加一个自己本地的背景图图片
            await new Promise((resolve) => {
              bgImg.src = "../../assets/image/整体背景.png";
              bgImg.onload = resolve
            })
            //该小程序的本地二维码图片
            const qrcodeImg = canvas.createImage()
            await new Promise(resolve => {
              qrcodeImg.src = "../../assets/image/小程序码.png" // 你自己的小程序码路径
              qrcodeImg.onload = resolve
            })

            //富文本内容
            const html = data.sayingStr;
            // 1. 万能清洗 + 解析富文本
            function parseRichTextToParagraphs(html) {
              const imgReg = /<img[^>]*src=["']([^"']+)["']/gi;
              const images = [];
              let match;
              while ((match = imgReg.exec(html)) !== null) {
                images.push(match[1]); // 收集所有图片地址
              }
              // 1. 替换所有标签
              let text = html
                .replace(/<p\b[^>]*>/gi, '') // 去掉 <p ...>
                .replace(/<\/p>/gi, '\n') // </p> 换成换行
                .replace(/<br\s*\/?>/gi, '\n') // <br> 换成换行
                .replace(/&nbsp;/gi, ' ') // 空格
                .replace(/<[^>]+>/gi, '') // 删掉所有剩余标签
                .replace(/[ \t]+/g, ' '); // 多空格变单空格

              // 2. 拆成段落
              let paragraphs = text.split('\n').map(p => p.trim()).filter(p => p);

              return {
                paragraphs,
                images
              }
            }

            // 解析成整齐段落
            const {
              paragraphs,
              images
            } = parseRichTextToParagraphs(html);
            const loadedImages = [];
            for (let src of images) {
              const img = canvas.createImage();
              await new Promise((resolve) => {
                //baseDomain 是我需要拼接的img地址,也是根据自己情况来
                img.src = baseDomain + src;
                img.onload = () => {
                  loadedImages.push(img);
                  resolve();
                };
                img.onerror = resolve; // 加载失败也继续
              });
            }
            const boxPadding = 20; // 盒子内边距20
            const paddingX = 30; // 左右边距
            const maxWidth = 340; // 文字最大宽度
            const lineHeight = 35; // 行高
            const paragraphSpace = 25; // 段落间距
            let startY = 250;
            let tempY = startY;
            paragraphs.forEach((p) => {
              let line = '';
              // let ty = tempY;
              for (let i = 0; i < p.length; i++) {
                const testLine = line + p[i];
                const w = ctx.measureText(testLine).width;
                if (w > maxWidth && i > 0) {
                  line = p[i];
                  tempY += lineHeight;
                } else {
                  line = testLine;
                }
              }
              tempY += lineHeight + paragraphSpace;
            });

            let totalContentHeight = tempY - 250; // 原有文字高度

            // 把所有图片高度加进去
            loadedImages.forEach(img => {
              if (!img.width) return;
              const imgWidth = 320;
              const imgHeight = (img.height / img.width) * imgWidth;
              totalContentHeight += imgHeight + 15; // 图片高度 + 间距
            });

            // 最终画布高度
            canvasHeight = 250 + totalContentHeight + 100; // 顶部高度 + 内容 + 底部二维码留白
            canvas.width = canvasWidth * dpr
            canvas.height = canvasHeight * dpr
            ctx.scale(dpr, dpr)
            ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight)

            // 头部标题文字
            ctx.fillStyle = '#000'
            ctx.font = 'bold 20px sans-serif'
            ctx.textAlign = 'center';
            ctx.fillText('测试', 200, 30)

            // 人物头像
            const avatorWidth = 100
            const avatorHeight = 100
            const avatarX = (canvasWidth - avatorWidth) / 2 // 头像水平居中
            const avatarY = 60 // 头像y坐标

            // 加载头像
            const avatorImg = canvas.createImage()
            await new Promise((resolve, reject) => {
              //我自己获取的头像数据
              avatorImg.src = base_URL + data.avatar;
              avatorImg.onload = resolve
              avatorImg.onerror = reject
            })

            // 画圆形头像(核心)
            ctx.save() // 保存画布状态
            ctx.arc(
              avatarX + avatorWidth / 2, // 圆心x
              avatarY + avatorHeight / 2, // 圆心y
              avatorWidth / 2, // 半径=35
              0,
              2 * Math.PI
            )
            ctx.clip() // 裁剪成圆形

            ctx.drawImage(avatorImg, avatarX, avatarY, avatorWidth, avatorHeight)

            ctx.restore() // 恢复画布,不影响后面画图

            //人员名称
            ctx.font = '18px sans-serif'
            //我自己获取的名字数据
            ctx.fillText(data.nickName, 200, 190)

            const boxX = paddingX - boxPadding
            const boxY = startY - boxPadding - 10
            const boxW = canvasWidth - (paddingX - boxPadding) * 2
            const boxH = tempY - startY + boxPadding * 2 - 50

            const bottomSpace = 10 // 离边框10px
            const qrcodeSize = 80 // 小程序码大小
            const qrcodeMargin = 10
            //周围加了一个白色边框

            ctx.strokeStyle = '#fff'; // 白色边框
            ctx.lineWidth = 1; // 边框粗细
            ctx.strokeRect(boxX, boxY, boxW, boxH);


            ctx.font = '18px sans-serif';
            ctx.fillStyle = '#333';
            ctx.textAlign = 'left'; // 正文一律左对齐最自然

            // ==========================
            // 3. 自动换行函数(内置,不依赖外部)
            // ==========================
            function drawText(ctx, text, x, y) {
              let line = '';
              let yPos = y;
              for (let i = 0; i < text.length; i++) {
                const testLine = line + text[i];
                const metrics = ctx.measureText(testLine);
                if (metrics.width > maxWidth && i > 0) {
                  ctx.fillText(line, x, yPos);
                  line = text[i];
                  yPos += lineHeight;
                } else {
                  line = testLine;
                }
              }
              ctx.fillText(line, x, yPos);
              return yPos;
            }
            paragraphs.forEach((p) => {
              // 绘制一段
              const endY = drawText(ctx, p, paddingX, startY);
              // 移动到下一段位置
              startY = endY + paragraphSpace;
            });
            const imgWidth = 250; // 图片宽度
            const imgMargin = 15;
            let imgX = (canvasWidth - imgWidth) / 2;
            let currentY = startY;
            loadedImages.forEach((img) => {
              if (!img.width) return;
              const imgHeight = (img.height / img.width) * imgWidth;
              // 绘制图片(居中)
              ctx.drawImage(img, imgX, currentY, imgWidth, imgHeight);
              // 往下移动
              currentY += imgHeight + imgMargin;
            });
            startY = currentY;

            // 1. 左边文字(左对齐)
            const boxBottomY = boxY + boxH;
            const textY = startY + 30;

            // 左边文字(左对齐)
            ctx.fillStyle = '#999';
            ctx.font = 'bold 16px sans-serif';
            ctx.textAlign = 'left';
            ctx.fillText('长按识别小程序码', paddingX, textY);

            // 右边二维码(右对齐 + 和文字垂直居中)
            const qrcodeX = canvasWidth - paddingX - qrcodeSize;
            const qrcodeY = textY - qrcodeSize / 2 - 10; // 🔥 核心:垂直居中
            ctx.drawImage(qrcodeImg, qrcodeX, qrcodeY, qrcodeSize, qrcodeSize);


            // ---------- Canvas 转图片 ----------
            wx.canvasToTempFilePath({
              canvas,
              fileType: 'png',
              quality: 1,
              success: (res) => {
                this.setData({
                  imagePath: res.tempFilePath
                })
                wx.showShareImageMenu({
                  path: res.tempFilePath,
                  success: () => {
                    console.log('打开分享菜单成功')
                  },
                  fail: (err) => {
                    console.log('分享菜单失败', err)
                  }
                })

                wx.hideLoading()
              },
              fail: (err) => {
                console.error('生成失败', err)
                wx.hideLoading()
              }
            })
          })
      }
    })
  },
相关推荐
万岳软件开发小城16 小时前
陪诊APP+小程序一体化搭建方案:如何低成本打造医疗陪护平台?
小程序·医院陪诊系统源码·陪诊软件开发·陪诊平台开发·陪诊小程序开发
lichenyang45320 小时前
Expo 小程序媒体库功能设计与实现记录
小程序
经济元宇宙1 天前
2026混合开发工具选型:小程序生态适配测评
小程序
lpfasd1232 天前
微信小程序虚拟支付(道具直购)踩坑全记录:从-15005到支付成功
微信小程序·小程序
crazy_wsp2 天前
使用AI从0到1上线微信小程序
人工智能·微信小程序·小程序
小宋的踩坑日记2 天前
全网最全!Tailwind/Unocss 类名速查表,前端开发必备神器!
css·小程序·前端框架
低代码布道师2 天前
健身房私教课小程序需求规格说明书
小程序·规格说明书
m0_462803883 天前
培训分组与记分操作指南
微信小程序
浩冉学编程3 天前
微信小程序中基于java后端实现官方的文本内容安全识别msgSecCheck
java·前端·安全·微信小程序·小程序·微信公众平台·内容安全审核
ZC跨境爬虫3 天前
Python Django开发者转向微信小程序:从架构理解到第一行代码的完整准备指南
开发语言·python·ui·微信小程序·django