This commit is contained in:
张成
2025-12-04 14:10:32 +08:00
parent ec87be99db
commit 61b7309c32
4 changed files with 110 additions and 92 deletions

View File

@@ -3,6 +3,7 @@ import { View, Canvas } from "@tarojs/components";
import { forwardRef, useImperativeHandle } from "react";
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png";
import docCopySvg from "@/static/ntrp/ntrp_doc_copy.svg";
import { OSS_BASE_URL } from "@/config/api";
interface RadarChartV2Props {
data: [string, number][];
@@ -33,16 +34,18 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const maxValue = 100;
const levels = 5;
const radius = 100; // 半径 100直径 200圆圈保持 200*200
// 在 exportCanvasV2 中绘制雷达图的函数
function drawRadarChart(ctx: CanvasRenderingContext2D, radarX: number, radarY: number, radarSize: number) {
// 雷达图中心点在 350*600 画布中的位置
// 雷达图中心点位置radarSize 已经是2倍图尺寸
const center = {
x: radarX + radarSize / 2, // 75 + 100 = 175
y: radarY + radarSize / 2 // 252 + 100 = 352
x: radarX + radarSize / 2,
y: radarY + radarSize / 2
};
// 计算实际半径radarSize 是直径,半径是直径的一半)
const radius = radarSize / 2;
// 启用抗锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
@@ -81,7 +84,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
} else {
ctx.strokeStyle = "#D5D5D5";
}
ctx.lineWidth = 1;
ctx.lineWidth = 1 * (radarSize / 200); // 根据2倍图调整线宽
ctx.stroke();
}
@@ -96,15 +99,15 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.moveTo(center.x, center.y);
ctx.lineTo(x, y);
ctx.strokeStyle = "#E0E0E0";
ctx.lineWidth = 1;
ctx.lineWidth = 1 * (radarSize / 200); // 根据2倍图调整线宽
ctx.stroke();
// 标签 - 文字显示在圆圈外面
const offset = 10; // 文字距离圆圈的偏移量
const offset = 10 * (radarSize / 200); // 文字距离圆圈的偏移量2倍图
const textX = center.x + (radius + offset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle);
ctx.font = "12px sans-serif";
ctx.font = `${12 * (radarSize / 200)}px sans-serif`; // 根据2倍图调整字体大小
ctx.fillStyle = "#333";
ctx.textBaseline = "middle";
@@ -141,7 +144,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.fillStyle = "rgba(0,200,180,0.3)";
ctx.fill();
ctx.strokeStyle = "#00c8b4";
ctx.lineWidth = 3;
ctx.lineWidth = 3 * (radarSize / 200); // 根据2倍图调整线宽
ctx.stroke();
}
@@ -155,16 +158,6 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
});
}
// 获取图片信息
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
Taro.getImageInfo({
src,
success: ({ width, height }) => resolve({ width, height }),
fail: reject,
});
});
}
// 绘制圆角矩形
function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) {
@@ -206,9 +199,12 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
}) => {
return new Promise(async (resolve, reject) => {
try {
// 固定尺寸350*600
const width = 350; // 固定宽度
const height = 600; // 固定高
// 使用2倍图提高图片质量
const scale = 2; // 2倍图
const baseWidth = 350; // 基础宽
const baseHeight = 600; // 基础高度
const width = baseWidth * scale; // 实际宽度 700
const height = baseHeight * scale; // 实际高度 1200
// 创建导出画布
const query = Taro.createSelectorQuery();
@@ -223,7 +219,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
// 固定 canvas 尺寸,不使用 dpr
// 使用2倍图尺寸
canvas.width = width;
canvas.height = height;
@@ -231,28 +227,33 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 绘制背景 - 渐变背景
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, 'rgba(191, 255, 239, 1)');
gradient.addColorStop(1, 'rgba(242, 255, 252, 1)');
ctx.fillStyle = gradient;
// 绘制背景 - 使用 share_bg.png 背景图,撑满整个画布(从 OSS 动态加载)
try {
const shareBgUrl = `${OSS_BASE_URL}/images/share_bg.png`;
const bgImg = await loadImage(canvas, shareBgUrl);
ctx.drawImage(bgImg, 0, 0, width, height);
} catch (error) {
console.error("Failed to load background image:", error);
// 如果加载失败,使用白色背景作为兜底
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, width, height);
}
// 根据设计稿调整内边距和布局
const topPadding = 24; // 设计稿顶部内边距
const sidePadding = 28; // 设计稿左右内边距Frame 1912055062 的 x: 28
// 根据设计稿调整内边距和布局2倍图
const topPadding = 24 * scale; // 设计稿顶部内边距
const sidePadding = 28 * scale; // 设计稿左右内边距Frame 1912055062 的 x: 28
let currentY = topPadding;
// 绘制用户头像和装饰图片 - 根据设计稿尺寸
// 绘制用户头像和装饰图片 - 根据设计稿尺寸2倍图
if (options.avatarUrl) {
try {
const avatarSize = 43.46; // 设计稿头像尺寸
const avatarSize = 43.46 * scale; // 设计稿头像尺寸
const avatarImg = await loadImage(canvas, options.avatarUrl);
// 头像区域总宽度(头像 + 装饰图片重叠部分)
const avatarWrapWidth = 84.7; // 设计稿 Frame 1912055063 宽度
const avatarX = sidePadding + (294 - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarWrapWidth = 84.7 * scale; // 设计稿 Frame 1912055063 宽度
const avatarX = sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarY = currentY;
// 绘制头像圆形背景
@@ -262,19 +263,19 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.fillStyle = "#FFFFFF";
ctx.fill();
ctx.strokeStyle = "#EFEFEF";
ctx.lineWidth = 1;
ctx.lineWidth = 1 * scale;
ctx.stroke();
// 绘制头像(圆形裁剪)
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 0.97, 0, Math.PI * 2);
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 0.97 * scale, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(avatarImg, avatarX + 0.97, avatarY + 0.97, avatarSize - 1.94, avatarSize - 1.94);
ctx.drawImage(avatarImg, avatarX + 0.97 * scale, avatarY + 0.97 * scale, avatarSize - 1.94 * scale, avatarSize - 1.94 * scale);
ctx.restore();
// 绘制装饰图片DocCopy- 在头像右侧
const addonSize = 48; // 设计稿装饰图片尺寸
const addonX = avatarX + avatarSize - 10; // 重叠部分
const addonSize = 48 * scale; // 设计稿装饰图片尺寸
const addonX = avatarX + avatarSize - 10 * scale; // 重叠部分
const addonY = avatarY;
const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度
@@ -289,7 +290,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.rotate(addonRotation);
// 绘制装饰图片背景(圆角矩形,带渐变)
const borderRadius = 9.66; // 设计稿圆角
const borderRadius = 9.66 * scale; // 设计稿圆角
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius);
@@ -302,11 +303,11 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.globalAlpha = 1.0;
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 1.93; // 设计稿 strokeWeight
ctx.lineWidth = 1.93 * scale; // 设计稿 strokeWeight
ctx.stroke();
// 绘制装饰图片
const docSize = 26.18; // 设计稿内部图片尺寸
const docSize = 26.18 * scale; // 设计稿内部图片尺寸
const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度
ctx.rotate(docRotation);
ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize);
@@ -315,29 +316,29 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
console.error("Failed to load docCopy image:", error);
}
currentY += 48 + 20; // 头像区域高度 + gap
currentY += (48 + 20) * scale; // 头像区域高度 + gap
} catch (error) {
console.error("Failed to load avatar image:", error);
}
}
// 绘制文字区域 - 根据设计稿样式,居中对齐
const textCenterX = sidePadding + 294 / 2; // Frame 1912055062 的中心
const textGap = 6; // 设计稿 gap: 6px
// 绘制文字区域 - 根据设计稿样式,居中对齐2倍图
const textCenterX = sidePadding + (294 * scale) / 2; // Frame 1912055062 的中心
const textGap = 6 * scale; // 设计稿 gap: 6px
// 绘制标题 - 14px, font-weight: 300, line-height: 1.2
// 绘制标题 - 14px, font-weight: 300, line-height: 1.22倍图
if (options.title) {
ctx.fillStyle = "#000000";
ctx.font = "300 14px Noto Sans SC, sans-serif";
ctx.font = `300 ${14 * scale}px Noto Sans SC, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(options.title, textCenterX, currentY);
currentY += 14 * 1.2 + textGap; // line-height: 1.2 + gap
currentY += 14 * 1.2 * scale + textGap; // line-height: 1.2 + gap
}
// 绘制 NTRP 等级 - 36px, font-weight: 900, "NTRP" 黑色,等级数字 #00E5AD
// 绘制 NTRP 等级 - 36px, font-weight: 900, "NTRP" 黑色,等级数字 #00E5AD2倍图
if (options.ntrpLevel) {
ctx.font = "900 36px Noto Sans SC, sans-serif";
ctx.font = `900 ${36 * scale}px Noto Sans SC, sans-serif`;
ctx.textBaseline = "top";
const ntrpText = "NTRP";
@@ -356,53 +357,50 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.fillStyle = "#00E5AD";
ctx.fillText(levelText, startX + ntrpWidth, currentY);
currentY += 36 * 1.222 + textGap; // line-height: 1.222 + gap
currentY += 36 * 1.222 * scale + textGap; // line-height: 1.222 + gap
}
// 绘制描述文本 - 16px, font-weight: 600, line-height: 1.4
// 绘制描述文本 - 16px, font-weight: 600, line-height: 1.42倍图
if (options.levelDescription) {
ctx.fillStyle = "#000000";
ctx.font = "600 16px PingFang SC, sans-serif";
ctx.font = `600 ${16 * scale}px PingFang SC, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(options.levelDescription, textCenterX, currentY);
// 计算到雷达图的间距:雷达图 y: 252 - 当前 Y
const radarY = 252; // 设计稿雷达图位置
const radarY = 252 * scale; // 设计稿雷达图位置
currentY = radarY; // 直接设置到雷达图位置
}
// 直接绘制雷达图 - 根据设计稿调整尺寸和位置
const radarSize = 200; // 固定雷达图尺寸为 200*200
const radarX = 75; // 设计稿雷达图 x 位置
// 直接绘制雷达图 - 根据设计稿调整尺寸和位置2倍图
const radarSize = 200 * scale; // 固定雷达图尺寸为 200*200
const radarX = 75 * scale; // 设计稿雷达图 x 位置
const radarY = currentY;
// 直接在 exportCanvasV2 中绘制雷达图
drawRadarChart(ctx, radarX, radarY, radarSize);
currentY += radarSize; // 雷达图高度
// 绘制底部区域 - 根据设计稿调整(600px 高度
const bottomAreaY = 527; // 设计稿底部区域 y 位置
const qrSize = 64; // 设计稿二维码尺寸64*64
const qrX = 276; // 设计稿二维码 x 位置
const qrY = 523; // 设计稿二维码 y 位置
// 绘制底部区域 - 根据设计稿调整(2倍图
const qrSize = 64 * scale; // 设计稿二维码尺寸64*64
const qrX = 276 * scale; // 设计稿二维码 x 位置
const qrY = 523 * scale; // 设计稿二维码 y 位置
const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!";
// 绘制底部文字 - 设计稿fontSize: 12, fontWeight: 400, line-height: 1.5
// 绘制底部文字 - 设计稿fontSize: 12, fontWeight: 400, line-height: 1.52倍图
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
ctx.font = "400 12px Noto Sans SC, sans-serif";
ctx.font = `400 ${12 * scale}px Noto Sans SC, sans-serif`;
ctx.textAlign = "left";
ctx.textBaseline = "top";
const textX = 20; // 设计稿文字 x 位置
const textY = bottomAreaY;
const maxTextWidth = qrX - textX - 10; // 到二维码的间距
const textX = 20 * scale; // 设计稿文字 x 位置
const maxTextWidth = qrX - textX - 10 * scale; // 到二维码的间距
// 计算文字行数
const bottomWords = bottomTextContent.split("");
let bottomLine = "";
let textLines: string[] = [];
const lineHeight = 12 * 1.5; // fontSize * line-height
const lineHeight = 12 * 1.5 * scale; // fontSize * line-height
for (let i = 0; i < bottomWords.length; i++) {
const testBottomLine = bottomLine + bottomWords[i];
@@ -418,24 +416,34 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
textLines.push(bottomLine);
}
// 计算文字总高度
const textTotalHeight = textLines.length * lineHeight;
// 计算二维码底部位置
const qrBottomY = qrY + qrSize;
// 让文字底部和二维码底部对齐
const textY = qrBottomY - textTotalHeight;
// 绘制顶部标题和图标 - 设计稿 Frame 20360814262倍图
// 图标应该在文字上方gap: 4px
const iconGap = 4 * scale; // 图标和文字之间的间距
const topTitleX = textX;
// 绘制上面的标题 logo
try {
const iconSize = 28 * scale;
const iconImg = await loadImage(canvas, shareLogoSvg);
// 图标位置:文字顶部上方 iconSize + gap
const iconY = textY - iconSize - iconGap;
ctx.drawImage(iconImg, topTitleX, iconY, 235 * scale, iconSize);
} catch (error) {
console.error("Failed to load icon:", error);
}
// 绘制底部文字
textLines.forEach((lineText, index) => {
ctx.fillText(lineText, textX, textY + index * lineHeight);
});
// 绘制顶部标题和图标 - 设计稿 Frame 2036081426
const topTitleY = bottomAreaY - 4; // gap: 4px
const topTitleX = textX;
// 绘制图标28*28
try {
const iconSize = 28;
const iconImg = await loadImage(canvas, shareLogoSvg);
ctx.drawImage(iconImg, topTitleX, topTitleY - iconSize - 4, 235,iconSize);
} catch (error) {
console.error("Failed to load icon:", error);
}
// 绘制二维码 - 设计稿位置
@@ -470,11 +478,11 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
return (
<View>
{/* 隐藏的导出画布 */}
{/* 隐藏的导出画布 - 2倍图尺寸 */}
<Canvas
type="2d"
id="exportCanvasV2"
style={{ position: "fixed", top: "-9999px", left: "-9999px", width: "350px", height: "600px" }}
style={{ position: "fixed", top: "-9999px", left: "-9999px", width: "700px", height: "1200px" }}
/>
</View>
);

