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 = ({ data, className = '', onGenerated = () => { } }) => { const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径 const [isDrawing, setIsDrawing] = useState(false) // 防止重复绘制 const [canvasNode, setCanvasNode] = useState(null) // 2D Canvas 节点 const [ctx2d, setCtx2d] = useState(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 => { 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 ( { }} onTouchMove={() => { }} onTouchEnd={() => { }} onTouchCancel={() => { }} /> ) } export default ShareCardCanvas