Files
mini-programs/src/components/ShareCardCanvas/index.tsx
2026-02-14 12:59:21 +08:00

720 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react'
import { View, Canvas } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { OSS_BASE } from "@/config/api";
// 分享卡片数据接口
export interface ShareCardData {
// 用户信息
userAvatar: string // 用户头像URL
userNickname: string // 用户昵称
// 球局信息
gameType: string // 球局类型,如"单打"
skillLevel: string // 技能等级,如"NTRP 2.5 - 3.0"
gameDate: string // 日期,如"6月20日(周五)"
gameTime: string // 时间,如"下午5点 2小时"
venueName: string // 场地名称,如"因乐驰网球俱乐部(嘉定江桥万达店)"
venueImages: string[] // 场地图片URL
}
// 组件Props接口
export interface ShareCardCanvasProps {
data: ShareCardData
width?: number // 卡片宽度默认375
height?: number // 卡片高度默认500
onGenerated?: (imagePath: string) => void // 生成完成回调
className?: string
}
const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
data,
className = '',
onGenerated = () => { }
}) => {
const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径
const [isDrawing, setIsDrawing] = useState(false) // 防止重复绘制
const [canvasNode, setCanvasNode] = useState<any>(null) // 2D Canvas 节点
const [ctx2d, setCtx2d] = useState<any>(null) // 2D 上下文
const [is2dCtx, setIs2dCtx] = useState(false) // 是否为 2D 上下文
// 设计稿尺寸
const designWidth = 500
const designHeight = 400
// 获取屏幕宽度如果没有传入width则使用屏幕宽度
const windowWidth = Taro.getSystemInfoSync().windowWidth
// 获取 DPR - 使用系统像素比确保高清显示
// const systemDpr = Taro.getSystemInfoSync().pixelRatio
const dpr = 1
// Math.min(systemDpr, 3) // 限制最大dpr为3避免过度放大
// 2. 计算缩放比例(设备宽度 / 设计稿宽度)
const scale = windowWidth / designWidth
// 3. 计算实际显示尺寸(按比例缩放)
const canvasWidth = designWidth * scale
const canvasHeight = designHeight * scale
// 2D Canvas 字体设置
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
const fam = family || 'Noto Sans SC'
const wt = weight || '500'
ctx.font = `${wt} ${fontSize}px "${fam}"`
}
// 绘制加粗文字(单行)
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily: string = 'Noto Sans SC', fontWeight: string = '500') => {
// 设置字体样式
setFont2D(ctx, fontSize, fontFamily, fontWeight)
ctx.fillStyle = color
ctx.textAlign = 'left'
ctx.textBaseline = 'top'
// 绘制加粗效果:多次绘制并偏移
ctx.fillText(text, x, y)
// if (isBold) {
// ctx.fillText(text, x + 1, y)
// ctx.fillText(text, x, y + 1)
// ctx.fillText(text, x + 1, y + 1)
// }
}
// 绘制圆角矩形函数
const drawRoundedRect = (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.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
ctx.fill();
}
// 绘制标签函数(通用)
const drawLabel = (ctx: any, x: number, y: number, width: number, height: number, radius: number, text: string, fontSize: number, textColor: string = '#000000', bgColor: string = '#FFFFFF', borderColor: string = '#E0E0E0') => {
// 绘制背景
ctx.fillStyle = bgColor
drawRoundedRect(ctx, x, y, width, height, radius)
// 绘制边框
ctx.strokeStyle = borderColor
ctx.lineWidth = 1 * dpr
ctx.stroke()
// 绘制文字
const textCenterX = x + width / 2
const textCenterY = y + height / 2
ctx.fillStyle = textColor
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
setFont2D(ctx, fontSize)
ctx.save()
ctx.translate(textCenterX, textCenterY)
ctx.fillText(text, 0, 0)
ctx.restore()
}
// 加载图片 - 微信小程序版本
const loadImage = (src: string, canvas?: any): Promise<any> => {
return new Promise((resolve, reject) => {
if (!canvas || typeof canvas.createImage !== 'function') {
reject(new Error('2D canvas is required to load images'))
return
}
try {
const img = canvas.createImage()
img.onload = () => resolve(img)
img.onerror = (e: any) => reject(e)
img.src = src
} catch (e) {
reject(e)
}
})
}
// 绘制SVG路径到Canvas
const drawSVGPathToCanvas = (ctx: any) => {
// 设置绘制样式
ctx.strokeStyle = '#00E5AD';
ctx.lineWidth = scale * 3 * dpr;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.save();
// 移动到指定位置并缩放
ctx.translate(scale * 200 * dpr, scale * 90 * dpr);
const scaleValue = 0.8
ctx.scale(scaleValue, scaleValue);
// 手动绘制SVG路径微信小程序不支持Path2D
ctx.beginPath();
ctx.moveTo(109.808, 6.10642);
ctx.bezierCurveTo(89.5111, -2.79359, 33.4629, 3.77737, 7.49927, 30.2355);
ctx.bezierCurveTo(4.78286, 33.0044, 1.93351, 36.3827, 1.66282, 41.2355);
ctx.bezierCurveTo(1.21704, 49.1959, 7.56651, 55.1359, 13.1231, 57.7309);
ctx.bezierCurveTo(20.6373, 61.24, 28.4568, 62.6334, 36.2033, 63.2984);
ctx.bezierCurveTo(58.8813, 65.247, 81.6582, 60.9714, 101.719, 48.424);
ctx.bezierCurveTo(105.278, 46.1993, 108.823, 43.6365, 111.378, 39.5712);
ctx.bezierCurveTo(113.934, 35.5058, 115.361, 29.6397, 114.14, 24.1205);
ctx.bezierCurveTo(112.685, 17.5296, 107.961, 13.1328, 103.328, 10.8187);
ctx.bezierCurveTo(94.2761, 6.29607, 84.6677, 7.3453, 75.41, 8.45251);
// 绘制路径
ctx.stroke();
// 恢复上下文状态
ctx.restore();
}
// 绘制右上角场地图片
const drawVenueImages = async (ctx: any, venueImageConfig: any, canvas?: any) => {
// 如果只有一张图
const playerImgX = venueImageConfig.venueImgX
const playerImgY = venueImageConfig.venueImgY
const playerImgSize = venueImageConfig.venueImgSize
const borderRadius = venueImageConfig.borderRadius
const padding = venueImageConfig.padding
const rotation = venueImageConfig.rotation // 旋转-8度
const venueImage = venueImageConfig.venueImage
try {
const playerImgPath = await loadImage(venueImage, canvas)
ctx.save()
// 移动到旋转中心点
const centerX = playerImgX + playerImgSize / 2
const centerY = playerImgY + playerImgSize / 2
ctx.translate(centerX, centerY)
// 旋转-8度
ctx.rotate((rotation * Math.PI) / 180)
// 1. 先绘制白色圆角矩形背景
ctx.fillStyle = '#FFFFFF'
ctx.beginPath()
// 使用更精确的圆角矩形绘制
const rectX = -playerImgSize / 2
const rectY = -playerImgSize / 2
const rectWidth = playerImgSize
const rectHeight = playerImgSize
// 从左上角开始,顺时针绘制
// 左上角圆角
ctx.moveTo(rectX + borderRadius, rectY)
ctx.quadraticCurveTo(rectX, rectY, rectX, rectY + borderRadius)
// 左边
ctx.lineTo(rectX, rectY + rectHeight - borderRadius)
// 左下角圆角
ctx.quadraticCurveTo(rectX, rectY + rectHeight, rectX + borderRadius, rectY + rectHeight)
// 下边
ctx.lineTo(rectX + rectWidth - borderRadius, rectY + rectHeight)
// 右下角圆角
ctx.quadraticCurveTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth, rectY + rectHeight - borderRadius)
// 右边
ctx.lineTo(rectX + rectWidth, rectY + borderRadius)
// 右上角圆角
ctx.quadraticCurveTo(rectX + rectWidth, rectY, rectX + rectWidth - borderRadius, rectY)
// 上边
ctx.lineTo(rectX + borderRadius, rectY)
ctx.closePath()
ctx.fill()
// 2. 绘制图片带4px内边距
const imgX = -playerImgSize / 2 + padding
const imgY = -playerImgSize / 2 + padding
const imgSize = playerImgSize - padding * 2
// 设置圆角裁剪区域
ctx.beginPath()
const imgRadius = borderRadius - padding
// 从左上角开始,顺时针绘制
// 左上角圆角
ctx.moveTo(imgX + imgRadius, imgY)
ctx.quadraticCurveTo(imgX, imgY, imgX, imgY + imgRadius)
// 左边
ctx.lineTo(imgX, imgY + imgSize - imgRadius)
// 左下角圆角
ctx.quadraticCurveTo(imgX, imgY + imgSize, imgX + imgRadius, imgY + imgSize)
// 下边
ctx.lineTo(imgX + imgSize - imgRadius, imgY + imgSize)
// 右下角圆角
ctx.quadraticCurveTo(imgX + imgSize, imgY + imgSize, imgX + imgSize, imgY + imgSize - imgRadius)
// 右边
ctx.lineTo(imgX + imgSize, imgY + imgRadius)
// 右上角圆角
ctx.quadraticCurveTo(imgX + imgSize, imgY, imgX + imgSize - imgRadius, imgY)
// 上边
ctx.lineTo(imgX + imgRadius, imgY)
ctx.closePath()
ctx.clip()
// 绘制图片
ctx.drawImage(playerImgPath, imgX, imgY, imgSize, imgSize)
ctx.restore()
} catch (error) {
// 如果图片加载失败,绘制占位符
ctx.save()
const centerX = playerImgX + playerImgSize / 2
const centerY = playerImgY + playerImgSize / 2
ctx.translate(centerX, centerY)
ctx.rotate((rotation * Math.PI) / 180)
// 绘制白色圆角矩形背景
ctx.fillStyle = '#FFFFFF'
ctx.beginPath()
const rectX = -playerImgSize / 2
const rectY = -playerImgSize / 2
const rectWidth = playerImgSize
const rectHeight = playerImgSize
// 从左上角开始,顺时针绘制
// 左上角圆角
ctx.moveTo(rectX + borderRadius, rectY)
ctx.quadraticCurveTo(rectX, rectY, rectX, rectY + borderRadius)
// 左边
ctx.lineTo(rectX, rectY + rectHeight - borderRadius)
// 左下角圆角
ctx.quadraticCurveTo(rectX, rectY + rectHeight, rectX + borderRadius, rectY + rectHeight)
// 下边
ctx.lineTo(rectX + rectWidth - borderRadius, rectY + rectHeight)
// 右下角圆角
ctx.quadraticCurveTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth, rectY + rectHeight - borderRadius)
// 右边
ctx.lineTo(rectX + rectWidth, rectY + borderRadius)
// 右上角圆角
ctx.quadraticCurveTo(rectX + rectWidth, rectY, rectX + rectWidth - borderRadius, rectY)
// 上边
ctx.lineTo(rectX + borderRadius, rectY)
ctx.closePath()
ctx.fill()
// 绘制灰色占位符(带内边距)
const imgX = -playerImgSize / 2 + padding
const imgY = -playerImgSize / 2 + padding
const imgSize = playerImgSize - padding * 2
ctx.fillStyle = '#E0E0E0'
ctx.beginPath()
const imgRadius = borderRadius - padding
// 从左上角开始,顺时针绘制
// 左上角圆角
ctx.moveTo(imgX + imgRadius, imgY)
ctx.quadraticCurveTo(imgX, imgY, imgX, imgY + imgRadius)
// 左边
ctx.lineTo(imgX, imgY + imgSize - imgRadius)
// 左下角圆角
ctx.quadraticCurveTo(imgX, imgY + imgSize, imgX + imgRadius, imgY + imgSize)
// 下边
ctx.lineTo(imgX + imgSize - imgRadius, imgY + imgSize)
// 右下角圆角
ctx.quadraticCurveTo(imgX + imgSize, imgY + imgSize, imgX + imgSize, imgY + imgSize - imgRadius)
// 右边
ctx.lineTo(imgX + imgSize, imgY + imgRadius)
// 右上角圆角
ctx.quadraticCurveTo(imgX + imgSize, imgY, imgX + imgSize - imgRadius, imgY)
// 上边
ctx.lineTo(imgX + imgRadius, imgY)
ctx.closePath()
ctx.fill()
ctx.restore()
console.log('球员图片占位符绘制完成')
}
}
// 绘制分享卡片
const drawShareCard = async (ctx: any) => {
// 防止重复绘制
if (isDrawing) {
console.log('正在绘制中,跳过重复绘制')
return
}
return new Promise(async (resolve, reject) => {
console.log('开始绘制分享卡片...')
setIsDrawing(true)
try {
// 设置Canvas的实际尺寸使用dpr确保高清显示
const canvasWidthPx = canvasWidth * dpr
const canvasHeightPx = canvasHeight * dpr
// 清空画布
ctx.clearRect(0, 0, canvasWidthPx, canvasHeightPx)
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)
gradient.addColorStop(0, '#BFFFEF')
gradient.addColorStop(1, '#F2FFFC')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
console.log('背景绘制完成')
// 绘制背景条纹 已完成
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
ctx.lineWidth = 2
for (let i = 0; i < canvasWidthPx; i += 4) {
ctx.beginPath()
ctx.moveTo(i, 0)
ctx.lineTo(i, canvasHeightPx)
ctx.stroke()
}
// 绘制用户头像(左上角) 已完成
const avatarSize = scale * 32 * dpr // 32px * dpr
const avatarX = scale * 35 * dpr // 距离左侧35px
const avatarY = scale * 35 * dpr // 距离顶部35px
try {
const avatarPath = await loadImage(data.userAvatar, canvasNode)
// 微信小程序中绘制圆形头像需要特殊处理
ctx.save()
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize)
ctx.restore()
} catch (error) {
// 如果头像加载失败,绘制默认头像
ctx.fillStyle = '#CCCCCC'
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.fill()
}
// 绘制用户昵称 已完成
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
const nicknameFontSize = scale * 18 * dpr
// 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
// 绘制"邀你加入"
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900')
// 绘制"球局"特殊样式
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
const qiuJuFontSize = scale * 44 * dpr
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900')
// 测试绘制网络图片
drawSVGPathToCanvas(ctx)
// 绘制球员图片(右上角)已完成
let venueBaseConfig = {
venueImgX: scale * 340 * dpr,
venueImgY: scale * 35 * dpr,
rotation: scale * -8, // 旋转-8度
venueImgSize: scale * 124 * dpr,
borderRadius: scale * 24 * dpr,
padding: scale * 4 * dpr,
venueImage: data.venueImages?.[0]
}
if (data.venueImages.length > 1) {
// 后面的图
const venueBackConfig = {
...venueBaseConfig,
venueImage: data.venueImages?.[1],
venueImgX: scale * 400 * dpr,
venueImgY: scale * 35 * dpr,
rotation: scale * -10, // 旋转-10度
}
await drawVenueImages(ctx, venueBackConfig, canvasNode)
// 前面的图
const venueFrontConfig = {
...venueBaseConfig,
venueImage: data.venueImages?.[0],
rotation: scale * 8, // 旋转-8度
}
await drawVenueImages(ctx, venueFrontConfig, canvasNode)
} else {
await drawVenueImages(ctx, venueBaseConfig, canvasNode)
}
// 绘制球局信息区域
const infoStartY = scale * 192
const infoSpacing = scale * 64
// 球局类型和技能等级
const gameInfoY = infoStartY
// 图标大小
const iconSize = scale * 48
// 图标距离左侧距离
const iconX = scale * 35
// 文本距离左侧距离
const textX = iconX + iconSize + 20
// 绘制网球图标
const tennisBallPath = await loadImage(`${OSS_BASE}/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
// 绘制"单打"标签
const danDaX = scale * 100
const danDaY = scale * 196
const danDaHeight = scale * 40 * dpr
const danDaRadius = scale * 20 * dpr
const danDaFontSize = scale * 22 * dpr
// 根据内容动态计算标签宽度(左右内边距)
const danDaPaddingX = scale * 16 * dpr
setFont2D(ctx, danDaFontSize)
const danDaTextWidth = ctx.measureText(data.gameType).width
const danDaWidth = danDaTextWidth + danDaPaddingX * 2
drawLabel(ctx, danDaX, danDaY, danDaWidth, danDaHeight, danDaRadius, data.gameType, danDaFontSize)
// 绘制技能等级标签(基于“单打”标签实际宽度后移)
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 skillPaddingX = scale * 20 * dpr
setFont2D(ctx, skillFontSize)
const skillTextWidth = ctx.measureText(data.skillLevel).width
const skillWidth = skillTextWidth + skillPaddingX * 2
drawLabel(ctx, skillX, skillY, skillWidth, skillHeight, skillRadius, data.skillLevel, skillFontSize)
// 绘制日期时间
const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing
const timeInfoFontSize = scale * 24 * dpr
const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
// 绘制日期(绿色)
drawBoldText(ctx, data.gameDate, dateX, timeInfoY + 8, timeInfoFontSize, '#00E5AD')
// 绘制时间(黑色)
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
// 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 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')
// 绘制完成调用draw方法
console.log('开始调用ctx.draw()')
const doExport = () => {
console.log('Canvas绘制完成开始生成图片...')
const opts: any = {
fileType: 'png',
quality: 1,
success: (res: any) => {
console.log('图片生成成功:', res.tempFilePath)
setIsDrawing(false)
resolve(res.tempFilePath)
onGenerated?.(res.tempFilePath)
setTempImagePath(res.tempFilePath)
},
fail: (error: any) => {
console.warn('图片生成失败:', error)
setIsDrawing(false)
reject(error)
}
}
if (canvasNode) {
opts.canvas = canvasNode
} else {
opts.canvasId = 'shareCardCanvas'
}
;(Taro as any).canvasToTempFilePath(opts)
}
if (typeof (ctx as any).draw === 'function') {
;(ctx as any).draw(false, () => setTimeout(doExport, 300))
} else {
setTimeout(doExport, 100)
}
console.log('Canvas绘制命令已发送')
} catch (error) {
console.warn('绘制分享卡片失败:', error)
setIsDrawing(false) // 绘制失败,重置状态
Taro.showToast({
title: '生成分享卡片失败',
icon: 'none'
})
reject(error)
}
})
}
// 手动分享方法(已移除,由父组件处理分享)
// 使用 HTTPS 远程字体woff2加载到小程序渲染层不改字号
useEffect(() => {
try {
(Taro as any).loadFontFace({
global: true,
family: 'Noto Sans SC',
source: 'url("https://fonts.gstatic.com/s/notosanssc/v39/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaGzjCnYlNbPzT7HEL7j12XCOHJKg4RgZw3nFTvwZ8atTsBvwlvRUk7mYP2g.24.woff2")',
desc: { style: 'normal', weight: '700' },
success: () => {
console.log('===Noto Sans SC 远程字体加载成功')
try {
if (ctx2d && is2dCtx) {
drawShareCard(ctx2d)
} else {
console.log('字体已加载,但 2D 上下文尚未就绪,等待初始化后再绘制')
}
} catch (e) {
console.warn('===字体加载成功但重绘失败(忽略)', e)
}
},
fail: (err: any) => {
console.warn('===Noto Sans SC 远程字体加载失败:', err)
}
})
} catch (e) {
console.warn('===loadFontFace 不可用,跳过远程字体加载', e)
}
}, [ctx2d, is2dCtx])
// 初始化 2D Canvas可回退旧版
useEffect(() => {
try {
;(Taro as any).createSelectorQuery()
.select('#shareCardCanvas')
.fields({ node: true, size: true })
.exec((res: any[]) => {
const data = res && res[0]
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)
setCanvasNode(canvas)
setCtx2d(context)
setIs2dCtx(true)
console.log('2D Canvas 初始化成功')
} else {
setIs2dCtx(false)
console.log('2D Canvas 不可用,回退旧版 Canvas')
}
})
} catch (e) {
console.warn('初始化 2D Canvas 失败,回退旧版', e)
setIs2dCtx(false)
}
}, [])
// 组件挂载后绘制(仅在 2D 上下文就绪后绘制,避免回退导致 getImageInfo 404
useEffect(() => {
if (data && !isDrawing && !tempImagePath && ctx2d && is2dCtx) {
console.log('组件挂载,开始绘制分享卡片')
// 延迟一下确保Canvas已经渲染
setTimeout(() => {
console.log('使用 2D Canvas 开始绘制')
drawShareCard(ctx2d)
}, 500)
}
}, [data, ctx2d, is2dCtx]) // 等 2D 上下文可用后再绘制
// 暴露分享方法给父组件
useEffect(() => {
if (onGenerated && tempImagePath) {
onGenerated(tempImagePath)
}
}, [tempImagePath]) // 只依赖tempImagePath移除onGenerated避免无限循环
return (
<View className={`share-card-canvas ${className}`}>
<Canvas
canvasId="shareCardCanvas"
id="shareCardCanvas"
type="2d"
style={{
width: `${canvasWidth}px`,
height: `${canvasHeight}px`,
// position: 'absolute', // 绝对定位避免影响布局
// top: '-9999px', // 移出可视区域
// left: '-9999px'
}}
width={`${canvasWidth * dpr}`}
height={`${canvasHeight * dpr}`}
disableScroll={true}
onTouchStart={() => { }}
onTouchMove={() => { }}
onTouchEnd={() => { }}
onTouchCancel={() => { }}
/>
</View>
)
}
export default ShareCardCanvas