import { useEffect, useImperativeHandle, forwardRef } from "react"; import { Canvas } from "@tarojs/components"; import Taro from "@tarojs/taro"; function getImageWh(src): Promise<{ width: number; height: number }> { return new Promise((resolve) => { Taro.getImageInfo({ src, success: ({ width, height }) => resolve({ width, height }), }); }); } const qrCodeUrl = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png"; const ringUrl = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; const dateIcon = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg"; const mapIcon = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg"; // const logo = "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg" const logoText = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png"; const Poster = (props, ref) => { const { data } = props; const { playType, ntrp, mainCoursal, nickname, avatarUrl, title, locationName, date, time, } = data; useEffect(() => { drawCard(); }, []); useImperativeHandle(ref, () => ({ generateImage: () => new Promise((resolve, reject) => { const query = Taro.createSelectorQuery(); query .select("#cardCanvas") .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; // ⚠️ 关键:传 canvas,而不是 canvasId Taro.canvasToTempFilePath({ canvas, success: (res) => resolve(res.tempFilePath), fail: (err) => reject(err), }); }); }), })); const drawCard = async () => { const query = Taro.createSelectorQuery(); query .select("#cardCanvas") .fields({ node: true, size: true }) .exec(async (res) => { const canvas = res[0].node; const ctx = canvas.getContext("2d"); const dpr = Taro.getWindowInfo().pixelRatio; const width = 600; // px const height = 1000; canvas.width = width * dpr; canvas.height = height * dpr; ctx.scale(dpr, dpr); // 背景卡片 // roundRect(ctx, 0, 0, width, height, 20, "#fff"); // 整体背景 roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC"); // 顶部图片 const img = await loadImage(canvas, mainCoursal); // roundRect(ctx, 20, 20, width - 40, width - 40, 20, "#fff"); await drawCoverImage( ctx, mainCoursal, img, 10, 10, width - 20, width - 20, 20 ); // 标签 let left = drawTag(ctx, playType, 18, 18); drawTag(ctx, ntrp, left + 4, 18); let top = width - 10; left = 16; top += 16; // 用户头像(圆形) const avatar = await loadImage(canvas, avatarUrl); ctx.save(); ctx.beginPath(); ctx.arc(left + 30, top + 30, 30, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(avatar, left, top, 60, 60); ctx.restore(); // 头像右边 6 left += 66; top += 40; // 用户名 + 邀请 ctx.fillStyle = "#333"; ctx.font = "bold 28px sans-serif"; // ctx.fillText("华巴轮卡 邀你加入球局", 100, 370); const nickNameText = `${nickname} 邀你加入`; ctx.fillText(nickNameText, left, top); let textW = ctx.measureText(nickNameText).width; left += textW; ctx.fillStyle = "#00B578"; ctx.fillText("球局", left, top); const ringImg = await loadImage(canvas, ringUrl); ctx.drawImage(ringImg, left - 10, top - 30, 80, 36); left = 16; top += 60; // 活动标题 ctx.fillStyle = "#333"; ctx.font = "bold 34px sans-serif"; let r = drawTextWrap(ctx, title, left, top, width - 32, 40); top = r.top + 40; left = 16; const dateImg = await loadImage(canvas, dateIcon); await drawCoverImage(ctx, dateIcon, dateImg, left, top, 40, 40, 8); left += 40 + 8; top += 30; // 时间 ctx.font = "26px sans-serif"; ctx.fillStyle = "#00B578"; ctx.fillText(date, left, top); textW = ctx.measureText(date).width; left += 8 + textW; ctx.fillStyle = "#333"; ctx.fillText(time, left, top); left = 16; top += 24; const mapImg = await loadImage(canvas, mapIcon); await drawCoverImage(ctx, dateIcon, mapImg, left, top, 40, 40, 8); left += 40 + 8; top += 30; // 地址 ctx.fillStyle = "#666"; ctx.font = "26px sans-serif"; r = drawTextWrap(ctx, locationName, left, top, width - 32 - left, 34); left = 16; top = r.top + 60; const logoWh = await getImageWh(logoText); console.log(logoWh); const logoTextImg = await loadImage(canvas, logoText); ctx.drawImage( logoTextImg, left, top, 400, // 56 400 / (logoWh.width / logoWh.height) ); const qrImg = await loadImage(canvas, qrCodeUrl); ctx.drawImage(qrImg, width - 12 - 150, top - 50, 160, 160); left = 16; top += 400 / (logoWh.width / logoWh.height) + 30; // 底部文字 ctx.fillStyle = "#333"; ctx.font = "20px sans-serif"; ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top); // 小程序码 // const qrcode = await loadImage(canvas, "小程序码路径"); // ctx.drawImage(qrcode, 480, 880, 100, 100); // 导出图片 // Taro.canvasToTempFilePath({ // canvas, // success: (res) => { // console.log("导出路径", res.tempFilePath); // }, // fail: (err) => console.error(err), // }); }); }; const roundRectGradient = (ctx, x, y, w, h, r, color1, color2) => { const gradient = ctx.createLinearGradient(x, y, x, y + h); gradient.addColorStop(0, color1); gradient.addColorStop(1, color2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); }; const drawCoverImage = async (ctx, src, img, x, y, w, h, r = 0) => { const { width, height } = await getImageWh(src); const imgW = width; const imgH = height; // 计算缩放比例,取较大值,保证完全覆盖 const scale = Math.max(w / imgW, h / imgH); // 缩放后的宽高 const newW = imgW * scale; const newH = imgH * scale; // 居中偏移 const offsetX = x + (w - newW) / 2; const offsetY = y + (h - newH) / 2; ctx.save(); // 如果需要圆角 if (r > 0) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.clip(); } // 绘制图片 (cover 效果) ctx.drawImage(img, offsetX, offsetY, newW, newH); ctx.restore(); }; /** 加载图片 */ const loadImage = (canvas, src) => { return new Promise((resolve) => { const img = canvas.createImage(); img.onload = () => resolve(img); img.src = src; }); }; /** 圆角矩形 */ const roundRect = (ctx, x, y, w, h, r, fillStyle) => { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.fillStyle = fillStyle; ctx.fill(); }; /** 绘制标签 */ const drawTag = (ctx, text, x, y) => { ctx.font = "22px sans-serif"; const padding = 12; const textWidth = ctx.measureText(text).width; roundRect(ctx, x, y, textWidth + padding * 2, 40, 20, "#fff"); ctx.fillStyle = "#333"; ctx.fillText(text, x + padding, y + 28); return x + textWidth + padding * 2; }; /** 文本换行 */ const drawTextWrap = (ctx, text, x, y, maxWidth, lineHeight) => { let line = ""; const lines = []; for (let char of text) { const testLine = line + char; if (ctx.measureText(testLine).width > maxWidth) { lines.push(line); line = char; } else { line = testLine; } } if (line) lines.push(line); lines.forEach((l, i) => { ctx.fillText(l, x, y + i * lineHeight); }); const lastLineText = lines.at(-1); return { left: x + ctx.measureText(lastLineText), top: y + (lines.length - 1) * lineHeight, }; }; return ( ); }; export default forwardRef(Poster);