diff --git a/src/components/Radar/indexV2.tsx b/src/components/Radar/indexV2.tsx index 361e0b7..27cc167 100644 --- a/src/components/Radar/indexV2.tsx +++ b/src/components/Radar/indexV2.tsx @@ -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((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((props, ref) } else { ctx.strokeStyle = "#D5D5D5"; } - ctx.lineWidth = 1; + ctx.lineWidth = 1 * (radarSize / 200); // 根据2倍图调整线宽 ctx.stroke(); } @@ -96,15 +99,15 @@ const RadarChartV2 = forwardRef((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((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((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((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((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((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((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((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((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((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((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((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((props, ref) return ( - {/* 隐藏的导出画布 */} + {/* 隐藏的导出画布 - 2倍图尺寸 */} ); diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx index d923d36..7deef7c 100644 --- a/src/other_pages/ntrp-evaluate/index.tsx +++ b/src/other_pages/ntrp-evaluate/index.tsx @@ -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() { - 你的 NTRP 测试结果为 + + {(userInfo as any)?.nickname + ? `${(userInfo as any).nickname}的 NTRP 测试结果为` + : "你的 NTRP 测试结果为"} + NTRP @@ -755,10 +763,12 @@ function Result() { diff --git a/src/static/ntrp/ntrp_share_logo.png b/src/static/ntrp/ntrp_share_logo.png index 1a0c635..99b82f6 100644 Binary files a/src/static/ntrp/ntrp_share_logo.png and b/src/static/ntrp/ntrp_share_logo.png differ diff --git a/src/static/ntrp/share_bg.png b/src/static/ntrp/share_bg.png new file mode 100644 index 0000000..1234dc7 Binary files /dev/null and b/src/static/ntrp/share_bg.png differ