This commit is contained in:
张成
2025-12-03 23:18:48 +08:00
parent 540f554af0
commit 9d5c7ce08e
5 changed files with 321 additions and 83 deletions

View File

@@ -1,12 +1,15 @@
import Taro from "@tarojs/taro";
import { View, Canvas } from "@tarojs/components";
import { useEffect, forwardRef, useImperativeHandle } from "react";
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.svg";
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png";
import docCopySvg from "@/static/ntrp/ntrp_doc_copy.svg";
interface RadarChartV2Props {
data: [string, number][];
title?: string;
ntrpLevel?: string;
levelDescription?: string;
avatarUrl?: string;
qrCodeUrl?: string;
bottomText?: string;
}
@@ -16,6 +19,8 @@ export interface RadarChartV2Ref {
generateFullImage: (options: {
title?: string;
ntrpLevel?: string;
levelDescription?: string;
avatarUrl?: string;
qrCodeUrl?: string;
bottomText?: string;
width?: number;
@@ -24,7 +29,7 @@ export interface RadarChartV2Ref {
}
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref) => {
const { data, title, ntrpLevel, qrCodeUrl, bottomText } = props;
const { data, title, ntrpLevel, levelDescription, qrCodeUrl, bottomText } = props;
const maxValue = 100;
const levels = 5;
@@ -180,6 +185,21 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
});
}
// 绘制圆角矩形
function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
// 格式化 NTRP 显示
function formatNtrpDisplay(level: string): string {
if (!level) return "";
@@ -209,6 +229,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
generateFullImage: async (options: {
title?: string;
ntrpLevel?: string;
levelDescription?: string;
qrCodeUrl?: string;
bottomText?: string;
width?: number;
@@ -269,22 +290,130 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
let currentY = topPadding;
// 绘制标题 - 根据设计稿调整字体大小和间距
if (options.title) {
ctx.fillStyle = "#000000";
ctx.font = "400 28px sans-serif"; // 设计稿字体大小
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText(options.title, sidePadding, currentY);
currentY += 44; // 标题到 NTRP 等级的间距
// 绘制用户头像和装饰图片 - 参考 Result 组件布局,居中显示
if (options.avatarUrl) {
try {
const avatarSize = 50; // Result 组件使用 0.5 倍数,所以是 50px
const avatarImg = await loadImage(canvas, options.avatarUrl);
// 头像区域总宽度(头像 + 装饰图片重叠部分)
const avatarWrapWidth = 50 + 44 - 10; // 头像宽度 + 装饰宽度 - 重叠部分
const avatarX = (width - avatarWrapWidth) / 2 + 10; // 居中,考虑装饰图片的位置
const avatarY = currentY;
// 绘制头像圆形背景
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.fillStyle = "#FFFFFF";
ctx.fill();
ctx.strokeStyle = "#EFEFEF";
ctx.lineWidth = 1;
ctx.stroke();
// 绘制头像(圆形裁剪)
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 1, 0, Math.PI * 2);
ctx.clip();
const innerAvatarSize = 45; // 头像内部尺寸 90px * 0.5
ctx.drawImage(avatarImg, avatarX + 2.5, avatarY + 2.5, innerAvatarSize, innerAvatarSize);
ctx.restore();
// 绘制装饰图片DocCopy- 在头像右侧
const addonSize = 44; // 88px * 0.5
const addonX = avatarX + avatarSize - 10; // margin-left: -10px (20px * 0.5)
const addonY = avatarY;
const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度
try {
const docCopyImg = await loadImage(canvas, docCopySvg);
ctx.save();
// 移动到旋转中心
const centerX = addonX + addonSize / 2;
const centerY = addonY + addonSize / 2;
ctx.translate(centerX, centerY);
ctx.rotate(addonRotation);
// 绘制装饰图片背景(圆角矩形,带渐变)
const borderRadius = 10; // 20px * 0.5
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius);
ctx.fill();
// 添加渐变背景色
ctx.globalAlpha = 0.2;
ctx.fillStyle = "rgba(89, 255, 214, 1)"; // rgba(89, 255, 214, 0.2)
ctx.fill();
ctx.globalAlpha = 1.0;
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 2;
ctx.stroke();
// 绘制装饰图片
const docSize = 24; // 48px * 0.5
const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度
ctx.rotate(docRotation);
ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize);
ctx.restore();
} catch (error) {
console.error("Failed to load docCopy image:", error);
}
currentY += avatarSize + 20; // 头像到底部文字区域的间距
} catch (error) {
console.error("Failed to load avatar image:", error);
}
}
// 绘制 NTRP 等级 - 根据设计稿调整字体大小和颜色
// 绘制文字区域 - 完全参考 Result 组件样式,居中对齐
const textCenterX = width / 2;
const textGap = 6; // gap: 6px
// 绘制标题 - 14px, font-weight: 300
if (options.title) {
ctx.fillStyle = "#000000";
ctx.font = "300 14px PingFang SC, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(options.title, textCenterX, currentY);
currentY += 14 + textGap; // 行高 + gap
}
// 绘制 NTRP 等级 - 36px, font-weight: 900, "NTRP" 黑色,等级数字 #00e5ad, gap: 8px
if (options.ntrpLevel) {
ctx.font = "900 36px Noto Sans SC, sans-serif";
ctx.textBaseline = "top";
const ntrpText = "NTRP";
const levelText = formatNtrpDisplay(options.ntrpLevel);
const ntrpWidth = ctx.measureText(ntrpText).width;
const levelWidth = ctx.measureText(levelText).width;
const totalWidth = ntrpWidth + 8 + levelWidth; // gap: 8px
const startX = textCenterX - totalWidth / 2;
// 绘制 "NTRP"(黑色)
ctx.fillStyle = "#000000";
ctx.textAlign = "left";
ctx.fillText(ntrpText, startX, currentY);
// 绘制等级数字(#00e5ad
ctx.fillStyle = "#00E5AD";
ctx.font = "bold 48px sans-serif"; // 设计稿字体大小
ctx.fillText(`NTRP ${formatNtrpDisplay(options.ntrpLevel)}`, sidePadding, currentY);
currentY += 72; // NTRP 等级到雷达图的间距
ctx.fillText(levelText, startX + ntrpWidth + 8, currentY);
currentY += 44 + textGap; // line-height: 44px + gap
}
// 绘制描述文本 - 16px, font-weight: 600
if (options.levelDescription) {
ctx.fillStyle = "#000000";
ctx.font = "600 16px PingFang SC, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(options.levelDescription, textCenterX, currentY);
currentY += 16 + 40; // 行高 + 到雷达图的间距
}
// 绘制雷达图 - 根据设计稿调整尺寸和位置
@@ -303,14 +432,78 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 绘制底部区域 - 根据设计稿调整
const qrSize = 100; // 设计稿二维码尺寸
const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!";
// 计算底部区域布局 - 从底部向上计算
const bottomTextHeight = 28; // 底部文字高度
const bottomSpacing = 30; // 底部间距
const bottomTextLineHeight = 28; // 行高
// 绘制二维码 - 右下角
// 绘制底部文字,确定文字的实际高度
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
ctx.font = "400 20px sans-serif"; // 设计稿字体大小
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
const textX = sidePadding;
const textY = height - bottomPadding;
const maxTextWidth = width - sidePadding * 2 - qrSize - 20; // 预留二维码和间距
// 计算文字行数
const bottomWords = bottomTextContent.split("");
let bottomLine = "";
let textLines = [];
for (let i = 0; i < bottomWords.length; i++) {
const testBottomLine = bottomLine + bottomWords[i];
const metrics = ctx.measureText(testBottomLine);
if (metrics.width > maxTextWidth && i > 0) {
textLines.push(bottomLine);
bottomLine = bottomWords[i];
} else {
bottomLine = testBottomLine;
}
}
if (bottomLine) {
textLines.push(bottomLine);
}
const actualTextHeight = textLines.length * bottomTextLineHeight;
// 绘制底部文字
textLines.forEach((lineText, index) => {
ctx.fillText(lineText, textX, textY - (textLines.length - 1 - index) * bottomTextLineHeight);
});
// 绘制底部 SVG logo 图片 - 在文字上方
const logoSvgY = height - bottomPadding - actualTextHeight - 40; // 在文字上方间距40px
try {
const logoSvgPath = shareLogoSvg;
// 先获取图片的实际尺寸,保持宽高比
const logoInfo = await getImageInfo(logoSvgPath);
const logoSvgImg = await loadImage(canvas, logoSvgPath);
// 根据设计稿,目标宽度为 235px按比例计算高度
const targetWidth = 235;
const aspectRatio = logoInfo.width / logoInfo.height;
const targetHeight = targetWidth / aspectRatio;
const logoSvgX = sidePadding; // 左边对齐
// 按实际宽高比绘制,避免拉伸
ctx.drawImage(
logoSvgImg,
logoSvgX,
logoSvgY,
targetWidth,
targetHeight
);
} catch (error) {
console.error("Failed to load logo SVG:", error);
}
// 绘制二维码 - 右下角,与文字底部对齐
const qrX = width - sidePadding - qrSize;
const qrY = height - bottomPadding - qrSize - bottomTextHeight - 20; // 在文字上方
const qrY = height - bottomPadding - qrSize;
if (options.qrCodeUrl) {
try {
@@ -321,26 +514,6 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
}
}
// 绘制底部 SVG logo 图片 - 根据设计稿SVG 包含 logo 和文字
try {
const logoSvgPath = shareLogoSvg;
const logoSvgImg = await loadImage(canvas, logoSvgPath);
const logoSvgWidth = 235; // SVG 原始宽度
const logoSvgHeight = 28; // SVG 原始高度
const logoSvgX = sidePadding; // 左边对齐
const logoSvgY = height - bottomPadding - logoSvgHeight; // 底部对齐
ctx.drawImage(
logoSvgImg,
logoSvgX,
logoSvgY,
logoSvgWidth,
logoSvgHeight
);
} catch (error) {
console.error("Failed to load logo SVG:", error);
}
// 导出图片
Taro.canvasToTempFilePath({
canvas,

View File

@@ -23,6 +23,7 @@ import FollowUserCard from './FollowUserCard/index';
import Comments from "./Comments";
import GeneralNavbar from "./GeneralNavbar";
import RadarChart from './Radar'
import RadarChartV2 from './Radar/indexV2'
import EmptyState from './EmptyState';
import NTRPTestEntryCard from './NTRPTestEntryCard'
@@ -53,6 +54,7 @@ export {
Comments,
GeneralNavbar,
RadarChart,
RadarChartV2,
EmptyState,
NTRPTestEntryCard,
};