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

View File

@@ -3,6 +3,8 @@ import Taro from "@tarojs/taro";
// const qrCodeUrl = // const qrCodeUrl =
// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png"; // "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 = const ringUrl =
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; "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( function roundRectGradient(
ctx: any, ctx: any,
@@ -131,6 +135,57 @@ async function drawCoverImage(
ctx.restore(); 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( function roundRect(
ctx: any, ctx: any,
@@ -156,9 +211,47 @@ function roundRect(
ctx.fill(); 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) { 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 padding = 12;
const textWidth = ctx.measureText(text).width; const textWidth = ctx.measureText(text).width;
roundRect(ctx, x, y, textWidth + padding * 2, 40, 20, "#fff"); 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"); 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); const mainImg = await loadImage(canvas, data.mainCoursal);
await drawCoverImage( await drawRotateCoverImage(
ctx, ctx,
canvas, canvas,
data.mainCoursal, data.mainCoursal,
mainImg, mainImg,
10, 75,
10, 105,
width - 20, width - 150,
width - 20, width - 150,
20 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); let left = drawTag(ctx, data.playType, 20, 20);
drawTag(ctx, data.ntrp, left + 4, 18); drawTag(ctx, data.ntrp, left + 8, 20);
let top = width - 10 + 16; let top = width - 10 + 16;
left = 16; left = 20;
// 用户头像 // 用户头像
const avatarImg = await loadImage(canvas, data.avatarUrl); const avatarImg = await loadImage(canvas, data.avatarUrl);
@@ -246,7 +355,7 @@ export async function generatePosterImage(data: any): Promise<string> {
top += 40; top += 40;
// 用户名 + 邀请 // 用户名 + 邀请
ctx.fillStyle = "#333"; ctx.fillStyle = "#000";
ctx.font = "bold 28px sans-serif"; ctx.font = "bold 28px sans-serif";
const nickNameText = `${data.nickname} 邀你加入`; const nickNameText = `${data.nickname} 邀你加入`;
ctx.fillText(nickNameText, left, top); ctx.fillText(nickNameText, left, top);
@@ -258,16 +367,16 @@ export async function generatePosterImage(data: any): Promise<string> {
const ringImg = await loadImage(canvas, ringUrl); const ringImg = await loadImage(canvas, ringUrl);
ctx.drawImage(ringImg, left - 10, top - 30, 80, 36); ctx.drawImage(ringImg, left - 10, top - 30, 80, 36);
left = 16; left = 20;
top += 60; top += 60;
// 活动标题 // 活动标题
ctx.fillStyle = "#333"; ctx.fillStyle = "#000";
ctx.font = "bold 34px sans-serif"; ctx.font = "bold 34px sans-serif";
let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40); let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40);
top = r.top + 40; top = r.top + 30;
left = 16; left = 20;
const dateImg = await loadImage(canvas, dateIcon); const dateImg = await loadImage(canvas, dateIcon);
await drawCoverImage( await drawCoverImage(
@@ -279,34 +388,34 @@ export async function generatePosterImage(data: any): Promise<string> {
top, top,
40, 40,
40, 40,
8 12
); );
left += 40 + 8; left += 40 + 16;
top += 30; top += 30;
ctx.font = "26px sans-serif"; ctx.font = "500 26px sans-serif";
ctx.fillStyle = "#00B578"; ctx.fillStyle = "#00B578";
ctx.fillText(data.date, left, top); ctx.fillText(data.date, left, top);
textW = ctx.measureText(data.date).width; textW = ctx.measureText(data.date).width;
left += 8 + textW; left += 8 + textW;
ctx.fillStyle = "#333"; ctx.fillStyle = "#000";
ctx.fillText(data.time, left, top); ctx.fillText(data.time, left, top);
left = 16; left = 20;
top += 24; top += 24;
const mapImg = await loadImage(canvas, mapIcon); 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; top += 30;
ctx.fillStyle = "#666"; ctx.fillStyle = "#000";
ctx.font = "26px sans-serif"; ctx.font = "500 26px sans-serif";
r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34); r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34);
left = 16; left = 20;
top = r.top + 60; top = r.top + 60;
const logoWh = await getImageWh(logoText); const logoWh = await getImageWh(logoText);
@@ -314,22 +423,23 @@ export async function generatePosterImage(data: any): Promise<string> {
ctx.drawImage( ctx.drawImage(
logoTextImg, logoTextImg,
left, left,
top, // top,
height - logoWh.height - 30 - 30,
400, 400,
400 / (logoWh.width / logoWh.height) 400 / (logoWh.width / logoWh.height)
); );
const qrImg = await loadImage(canvas, data.qrCodeUrl); const qrImg = await loadImage(canvas, data.qrCodeUrl);
roundRectGradient(ctx, width - 12 - 150, top - 50, 140, 140, 20, "#fff", "#fff") // roundRectGradient(ctx, width - 12 - 150, height - 22 - 140, 140, 140, 20, "#fff", "#fff")
ctx.drawImage(qrImg, width - 12 - 145, top - 45, 130, 130); ctx.drawImage(qrImg, width - 22 - 100, height - 22 - 100 - 2, 100, 100);
left = 16; left = 20;
top += 400 / (logoWh.width / logoWh.height) + 30; // top += 400 / (logoWh.width / logoWh.height) + 30;
ctx.fillStyle = "#333"; ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
ctx.font = "20px sans-serif"; ctx.font = "400 20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top); ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, height - 30/* top */);
// 导出图片 // 导出图片
const { tempFilePath } = await Taro.canvasToTempFilePath({ const { tempFilePath } = await Taro.canvasToTempFilePath({