mergeCode

This commit is contained in:
筱野
2025-12-08 21:43:32 +08:00
24 changed files with 651 additions and 509 deletions

View File

@@ -248,7 +248,7 @@
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.8) 0%,
#fff 100%
#fafafa 100%
);
z-index: 999;
padding: 3px 4px 3px 4px;

View File

@@ -180,6 +180,11 @@ const HomeNavbar = (props: IProps) => {
// const currentAddress = city + district;
const handleInputClick = () => {
// 关闭城市选择器
if (cityPopupVisible) {
setCityPopupVisible(false);
}
const currentPagePath = getCurrentFullPath();
if (currentPagePath === "/game_pages/searchResult/index") {
(Taro as any).navigateBack();
@@ -192,6 +197,11 @@ const HomeNavbar = (props: IProps) => {
// 点击logo
const handleLogoClick = () => {
// 关闭城市选择器
if (cityPopupVisible) {
setCityPopupVisible(false);
}
// 如果当前在列表页,点击后页面回到顶部
if (getCurrentFullPath() === "/main_pages/index") {
// 使用父组件传递的滚动方法(适配 ScrollView
@@ -212,6 +222,11 @@ const HomeNavbar = (props: IProps) => {
};
const handleInputLeftIconClick = () => {
// 关闭城市选择器
if (cityPopupVisible) {
setCityPopupVisible(false);
}
if (leftIconClick) {
leftIconClick();
} else {
@@ -231,11 +246,11 @@ const HomeNavbar = (props: IProps) => {
// 处理城市切换(仅刷新数据,不保存缓存)
const handleCityChangeWithoutCache = async () => {
// 切换城市后,同时更新两个列表接口获取数据
// 先调用列表接口
if (refreshBothLists) {
await refreshBothLists();
}
// 更新球局数量
// 列表接口完成后,再调用数量接口
if (fetchGetGamesCount) {
await fetchGetGamesCount();
}
@@ -246,21 +261,17 @@ const HomeNavbar = (props: IProps) => {
// 用户手动选择的城市不保存到缓存(临时切换)
console.log("用户手动选择城市(不保存缓存):", _newArea);
// 先更新 area 状态(用于界面显示)
// 先更新 area 状态(用于界面显示和接口参数
updateArea(_newArea);
// 确保状态更新完成后再调用接口
// 切换城市后,同时更新两个列表接口获取数据,传入新的城市信息
const promises: Promise<any>[] = [];
// 先调用列表接口(会使用更新后的 state.area
if (refreshBothLists) {
promises.push(refreshBothLists(_newArea));
await refreshBothLists();
}
// 更新球局数量,直接传入新的城市信息,不依赖状态更新时序
// 列表接口完成后,再调用数量接口(会使用更新后的 state.area
if (fetchGetGamesCount) {
promises.push(fetchGetGamesCount(_newArea));
await fetchGetGamesCount();
}
// 并行执行,提高性能
await Promise.all(promises);
};
return (

View File

@@ -246,7 +246,7 @@ const ListCard: React.FC<ListCardProps> = ({
<View className="smoothWrapper">
<Image
className="iconListPlayingGame"
src={require("@/static/list/changdaqiuju.png")}
src={img.ICON_LIST_CHANGDA_QIuju}
mode="widthFix"
/>
{/* <Text className="smoothTitle">{game_type}</Text> */}

View File

@@ -9,9 +9,7 @@ import { Button, Input, View, Text, Image } from "@tarojs/components";
import Taro from "@tarojs/taro";
import classnames from "classnames";
import CommonPopup from "../CommonPopup";
import { getCurrentFullPath } from "@/utils";
import evaluateService from "@/services/evaluateService";
import { useUserActions } from "@/store/userStore";
import { useUserActions, useUserInfo } from "@/store/userStore";
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
import { useNtrpLevels } from "@/store/pickerOptionsStore";
import NTRPTestEntryCard from "../NTRPTestEntryCard";
@@ -67,6 +65,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
const [ntrp, setNtrp] = useState<string>("");
const [guideShow, setGuideShow] = useState(() => showGuide);
const { updateUserInfo } = useUserActions();
const userInfo = useUserInfo();
const ntrpLevels = useNtrpLevels();
const options = [
ntrpLevels.map((item) => ({
@@ -97,23 +96,22 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
// });
// }
// 当弹窗打开或用户信息变化时,从用户信息中提取并更新 ntrp 状态
useEffect(() => {
getNtrp();
}, []);
async function getNtrp() {
const res = await evaluateService.getLastResult();
if (res.code === 0 && res.data.has_ntrp_level) {
const match = res.data.user_ntrp_level.match(/-?\d+(\.\d+)?/);
if (!match) {
if (visible) {
if (userInfo?.ntrp_level) {
// 从 ntrp_level 中提取数字部分(如 "2.5" 或 "NTRP 2.5"
const match = String(userInfo.ntrp_level).match(/-?\d+(\.\d+)?/);
if (match) {
setNtrp(match[0]);
} else {
setNtrp("");
}
} else {
setNtrp("");
return;
}
setNtrp(match[0] as string);
} else {
setNtrp("");
}
}
}, [visible, userInfo?.ntrp_level]);
// const showEntry =
// displayCondition === "auto"
@@ -128,7 +126,9 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
async function handleChangeNtrp() {
Taro.showLoading({ title: "修改中" });
// 更新用户信息,会自动更新 store 中的 ntrp_level
await updateUserInfo({ ntrp_level: ntrp });
Taro.hideLoading();
Taro.showToast({
title: "NTRP水平修改成功",
icon: "none",

View File

@@ -2,9 +2,9 @@ import React, { useState, useEffect, useCallback, memo } from "react";
import { View, Image, Text } from "@tarojs/components";
import { requireLoginWithPhone } from "@/utils/helper";
import Taro from "@tarojs/taro";
import { useUserInfo, useUserActions } from "@/store/userStore";
import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils";
import evaluateService, { StageType } from "@/services/evaluateService";
import { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
import ArrowRight from "@/static/ntrp/ntrp_arrow_right_color.svg";
@@ -19,15 +19,16 @@ function NTRPTestEntryCard(props: {
type: EvaluateScene;
evaluateCallback?: EvaluateCallback;
}) {
const [testFlag, setTestFlag] = useState(false);
const [hasTestInLastMonth, setHasTestInLastMonth] = useState(false);
const { type, evaluateCallback } = props;
const userInfo = useUserInfo();
const { setCallback } = useEvaluate();
const { fetchUserInfo } = useUserActions();
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
console.log(userInfo);
// 从全局状态中获取测试结果,如果不存在则调用接口(使用请求锁避免重复调用)
useEffect(() => {
const init = async () => {
// 先等待静默登录完成
@@ -36,15 +37,17 @@ function NTRPTestEntryCard(props: {
if (!userInfo.id) {
await fetchUserInfo();
}
// 获取测试结果
const res = await evaluateService.getLastResult();
if (res.code === 0) {
setTestFlag(res.data.has_test_record);
setHasTestInLastMonth(res.data.has_test_in_last_month);
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
if (!lastTestResult) {
await fetchLastTestResult();
}
};
init();
}, [userInfo]);
}, [userInfo.id, fetchUserInfo, fetchLastTestResult, lastTestResult]);
// 从全局状态中计算标志位
const testFlag = lastTestResult?.has_test_record || false;
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
const handleTest = useCallback(
function () {

View File

@@ -34,6 +34,9 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
}) => {
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
@@ -54,87 +57,28 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const canvasWidth = designWidth * scale
const canvasHeight = designHeight * scale
// 绘制加粗文字(单行)
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)
}
// 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}"`
}
ctx.setFillStyle(color)
ctx.setTextAlign('left')
ctx.setTextBaseline('top')
// 绘制加粗文字(单行)
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)
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)
}
// if (isBold) {
// ctx.fillText(text, x + 1, y)
// ctx.fillText(text, x, y + 1)
// ctx.fillText(text, x + 1, y + 1)
// }
}
// 绘制圆角矩形函数
@@ -156,22 +100,22 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 绘制标签函数(通用)
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)
ctx.fillStyle = bgColor
drawRoundedRect(ctx, x, y, width, height, radius)
// 绘制边框
ctx.setStrokeStyle(borderColor)
ctx.setLineWidth(1 * dpr)
ctx.strokeStyle = borderColor
ctx.lineWidth = 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.fillStyle = textColor
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
setFont2D(ctx, fontSize)
ctx.save()
ctx.translate(textCenterX, textCenterY)
@@ -180,28 +124,35 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
}
// 加载图片 - 微信小程序版本
const loadImage = (src: string): Promise<string> => {
const loadImage = (src: string, canvas?: any): Promise<any> => {
return new Promise((resolve, reject) => {
Taro.getImageInfo({
src: src,
success: (res) => resolve(res.path),
fail: 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.setStrokeStyle('#48D800');
ctx.setLineWidth(scale * 3 * dpr);
ctx.setLineCap('round');
ctx.setLineJoin('round');
ctx.strokeStyle = '#00E5AD';
ctx.lineWidth = scale * 3 * dpr;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.save();
// 移动到指定位置并缩放
ctx.translate(scale * 210 * dpr, scale * 90 * dpr);
ctx.translate(scale * 200 * dpr, scale * 90 * dpr);
const scaleValue = 0.8
ctx.scale(scaleValue, scaleValue);
@@ -226,7 +177,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
}
// 绘制右上角场地图片
const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
const drawVenueImages = async (ctx: any, venueImageConfig: any, canvas?: any) => {
// 如果只有一张图
const playerImgX = venueImageConfig.venueImgX
const playerImgY = venueImageConfig.venueImgY
@@ -237,7 +188,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const venueImage = venueImageConfig.venueImage
try {
const playerImgPath = await loadImage(venueImage)
const playerImgPath = await loadImage(venueImage, canvas)
ctx.save()
// 移动到旋转中心点
@@ -249,7 +200,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
ctx.rotate((rotation * Math.PI) / 180)
// 1. 先绘制白色圆角矩形背景
ctx.setFillStyle('#FFFFFF')
ctx.fillStyle = '#FFFFFF'
ctx.beginPath()
// 使用更精确的圆角矩形绘制
@@ -338,7 +289,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
ctx.rotate((rotation * Math.PI) / 180)
// 绘制白色圆角矩形背景
ctx.setFillStyle('#FFFFFF')
ctx.fillStyle = '#FFFFFF'
ctx.beginPath()
const rectX = -playerImgSize / 2
@@ -380,7 +331,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const imgY = -playerImgSize / 2 + padding
const imgSize = playerImgSize - padding * 2
ctx.setFillStyle('#E0E0E0')
ctx.fillStyle = '#E0E0E0'
ctx.beginPath()
const imgRadius = borderRadius - padding
@@ -448,15 +399,15 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 绘制背景 - 渐变色 已完成
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
gradient.addColorStop(0, '#D8FFE5')
gradient.addColorStop(1, '#F9FFFB')
ctx.setFillStyle(gradient)
gradient.addColorStop(0, '#BFFFEF')
gradient.addColorStop(1, '#F2FFFC')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
console.log('背景绘制完成')
// 绘制背景条纹 已完成
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
ctx.setLineWidth(2)
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)
@@ -470,7 +421,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const avatarY = scale * 35 * dpr // 距离顶部35px
try {
const avatarPath = await loadImage(data.userAvatar)
const avatarPath = await loadImage(data.userAvatar, canvasNode)
// 微信小程序中绘制圆形头像需要特殊处理
ctx.save()
ctx.beginPath()
@@ -480,7 +431,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
ctx.restore()
} catch (error) {
// 如果头像加载失败,绘制默认头像
ctx.setFillStyle('#CCCCCC')
ctx.fillStyle = '#CCCCCC'
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.fill()
@@ -490,7 +441,8 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
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"')
// 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
@@ -498,12 +450,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const inviteFontSize = scale * 44 * dpr
// 绘制"邀你加入"
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', "Noto Sans SC")
drawBoldText(ctx, '邀你加入', inviteX, inviteY, inviteFontSize, '#000000', 'Noto Sans SC', '900')
// 绘制"球局"特殊样式
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 5 * dpr
const qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
const qiuJuFontSize = scale * 44 * dpr
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#48D800', '"Noto Sans SC"')
drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900')
// 测试绘制网络图片
drawSVGPathToCanvas(ctx)
@@ -528,16 +480,16 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
venueImgY: scale * 35 * dpr,
rotation: scale * -10, // 旋转-10度
}
await drawVenueImages(ctx, venueBackConfig)
await drawVenueImages(ctx, venueBackConfig, canvasNode)
// 前面的图
const venueFrontConfig = {
...venueBaseConfig,
venueImage: data.venueImages?.[0],
rotation: scale * 8, // 旋转-8度
}
await drawVenueImages(ctx, venueFrontConfig)
await drawVenueImages(ctx, venueFrontConfig, canvasNode)
} else {
await drawVenueImages(ctx, venueBaseConfig)
await drawVenueImages(ctx, venueBaseConfig, canvasNode)
}
// 绘制球局信息区域
@@ -554,26 +506,35 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const textX = iconX + iconSize + 20
// 绘制网球图标
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`)
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
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
// 根据内容动态计算标签宽度(左右内边距)
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 skillX = scale * 190
// 绘制技能等级标签(基于“单打”标签实际宽度后移)
const labelGap = scale * 16 // 两个标签之间的间距(不乘 dpr保持视觉间距
const skillX = danDaX + danDaWidth + labelGap
const skillY = scale * 196
const skillWidth = scale * 180 * dpr
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)
@@ -581,48 +542,56 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing
const timeInfoFontSize = scale * 24 * dpr
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
// 绘制日期(绿色)
drawText(ctx, data.gameDate, dateX, timeInfoY + 8, 300, timeInfoFontSize, '#4CAF50')
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')
// 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_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode)
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
drawText(ctx, data.venueName, danDaX, locationInfoY + 10, 600, locationFontSize, '#000000')
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
// 绘制完成调用draw方法
console.log('开始调用ctx.draw()')
ctx.draw(false, () => {
const doExport = () => {
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完全渲染
})
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.error('图片生成失败:', 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) {
@@ -639,20 +608,79 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 手动分享方法(已移除,由父组件处理分享)
// 组件挂载后绘制
// 使用 HTTPS 远程字体woff2加载到小程序渲染层不改字号
useEffect(() => {
if (data && !isDrawing && !tempImagePath) {
console.log('组件挂载,开始绘制分享卡片')
// 延迟一下确保Canvas已经渲染
setTimeout(() => {
// 在微信小程序中需要使用Taro.createCanvasContext
const ctx = Taro.createCanvasContext('shareCardCanvas')
console.log('Canvas上下文创建成功:', ctx)
drawShareCard(ctx)
}, 500)
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)
}
}, [data]) // 只依赖data移除canvasWidth避免无限循环
}, [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(() => {
@@ -666,10 +694,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
<View className={`share-card-canvas ${className}`}>
<Canvas
canvasId="shareCardCanvas"
id="shareCardCanvas"
type="2d"
style={{
width: `${canvasWidth}px`,
height: `${canvasHeight}px`,
position: 'absolute', // 绝对定位避免影响布局
// position: 'absolute', // 绝对定位避免影响布局
// top: '-9999px', // 移出可视区域
// left: '-9999px'
}}

View File

@@ -6,7 +6,7 @@ import "./index.scss";
import { EditModal } from "@/components";
import { UserService, PickerOption } from "@/services/userService";
import { PopupPicker } from "@/components/Picker/index";
import { useUserActions, useNicknameChangeStatus } from "@/store/userStore";
import { useUserActions, useNicknameChangeStatus, useLastTestResult } from "@/store/userStore";
import { UserInfoType } from "@/services/userService";
import {
useCities,
@@ -15,7 +15,6 @@ import {
} from "@/store/pickerOptionsStore";
import { formatNtrpDisplay } from "@/utils/helper";
import { useGlobalState } from "@/store/global";
import evaluateService from "@/services/evaluateService";
// 用户信息接口
// export interface UserInfo {
@@ -83,9 +82,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
}) => {
const nickname_change_status = useNicknameChangeStatus();
const { setShowGuideBar } = useGlobalState();
const { updateUserInfo, updateNickname } = useUserActions();
const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
const ntrpLevels = useNtrpLevels();
const [ntrpTested, setNtrpTested] = useState(false);
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
// 使用 useRef 记录上一次的 user_info只在真正变化时打印
const prevUserInfoRef = useRef<Partial<UserInfoType>>();
@@ -98,15 +98,14 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
console.log("UserInfoCard 用户信息变化:", user_info);
prevUserInfoRef.current = user_info;
}
const getLastResult = async () => {
// 获取测试结果
const res = await evaluateService.getLastResult();
if (res.code === 0) {
setNtrpTested(res.data.has_test_in_last_month);
}
};
getLastResult();
}, [user_info]);
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
if (!lastTestResult && user_info?.id) {
fetchLastTestResult();
}
}, [user_info?.id, lastTestResult, fetchLastTestResult]);
// 从全局状态中获取测试状态
const ntrpTested = lastTestResult?.has_test_in_last_month || false;
// 编辑个人简介弹窗状态
const [edit_modal_visible, setEditModalVisible] = useState(false);