From 9a147882837497f7e3ab4909cd0a821a54d00742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Sun, 7 Dec 2025 22:55:56 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E6=98=AF=E5=90=A6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=80=99=E8=A1=A5=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/detailService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/detailService.ts b/src/services/detailService.ts index 67a78ea..0467c65 100644 --- a/src/services/detailService.ts +++ b/src/services/detailService.ts @@ -87,8 +87,8 @@ export enum MATCH_STATUS { // 是否支持候补 export enum IsSubstituteSupported { - SUPPORT = '0', // 支持 - NOTSUPPORT = '1', // 不支持 + SUPPORT = '1', // 支持 + NOTSUPPORT = '0', // 不支持 } export interface UpdateLocationRes { From 758a67cec748d65577e6f5d476222f6636ba08f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=91=9E?= Date: Mon, 8 Dec 2025 00:03:53 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E5=A4=84=E7=90=86=E5=88=86=E4=BA=ABcanvas?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E5=8F=8A=E5=8A=A0=E7=B2=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ShareCardCanvas/index.tsx | 348 ++++++++++++----------- src/utils/share.ts | 225 ++++----------- 2 files changed, 244 insertions(+), 329 deletions(-) diff --git a/src/components/ShareCardCanvas/index.tsx b/src/components/ShareCardCanvas/index.tsx index 20a35b6..da83484 100644 --- a/src/components/ShareCardCanvas/index.tsx +++ b/src/components/ShareCardCanvas/index.tsx @@ -34,6 +34,9 @@ const ShareCardCanvas: React.FC = ({ }) => { 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 @@ -54,87 +57,28 @@ const ShareCardCanvas: React.FC = ({ 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 = ({ // 绘制标签函数(通用) 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 = ({ } // 加载图片 - 微信小程序版本 - const loadImage = (src: string): Promise => { + const loadImage = (src: string, canvas?: any): Promise => { 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 = ({ } // 绘制右上角场地图片 - 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 = ({ 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 = ({ ctx.rotate((rotation * Math.PI) / 180) // 1. 先绘制白色圆角矩形背景 - ctx.setFillStyle('#FFFFFF') + ctx.fillStyle = '#FFFFFF' ctx.beginPath() // 使用更精确的圆角矩形绘制 @@ -338,7 +289,7 @@ const ShareCardCanvas: React.FC = ({ 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 = ({ 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 = ({ // 绘制背景 - 渐变色 已完成 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ // 手动分享方法(已移除,由父组件处理分享) - - // 组件挂载后绘制 + // 使用 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 = ({ { - 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 } - } - } +// 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) => { - // 设置字体样式 - 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 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) + // } } // 绘制圆角矩形函数 @@ -178,22 +83,22 @@ const drawRoundedRect = (ctx: any, x: number, y: number, width: number, height: // 绘制标签函数(通用) 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) @@ -201,49 +106,31 @@ const drawLabel = (ctx: any, x: number, y: number, width: number, height: number ctx.restore() } -// 加载图片 - 微信小程序版本 -// const loadImage = (src: string): Promise => { -// return new Promise((resolve, reject) => { -// Taro.getImageInfo({ -// src: src, -// success: (res) => resolve(res.path), -// fail: reject -// }) -// }) -// } - -// 工具函数 - OffscreenCanvas 下加载图片(从 runtime.offscreen 读取) +// 工具函数 - OffscreenCanvas 下加载图片(使用 offscreen.createImage) const loadImage = (src: string): Promise => { 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 - }) + try { + 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 = src + } catch (e) { + reject(e) + } }) } // 绘制SVG路径到Canvas const drawSVGPathToCanvas = (ctx: any) => { // 设置绘制样式 - ctx.setStrokeStyle('#00E5AD'); - 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(); @@ -296,7 +183,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => { ctx.rotate((rotation * Math.PI) / 180) // 1. 先绘制白色圆角矩形背景 - ctx.setFillStyle('#FFFFFF') + ctx.fillStyle = '#FFFFFF' ctx.beginPath() // 使用更精确的圆角矩形绘制 @@ -385,7 +272,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => { ctx.rotate((rotation * Math.PI) / 180) // 绘制白色圆角矩形背景 - ctx.setFillStyle('#FFFFFF') + ctx.fillStyle = '#FFFFFF' ctx.beginPath() const rectX = -playerImgSize / 2 @@ -427,7 +314,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => { const imgY = -playerImgSize / 2 + padding const imgSize = playerImgSize - padding * 2 - ctx.setFillStyle('#E0E0E0') + ctx.fillStyle = '#E0E0E0' ctx.beginPath() const imgRadius = borderRadius - padding @@ -471,8 +358,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro console.log('开始绘制分享卡片...') try { - // 先对 2D 上下文做一次 API 兼容处理 - polyfillCanvasContext(ctx) // 设置Canvas的实际尺寸(使用dpr确保高清显示) const canvasWidthPx = canvasWidth * dpr const canvasHeightPx = canvasHeight * dpr @@ -492,13 +377,13 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx) gradient.addColorStop(0, '#BFFFEF') gradient.addColorStop(1, '#F2FFFC') - ctx.setFillStyle(gradient) + 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) @@ -532,7 +417,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro 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 @@ -540,12 +425,12 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro 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 + 4 * dpr const qiuJuFontSize = scale * 44 * dpr - drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', '"Noto Sans SC"') + drawBoldText(ctx, '球局', qiuJuX, inviteY, qiuJuFontSize, '#00E5AD', 'Noto Sans SC', '900') // 测试绘制网络图片 drawSVGPathToCanvas(ctx) @@ -607,7 +492,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const danDaFontSize = scale * 22 * dpr // 根据内容动态计算标签宽度(左右内边距) const danDaPaddingX = scale * 16 * dpr - ctx.setFontSize(danDaFontSize) + setFont2D(ctx, danDaFontSize) const danDaTextWidth = ctx.measureText(data.gameType).width const danDaWidth = danDaTextWidth + danDaPaddingX * 2 @@ -622,7 +507,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const skillFontSize = scale * 22 * dpr // 根据内容动态计算技能标签宽度 const skillPaddingX = scale * 20 * dpr - ctx.setFontSize(skillFontSize) + setFont2D(ctx, skillFontSize) const skillTextWidth = ctx.measureText(data.skillLevel).width const skillWidth = skillTextWidth + skillPaddingX * 2 @@ -635,19 +520,19 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`) ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize) - // 绘制日期(绿色) - drawText(ctx, data.gameDate, dateX, timeInfoY + 8, 300, timeInfoFontSize, '#00E5AD') + // 绘制日期(绿色,非描边粗体) + 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_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`) 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') try { const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null From 175e5814e3ad4b2a01b4df038aed886f46b765df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Mon, 8 Dec 2025 09:58:59 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=AD=E8=AE=BE=E7=BD=AE=E4=B8=BA=202.5=EF=BC=8C=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=89=93=E5=BC=80=E6=97=B6=E6=AD=A3=E7=A1=AE=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=202.5=EF=BC=8C=E4=B8=8D=E5=86=8D=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=201.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NTRPEvaluatePopup/index.tsx | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/NTRPEvaluatePopup/index.tsx b/src/components/NTRPEvaluatePopup/index.tsx index fc4f78f..fc541ed 100644 --- a/src/components/NTRPEvaluatePopup/index.tsx +++ b/src/components/NTRPEvaluatePopup/index.tsx @@ -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(""); 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", From a8dca0dd71a1775eb82c8a52290e6959f85c0215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Mon, 8 Dec 2025 13:32:12 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=8F=AA=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=BF=85=E9=A1=BB=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/HomeNavbar/index.tsx | 8 +- src/components/NTRPTestEntryCard/index.tsx | 25 ++--- src/components/UserInfo/index.tsx | 25 +++-- src/container/listContainer/index.tsx | 25 ++--- src/main_pages/components/ListPageContent.tsx | 92 ++++++++++++++++-- .../components/MessagePageContent.tsx | 18 +++- .../components/MyselfPageContent.tsx | 33 ++++++- src/main_pages/index.tsx | 5 +- src/store/listStore.ts | 13 +-- src/store/userStore.ts | 93 ++++++++++++------- 10 files changed, 245 insertions(+), 92 deletions(-) diff --git a/src/components/HomeNavbar/index.tsx b/src/components/HomeNavbar/index.tsx index 6e96650..ff5cc8e 100644 --- a/src/components/HomeNavbar/index.tsx +++ b/src/components/HomeNavbar/index.tsx @@ -231,13 +231,13 @@ const HomeNavbar = (props: IProps) => { // 处理城市切换(仅刷新数据,不保存缓存) const handleCityChangeWithoutCache = async () => { - // 切换城市后,同时更新两个列表接口获取数据 + // 切换城市后,同时更新两个列表接口获取数据,传入当前的 area if (refreshBothLists) { - await refreshBothLists(); + await refreshBothLists(area); } - // 更新球局数量 + // 更新球局数量,传入当前的 area,确保接口请求的地址与界面显示一致 if (fetchGetGamesCount) { - await fetchGetGamesCount(); + await fetchGetGamesCount(area); } }; diff --git a/src/components/NTRPTestEntryCard/index.tsx b/src/components/NTRPTestEntryCard/index.tsx index f4ea19b..ecd397d 100644 --- a/src/components/NTRPTestEntryCard/index.tsx +++ b/src/components/NTRPTestEntryCard/index.tsx @@ -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 () { diff --git a/src/components/UserInfo/index.tsx b/src/components/UserInfo/index.tsx index 507ab3e..fb6f2c1 100644 --- a/src/components/UserInfo/index.tsx +++ b/src/components/UserInfo/index.tsx @@ -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 = ({ }) => { 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>(); @@ -98,15 +98,14 @@ const UserInfoCardComponent: React.FC = ({ 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); diff --git a/src/container/listContainer/index.tsx b/src/container/listContainer/index.tsx index 46abaab..ec5a412 100644 --- a/src/container/listContainer/index.tsx +++ b/src/container/listContainer/index.tsx @@ -3,11 +3,9 @@ import ListCard from "@/components/ListCard"; import ListLoadError from "@/components/ListLoadError"; import ListCardSkeleton from "@/components/ListCardSkeleton"; import { useReachBottom } from "@tarojs/taro"; -import { useUserInfo, useUserActions } from "@/store/userStore"; -import { setStorage, getStorage } from "@/store/storage"; +import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore"; import { NTRPTestEntryCard } from "@/components"; import { EvaluateScene } from "@/store/evaluateStore"; -import evaluateService from "@/services/evaluateService"; import { waitForAuthInit } from "@/utils/authInit"; import "./index.scss"; import { useRef, useEffect, useState, useMemo } from "react"; @@ -40,10 +38,11 @@ const ListContainer = (props) => { const [showNumber, setShowNumber] = useState(0); const [showSkeleton, setShowSkeleton] = useState(false); - const [hasTestInLastMonth, setHasTestInLastMonth] = useState(false); const userInfo = useUserInfo(); - const { fetchUserInfo } = useUserActions(); + const { fetchUserInfo, fetchLastTestResult } = useUserActions(); + // 使用全局状态中的测试结果,避免重复调用接口 + const lastTestResult = useLastTestResult(); useReachBottom(() => { // 加载更多方法 @@ -100,17 +99,21 @@ const ListContainer = (props) => { // 先等待静默登录完成 await waitForAuthInit(); // 然后再获取用户信息 - if (!userInfo.id) { + const userInfoId = userInfo && 'id' in userInfo ? userInfo.id : null; + if (!userInfoId) { await fetchUserInfo(); + return; // 等待下一次 useEffect 触发(此时 userInfo.id 已有值) } - // 获取测试结果 - const res = await evaluateService.getLastResult(); - if (res.code === 0) { - setHasTestInLastMonth(res.data.has_test_in_last_month); + // 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次) + if (!lastTestResult) { + await fetchLastTestResult(); } }; init(); - }, [evaluateFlag, userInfo.id]); + }, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]); + + // 从全局状态中获取测试状态 + const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false; if (error) { return ; diff --git a/src/main_pages/components/ListPageContent.tsx b/src/main_pages/components/ListPageContent.tsx index cc97209..b23c60e 100644 --- a/src/main_pages/components/ListPageContent.tsx +++ b/src/main_pages/components/ListPageContent.tsx @@ -13,6 +13,7 @@ import { useDictionaryStore } from "@/store/dictionaryStore"; import { saveImage, navigateTo } from "@/utils"; export interface ListPageContentProps { + isActive?: boolean; // 是否处于激活状态(当前显示的页面) onNavStateChange?: (state: { isShowInputCustomerNavBar?: boolean; isDistanceFilterVisible?: boolean; @@ -26,6 +27,7 @@ export interface ListPageContentProps { } const ListPageContent: React.FC = ({ + isActive = true, onNavStateChange, onScrollToTop: _onScrollToTop, scrollToTopTrigger, @@ -53,6 +55,7 @@ const ListPageContent: React.FC = ({ initialFilterSearch, loadMoreMatches, fetchGetGamesCount, + refreshBothLists, updateDistanceQuickFilter, getCities, getCityQrCode, @@ -86,6 +89,9 @@ const ListPageContent: React.FC = ({ const [showSearchBar, setShowSearchBar] = useState(true); const [scrollTop, setScrollTop] = useState(0); const [refreshing, setRefreshing] = useState(false); + // 记录上一次加载数据时的城市,用于检测城市变化 + const lastLoadedAreaRef = useRef<[string, string] | null>(null); + const prevIsActiveRef = useRef(isActive); // 处理距离筛选显示/隐藏 const handleDistanceFilterVisibleChange = useCallback( @@ -200,11 +206,80 @@ const ListPageContent: React.FC = ({ getCityQrCode(); getDistricts(); // 新增:获取行政区列表 - getLocation().catch((error) => { - console.error('获取位置信息失败:', error); - }); + // 只有当页面激活时才加载位置和列表数据 + if (isActive) { + getLocation().catch((error) => { + console.error('获取位置信息失败:', error); + }); + } + }, [isActive]); - }, []); + // 当页面从非激活状态切换为激活状态时,检查城市是否变化,如果变化则重新加载数据 + useEffect(() => { + // 如果从非激活状态变为激活状态(切回列表页) + if (isActive && !prevIsActiveRef.current) { + const currentArea = area; + const lastArea = lastLoadedAreaRef.current; + + // 检查城市是否发生变化(比较省份) + const currentProvince = currentArea?.[1] || ""; + const lastProvince = lastArea?.[1] || ""; + + // 如果城市发生变化,或者地址存在但不一致,需要重新加载数据 + // 注意:即使 lastArea 为空,只要 currentArea 存在,也应该加载数据 + if (currentProvince && (currentProvince !== lastProvince || !lastArea)) { + console.log("切回列表页,检测到地址变化或不一致,重新加载数据:", { + lastArea, + currentArea, + lastProvince, + currentProvince, + }); + + // 地址发生变化或不一致,重新加载数据和球局数量 + const promises: Promise[] = []; + if (refreshBothLists) { + promises.push(refreshBothLists(currentArea)); + } + if (fetchGetGamesCount) { + promises.push(fetchGetGamesCount(currentArea)); + } + Promise.all(promises).then(() => { + // 数据加载完成后,更新记录的城市(记录为上一次在列表页加载数据时的城市) + if (currentArea) { + lastLoadedAreaRef.current = [...currentArea] as [string, string]; + } + }).catch((error) => { + console.error("重新加载数据失败:", error); + }); + } + } + + // 如果是首次加载且列表页激活,记录当前城市(用于后续比较) + if (isActive && !lastLoadedAreaRef.current && area) { + lastLoadedAreaRef.current = [...area] as [string, string]; + } + + // 更新上一次的激活状态 + prevIsActiveRef.current = isActive; + }, [isActive, area, refreshBothLists, fetchGetGamesCount]); + + // 监听城市变化(在列表页激活状态下),当城市切换后立即更新记录 + // 注意:这个 useEffect 用于处理在列表页激活状态下切换城市的情况 + // 当用户在列表页切换城市时,HomeNavbar 的 handleCityChange 已经会调用 refreshBothLists + // 这里只需要同步更新 lastLoadedAreaRef,确保后续检测逻辑正确 + useEffect(() => { + // 如果页面激活且城市发生变化(用户在列表页切换了城市) + if (isActive && area) { + const currentProvince = area[1] || ""; + const lastProvince = lastLoadedAreaRef.current?.[1] || ""; + + // 如果城市发生变化,立即更新记录(因为 refreshBothLists 已经在 HomeNavbar 中调用) + if (currentProvince && currentProvince !== lastProvince) { + // 立即更新记录,确保地址显示和使用的地址一致 + lastLoadedAreaRef.current = [...area] as [string, string]; + } + } + }, [isActive, area]); useEffect(() => { if (pageOption?.page === 1 && matches?.length > 0) { @@ -237,8 +312,13 @@ const ListPageContent: React.FC = ({ console.error("更新用户位置失败:", error); } } - fetchGetGamesCount(); - getMatchesData(); + // 传入当前的 area,确保接口请求的地址与界面显示一致 + await fetchGetGamesCount(area); + await getMatchesData(); + // 初始数据加载完成后,记录当前城市 + if (area && isActive) { + lastLoadedAreaRef.current = [...area] as [string, string]; + } return location; }; diff --git a/src/main_pages/components/MessagePageContent.tsx b/src/main_pages/components/MessagePageContent.tsx index 73140cd..8f79305 100644 --- a/src/main_pages/components/MessagePageContent.tsx +++ b/src/main_pages/components/MessagePageContent.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import React from "react"; import { View, Text, Image, ScrollView } from "@tarojs/components"; import { EmptyState } from "@/components"; import SubscribeNotificationTip from "@/components/SubscribeNotificationTip"; @@ -25,7 +26,11 @@ interface MessageItem { type MessageCategory = "comment" | "follow"; -const MessagePageContent = () => { +interface MessagePageContentProps { + isActive?: boolean; +} + +const MessagePageContent: React.FC = ({ isActive = true }) => { const { statusNavbarHeightInfo } = useGlobalState() || {}; const { totalHeight = 98 } = statusNavbarHeightInfo || {}; @@ -34,6 +39,7 @@ const MessagePageContent = () => { const [loading, setLoading] = useState(false); const [reachedBottom, setReachedBottom] = useState(false); const [refreshing, setRefreshing] = useState(false); + const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据 // 从 store 获取红点信息 const reddotInfo = useReddotInfo(); @@ -58,10 +64,14 @@ const MessagePageContent = () => { } }; + // 只有当页面激活且未加载过数据时才加载接口 useEffect(() => { - getNoticeList(); - fetchReddotInfo(); - }, []); + if (isActive && !hasLoaded) { + getNoticeList(); + fetchReddotInfo(); + setHasLoaded(true); + } + }, [isActive, hasLoaded]); const filteredMessages = messageList; diff --git a/src/main_pages/components/MyselfPageContent.tsx b/src/main_pages/components/MyselfPageContent.tsx index 27ff916..b6103ff 100644 --- a/src/main_pages/components/MyselfPageContent.tsx +++ b/src/main_pages/components/MyselfPageContent.tsx @@ -8,14 +8,19 @@ import ListContainer from "@/container/listContainer"; import { TennisMatch } from "@/../types/list/types"; import { NTRPTestEntryCard } from "@/components"; import { EvaluateScene } from "@/store/evaluateStore"; -import { useUserInfo } from "@/store/userStore"; +import { useUserInfo, useUserActions } from "@/store/userStore"; import { usePickerOption } from "@/store/pickerOptionsStore"; import { useGlobalState } from "@/store/global"; -const MyselfPageContent: React.FC = () => { +interface MyselfPageContentProps { + isActive?: boolean; +} + +const MyselfPageContent: React.FC = ({ isActive = true }) => { const pickerOption = usePickerOption(); const { statusNavbarHeightInfo } = useGlobalState() || {}; const { totalHeight = 98 } = statusNavbarHeightInfo || {}; + const { fetchUserInfo } = useUserActions(); const instance = (Taro as any).getCurrentInstance(); const user_id = instance.router?.params?.userid || ""; @@ -29,6 +34,7 @@ const MyselfPageContent: React.FC = () => { const [active_tab, setActiveTab] = useState<"hosted" | "participated">( "hosted" ); + const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据 const [collapseProfile, setCollapseProfile] = useState(false); @@ -37,6 +43,16 @@ const MyselfPageContent: React.FC = () => { pickerOption.getProfessions(); }, []); + // 当页面激活时,确保用户信息已加载 + useEffect(() => { + if (isActive) { + // 如果用户信息不存在或没有 id,则加载用户信息 + if (!user_info || !("id" in user_info) || !user_info.id) { + fetchUserInfo(); + } + } + }, [isActive, user_info, fetchUserInfo]); + const { useDidShow } = Taro as any; useDidShow(() => { // 确保从编辑页面返回时刷新数据 @@ -92,11 +108,20 @@ const MyselfPageContent: React.FC = () => { } }, [active_tab, user_info, classifyGameRecords]); + // 只有当页面激活且未加载过数据时才加载接口 useEffect(() => { - if (!loading) { + if (isActive && !hasLoaded && !loading && user_info && "id" in user_info) { + load_game_data(); + setHasLoaded(true); + } + }, [isActive, hasLoaded, loading, load_game_data, user_info]); + + // 当 active_tab 切换时,如果页面已激活,重新加载数据 + useEffect(() => { + if (isActive && hasLoaded && !loading && user_info && "id" in user_info) { load_game_data(); } - }, [loading, load_game_data]); + }, [active_tab, isActive, hasLoaded, loading, load_game_data, user_info]); const handle_follow = async () => { try { diff --git a/src/main_pages/index.tsx b/src/main_pages/index.tsx index 752276c..65943ed 100644 --- a/src/main_pages/index.tsx +++ b/src/main_pages/index.tsx @@ -230,6 +230,7 @@ const MainPage: React.FC = () => { className={`tab-content ${currentTab === "list" ? "active" : ""}`} > { - + {/* 我的页内容 */} - + {/* 底部导航栏 */} diff --git a/src/store/listStore.ts b/src/store/listStore.ts index afe3095..ad2c6fd 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -193,12 +193,11 @@ export const useListStore = create()((set, get) => ({ const distanceQuickFilter = currentPageState?.distanceQuickFilter || {}; const { distanceFilter, order, district } = distanceQuickFilter || {}; - // 优先使用 overrideArea(切换城市时传入),其次使用 area 状态中的省份,最后使用用户信息中的 + // 优先使用 overrideArea(切换城市时传入),其次使用 area 状态 + // area 会在 userLastLocationProvince 更新时自动同步,所以这里直接使用 area 即可 const areaProvince = overrideArea?.at(1) || state.area?.at(1) || ""; const userInfo = useUser.getState().user as any; - const userLastLocationProvince = userInfo?.last_location_province || ""; - // 优先使用切换后的城市,如果没有切换过则使用用户信息中的 - const last_location_province = areaProvince || userLastLocationProvince; + const last_location_province = areaProvince; const last_location_city = userInfo?.last_location_city || ""; // city 参数逻辑: @@ -530,7 +529,8 @@ export const useListStore = create()((set, get) => ({ // 使用 Promise.resolve 确保状态更新后再调用接口 Promise.resolve().then(() => { const freshState = get(); // 重新获取最新状态 - freshState.fetchGetGamesCount(); + // 传入当前的 area,确保接口请求的地址与界面显示一致 + freshState.fetchGetGamesCount(freshState.area); }); }, @@ -551,7 +551,8 @@ export const useListStore = create()((set, get) => ({ Promise.resolve().then(() => { const freshState = get(); // 重新获取最新状态 freshState.getMatchesData(); - freshState.fetchGetGamesCount(); + // 传入当前的 area,确保接口请求的地址与界面显示一致 + freshState.fetchGetGamesCount(freshState.area); }); }, diff --git a/src/store/userStore.ts b/src/store/userStore.ts index def90fe..1f5d3a9 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -7,6 +7,8 @@ import { NicknameChangeStatus, updateNickname as updateNicknameApi, } from "@/services/userService"; +import evaluateService, { LastTimeTestResult } from "@/services/evaluateService"; +import { useListStore } from "./listStore"; export interface UserState { user: UserInfoType | {}; @@ -15,35 +17,11 @@ export interface UserState { nicknameChangeStatus: Partial; checkNicknameChangeStatus: () => void; updateNickname: (nickname: string) => void; + // NTRP 测试结果缓存 + lastTestResult: LastTimeTestResult | null; + fetchLastTestResult: () => Promise; } -// 请求锁,防止重复请求 -let fetchingUserInfo = false; -let fetchUserInfoPromise: Promise | null = null; - -const fetchUserInfoWithLock = async (set) => { - // 如果正在请求,直接返回现有的 Promise - if (fetchingUserInfo && fetchUserInfoPromise) { - return fetchUserInfoPromise; - } - - fetchingUserInfo = true; - fetchUserInfoPromise = (async () => { - try { - const res = await fetchUserProfile(); - set({ user: res.data }); - return res.data; - } catch (error) { - console.error("获取用户信息失败:", error); - return undefined; - } finally { - fetchingUserInfo = false; - fetchUserInfoPromise = null; - } - })(); - - return fetchUserInfoPromise; -}; const getTimeNextDate = (time: string) => { const date = new Date(time); @@ -56,15 +34,50 @@ const getTimeNextDate = (time: string) => { export const useUser = create()((set) => ({ user: {}, - fetchUserInfo: () => fetchUserInfoWithLock(set), + fetchUserInfo: async () => { + try { + const res = await fetchUserProfile(); + const userData = res.data; + set({ user: userData }); + + // 当 userLastLocationProvince 更新时,同步更新 area + if (userData?.last_location_province) { + const listStore = useListStore.getState(); + const currentArea = listStore.area; + // 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新 + if (!currentArea || currentArea[1] !== userData.last_location_province) { + const newArea: [string, string] = ["中国", userData.last_location_province]; + listStore.updateArea(newArea); + } + } + + return userData; + } catch (error) { + console.error("获取用户信息失败:", error); + return undefined; + } + }, updateUserInfo: async (userInfo: Partial) => { try { // 先更新后端 await updateUserProfile(userInfo); // 然后立即更新本地状态(乐观更新) - set((state) => ({ - user: { ...state.user, ...userInfo }, - })); + set((state) => { + const newUser = { ...state.user, ...userInfo }; + + // 当 userLastLocationProvince 更新时,同步更新 area + if (userInfo.last_location_province) { + const listStore = useListStore.getState(); + const currentArea = listStore.area; + // 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新 + if (!currentArea || currentArea[1] !== userInfo.last_location_province) { + const newArea: [string, string] = ["中国", userInfo.last_location_province]; + listStore.updateArea(newArea); + } + } + + return { user: newUser }; + }); // 不再每次都重新获取完整用户信息,减少请求次数 // 只有在更新头像等需要服务器返回新URL的字段时才需要重新获取 // 如果需要确保数据一致性,可以在特定场景下手动调用 fetchUserInfo @@ -99,6 +112,21 @@ export const useUser = create()((set) => ({ console.error("更新用户昵称失败:", error); } }, + // NTRP 测试结果缓存 + lastTestResult: null, + fetchLastTestResult: async () => { + try { + const res = await evaluateService.getLastResult(); + if (res.code === 0) { + set({ lastTestResult: res.data }); + return res.data; + } + return null; + } catch (error) { + console.error("获取NTRP测试结果失败:", error); + return null; + } + }, })); export const useUserInfo = () => useUser((state) => state.user); @@ -111,4 +139,7 @@ export const useUserActions = () => updateUserInfo: state.updateUserInfo, checkNicknameChangeStatus: state.checkNicknameChangeStatus, updateNickname: state.updateNickname, + fetchLastTestResult: state.fetchLastTestResult, })); + +export const useLastTestResult = () => useUser((state) => state.lastTestResult); From 0dd0b711f9726a58b82538e7e840eb3fd508928b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Mon, 8 Dec 2025 15:56:56 +0800 Subject: [PATCH 5/7] 1 --- src/components/HomeNavbar/index.tsx | 37 ++++++---- src/components/ListCard/index.tsx | 2 +- src/config/images.js | 1 + src/main_pages/components/ListPageContent.tsx | 36 +++++----- src/main_pages/index.tsx | 8 ++- src/store/listStore.ts | 71 ++++++++++++------- types/list/types.ts | 6 +- 7 files changed, 103 insertions(+), 58 deletions(-) diff --git a/src/components/HomeNavbar/index.tsx b/src/components/HomeNavbar/index.tsx index ff5cc8e..3929053 100644 --- a/src/components/HomeNavbar/index.tsx +++ b/src/components/HomeNavbar/index.tsx @@ -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,13 +246,13 @@ const HomeNavbar = (props: IProps) => { // 处理城市切换(仅刷新数据,不保存缓存) const handleCityChangeWithoutCache = async () => { - // 切换城市后,同时更新两个列表接口获取数据,传入当前的 area + // 先调用列表接口 if (refreshBothLists) { - await refreshBothLists(area); + await refreshBothLists(); } - // 更新球局数量,传入当前的 area,确保接口请求的地址与界面显示一致 + // 列表接口完成后,再调用数量接口 if (fetchGetGamesCount) { - await fetchGetGamesCount(area); + await fetchGetGamesCount(); } }; @@ -246,21 +261,17 @@ const HomeNavbar = (props: IProps) => { // 用户手动选择的城市不保存到缓存(临时切换) console.log("用户手动选择城市(不保存缓存):", _newArea); - // 先更新 area 状态(用于界面显示) + // 先更新 area 状态(用于界面显示和接口参数) updateArea(_newArea); - // 确保状态更新完成后再调用接口 - // 切换城市后,同时更新两个列表接口获取数据,传入新的城市信息 - const promises: Promise[] = []; + // 先调用列表接口(会使用更新后的 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 ( diff --git a/src/components/ListCard/index.tsx b/src/components/ListCard/index.tsx index 291b0b9..c865790 100644 --- a/src/components/ListCard/index.tsx +++ b/src/components/ListCard/index.tsx @@ -246,7 +246,7 @@ const ListCard: React.FC = ({ {/* {game_type} */} diff --git a/src/config/images.js b/src/config/images.js index 2dc8e5c..57e03c6 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -69,4 +69,5 @@ export default { ICON_CLOSE: require('@/static/publishBall/icon-close.svg'), ICON_LIST_NTPR: require('@/static/list/ntpr.svg'), ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'), + ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'), } diff --git a/src/main_pages/components/ListPageContent.tsx b/src/main_pages/components/ListPageContent.tsx index b23c60e..3a8528f 100644 --- a/src/main_pages/components/ListPageContent.tsx +++ b/src/main_pages/components/ListPageContent.tsx @@ -236,21 +236,24 @@ const ListPageContent: React.FC = ({ }); // 地址发生变化或不一致,重新加载数据和球局数量 - const promises: Promise[] = []; - if (refreshBothLists) { - promises.push(refreshBothLists(currentArea)); - } - if (fetchGetGamesCount) { - promises.push(fetchGetGamesCount(currentArea)); - } - Promise.all(promises).then(() => { - // 数据加载完成后,更新记录的城市(记录为上一次在列表页加载数据时的城市) - if (currentArea) { - lastLoadedAreaRef.current = [...currentArea] as [string, string]; + // 先调用列表接口,然后在列表接口完成后调用数量接口 + (async () => { + try { + if (refreshBothLists) { + await refreshBothLists(); + } + // 列表接口完成后,再调用数量接口 + if (fetchGetGamesCount) { + await fetchGetGamesCount(); + } + // 数据加载完成后,更新记录的城市(记录为上一次在列表页加载数据时的城市) + if (currentArea) { + lastLoadedAreaRef.current = [...currentArea] as [string, string]; + } + } catch (error) { + console.error("重新加载数据失败:", error); } - }).catch((error) => { - console.error("重新加载数据失败:", error); - }); + })(); } } @@ -312,9 +315,10 @@ const ListPageContent: React.FC = ({ console.error("更新用户位置失败:", error); } } - // 传入当前的 area,确保接口请求的地址与界面显示一致 - await fetchGetGamesCount(area); + // 先调用列表接口 await getMatchesData(); + // 列表接口完成后,再调用数量接口 + await fetchGetGamesCount(); // 初始数据加载完成后,记录当前城市 if (area && isActive) { lastLoadedAreaRef.current = [...area] as [string, string]; diff --git a/src/main_pages/index.tsx b/src/main_pages/index.tsx index 65943ed..90af2fe 100644 --- a/src/main_pages/index.tsx +++ b/src/main_pages/index.tsx @@ -96,6 +96,12 @@ const MainPage: React.FC = () => { if (code === currentTab) { return; } + + // 切换标签时关闭城市选择器 + if (isCityPickerVisible) { + setIsCityPickerVisible(false); + } + setCurrentTab(code as TabType); // 切换标签时滚动到顶部 (Taro as any).pageScrollTo({ @@ -103,7 +109,7 @@ const MainPage: React.FC = () => { duration: 300, }); }, - [currentTab] + [currentTab, isCityPickerVisible] ); // 处理发布菜单显示/隐藏 diff --git a/src/store/listStore.ts b/src/store/listStore.ts index ad2c6fd..0c41c7c 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -17,7 +17,6 @@ import { ListState, IPayload, } from "../../types/list/types"; -import { useUser } from "./userStore"; function translateCityData(dataTree) { return dataTree.map((item) => { @@ -185,7 +184,8 @@ export const useListStore = create()((set, get) => ({ gamesNum: 0, // 组装搜索数据 - getSearchParams: (overrideArea?: [string, string]) => { + // 注意:始终使用 state.area,不接收 overrideArea 参数,确保参数一致性 + getSearchParams: () => { const state = get(); const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState; const filterOptions = currentPageState?.filterOptions || {}; @@ -193,12 +193,9 @@ export const useListStore = create()((set, get) => ({ const distanceQuickFilter = currentPageState?.distanceQuickFilter || {}; const { distanceFilter, order, district } = distanceQuickFilter || {}; - // 优先使用 overrideArea(切换城市时传入),其次使用 area 状态 - // area 会在 userLastLocationProvince 更新时自动同步,所以这里直接使用 area 即可 - const areaProvince = overrideArea?.at(1) || state.area?.at(1) || ""; - const userInfo = useUser.getState().user as any; + // 始终使用 state.area,确保所有接口使用一致的城市参数 + const areaProvince = state.area?.at(1) || ""; const last_location_province = areaProvince; - const last_location_city = userInfo?.last_location_city || ""; // city 参数逻辑: // 1. 如果选择了行政区(district 有值),使用行政区的名称(label) @@ -216,6 +213,8 @@ export const useListStore = create()((set, get) => ({ // 使用 filterOptions 中的 dateRange const dateRange: [string, string] = filterOptions?.dateRange || defaultDateRange; + // 构建 searchOption + // 注意:province 必须从 state.area 获取,不能依赖 filterOptions 中可能存在的旧值 const searchOption: any = { ...filterOptions, title: state.searchValue, @@ -223,7 +222,8 @@ export const useListStore = create()((set, get) => ({ ntrpMax: filterOptions?.ntrp?.[1], dateRange: dateRange, // 确保始终是两个值的数组 distanceFilter: distanceFilter, - province: last_location_province, // 使用 province 替代 last_location_province + // 显式设置 province,确保始终使用 state.area 中的最新值 + province: last_location_province, // 始终使用 state.area 中的 province,确保城市参数一致 }; // 只在有值时添加 city 参数 @@ -336,13 +336,14 @@ export const useListStore = create()((set, get) => ({ }, // 同时更新两个列表接口(常规列表和智能排序列表) - refreshBothLists: async (overrideArea?: [string, string]) => { + // 注意:不再接收 overrideArea 参数,始终使用 state.area + refreshBothLists: async () => { const state = get(); const { getSearchParams, setListData } = state; const { getGamesList, getGamesIntegrateList } = await import("../services/listApi"); try { - const searchParams = getSearchParams(overrideArea) || {}; + const searchParams = getSearchParams() || {}; // 调用常规列表接口 const listParams = { @@ -407,11 +408,24 @@ export const useListStore = create()((set, get) => ({ }, // 获取球局数量 - fetchGetGamesCount: async (overrideArea?: [string, string]) => { + // 注意:必须和 games/integrate_list 使用相同的参数构建逻辑,确保数据一致性 + // 不再接收 overrideArea 参数,始终使用 state.area + fetchGetGamesCount: async () => { const state = get(); const { getSearchParams } = state; - const params = getSearchParams(overrideArea) || {}; - console.log("fetchGetGamesCount 参数:", { overrideArea, params: JSON.stringify(params) }); + const searchParams = getSearchParams() || {}; + + // 使用和 games/integrate_list 相同的参数构建逻辑 + const params = { + ...searchParams, + order: "", // 和 integrate_list 保持一致 + seachOption: { + ...searchParams.seachOption, + isRefresh: true, // 和 integrate_list 保持一致 + }, + }; + + console.log("fetchGetGamesCount 参数:", { area: state.area, params: JSON.stringify(params) }); const resData = (await getGamesCount(params)) || {}; const gamesNum = resData?.data?.count || 0; console.log("fetchGetGamesCount 结果:", gamesNum); @@ -527,10 +541,13 @@ export const useListStore = create()((set, get) => ({ }); // 使用 Promise.resolve 确保状态更新后再调用接口 - Promise.resolve().then(() => { + // 先调用列表接口,然后在列表接口完成后调用数量接口 + Promise.resolve().then(async () => { const freshState = get(); // 重新获取最新状态 - // 传入当前的 area,确保接口请求的地址与界面显示一致 - freshState.fetchGetGamesCount(freshState.area); + // 先调用列表接口 + await freshState.getMatchesData(); + // 列表接口完成后,再调用数量接口 + await freshState.fetchGetGamesCount(); }); }, @@ -548,16 +565,18 @@ export const useListStore = create()((set, get) => ({ }); // 使用 Promise.resolve 确保状态更新后再调用接口 - Promise.resolve().then(() => { + // 先调用列表接口,然后在列表接口完成后调用数量接口 + Promise.resolve().then(async () => { const freshState = get(); // 重新获取最新状态 - freshState.getMatchesData(); - // 传入当前的 area,确保接口请求的地址与界面显示一致 - freshState.fetchGetGamesCount(freshState.area); + // 先调用列表接口 + await freshState.getMatchesData(); + // 列表接口完成后,再调用数量接口 + await freshState.fetchGetGamesCount(); }); }, // 清空综合筛选选项 - clearFilterOptions: () => { + clearFilterOptions: async () => { const state = get(); const { getMatchesData, fetchGetGamesCount } = state; @@ -566,8 +585,10 @@ export const useListStore = create()((set, get) => ({ filterCount: 0, pageOption: defaultPageOption, }); - getMatchesData(); - fetchGetGamesCount(); + // 先调用列表接口 + await getMatchesData(); + // 列表接口完成后,再调用数量接口 + await fetchGetGamesCount(); }, // 加载更多数据 @@ -616,8 +637,10 @@ export const useListStore = create()((set, get) => ({ if (!isSearchData) { return; } - await fetchGetGamesCount(); + // 先调用列表接口 await getMatchesData(); + // 列表接口完成后,再调用数量接口 + await fetchGetGamesCount(); }, // 更新store数据 diff --git a/types/list/types.ts b/types/list/types.ts index 69c19de..bc1b8b1 100644 --- a/types/list/types.ts +++ b/types/list/types.ts @@ -111,11 +111,11 @@ export interface ListActions { getSearchHistory: () => Promise; clearHistory: () => void; searchSuggestion: (val: string) => Promise; - getSearchParams: (overrideArea?: [string, string]) => Record; + getSearchParams: () => Record; loadMoreMatches: () => Promise; initialFilterSearch: (isSearchData?: boolean) => void; setListData: (payload: IPayload) => void; - fetchGetGamesCount: (overrideArea?: [string, string]) => Promise; + fetchGetGamesCount: () => Promise; getCurrentPageState: () => { currentPageState: any; currentPageKey: string }; updateCurrentPageState: (payload: Record) => void; updateDistanceQuickFilter: (payload: Record) => void; @@ -123,7 +123,7 @@ export interface ListActions { getCityQrCode: () => Promise; getDistricts: () => Promise; // 新增:获取行政区 updateArea: (payload: [string, string]) => void; - refreshBothLists: (overrideArea?: [string, string]) => Promise; + refreshBothLists: () => Promise; } export interface IPayload { From 0bbb9f5ce0a464c59d88686e2ac1a143af7406a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Mon, 8 Dec 2025 16:01:30 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8F=82=E4=B8=8E?= =?UTF-8?q?=E8=80=85=E5=88=97=E8=A1=A8=E5=AE=BD=E5=BA=A6=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Participants/index.module.scss | 6 +++- .../detail/components/Participants/index.tsx | 32 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/game_pages/detail/components/Participants/index.module.scss b/src/game_pages/detail/components/Participants/index.module.scss index 4c4a2d1..ee74e03 100644 --- a/src/game_pages/detail/components/Participants/index.module.scss +++ b/src/game_pages/detail/components/Participants/index.module.scss @@ -59,9 +59,13 @@ &-scroll { flex: 0 0 auto; - width: calc(100% - 116px); + width: 100%; height: 100%; + &.withApplication { + width: calc(100% - 116px); + } + &-content { display: flex; flex-direction: row; diff --git a/src/game_pages/detail/components/Participants/index.tsx b/src/game_pages/detail/components/Participants/index.tsx index c0d5161..64761bb 100644 --- a/src/game_pages/detail/components/Participants/index.tsx +++ b/src/game_pages/detail/components/Participants/index.tsx @@ -2,10 +2,11 @@ import React, { useRef } from "react"; import Taro from "@tarojs/taro"; import { View, Text, Image, ScrollView } from "@tarojs/components"; import dayjs from "dayjs"; +import classnames from "classnames"; import img from "@/config/images"; import { useUserInfo } from "@/store/userStore"; import { formatNtrpDisplay, toast, navto } from "@/utils/helper"; -import RMB_ICON from "@/static/detail/rmb.svg"; +// import RMB_ICON from "@/static/detail/rmb.svg"; import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService"; import OrderService from "@/services/orderService"; import styles from "./index.module.scss"; @@ -183,18 +184,18 @@ export default function Participants(props) { if (!user_action_status) { return; } - const priceStrArr = price?.toString().split(".") ?? []; - const displayPrice = is_organizer ? ( - <> - 0 - {/* .00 */} - - ) : ( - <> - {priceStrArr[0]} - .{priceStrArr[1]} - - ); + // const priceStrArr = price?.toString().split(".") ?? []; + // const displayPrice = is_organizer ? ( + // <> + // 0 + // {/* .00 */} + // + // ) : ( + // <> + // {priceStrArr[0]} + // .{priceStrArr[1]} + // + // ); // user_action_status.can_assess = true; // user_action_status.can_join = false; // console.log(user_action_status, "user_action"); @@ -323,7 +324,10 @@ export default function Participants(props) { {/* participants list */} Date: Mon, 8 Dec 2025 17:00:28 +0800 Subject: [PATCH 7/7] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E8=89=B2=E6=98=BE=E7=A4=BA=E4=B8=8D=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Comments/index.module.scss | 2 +- .../components/GameInfo/index.module.scss | 20 +++++++------------ .../OrganizerInfo/index.module.scss | 18 +++++++++++++++-- .../components/Participants/index.module.scss | 18 +++++++++++++++-- .../components/StickyBottom/index.module.scss | 11 +++++----- .../detail/components/StickyBottom/index.tsx | 2 +- 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/components/Comments/index.module.scss b/src/components/Comments/index.module.scss index 04d7671..d99641d 100644 --- a/src/components/Comments/index.module.scss +++ b/src/components/Comments/index.module.scss @@ -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; diff --git a/src/game_pages/detail/components/GameInfo/index.module.scss b/src/game_pages/detail/components/GameInfo/index.module.scss index d6f833b..f93772b 100644 --- a/src/game_pages/detail/components/GameInfo/index.module.scss +++ b/src/game_pages/detail/components/GameInfo/index.module.scss @@ -23,15 +23,13 @@ flex-direction: column; align-items: center; gap: 4px; - border-radius: 12px; + overflow: scroll; border: 0.5px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.25); - overflow: hidden; color: #fff; - // background: #536272; + border-radius: 12px; flex-shrink: 0; font-family: "Quicksand"; - // opacity: 0; animation: intro 0.3s ease-in forwards; @keyframes intro { @@ -52,25 +50,22 @@ box-sizing: border-box; justify-content: center; align-items: center; - // border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: #7b828b; - font-weight: 600; + border-top-left-radius: 12px; + border-top-right-radius: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.25); + overflow: scroll; + font-weight: 600; } .day { display: flex; width: 48px; height: 30px; - // padding-bottom: 6px; box-sizing: border-box; flex-direction: column; align-items: center; font-weight: 700; - // border: 0.5px solid rgba(255, 255, 255, 0.08); - // background: rgba(255, 255, 255, 0.25); - // background-color: #536272; } } @@ -157,13 +152,12 @@ border-radius: 12px; padding: 14px; box-sizing: border-box; - // background: #4d5865; display: flex; justify-content: center; align-items: center; - overflow: hidden; border: 0.5px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.25); + overflow: scroll; animation: intro 0.3s ease-in forwards; @keyframes intro { diff --git a/src/game_pages/detail/components/OrganizerInfo/index.module.scss b/src/game_pages/detail/components/OrganizerInfo/index.module.scss index 5843350..6539c5c 100644 --- a/src/game_pages/detail/components/OrganizerInfo/index.module.scss +++ b/src/game_pages/detail/components/OrganizerInfo/index.module.scss @@ -156,10 +156,24 @@ gap: 6px; flex: 0 0 auto; border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.16); + // border: 1px solid rgba(255, 255, 255, 0.1); + // background: rgba(255, 255, 255, 0.16); + overflow: scroll; padding: 12px 0 12px 15px; box-sizing: border-box; + position: relative; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0.5px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.16); + z-index: -1; + } &-title { display: flex; diff --git a/src/game_pages/detail/components/Participants/index.module.scss b/src/game_pages/detail/components/Participants/index.module.scss index ee74e03..65a59d6 100644 --- a/src/game_pages/detail/components/Participants/index.module.scss +++ b/src/game_pages/detail/components/Participants/index.module.scss @@ -84,9 +84,23 @@ align-items: center; gap: 4px; border-radius: 20px; - border: 0.5px solid rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.16); + // border: 0.5px solid rgba(255, 255, 255, 0.2); + // background: rgba(255, 255, 255, 0.16); flex: 0 0 auto; + overflow: scroll; + position: relative; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + z-index: -1; + } .participants-list-item-avatar { width: 60px; diff --git a/src/game_pages/detail/components/StickyBottom/index.module.scss b/src/game_pages/detail/components/StickyBottom/index.module.scss index 7797701..64ecb7e 100644 --- a/src/game_pages/detail/components/StickyBottom/index.module.scss +++ b/src/game_pages/detail/components/StickyBottom/index.module.scss @@ -81,7 +81,7 @@ // gap: 12px; flex: 1 0 0; border-radius: 16px; - // border: 1px solid rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.06); // background: #fff; overflow: hidden; @@ -105,7 +105,7 @@ // font-style: italic; font-size: 20px; border-radius: 16px 0 0 16px; - border: 2px solid rgba(0, 0, 0, 0.06); + // border: 2px solid rgba(0, 0, 0, 0.06); background: linear-gradient(95deg, #fff 20.85%, #eaeaea 73.29%); box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); backdrop-filter: blur(16px); @@ -169,16 +169,15 @@ color: #fff; pointer-events: all; border-radius: 0 16px 16px 0; - border-top: 2px solid rgba(255, 255, 255, 0.06); - border-right: 2px solid rgba(255, 255, 255, 0.06); - border-bottom: 2px solid rgba(255, 255, 255, 0.06); + // border-top: 2px solid rgba(255, 255, 255, 0.06); + // border-right: 2px solid rgba(255, 255, 255, 0.06); + // border-bottom: 2px solid rgba(255, 255, 255, 0.06); background: radial-gradient( 223.82% 178.84% at -64.5% 0%, #525252 16.88%, #222 54.19%, #000 100% ); - // box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); backdrop-filter: blur(16px); } } diff --git a/src/game_pages/detail/components/StickyBottom/index.tsx b/src/game_pages/detail/components/StickyBottom/index.tsx index ec5e501..ea560bd 100644 --- a/src/game_pages/detail/components/StickyBottom/index.tsx +++ b/src/game_pages/detail/components/StickyBottom/index.tsx @@ -9,7 +9,7 @@ import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService"; import { GameManagePopup, NTRPEvaluatePopup } from "@/components"; import { useUserInfo } from "@/store/userStore"; import img from "@/config/images"; -import RMB_ICON from "@/static/detail/rmb.svg"; +// import RMB_ICON from "@/static/detail/rmb.svg"; import { toast, navto } from "@/utils/helper"; import styles from "./index.module.scss";