1
This commit is contained in:
@@ -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;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
// 绘制背景 - 使用 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.2(2倍图)
|
||||
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" 黑色,等级数字 #00E5AD(2倍图)
|
||||
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.4(2倍图)
|
||||
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.5(2倍图)
|
||||
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 2036081426(2倍图)
|
||||
// 图标应该在文字上方,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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user