微信小程序使用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()
              }
            })
          })
      }
    })
  },
相关推荐
杰建云1672 小时前
小程序如何做活动?
小程序·小程序制作
这是个栗子2 小时前
【微信小程序问题解决】微信小程序全局 navigationBarTitleText 不起作用
微信小程序·小程序·导航栏
lpfasd1232 小时前
从“惯性思维”到“规则驱动”:一次微信小程序修复引发的 AI 编程范式思考
人工智能·微信小程序·小程序
万岳科技程序员小金2 小时前
从0到1搭建AI真人数字人小程序:源码方案与落地流程详解
人工智能·小程序·ai数字人小程序·ai数字人系统源码·ai数字人软件开发·ai真人数字人平台搭建
Evavava啊3 小时前
微信小程序H5页面iOS视频播放问题解决方案
ios·微信小程序·音视频·h5·http 响应头
星空下的曙光3 小时前
uniapp编译到微信小程序接口获取不到数据uni.request
微信小程序·小程序·uni-app
今天不要写bug3 小时前
Taro小程序微信、支付宝双端实现二维码图片生成
微信·小程序·taro
文慧的科技江湖20 小时前
OCPP 1.6 与 2.0.1 核心消息差异对照表 - 慧知开源充电桩平台
小程序·开源·ocpp协议·慧知开源充电桩平台
Greg_Zhong20 小时前
微信小程序中便捷实现自定义底部tab栏
微信小程序·自定义底部tab