Merge branch 'feature/juguohong/20250816'
This commit is contained in:
@@ -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避免无限循环
|
||||
|
||||
@@ -19,6 +19,7 @@ import { updateUserLocation } from "@/services/userService";
|
||||
// import ShareCardCanvas from "@/components/ShareCardCanvas";
|
||||
import { useUserActions } from "@/store/userStore";
|
||||
import { useDictionaryStore } from "@/store/dictionaryStore";
|
||||
// import generateShareImage from '@/utils/share'
|
||||
|
||||
const ListPage = () => {
|
||||
// 从 store 获取数据和方法
|
||||
@@ -294,6 +295,27 @@ const ListPage = () => {
|
||||
initDictionaryData();
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// generateShareImage({
|
||||
// userAvatar: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
// userNickname: "华巴抡卡",
|
||||
// gameType: "单打",
|
||||
// skillLevel: "NTRP 2.5 - 3.0",
|
||||
// gameDate: "6月20日(周五)",
|
||||
// gameTime: "下午5点 2小时",
|
||||
// venueName: "因乐驰网球俱乐部(嘉定江桥万达店)",
|
||||
// venueImages: ["https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
// //"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
|
||||
// ],
|
||||
// playerImage: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
|
||||
// }).then((imagePath) => {
|
||||
// console.log('===imagePath666', imagePath)
|
||||
// if (imagePath) {
|
||||
// setShareImagePath(imagePath)
|
||||
// }
|
||||
// })
|
||||
// }, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 自定义导航 */}
|
||||
@@ -361,21 +383,23 @@ const ListPage = () => {
|
||||
</View>
|
||||
</View>
|
||||
{/* 测试分享功能 */}
|
||||
{/* <ShareCardCanvas data={
|
||||
{
|
||||
userAvatar: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
userNickname: "华巴抡卡",
|
||||
gameType: "单打",
|
||||
skillLevel: "NTRP 2.5 - 3.0",
|
||||
gameDate: "6月20日(周五)",
|
||||
gameTime: "下午5点 2小时",
|
||||
venueName: "因乐驰网球俱乐部(嘉定江桥万达店)",
|
||||
venueImage: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
playerImage: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
|
||||
}
|
||||
}
|
||||
onGenerated={handleShare}
|
||||
/> */}
|
||||
{/* <ShareCardCanvas data={
|
||||
{
|
||||
userAvatar: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
userNickname: "华巴抡卡",
|
||||
gameType: "单打",
|
||||
skillLevel: "NTRP 2.5 - 3.0",
|
||||
gameDate: "6月20日(周五)",
|
||||
gameTime: "下午5点 2小时",
|
||||
venueName: "因乐驰网球俱乐部(嘉定江桥万达店)",
|
||||
venueImages: ["https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
|
||||
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
|
||||
],
|
||||
playerImage: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
|
||||
}
|
||||
}
|
||||
onGenerated={handleShare}
|
||||
/> */}
|
||||
<GuideBar currentPage="list" />
|
||||
</>
|
||||
);
|
||||
|
||||
706
src/utils/share.ts
Normal file
706
src/utils/share.ts
Normal file
@@ -0,0 +1,706 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface ShareCardData {
|
||||
userAvatar: string
|
||||
userNickname: string
|
||||
gameType: string
|
||||
skillLevel: string
|
||||
gameDate: string
|
||||
gameTime: string
|
||||
venueName: string
|
||||
venueImages: string[]
|
||||
playerImage?: string
|
||||
}
|
||||
|
||||
export interface GenerateShareImageOptions {
|
||||
canvasId?: string
|
||||
width?: number
|
||||
height?: number
|
||||
dpr?: number
|
||||
}
|
||||
|
||||
// 设计稿尺寸
|
||||
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
|
||||
|
||||
// 模块级运行时:存放公共对象/参数,避免层层传参
|
||||
const runtime: {
|
||||
offscreen?: any
|
||||
} = {}
|
||||
|
||||
// 兼容适配:将微信 CanvasContext 的 set* 方法映射到标准 Canvas 2D 属性
|
||||
const polyfillCanvasContext = (ctx: any) => {
|
||||
if (ctx && typeof ctx === 'object') {
|
||||
if (typeof ctx.setFillStyle !== 'function') {
|
||||
ctx.setFillStyle = (v: any) => { ctx.fillStyle = v }
|
||||
}
|
||||
if (typeof ctx.setStrokeStyle !== 'function') {
|
||||
ctx.setStrokeStyle = (v: any) => { ctx.strokeStyle = v }
|
||||
}
|
||||
if (typeof ctx.setLineWidth !== 'function') {
|
||||
ctx.setLineWidth = (v: number) => { ctx.lineWidth = v }
|
||||
}
|
||||
if (typeof ctx.setLineCap !== 'function') {
|
||||
ctx.setLineCap = (v: any) => { ctx.lineCap = v }
|
||||
}
|
||||
if (typeof ctx.setLineJoin !== 'function') {
|
||||
ctx.setLineJoin = (v: any) => { ctx.lineJoin = v }
|
||||
}
|
||||
if (typeof ctx.setTextAlign !== 'function') {
|
||||
ctx.setTextAlign = (v: any) => { ctx.textAlign = v }
|
||||
}
|
||||
if (typeof ctx.setTextBaseline !== 'function') {
|
||||
ctx.setTextBaseline = (v: any) => { ctx.textBaseline = v }
|
||||
}
|
||||
if (typeof ctx.setFontSize !== 'function') {
|
||||
ctx.setFontSize = (size: number) => { ctx.font = `${size}px sans-serif` }
|
||||
}
|
||||
if (typeof ctx.setFont !== 'function') {
|
||||
ctx.setFont = (fontStr: string) => { ctx.font = fontStr }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制加粗文字(单行)
|
||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily?: string) => {
|
||||
// 设置字体样式
|
||||
if (fontFamily) {
|
||||
try {
|
||||
// 尝试使用setFont方法(如果支持)
|
||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
||||
} catch (error) {
|
||||
// 如果不支持setFont,回退到setFontSize
|
||||
ctx.setFontSize(fontSize)
|
||||
}
|
||||
} else {
|
||||
ctx.setFontSize(fontSize)
|
||||
}
|
||||
|
||||
ctx.setFillStyle(color)
|
||||
ctx.setTextAlign('left')
|
||||
ctx.setTextBaseline('top')
|
||||
|
||||
// 绘制加粗效果:多次绘制并偏移
|
||||
ctx.fillText(text, x, y)
|
||||
ctx.fillText(text, x + 1, y)
|
||||
ctx.fillText(text, x, y + 1)
|
||||
ctx.fillText(text, x + 1, y + 1)
|
||||
}
|
||||
|
||||
// 绘制文字(支持自动换行)- 微信小程序版本
|
||||
const drawText = (ctx: any, text: string, x: number, y: number, maxWidth: number, fontSize: number, color: string, bold: boolean = false, fontFamily?: string) => {
|
||||
// 设置字体样式
|
||||
if (fontFamily) {
|
||||
try {
|
||||
// 尝试使用setFont方法(如果支持)
|
||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
||||
} catch (error) {
|
||||
// 如果不支持setFont,回退到setFontSize
|
||||
ctx.setFontSize(fontSize)
|
||||
}
|
||||
} else {
|
||||
ctx.setFontSize(fontSize)
|
||||
}
|
||||
|
||||
ctx.setFillStyle(color)
|
||||
ctx.setTextAlign('left')
|
||||
ctx.setTextBaseline('top')
|
||||
|
||||
const words = text.split('')
|
||||
let line = ''
|
||||
let lineY = y
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const testLine = line + words[i]
|
||||
// 微信小程序中measureText返回的是对象,需要访问width属性
|
||||
const metrics = ctx.measureText(testLine)
|
||||
const testWidth = metrics.width
|
||||
|
||||
if (testWidth > maxWidth && i > 0) {
|
||||
if (bold) {
|
||||
// 绘制加粗效果:多次绘制并偏移
|
||||
ctx.fillText(line, x, lineY)
|
||||
ctx.fillText(line, x + 1, lineY)
|
||||
ctx.fillText(line, x, lineY + 1)
|
||||
ctx.fillText(line, x + 1, lineY + 1)
|
||||
} else {
|
||||
ctx.fillText(line, x, lineY)
|
||||
}
|
||||
line = words[i]
|
||||
lineY += fontSize * 1.2
|
||||
} else {
|
||||
line = testLine
|
||||
}
|
||||
}
|
||||
|
||||
if (bold) {
|
||||
// 绘制加粗效果:多次绘制并偏移
|
||||
ctx.fillText(line, x, lineY)
|
||||
ctx.fillText(line, x + 1, lineY)
|
||||
ctx.fillText(line, x, lineY + 1)
|
||||
ctx.fillText(line, x + 1, lineY + 1)
|
||||
} else {
|
||||
ctx.fillText(line, x, lineY)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制圆角矩形函数
|
||||
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.setFillStyle(bgColor)
|
||||
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||
|
||||
// 绘制边框
|
||||
ctx.setStrokeStyle(borderColor)
|
||||
ctx.setLineWidth(1 * dpr)
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制文字
|
||||
const textCenterX = x + width / 2
|
||||
const textCenterY = y + height / 2
|
||||
|
||||
ctx.setFillStyle(textColor)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.setTextBaseline('middle')
|
||||
ctx.setFontSize(fontSize)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(textCenterX, textCenterY)
|
||||
ctx.fillText(text, 0, 0)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 加载图片 - 微信小程序版本
|
||||
// const loadImage = (src: string): Promise<string> => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// Taro.getImageInfo({
|
||||
// src: src,
|
||||
// success: (res) => resolve(res.path),
|
||||
// fail: reject
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// 工具函数 - OffscreenCanvas 下加载图片(从 runtime.offscreen 读取)
|
||||
const loadImage = (src: string): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
try {
|
||||
// @ts-ignore - createImage 为小程序 OffscreenCanvas 能力
|
||||
const off = runtime.offscreen
|
||||
if (!off || typeof off.createImage !== 'function') {
|
||||
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
||||
}
|
||||
const img = off.createImage()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = res.path
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制SVG路径到Canvas
|
||||
const drawSVGPathToCanvas = (ctx: any) => {
|
||||
// 设置绘制样式
|
||||
ctx.setStrokeStyle('#48D800');
|
||||
ctx.setLineWidth(scale * 3 * dpr);
|
||||
ctx.setLineCap('round');
|
||||
ctx.setLineJoin('round');
|
||||
|
||||
ctx.save();
|
||||
|
||||
// 移动到指定位置并缩放
|
||||
ctx.translate(scale * 210 * 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) => {
|
||||
// 如果只有一张图
|
||||
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 (ctx: any, data: ShareCardData, offscreen: any): Promise<string> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.log('开始绘制分享卡片...')
|
||||
|
||||
try {
|
||||
// 先对 2D 上下文做一次 API 兼容处理
|
||||
polyfillCanvasContext(ctx)
|
||||
// 设置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, '#D8FFE5')
|
||||
gradient.addColorStop(1, '#F9FFFB')
|
||||
ctx.setFillStyle(gradient)
|
||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||
console.log('背景绘制完成')
|
||||
|
||||
// 绘制背景条纹 已完成
|
||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
||||
ctx.setLineWidth(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)
|
||||
// 微信小程序中绘制圆形头像需要特殊处理
|
||||
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.setFillStyle('#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 - 24 * dpr) / 2 // 与头像水平居中对齐
|
||||
const nicknameFontSize = scale * 24 * dpr
|
||||
drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, '"Noto Sans SC"')
|
||||
|
||||
// 绘制"邀你加入球局"文案
|
||||
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")
|
||||
|
||||
// 绘制"球局"特殊样式
|
||||
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 5 * dpr
|
||||
const qiuJuFontSize = scale * 44 * dpr
|
||||
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#48D800', '"Noto Sans SC"')
|
||||
|
||||
// 测试绘制网络图片
|
||||
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)
|
||||
// 前面的图
|
||||
const venueFrontConfig = {
|
||||
...venueBaseConfig,
|
||||
venueImage: data.venueImages?.[0],
|
||||
rotation: scale * 8, // 旋转-8度
|
||||
}
|
||||
await drawVenueImages(ctx, venueFrontConfig)
|
||||
} else {
|
||||
await drawVenueImages(ctx, venueBaseConfig)
|
||||
}
|
||||
|
||||
// 绘制球局信息区域
|
||||
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('https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg')
|
||||
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制"单打"标签
|
||||
const danDaX = scale * 100
|
||||
const danDaY = scale * 196
|
||||
const danDaWidth = scale * 76 * dpr
|
||||
const danDaHeight = scale * 40 * dpr
|
||||
const danDaRadius = scale * 20 * dpr
|
||||
const danDaFontSize = scale * 22 * dpr
|
||||
|
||||
drawLabel(ctx, danDaX, danDaY, danDaWidth, danDaHeight, danDaRadius, data.gameType, danDaFontSize)
|
||||
|
||||
// 绘制技能等级标签
|
||||
const skillX = scale * 190
|
||||
const skillY = scale * 196
|
||||
const skillWidth = scale * 180 * dpr
|
||||
const skillHeight = scale * 40 * dpr
|
||||
const skillRadius = scale * 20 * dpr
|
||||
const skillFontSize = scale * 22 * dpr
|
||||
|
||||
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("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg")
|
||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制日期(绿色)
|
||||
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 + 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 + 10, 600, locationFontSize, '#000000')
|
||||
|
||||
try {
|
||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {
|
||||
wxAny.canvasToTempFilePath({
|
||||
canvas: offscreen,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: (res: any) => {
|
||||
console.log('===res666', res)
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch { }
|
||||
reject(new Error('无法导出图片(OffscreenCanvas 转文件失败)'))
|
||||
console.log('Canvas绘制命令已发送')
|
||||
|
||||
} catch (error) {
|
||||
console.error('绘制分享卡片失败:', error)
|
||||
Taro.showToast({
|
||||
title: '生成分享卡片失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let isDrawing = false
|
||||
export async function generateShareImage(data: ShareCardData): Promise<string> {
|
||||
if (isDrawing) {
|
||||
return ''
|
||||
}
|
||||
// 如果传入了 canvasId,则优先使用页面 Canvas 进行绘制(可见且稳定)
|
||||
const anyGlobal: any = Taro as any
|
||||
const weappGlobal: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
const createOffscreen = anyGlobal?.createOffscreenCanvas || weappGlobal?.createOffscreenCanvas || (Taro as any)?.createOffscreenCanvas
|
||||
if (!createOffscreen) {
|
||||
throw new Error('OffscreenCanvas 不可用')
|
||||
}
|
||||
|
||||
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
||||
const canvasWidthPx = canvasWidth * dpr
|
||||
const canvasHeightPx = canvasHeight * dpr
|
||||
|
||||
|
||||
// @ts-ignore - 小程序下 createOffscreenCanvas 接口
|
||||
const offscreen = createOffscreen({ type: '2d', width: canvasWidthPx, height: canvasHeightPx })
|
||||
// @ts-ignore
|
||||
let ctx: CanvasRenderingContext2D = offscreen.getContext('2d')
|
||||
// 记录到 runtime(供 loadImage 使用)
|
||||
runtime.offscreen = offscreen
|
||||
isDrawing = true
|
||||
|
||||
const imagePath = await drawShareCard(ctx, data, offscreen)
|
||||
isDrawing = false
|
||||
return imagePath
|
||||
}
|
||||
|
||||
export default generateShareImage
|
||||
|
||||
|
||||
Reference in New Issue
Block a user