封装分享卡片方法

This commit is contained in:
李瑞
2025-10-01 21:52:56 +08:00
parent 3088777b86
commit 7fb0d5488e
3 changed files with 1009 additions and 241 deletions

View File

@@ -14,7 +14,7 @@ export interface ShareCardData {
gameDate: string // 日期,如"6月20日(周五)"
gameTime: string // 时间,如"下午5点 2小时"
venueName: string // 场地名称,如"因乐驰网球俱乐部(嘉定江桥万达店)"
venueImage: string // 场地图片URL
venueImages: string[] // 场地图片URL
// 可选信息
playerImage?: string // 球员图片URL
@@ -44,6 +44,11 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 获取屏幕宽度如果没有传入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
@@ -222,13 +227,201 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
ctx.restore();
}
// 获取 DPR - 使用系统像素比确保高清显示
// const systemDpr = Taro.getSystemInfoSync().pixelRatio
const dpr = 1
// Math.min(systemDpr, 3) // 限制最大dpr为3避免过度放大
// 绘制右上角场地图片
const drawVenueImages = async (ctx: any, venueImageConfig: 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)
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.setFillStyle('#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.setFillStyle('#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.setFillStyle('#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 () => {
const drawShareCard = async (ctx: any) => {
// 防止重复绘制
if (isDrawing) {
console.log('正在绘制中,跳过重复绘制')
@@ -240,10 +433,6 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
setIsDrawing(true)
try {
// 在微信小程序中需要使用Taro.createCanvasContext
const ctx = Taro.createCanvasContext('shareCardCanvas')
console.log('Canvas上下文创建成功:', ctx)
// 设置Canvas的实际尺寸使用dpr确保高清显示
const canvasWidthPx = canvasWidth * dpr
const canvasHeightPx = canvasHeight * dpr
@@ -322,192 +511,35 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
drawSVGPathToCanvas(ctx)
// 绘制球员图片(右上角)已完成
const playerImgX = scale * 340 * dpr
const playerImgY = scale * 35 * dpr
const playerImgSize = scale * 124 * dpr
const borderRadius = scale * 24 * dpr
const padding = scale * 4 * dpr
const rotation = scale * -8 // 旋转-8度
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]
}
try {
const playerImgPath = await loadImage(data.playerImage || data.venueImage)
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.setFillStyle('#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.setFillStyle('#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.setFillStyle('#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('球员图片占位符绘制完成')
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)
// 前面的图
const venueFrontConfig = {
...venueBaseConfig,
venueImage: data.venueImages?.[0],
rotation: scale * 8, // 旋转-8度
}
await drawVenueImages(ctx, venueFrontConfig)
} else {
await drawVenueImages(ctx, venueBaseConfig)
}
// 绘制球局信息区域
@@ -516,8 +548,11 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 球局类型和技能等级
const gameInfoY = infoStartY
const iconSize = scale * 40
// 图标大小
const iconSize = scale * 48
// 图标距离左侧距离
const iconX = scale * 35
// 文本距离左侧距离
const textX = iconX + iconSize + 20
// 绘制网球图标
@@ -552,45 +587,45 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
// 绘制日期(绿色)
drawText(ctx, data.gameDate, dateX, timeInfoY + 4, 300, timeInfoFontSize, '#4CAF50')
drawText(ctx, data.gameDate, dateX, timeInfoY + 8, 300, timeInfoFontSize, '#4CAF50')
// 绘制时间(黑色)
const timeX = textX + ctx.measureText(data.gameDate).width + 10 * dpr
drawText(ctx, data.gameTime, timeX, timeInfoY + 4, 300, timeInfoFontSize, '#000000')
drawText(ctx, data.gameTime, timeX, timeInfoY + 8, 300, timeInfoFontSize, '#000000')
// 绘制地点
const locationInfoY = infoStartY + infoSpacing * 2
const locationFontSize = scale * 22 * dpr
const locationPath = await loadImage("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg")
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
drawText(ctx, data.venueName, danDaX, locationInfoY + 4, 600, locationFontSize, '#000000')
drawText(ctx, data.venueName, danDaX, locationInfoY + 10, 600, locationFontSize, '#000000')
// 绘制完成调用draw方法
console.log('开始调用ctx.draw()')
ctx.draw(false, () => {
console.log('Canvas绘制完成开始生成图片...')
// 延迟一下再生成图片,确保绘制完成
setTimeout(() => {
Taro.canvasToTempFilePath({
canvasId: 'shareCardCanvas',
fileType: 'png',
quality: 1,
success: (res) => {
console.log('图片生成成功:', res.tempFilePath)
setIsDrawing(false) // 绘制完成,重置状态
resolve(res.tempFilePath)
onGenerated?.(res.tempFilePath)
setTempImagePath(res.tempFilePath)
},
fail: (error) => {
console.error('图片生成失败:', error)
setIsDrawing(false) // 绘制失败,重置状态
reject(error)
}
})
}, 500) // 延迟500ms确保Canvas完全渲染
})
console.log('Canvas绘制命令已发送')
// 绘制完成调用draw方法
console.log('开始调用ctx.draw()')
ctx.draw(false, () => {
console.log('Canvas绘制完成开始生成图片...')
// 延迟一下再生成图片,确保绘制完成
setTimeout(() => {
Taro.canvasToTempFilePath({
canvasId: 'shareCardCanvas',
fileType: 'png',
quality: 1,
success: (res) => {
console.log('图片生成成功:', res.tempFilePath)
setIsDrawing(false) // 绘制完成,重置状态
resolve(res.tempFilePath)
onGenerated?.(res.tempFilePath)
setTempImagePath(res.tempFilePath)
},
fail: (error) => {
console.error('图片生成失败:', error)
setIsDrawing(false) // 绘制失败,重置状态
reject(error)
}
})
}, 500) // 延迟500ms确保Canvas完全渲染
})
console.log('Canvas绘制命令已发送')
} catch (error) {
console.error('绘制分享卡片失败:', error)
@@ -613,7 +648,10 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
console.log('组件挂载,开始绘制分享卡片')
// 延迟一下确保Canvas已经渲染
setTimeout(() => {
drawShareCard()
// 在微信小程序中需要使用Taro.createCanvasContext
const ctx = Taro.createCanvasContext('shareCardCanvas')
console.log('Canvas上下文创建成功:', ctx)
drawShareCard(ctx)
}, 500)
}
}, [data]) // 只依赖data移除canvasWidth避免无限循环