View File

@@ -587,11 +587,15 @@ function Result() {
}
// 使用 RadarV2 的 generateFullImage 方法生成完整图片
const userNickname = (userInfo as any)?.nickname;
const titleText = userNickname
? `${userNickname}的 NTRP 测试结果为`
: "你的 NTRP 测试结果为";
const imageUrl = await radarV2Ref.current?.generateFullImage({
title: "你的 NTRP 测试结果为",
title: titleText,
ntrpLevel: result?.ntrp_level,
levelDescription: result?.level_description,
avatarUrl: userInfo?.avatar_url,
avatarUrl: (userInfo as any)?.avatar_url,
qrCodeUrl: finalQrCodeUrl,
bottomText: "长按识别二维码,快来加入,有你就有场!",
width: 750, // 设计稿宽度
@@ -690,7 +694,11 @@ function Result() {
</View>
<View className={styles.desc}>
<View className={styles.tip}>
<Text> NTRP </Text>
<Text>
{(userInfo as any)?.nickname
? `${(userInfo as any).nickname}的 NTRP 测试结果为`
: "你的 NTRP 测试结果为"}
</Text>
</View>
<View className={styles.levelWrap}>
<Text>NTRP</Text>
@@ -755,10 +763,12 @@ function Result() {
<RadarChartV2
ref={radarV2Ref}
data={radarData}
title="你的 NTRP 测试结果为"
title={(userInfo as any)?.nickname
? `${(userInfo as any).nickname}的 NTRP 测试结果为`
: "你的 NTRP 测试结果为"}
ntrpLevel={result?.ntrp_level}
levelDescription={result?.level_description}
avatarUrl={userInfo?.avatar_url}
avatarUrl={(userInfo as any)?.avatar_url}
qrCodeUrl={qrCodeUrl}
bottomText="长按识别二维码,快来加入,有你就有场!"
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB