最终实际效果

可进行富文本内容文字和图片的显示,并调用微信小程序的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(/ /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()
}
})
})
}
})
},