From 10dab7ee84579a0e5890f422c07dd7871df0b3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 22 Apr 2026 11:10:28 +0800 Subject: [PATCH] 1 --- src/components/ShareCardCanvas/index.tsx | 105 +++++++++++------------ src/other_pages/ntrp-evaluate/index.tsx | 5 +- src/utils/share.ts | 86 +++++++++---------- 3 files changed, 93 insertions(+), 103 deletions(-) diff --git a/src/components/ShareCardCanvas/index.tsx b/src/components/ShareCardCanvas/index.tsx index 17343ed..0cab395 100644 --- a/src/components/ShareCardCanvas/index.tsx +++ b/src/components/ShareCardCanvas/index.tsx @@ -45,10 +45,9 @@ const ShareCardCanvas: React.FC = ({ // 获取屏幕宽度,如果没有传入width则使用屏幕宽度 const windowWidth = Taro.getSystemInfoSync().windowWidth - // 获取 DPR - 使用系统像素比确保高清显示 - // const systemDpr = Taro.getSystemInfoSync().pixelRatio - const dpr = 1 - // Math.min(systemDpr, 3) // 限制最大dpr为3,避免过度放大 + // 获取 DPR:统一与 share.ts 策略,限制上限避免内存过高 + const systemDpr = (Taro as any).getSystemInfoSync?.().pixelRatio || 1 + const dpr = Math.min(Math.max(systemDpr, 1), 2) // 2. 计算缩放比例(设备宽度 / 设计稿宽度) const scale = windowWidth / designWidth @@ -105,7 +104,7 @@ const ShareCardCanvas: React.FC = ({ // 绘制边框 ctx.strokeStyle = borderColor - ctx.lineWidth = 1 * dpr + ctx.lineWidth = 1 ctx.stroke() // 绘制文字 @@ -145,14 +144,14 @@ const ShareCardCanvas: React.FC = ({ const drawSVGPathToCanvas = (ctx: any) => { // 设置绘制样式 ctx.strokeStyle = '#00E5AD'; - ctx.lineWidth = scale * 3 * dpr; + ctx.lineWidth = scale * 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.save(); // 移动到指定位置并缩放 - ctx.translate(scale * 200 * dpr, scale * 90 * dpr); + ctx.translate(scale * 200, scale * 90); const scaleValue = 0.8 ctx.scale(scaleValue, scaleValue); @@ -382,43 +381,39 @@ const ShareCardCanvas: React.FC = ({ setIsDrawing(true) try { - // 设置Canvas的实际尺寸(使用dpr确保高清显示) + // 统一坐标系:先用物理像素清空,再缩放到逻辑坐标绘制 const canvasWidthPx = canvasWidth * dpr const canvasHeightPx = canvasHeight * dpr - - // 清空画布 + if (typeof ctx.setTransform === 'function') { + ctx.setTransform(1, 0, 0, 1, 0, 0) + } ctx.clearRect(0, 0, canvasWidthPx, canvasHeightPx) + ctx.save() + ctx.scale(dpr, dpr) console.log('画布已清空') - // 如果dpr大于2,进行缩放处理以避免内容过大 - if (dpr > 2) { - const scale = 2 / dpr - ctx.scale(scale, scale) - console.log('应用缩放:', scale) - } - // 绘制背景 - 渐变色 已完成 - const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx) + const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight) gradient.addColorStop(0, '#BFFFEF') gradient.addColorStop(1, '#F2FFFC') ctx.fillStyle = gradient - ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx) + ctx.fillRect(0, 0, canvasWidth, canvasHeight) console.log('背景绘制完成') // 绘制背景条纹 已完成 ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)' ctx.lineWidth = 2 - for (let i = 0; i < canvasWidthPx; i += 4) { + for (let i = 0; i < canvasWidth; i += 4) { ctx.beginPath() ctx.moveTo(i, 0) - ctx.lineTo(i, canvasHeightPx) + ctx.lineTo(i, canvasHeight) ctx.stroke() } // 绘制用户头像(左上角) 已完成 - const avatarSize = scale * 32 * dpr // 32px * dpr - const avatarX = scale * 35 * dpr // 距离左侧35px - const avatarY = scale * 35 * dpr // 距离顶部35px + const avatarSize = scale * 32 + const avatarX = scale * 35 + const avatarY = scale * 35 try { const avatarPath = await loadImage(data.userAvatar, canvasNode) @@ -438,23 +433,23 @@ const ShareCardCanvas: React.FC = ({ } // 绘制用户昵称 已完成 - const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px - const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐 - const nicknameFontSize = scale * 18 * dpr + const nicknameX = avatarX + avatarSize + scale * 8 + const nicknameY = avatarY + (avatarSize - scale * 18) / 2 + const nicknameFontSize = scale * 18 // drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, 'Noto Sans SC') drawBoldText(ctx, data.userNickname, nicknameX, nicknameY, nicknameFontSize, '#000000', 'Noto Sans SC', '900') // 绘制"邀你加入球局"文案 - const inviteX = scale * 35 * dpr // 距离画布左侧35px - const inviteY = scale * 100 * dpr // 距离画布顶部79px - const inviteFontSize = scale * 44 * dpr + const inviteX = scale * 35 + const inviteY = scale * 100 + const inviteFontSize = scale * 44 // 绘制"邀你加入" drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900') // 绘制"球局"特殊样式 - const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr - const qiuJuFontSize = scale * 44 * dpr + const qiuJuX = inviteX + ctx.measureText('邀你加入').width + scale * 4 + const qiuJuFontSize = scale * 44 drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900') // 测试绘制网络图片 @@ -462,12 +457,12 @@ const ShareCardCanvas: React.FC = ({ // 绘制球员图片(右上角)已完成 let venueBaseConfig = { - venueImgX: scale * 340 * dpr, - venueImgY: scale * 35 * dpr, + venueImgX: scale * 340, + venueImgY: scale * 35, rotation: scale * -8, // 旋转-8度 - venueImgSize: scale * 124 * dpr, - borderRadius: scale * 24 * dpr, - padding: scale * 4 * dpr, + venueImgSize: scale * 124, + borderRadius: scale * 24, + padding: scale * 4, venueImage: data.venueImages?.[0] } @@ -476,8 +471,8 @@ const ShareCardCanvas: React.FC = ({ const venueBackConfig = { ...venueBaseConfig, venueImage: data.venueImages?.[1], - venueImgX: scale * 400 * dpr, - venueImgY: scale * 35 * dpr, + venueImgX: scale * 400, + venueImgY: scale * 35, rotation: scale * -10, // 旋转-10度 } await drawVenueImages(ctx, venueBackConfig, canvasNode) @@ -512,11 +507,11 @@ const ShareCardCanvas: React.FC = ({ // 绘制"单打"标签 const danDaX = scale * 100 const danDaY = scale * 196 - const danDaHeight = scale * 40 * dpr - const danDaRadius = scale * 20 * dpr - const danDaFontSize = scale * 22 * dpr + const danDaHeight = scale * 40 + const danDaRadius = scale * 20 + const danDaFontSize = scale * 22 // 根据内容动态计算标签宽度(左右内边距) - const danDaPaddingX = scale * 16 * dpr + const danDaPaddingX = scale * 16 setFont2D(ctx, danDaFontSize) const danDaTextWidth = ctx.measureText(data.gameType).width const danDaWidth = danDaTextWidth + danDaPaddingX * 2 @@ -527,11 +522,11 @@ const ShareCardCanvas: React.FC = ({ const labelGap = scale * 16 // 两个标签之间的间距(不乘 dpr,保持视觉间距) const skillX = danDaX + danDaWidth + labelGap const skillY = scale * 196 - const skillHeight = scale * 40 * dpr - const skillRadius = scale * 20 * dpr - const skillFontSize = scale * 22 * dpr + const skillHeight = scale * 40 + const skillRadius = scale * 20 + const skillFontSize = scale * 22 // 根据内容动态计算技能标签宽度 - const skillPaddingX = scale * 20 * dpr + const skillPaddingX = scale * 20 setFont2D(ctx, skillFontSize) const skillTextWidth = ctx.measureText(data.skillLevel).width const skillWidth = skillTextWidth + skillPaddingX * 2 @@ -541,7 +536,7 @@ const ShareCardCanvas: React.FC = ({ // 绘制日期时间 const dateX = danDaX const timeInfoY = infoStartY + infoSpacing - const timeInfoFontSize = scale * 24 * dpr + const timeInfoFontSize = scale * 24 const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode) ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize) @@ -549,17 +544,18 @@ const ShareCardCanvas: React.FC = ({ drawBoldText(ctx, data.gameDate, dateX, timeInfoY + 8, timeInfoFontSize, '#00E5AD') // 绘制时间(黑色) - const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr + const timeX = textX + ctx.measureText(data.gameDate).width + scale * 10 // drawText(ctx, data.gameTime, timeX, timeInfoY + 8, 300, timeInfoFontSize, '#000000') drawBoldText(ctx, data.gameTime, timeX, timeInfoY + 8, timeInfoFontSize, '#000000') // 绘制地点 const locationInfoY = infoStartY + infoSpacing * 2 - const locationFontSize = scale * 22 * dpr + const locationFontSize = scale * 22 const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode) ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize) drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000') + ctx.restore() // 绘制完成,调用draw方法 console.log('开始调用ctx.draw()') const doExport = () => { @@ -649,12 +645,9 @@ const ShareCardCanvas: React.FC = ({ if (data && data.node) { const canvas = data.node const context = canvas.getContext('2d') - // DPR 缩放,提升清晰度(当前 dpr = 1 也可正常显示) - const sys = (Taro as any).getSystemInfoSync?.() || {} - const ratio = sys.pixelRatio || 1 - canvas.width = canvasWidth * ratio - canvas.height = canvasHeight * ratio - context.scale(ratio, ratio) + // 仅设置物理像素尺寸,绘制阶段统一在 drawShareCard 中做 ctx.scale(dpr, dpr) + canvas.width = canvasWidth * dpr + canvas.height = canvasHeight * dpr setCanvasNode(canvas) setCtx2d(context) setIs2dCtx(true) diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx index e3fd27e..067024e 100644 --- a/src/other_pages/ntrp-evaluate/index.tsx +++ b/src/other_pages/ntrp-evaluate/index.tsx @@ -743,9 +743,12 @@ function Result() { rid && !Number.isNaN(Number(rid)) ? `/other_pages/ntrp-evaluate/index?stage=${StageType.RESULT}&id=${rid}&from_share=1` : `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`; + const shareNickname = fromShare + ? result?.sharer_nickname || "好友" + : (userInfo as any)?.nickname || "好友"; return { title: result?.ntrp_level - ? `来看看 NTRP ${formatNtrpDisplay(result.ntrp_level)} 的测评结果` + ? `来看看 ${shareNickname} 的测评结果` : "来测一测你的NTRP等级吧", imageUrl: result?.level_img || undefined, path: sharePath, diff --git a/src/utils/share.ts b/src/utils/share.ts index 1066873..570fd6c 100644 --- a/src/utils/share.ts +++ b/src/utils/share.ts @@ -87,7 +87,7 @@ const drawLabel = (ctx: any, x: number, y: number, width: number, height: number // 绘制边框 ctx.strokeStyle = borderColor - ctx.lineWidth = 1 * dpr + ctx.lineWidth = 1 ctx.stroke() // 绘制文字 @@ -142,14 +142,14 @@ const loadImage = (src: string): Promise => { const drawSVGPathToCanvas = (ctx: any) => { // 设置绘制样式 ctx.strokeStyle = '#00E5AD'; - ctx.lineWidth = scale * 3 * dpr; + ctx.lineWidth = scale * 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.save(); // 移动到指定位置并缩放 - ctx.translate(scale * 200 * dpr, scale * 90 * dpr); + ctx.translate(scale * 200, scale * 90); const scaleValue = 0.8 ctx.scale(scaleValue, scaleValue); @@ -372,43 +372,36 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro console.log('开始绘制分享卡片...') try { - // 设置Canvas的实际尺寸(使用dpr确保高清显示) + // 统一逻辑坐标:先按 dpr 缩放,再使用设计坐标绘制 const canvasWidthPx = canvasWidth * dpr const canvasHeightPx = canvasHeight * dpr - - // 清空画布 ctx.clearRect(0, 0, canvasWidthPx, canvasHeightPx) + ctx.save() + ctx.scale(dpr, dpr) console.log('画布已清空') - // 如果dpr大于2,进行缩放处理以避免内容过大 - if (dpr > 2) { - const scale = 2 / dpr - ctx.scale(scale, scale) - console.log('应用缩放:', scale) - } - // 绘制背景 - 渐变色 已完成 - const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx) + const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight) gradient.addColorStop(0, '#BFFFEF') gradient.addColorStop(1, '#F2FFFC') ctx.fillStyle = gradient - ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx) + ctx.fillRect(0, 0, canvasWidth, canvasHeight) console.log('背景绘制完成') // 绘制背景条纹 已完成 ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)' ctx.lineWidth = 2 - for (let i = 0; i < canvasWidthPx; i += 4) { + for (let i = 0; i < canvasWidth; i += 4) { ctx.beginPath() ctx.moveTo(i, 0) - ctx.lineTo(i, canvasHeightPx) + ctx.lineTo(i, canvasHeight) ctx.stroke() } // 绘制用户头像(左上角) 已完成 - const avatarSize = scale * 32 * dpr // 32px * dpr - const avatarX = scale * 35 * dpr // 距离左侧35px - const avatarY = scale * 35 * dpr // 距离顶部35px + const avatarSize = scale * 32 + const avatarX = scale * 35 + const avatarY = scale * 35 try { const avatarPath = await loadImage(data.userAvatar) @@ -428,22 +421,22 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro } // 绘制用户昵称 已完成 - const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px - const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐 - const nicknameFontSize = scale * 18 * dpr + const nicknameX = avatarX + avatarSize + scale * 8 + const nicknameY = avatarY + (avatarSize - scale * 18) / 2 + const nicknameFontSize = scale * 18 drawBoldText(ctx, data.userNickname, nicknameX, nicknameY, nicknameFontSize, '#000000', 'Noto Sans SC', '900') // 绘制"邀你加入球局"文案 - const inviteX = scale * 35 * dpr // 距离画布左侧35px - const inviteY = scale * 100 * dpr // 距离画布顶部79px - const inviteFontSize = scale * 44 * dpr + const inviteX = scale * 35 + const inviteY = scale * 100 + const inviteFontSize = scale * 44 // 绘制"邀你加入" drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900') // 绘制"球局"特殊样式 - const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr - const qiuJuFontSize = scale * 44 * dpr + const qiuJuX = inviteX + ctx.measureText('邀你加入').width + scale * 4 + const qiuJuFontSize = scale * 44 drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900') // 测试绘制网络图片 @@ -451,12 +444,12 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro // 绘制球员图片(右上角)已完成 let venueBaseConfig = { - venueImgX: scale * 340 * dpr, - venueImgY: scale * 35 * dpr, + venueImgX: scale * 340, + venueImgY: scale * 35, rotation: scale * -8, // 旋转-8度 - venueImgSize: scale * 124 * dpr, - borderRadius: scale * 24 * dpr, - padding: scale * 4 * dpr, + venueImgSize: scale * 124, + borderRadius: scale * 24, + padding: scale * 4, venueImage: data.venueImages?.[0] } @@ -465,8 +458,8 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const venueBackConfig = { ...venueBaseConfig, venueImage: data.venueImages?.[1], - venueImgX: scale * 400 * dpr, - venueImgY: scale * 35 * dpr, + venueImgX: scale * 400, + venueImgY: scale * 35, rotation: scale * -10, // 旋转-10度 } await drawVenueImages(ctx, venueBackConfig) @@ -501,11 +494,11 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro // 绘制"单打"标签 const danDaX = scale * 100 const danDaY = scale * 196 - const danDaHeight = scale * 40 * dpr - const danDaRadius = scale * 20 * dpr - const danDaFontSize = scale * 22 * dpr + const danDaHeight = scale * 40 + const danDaRadius = scale * 20 + const danDaFontSize = scale * 22 // 根据内容动态计算标签宽度(左右内边距) - const danDaPaddingX = scale * 16 * dpr + const danDaPaddingX = scale * 16 setFont2D(ctx, danDaFontSize) const danDaTextWidth = ctx.measureText(data.gameType).width const danDaWidth = danDaTextWidth + danDaPaddingX * 2 @@ -516,11 +509,11 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const labelGap = scale * 16 // 两个标签之间的间距(不乘 dpr,保持视觉间距) const skillX = danDaX + danDaWidth + labelGap const skillY = scale * 196 - const skillHeight = scale * 40 * dpr - const skillRadius = scale * 20 * dpr - const skillFontSize = scale * 22 * dpr + const skillHeight = scale * 40 + const skillRadius = scale * 20 + const skillFontSize = scale * 22 // 根据内容动态计算技能标签宽度 - const skillPaddingX = scale * 20 * dpr + const skillPaddingX = scale * 20 setFont2D(ctx, skillFontSize) const skillTextWidth = ctx.measureText(data.skillLevel).width const skillWidth = skillTextWidth + skillPaddingX * 2 @@ -530,7 +523,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro // 绘制日期时间 const dateX = danDaX const timeInfoY = infoStartY + infoSpacing - const timeInfoFontSize = scale * 24 * dpr + const timeInfoFontSize = scale * 24 const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`) ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize) @@ -538,15 +531,16 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro drawBoldText(ctx, data.gameDate, dateX, timeInfoY + 8, timeInfoFontSize, '#00E5AD') // 绘制时间(黑色) - const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr + const timeX = textX + ctx.measureText(data.gameDate).width + scale * 10 drawBoldText(ctx, data.gameTime, timeX, timeInfoY + 8, timeInfoFontSize, '#000000') // 绘制地点 const locationInfoY = infoStartY + infoSpacing * 2 - const locationFontSize = scale * 22 * dpr + const locationFontSize = scale * 22 const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`) ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize) drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000') + ctx.restore() try { const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {