feat: 详情页分享海报重新绘制

This commit is contained in:
2025-11-25 11:29:25 +08:00
parent dcbcbb49f6
commit 2c739255b7
2 changed files with 150 additions and 39 deletions

View File

@@ -4,6 +4,7 @@ import { View, Image, Text, Button } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
import classnames from "classnames";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { withAuth } from "@/components";
import GeneralNavbar from "@/components/GeneralNavbar";
@@ -17,6 +18,8 @@ import { genNTRPRequirementText } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import styles from "./index.module.scss";
dayjs.locale("zh-cn");
function SharePoster(props) {
const [url, setUrl] = useState("");
const { fetchUserInfo } = useUserActions();
@@ -63,8 +66,9 @@ function SharePoster(props) {
playType: play_type,
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
mainCoursal:
image_list[0] ||
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png",
image_list[0] && image_list[0].startsWith("http")
? image_list[0]
: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png",
nickname,
avatarUrl: avatar_url,
title,
@@ -84,10 +88,7 @@ function SharePoster(props) {
return (
<>
<GeneralNavbar
title="生成分享图"
className={styles.navbar}
/>
<GeneralNavbar title="生成分享图" className={styles.navbar} />
{url && (
<View className={styles.posterContainer}>
<View className={styles.posterWrap}>

View File

@@ -3,6 +3,8 @@ import Taro from "@tarojs/taro";
// const qrCodeUrl =
// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png";
const bgUrl = "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png";
const ringUrl =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png";
@@ -64,6 +66,8 @@ function loadImage(canvas: any, src: string): Promise<any> {
});
}
const deg2rad = d => d * Math.PI / 180;
/** 圆角矩形渐变 */
function roundRectGradient(
ctx: any,
@@ -131,6 +135,57 @@ async function drawCoverImage(
ctx.restore();
}
/** 绘制 cover 图片(支持圆角 + 旋转) */
async function drawRotateCoverImage(
ctx: any,
canvas: any,
src: string,
img: any,
x: number,
y: number,
w: number,
h: number,
r = 0,
rotate = 0 // 旋转角度(弧度)
) {
const { width, height } = await getImageWh(src);
const scale = Math.max(w / width, h / height);
const newW = width * scale;
const newH = height * scale;
const offsetX = x + (w - newW) / 2;
const offsetY = y + (h - newH) / 2;
ctx.save();
// 以 cover 区域中心为旋转点
const cx = x + w / 2;
const cy = y + h / 2;
ctx.translate(cx, cy);
ctx.rotate(rotate);
ctx.translate(-cx, -cy);
// ------- clipping注意要在旋转变换之后做 path-------
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();
}
/** 圆角矩形 */
function roundRect(
ctx: any,
@@ -156,9 +211,47 @@ function roundRect(
ctx.fill();
}
/** 圆角矩形(支持旋转) */
function roundRotateRect(
ctx: any,
x: number,
y: number,
w: number,
h: number,
r: number,
fillStyle: string,
rotate: number = 0 // 弧度
) {
ctx.save();
// 以圆角矩形中心作为旋转点
const cx = x + w / 2;
const cy = y + h / 2;
ctx.translate(cx, cy);
ctx.rotate(rotate);
ctx.translate(-cx, -cy);
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();
ctx.restore();
}
/** 绘制标签 */
function drawTag(ctx: any, text: string, x: number, y: number) {
ctx.font = "22px sans-serif";
ctx.font = "600 22px sans-serif";
const padding = 12;
const textWidth = ctx.measureText(text).width;
roundRect(ctx, x, y, textWidth + padding * 2, 40, 20, "#fff");
@@ -212,26 +305,42 @@ export async function generatePosterImage(data: any): Promise<string> {
// 背景渐变
roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC");
const bgImg = await loadImage(canvas, bgUrl);
ctx.drawImage(bgImg, 0, 0, width, height);
roundRotateRect(ctx, 70, 100, width - 140, width - 140, 20, '#fff', deg2rad(-6));
// 顶部图片
const mainImg = await loadImage(canvas, data.mainCoursal);
await drawCoverImage(
await drawRotateCoverImage(
ctx,
canvas,
data.mainCoursal,
mainImg,
10,
10,
width - 20,
width - 20,
20
75,
105,
width - 150,
width - 150,
20,
deg2rad(-6),
);
// await drawCoverImage(
// ctx,
// canvas,
// data.mainCoursal,
// mainImg,
// 10,
// 10,
// width - 20,
// width - 20,
// 20
// );
// 标签
let left = drawTag(ctx, data.playType, 18, 18);
drawTag(ctx, data.ntrp, left + 4, 18);
let left = drawTag(ctx, data.playType, 20, 20);
drawTag(ctx, data.ntrp, left + 8, 20);
let top = width - 10 + 16;
left = 16;
left = 20;
// 用户头像
const avatarImg = await loadImage(canvas, data.avatarUrl);
@@ -246,7 +355,7 @@ export async function generatePosterImage(data: any): Promise<string> {
top += 40;
// 用户名 + 邀请
ctx.fillStyle = "#333";
ctx.fillStyle = "#000";
ctx.font = "bold 28px sans-serif";
const nickNameText = `${data.nickname} 邀你加入`;
ctx.fillText(nickNameText, left, top);
@@ -258,16 +367,16 @@ export async function generatePosterImage(data: any): Promise<string> {
const ringImg = await loadImage(canvas, ringUrl);
ctx.drawImage(ringImg, left - 10, top - 30, 80, 36);
left = 16;
left = 20;
top += 60;
// 活动标题
ctx.fillStyle = "#333";
ctx.fillStyle = "#000";
ctx.font = "bold 34px sans-serif";
let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40);
top = r.top + 40;
left = 16;
top = r.top + 30;
left = 20;
const dateImg = await loadImage(canvas, dateIcon);
await drawCoverImage(
@@ -279,34 +388,34 @@ export async function generatePosterImage(data: any): Promise<string> {
top,
40,
40,
8
12
);
left += 40 + 8;
left += 40 + 16;
top += 30;
ctx.font = "26px sans-serif";
ctx.font = "500 26px sans-serif";
ctx.fillStyle = "#00B578";
ctx.fillText(data.date, left, top);
textW = ctx.measureText(data.date).width;
left += 8 + textW;
ctx.fillStyle = "#333";
ctx.fillStyle = "#000";
ctx.fillText(data.time, left, top);
left = 16;
left = 20;
top += 24;
const mapImg = await loadImage(canvas, mapIcon);
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 8);
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 12);
left += 40 + 8;
left += 40 + 16;
top += 30;
ctx.fillStyle = "#666";
ctx.font = "26px sans-serif";
ctx.fillStyle = "#000";
ctx.font = "500 26px sans-serif";
r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34);
left = 16;
left = 20;
top = r.top + 60;
const logoWh = await getImageWh(logoText);
@@ -314,22 +423,23 @@ export async function generatePosterImage(data: any): Promise<string> {
ctx.drawImage(
logoTextImg,
left,
top,
// top,
height - logoWh.height - 30 - 30,
400,
400 / (logoWh.width / logoWh.height)
);
const qrImg = await loadImage(canvas, data.qrCodeUrl);
roundRectGradient(ctx, width - 12 - 150, top - 50, 140, 140, 20, "#fff", "#fff")
ctx.drawImage(qrImg, width - 12 - 145, top - 45, 130, 130);
// roundRectGradient(ctx, width - 12 - 150, height - 22 - 140, 140, 140, 20, "#fff", "#fff")
ctx.drawImage(qrImg, width - 22 - 100, height - 22 - 100 - 2, 100, 100);
left = 16;
top += 400 / (logoWh.width / logoWh.height) + 30;
left = 20;
// top += 400 / (logoWh.width / logoWh.height) + 30;
ctx.fillStyle = "#333";
ctx.font = "20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top);
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
ctx.font = "400 20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, height - 30/* top */);
// 导出图片
const { tempFilePath } = await Taro.canvasToTempFilePath({