mergeCode
This commit is contained in:
@@ -248,7 +248,7 @@
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 255, 255, 0.8) 0%,
|
rgba(255, 255, 255, 0.8) 0%,
|
||||||
#fff 100%
|
#fafafa 100%
|
||||||
);
|
);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
padding: 3px 4px 3px 4px;
|
padding: 3px 4px 3px 4px;
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
// const currentAddress = city + district;
|
// const currentAddress = city + district;
|
||||||
|
|
||||||
const handleInputClick = () => {
|
const handleInputClick = () => {
|
||||||
|
// 关闭城市选择器
|
||||||
|
if (cityPopupVisible) {
|
||||||
|
setCityPopupVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
const currentPagePath = getCurrentFullPath();
|
const currentPagePath = getCurrentFullPath();
|
||||||
if (currentPagePath === "/game_pages/searchResult/index") {
|
if (currentPagePath === "/game_pages/searchResult/index") {
|
||||||
(Taro as any).navigateBack();
|
(Taro as any).navigateBack();
|
||||||
@@ -192,6 +197,11 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
|
|
||||||
// 点击logo
|
// 点击logo
|
||||||
const handleLogoClick = () => {
|
const handleLogoClick = () => {
|
||||||
|
// 关闭城市选择器
|
||||||
|
if (cityPopupVisible) {
|
||||||
|
setCityPopupVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
// 如果当前在列表页,点击后页面回到顶部
|
// 如果当前在列表页,点击后页面回到顶部
|
||||||
if (getCurrentFullPath() === "/main_pages/index") {
|
if (getCurrentFullPath() === "/main_pages/index") {
|
||||||
// 使用父组件传递的滚动方法(适配 ScrollView)
|
// 使用父组件传递的滚动方法(适配 ScrollView)
|
||||||
@@ -212,6 +222,11 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputLeftIconClick = () => {
|
const handleInputLeftIconClick = () => {
|
||||||
|
// 关闭城市选择器
|
||||||
|
if (cityPopupVisible) {
|
||||||
|
setCityPopupVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (leftIconClick) {
|
if (leftIconClick) {
|
||||||
leftIconClick();
|
leftIconClick();
|
||||||
} else {
|
} else {
|
||||||
@@ -231,11 +246,11 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
|
|
||||||
// 处理城市切换(仅刷新数据,不保存缓存)
|
// 处理城市切换(仅刷新数据,不保存缓存)
|
||||||
const handleCityChangeWithoutCache = async () => {
|
const handleCityChangeWithoutCache = async () => {
|
||||||
// 切换城市后,同时更新两个列表接口获取数据
|
// 先调用列表接口
|
||||||
if (refreshBothLists) {
|
if (refreshBothLists) {
|
||||||
await refreshBothLists();
|
await refreshBothLists();
|
||||||
}
|
}
|
||||||
// 更新球局数量
|
// 列表接口完成后,再调用数量接口
|
||||||
if (fetchGetGamesCount) {
|
if (fetchGetGamesCount) {
|
||||||
await fetchGetGamesCount();
|
await fetchGetGamesCount();
|
||||||
}
|
}
|
||||||
@@ -246,21 +261,17 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
// 用户手动选择的城市不保存到缓存(临时切换)
|
// 用户手动选择的城市不保存到缓存(临时切换)
|
||||||
console.log("用户手动选择城市(不保存缓存):", _newArea);
|
console.log("用户手动选择城市(不保存缓存):", _newArea);
|
||||||
|
|
||||||
// 先更新 area 状态(用于界面显示)
|
// 先更新 area 状态(用于界面显示和接口参数)
|
||||||
updateArea(_newArea);
|
updateArea(_newArea);
|
||||||
|
|
||||||
// 确保状态更新完成后再调用接口
|
// 先调用列表接口(会使用更新后的 state.area)
|
||||||
// 切换城市后,同时更新两个列表接口获取数据,传入新的城市信息
|
|
||||||
const promises: Promise<any>[] = [];
|
|
||||||
if (refreshBothLists) {
|
if (refreshBothLists) {
|
||||||
promises.push(refreshBothLists(_newArea));
|
await refreshBothLists();
|
||||||
}
|
}
|
||||||
// 更新球局数量,直接传入新的城市信息,不依赖状态更新时序
|
// 列表接口完成后,再调用数量接口(会使用更新后的 state.area)
|
||||||
if (fetchGetGamesCount) {
|
if (fetchGetGamesCount) {
|
||||||
promises.push(fetchGetGamesCount(_newArea));
|
await fetchGetGamesCount();
|
||||||
}
|
}
|
||||||
// 并行执行,提高性能
|
|
||||||
await Promise.all(promises);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ const ListCard: React.FC<ListCardProps> = ({
|
|||||||
<View className="smoothWrapper">
|
<View className="smoothWrapper">
|
||||||
<Image
|
<Image
|
||||||
className="iconListPlayingGame"
|
className="iconListPlayingGame"
|
||||||
src={require("@/static/list/changdaqiuju.png")}
|
src={img.ICON_LIST_CHANGDA_QIuju}
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
{/* <Text className="smoothTitle">{game_type}</Text> */}
|
{/* <Text className="smoothTitle">{game_type}</Text> */}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import { Button, Input, View, Text, Image } from "@tarojs/components";
|
|||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import CommonPopup from "../CommonPopup";
|
import CommonPopup from "../CommonPopup";
|
||||||
import { getCurrentFullPath } from "@/utils";
|
import { useUserActions, useUserInfo } from "@/store/userStore";
|
||||||
import evaluateService from "@/services/evaluateService";
|
|
||||||
import { useUserActions } from "@/store/userStore";
|
|
||||||
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
|
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
|
||||||
import { useNtrpLevels } from "@/store/pickerOptionsStore";
|
import { useNtrpLevels } from "@/store/pickerOptionsStore";
|
||||||
import NTRPTestEntryCard from "../NTRPTestEntryCard";
|
import NTRPTestEntryCard from "../NTRPTestEntryCard";
|
||||||
@@ -67,6 +65,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
|||||||
const [ntrp, setNtrp] = useState<string>("");
|
const [ntrp, setNtrp] = useState<string>("");
|
||||||
const [guideShow, setGuideShow] = useState(() => showGuide);
|
const [guideShow, setGuideShow] = useState(() => showGuide);
|
||||||
const { updateUserInfo } = useUserActions();
|
const { updateUserInfo } = useUserActions();
|
||||||
|
const userInfo = useUserInfo();
|
||||||
const ntrpLevels = useNtrpLevels();
|
const ntrpLevels = useNtrpLevels();
|
||||||
const options = [
|
const options = [
|
||||||
ntrpLevels.map((item) => ({
|
ntrpLevels.map((item) => ({
|
||||||
@@ -97,23 +96,22 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// 当弹窗打开或用户信息变化时,从用户信息中提取并更新 ntrp 状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getNtrp();
|
if (visible) {
|
||||||
}, []);
|
if (userInfo?.ntrp_level) {
|
||||||
|
// 从 ntrp_level 中提取数字部分(如 "2.5" 或 "NTRP 2.5")
|
||||||
async function getNtrp() {
|
const match = String(userInfo.ntrp_level).match(/-?\d+(\.\d+)?/);
|
||||||
const res = await evaluateService.getLastResult();
|
if (match) {
|
||||||
if (res.code === 0 && res.data.has_ntrp_level) {
|
setNtrp(match[0]);
|
||||||
const match = res.data.user_ntrp_level.match(/-?\d+(\.\d+)?/);
|
} else {
|
||||||
if (!match) {
|
setNtrp("");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
setNtrp("");
|
setNtrp("");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setNtrp(match[0] as string);
|
|
||||||
} else {
|
|
||||||
setNtrp("");
|
|
||||||
}
|
}
|
||||||
}
|
}, [visible, userInfo?.ntrp_level]);
|
||||||
|
|
||||||
// const showEntry =
|
// const showEntry =
|
||||||
// displayCondition === "auto"
|
// displayCondition === "auto"
|
||||||
@@ -128,7 +126,9 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
|||||||
|
|
||||||
async function handleChangeNtrp() {
|
async function handleChangeNtrp() {
|
||||||
Taro.showLoading({ title: "修改中" });
|
Taro.showLoading({ title: "修改中" });
|
||||||
|
// 更新用户信息,会自动更新 store 中的 ntrp_level
|
||||||
await updateUserInfo({ ntrp_level: ntrp });
|
await updateUserInfo({ ntrp_level: ntrp });
|
||||||
|
Taro.hideLoading();
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: "NTRP水平修改成功",
|
title: "NTRP水平修改成功",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { useState, useEffect, useCallback, memo } from "react";
|
|||||||
import { View, Image, Text } from "@tarojs/components";
|
import { View, Image, Text } from "@tarojs/components";
|
||||||
import { requireLoginWithPhone } from "@/utils/helper";
|
import { requireLoginWithPhone } from "@/utils/helper";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
|
||||||
// import { getCurrentFullPath } from "@/utils";
|
// import { getCurrentFullPath } from "@/utils";
|
||||||
import evaluateService, { StageType } from "@/services/evaluateService";
|
import { StageType } from "@/services/evaluateService";
|
||||||
import { waitForAuthInit } from "@/utils/authInit";
|
import { waitForAuthInit } from "@/utils/authInit";
|
||||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||||
import ArrowRight from "@/static/ntrp/ntrp_arrow_right_color.svg";
|
import ArrowRight from "@/static/ntrp/ntrp_arrow_right_color.svg";
|
||||||
@@ -19,15 +19,16 @@ function NTRPTestEntryCard(props: {
|
|||||||
type: EvaluateScene;
|
type: EvaluateScene;
|
||||||
evaluateCallback?: EvaluateCallback;
|
evaluateCallback?: EvaluateCallback;
|
||||||
}) {
|
}) {
|
||||||
const [testFlag, setTestFlag] = useState(false);
|
|
||||||
const [hasTestInLastMonth, setHasTestInLastMonth] = useState(false);
|
|
||||||
const { type, evaluateCallback } = props;
|
const { type, evaluateCallback } = props;
|
||||||
const userInfo = useUserInfo();
|
const userInfo = useUserInfo();
|
||||||
const { setCallback } = useEvaluate();
|
const { setCallback } = useEvaluate();
|
||||||
const { fetchUserInfo } = useUserActions();
|
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
|
||||||
|
// 使用全局状态中的测试结果,避免重复调用接口
|
||||||
|
const lastTestResult = useLastTestResult();
|
||||||
|
|
||||||
console.log(userInfo);
|
console.log(userInfo);
|
||||||
|
|
||||||
|
// 从全局状态中获取测试结果,如果不存在则调用接口(使用请求锁避免重复调用)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
// 先等待静默登录完成
|
// 先等待静默登录完成
|
||||||
@@ -36,15 +37,17 @@ function NTRPTestEntryCard(props: {
|
|||||||
if (!userInfo.id) {
|
if (!userInfo.id) {
|
||||||
await fetchUserInfo();
|
await fetchUserInfo();
|
||||||
}
|
}
|
||||||
// 获取测试结果
|
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
|
||||||
const res = await evaluateService.getLastResult();
|
if (!lastTestResult) {
|
||||||
if (res.code === 0) {
|
await fetchLastTestResult();
|
||||||
setTestFlag(res.data.has_test_record);
|
|
||||||
setHasTestInLastMonth(res.data.has_test_in_last_month);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
init();
|
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(
|
const handleTest = useCallback(
|
||||||
function () {
|
function () {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径
|
const [tempImagePath, setTempImagePath] = useState('') // 存储Canvas生成的图片路径
|
||||||
const [isDrawing, setIsDrawing] = useState(false) // 防止重复绘制
|
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
|
const designWidth = 500
|
||||||
@@ -54,87 +57,28 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const canvasWidth = designWidth * scale
|
const canvasWidth = designWidth * scale
|
||||||
const canvasHeight = designHeight * scale
|
const canvasHeight = designHeight * scale
|
||||||
|
|
||||||
// 绘制加粗文字(单行)
|
// 2D Canvas 字体设置
|
||||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily?: string) => {
|
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
|
||||||
// 设置字体样式
|
const fam = family || 'Noto Sans SC'
|
||||||
if (fontFamily) {
|
const wt = weight || '500'
|
||||||
try {
|
ctx.font = `${wt} ${fontSize}px "${fam}"`
|
||||||
// 尝试使用setFont方法(如果支持)
|
}
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
// 绘制加粗文字(单行)
|
||||||
ctx.setTextAlign('left')
|
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily: string = 'Noto Sans SC', fontWeight: string = '500') => {
|
||||||
ctx.setTextBaseline('top')
|
// 设置字体样式
|
||||||
|
setFont2D(ctx, fontSize, fontFamily, fontWeight)
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.textBaseline = 'top'
|
||||||
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
// 绘制加粗效果:多次绘制并偏移
|
||||||
ctx.fillText(text, x, y)
|
ctx.fillText(text, x, y)
|
||||||
ctx.fillText(text, x + 1, y)
|
// if (isBold) {
|
||||||
ctx.fillText(text, x, y + 1)
|
// ctx.fillText(text, x + 1, y)
|
||||||
ctx.fillText(text, x + 1, y + 1)
|
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制圆角矩形函数
|
// 绘制圆角矩形函数
|
||||||
@@ -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') => {
|
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)
|
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||||
|
|
||||||
// 绘制边框
|
// 绘制边框
|
||||||
ctx.setStrokeStyle(borderColor)
|
ctx.strokeStyle = borderColor
|
||||||
ctx.setLineWidth(1 * dpr)
|
ctx.lineWidth = 1 * dpr
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
const textCenterX = x + width / 2
|
const textCenterX = x + width / 2
|
||||||
const textCenterY = y + height / 2
|
const textCenterY = y + height / 2
|
||||||
|
|
||||||
ctx.setFillStyle(textColor)
|
ctx.fillStyle = textColor
|
||||||
ctx.setTextAlign('center')
|
ctx.textAlign = 'center'
|
||||||
ctx.setTextBaseline('middle')
|
ctx.textBaseline = 'middle'
|
||||||
ctx.setFontSize(fontSize)
|
setFont2D(ctx, fontSize)
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(textCenterX, textCenterY)
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.getImageInfo({
|
if (!canvas || typeof canvas.createImage !== 'function') {
|
||||||
src: src,
|
reject(new Error('2D canvas is required to load images'))
|
||||||
success: (res) => resolve(res.path),
|
return
|
||||||
fail: reject
|
}
|
||||||
})
|
try {
|
||||||
|
const img = canvas.createImage()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = (e: any) => reject(e)
|
||||||
|
img.src = src
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制SVG路径到Canvas
|
// 绘制SVG路径到Canvas
|
||||||
const drawSVGPathToCanvas = (ctx: any) => {
|
const drawSVGPathToCanvas = (ctx: any) => {
|
||||||
// 设置绘制样式
|
// 设置绘制样式
|
||||||
ctx.setStrokeStyle('#48D800');
|
ctx.strokeStyle = '#00E5AD';
|
||||||
ctx.setLineWidth(scale * 3 * dpr);
|
ctx.lineWidth = scale * 3 * dpr;
|
||||||
ctx.setLineCap('round');
|
ctx.lineCap = 'round';
|
||||||
ctx.setLineJoin('round');
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// 移动到指定位置并缩放
|
// 移动到指定位置并缩放
|
||||||
ctx.translate(scale * 210 * dpr, scale * 90 * dpr);
|
ctx.translate(scale * 200 * dpr, scale * 90 * dpr);
|
||||||
const scaleValue = 0.8
|
const scaleValue = 0.8
|
||||||
ctx.scale(scaleValue, scaleValue);
|
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 playerImgX = venueImageConfig.venueImgX
|
||||||
const playerImgY = venueImageConfig.venueImgY
|
const playerImgY = venueImageConfig.venueImgY
|
||||||
@@ -237,7 +188,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const venueImage = venueImageConfig.venueImage
|
const venueImage = venueImageConfig.venueImage
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const playerImgPath = await loadImage(venueImage)
|
const playerImgPath = await loadImage(venueImage, canvas)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
// 移动到旋转中心点
|
// 移动到旋转中心点
|
||||||
@@ -249,7 +200,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 1. 先绘制白色圆角矩形背景
|
// 1. 先绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
// 使用更精确的圆角矩形绘制
|
// 使用更精确的圆角矩形绘制
|
||||||
@@ -338,7 +289,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 绘制白色圆角矩形背景
|
// 绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
const rectX = -playerImgSize / 2
|
const rectX = -playerImgSize / 2
|
||||||
@@ -380,7 +331,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const imgY = -playerImgSize / 2 + padding
|
const imgY = -playerImgSize / 2 + padding
|
||||||
const imgSize = playerImgSize - padding * 2
|
const imgSize = playerImgSize - padding * 2
|
||||||
|
|
||||||
ctx.setFillStyle('#E0E0E0')
|
ctx.fillStyle = '#E0E0E0'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
const imgRadius = borderRadius - padding
|
const imgRadius = borderRadius - padding
|
||||||
|
|
||||||
@@ -448,15 +399,15 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
|
|
||||||
// 绘制背景 - 渐变色 已完成
|
// 绘制背景 - 渐变色 已完成
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
||||||
gradient.addColorStop(0, '#D8FFE5')
|
gradient.addColorStop(0, '#BFFFEF')
|
||||||
gradient.addColorStop(1, '#F9FFFB')
|
gradient.addColorStop(1, '#F2FFFC')
|
||||||
ctx.setFillStyle(gradient)
|
ctx.fillStyle = gradient
|
||||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||||
console.log('背景绘制完成')
|
console.log('背景绘制完成')
|
||||||
|
|
||||||
// 绘制背景条纹 已完成
|
// 绘制背景条纹 已完成
|
||||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
|
||||||
ctx.setLineWidth(2)
|
ctx.lineWidth = 2
|
||||||
for (let i = 0; i < canvasWidthPx; i += 4) {
|
for (let i = 0; i < canvasWidthPx; i += 4) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(i, 0)
|
ctx.moveTo(i, 0)
|
||||||
@@ -470,7 +421,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const avatarY = scale * 35 * dpr // 距离顶部35px
|
const avatarY = scale * 35 * dpr // 距离顶部35px
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const avatarPath = await loadImage(data.userAvatar)
|
const avatarPath = await loadImage(data.userAvatar, canvasNode)
|
||||||
// 微信小程序中绘制圆形头像需要特殊处理
|
// 微信小程序中绘制圆形头像需要特殊处理
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -480,7 +431,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果头像加载失败,绘制默认头像
|
// 如果头像加载失败,绘制默认头像
|
||||||
ctx.setFillStyle('#CCCCCC')
|
ctx.fillStyle = '#CCCCCC'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
|
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
@@ -490,7 +441,8 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
const nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
||||||
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
||||||
const nicknameFontSize = scale * 18 * dpr
|
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
|
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
||||||
@@ -498,12 +450,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const inviteFontSize = scale * 44 * dpr
|
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
|
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)
|
drawSVGPathToCanvas(ctx)
|
||||||
@@ -528,16 +480,16 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
venueImgY: scale * 35 * dpr,
|
venueImgY: scale * 35 * dpr,
|
||||||
rotation: scale * -10, // 旋转-10度
|
rotation: scale * -10, // 旋转-10度
|
||||||
}
|
}
|
||||||
await drawVenueImages(ctx, venueBackConfig)
|
await drawVenueImages(ctx, venueBackConfig, canvasNode)
|
||||||
// 前面的图
|
// 前面的图
|
||||||
const venueFrontConfig = {
|
const venueFrontConfig = {
|
||||||
...venueBaseConfig,
|
...venueBaseConfig,
|
||||||
venueImage: data.venueImages?.[0],
|
venueImage: data.venueImages?.[0],
|
||||||
rotation: scale * 8, // 旋转-8度
|
rotation: scale * 8, // 旋转-8度
|
||||||
}
|
}
|
||||||
await drawVenueImages(ctx, venueFrontConfig)
|
await drawVenueImages(ctx, venueFrontConfig, canvasNode)
|
||||||
} else {
|
} 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 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)
|
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
||||||
|
|
||||||
// 绘制"单打"标签
|
// 绘制"单打"标签
|
||||||
const danDaX = scale * 100
|
const danDaX = scale * 100
|
||||||
const danDaY = scale * 196
|
const danDaY = scale * 196
|
||||||
const danDaWidth = scale * 76 * dpr
|
|
||||||
const danDaHeight = scale * 40 * dpr
|
const danDaHeight = scale * 40 * dpr
|
||||||
const danDaRadius = scale * 20 * dpr
|
const danDaRadius = scale * 20 * dpr
|
||||||
const danDaFontSize = scale * 22 * 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)
|
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 skillY = scale * 196
|
||||||
const skillWidth = scale * 180 * dpr
|
|
||||||
const skillHeight = scale * 40 * dpr
|
const skillHeight = scale * 40 * dpr
|
||||||
const skillRadius = scale * 20 * dpr
|
const skillRadius = scale * 20 * dpr
|
||||||
const skillFontSize = scale * 22 * 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)
|
drawLabel(ctx, skillX, skillY, skillWidth, skillHeight, skillRadius, data.skillLevel, skillFontSize)
|
||||||
|
|
||||||
@@ -581,48 +542,56 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
const dateX = danDaX
|
const dateX = danDaX
|
||||||
const timeInfoY = infoStartY + infoSpacing
|
const timeInfoY = infoStartY + infoSpacing
|
||||||
const timeInfoFontSize = scale * 24 * dpr
|
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)
|
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
|
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 locationInfoY = infoStartY + infoSpacing * 2
|
||||||
const locationFontSize = scale * 22 * dpr
|
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)
|
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方法
|
// 绘制完成,调用draw方法
|
||||||
console.log('开始调用ctx.draw()')
|
console.log('开始调用ctx.draw()')
|
||||||
ctx.draw(false, () => {
|
const doExport = () => {
|
||||||
console.log('Canvas绘制完成,开始生成图片...')
|
console.log('Canvas绘制完成,开始生成图片...')
|
||||||
// 延迟一下再生成图片,确保绘制完成
|
const opts: any = {
|
||||||
setTimeout(() => {
|
fileType: 'png',
|
||||||
Taro.canvasToTempFilePath({
|
quality: 1,
|
||||||
canvasId: 'shareCardCanvas',
|
success: (res: any) => {
|
||||||
fileType: 'png',
|
console.log('图片生成成功:', res.tempFilePath)
|
||||||
quality: 1,
|
setIsDrawing(false)
|
||||||
success: (res) => {
|
resolve(res.tempFilePath)
|
||||||
console.log('图片生成成功:', res.tempFilePath)
|
onGenerated?.(res.tempFilePath)
|
||||||
setIsDrawing(false) // 绘制完成,重置状态
|
setTempImagePath(res.tempFilePath)
|
||||||
resolve(res.tempFilePath)
|
},
|
||||||
onGenerated?.(res.tempFilePath)
|
fail: (error: any) => {
|
||||||
setTempImagePath(res.tempFilePath)
|
console.error('图片生成失败:', error)
|
||||||
},
|
setIsDrawing(false)
|
||||||
fail: (error) => {
|
reject(error)
|
||||||
console.error('图片生成失败:', error)
|
}
|
||||||
setIsDrawing(false) // 绘制失败,重置状态
|
}
|
||||||
reject(error)
|
if (canvasNode) {
|
||||||
}
|
opts.canvas = canvasNode
|
||||||
})
|
} else {
|
||||||
}, 500) // 延迟500ms确保Canvas完全渲染
|
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绘制命令已发送')
|
console.log('Canvas绘制命令已发送')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -639,20 +608,79 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
|
|
||||||
// 手动分享方法(已移除,由父组件处理分享)
|
// 手动分享方法(已移除,由父组件处理分享)
|
||||||
|
|
||||||
|
// 使用 HTTPS 远程字体(woff2)加载到小程序渲染层;不改字号
|
||||||
// 组件挂载后绘制
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && !isDrawing && !tempImagePath) {
|
try {
|
||||||
console.log('组件挂载,开始绘制分享卡片')
|
(Taro as any).loadFontFace({
|
||||||
// 延迟一下确保Canvas已经渲染
|
global: true,
|
||||||
setTimeout(() => {
|
family: 'Noto Sans SC',
|
||||||
// 在微信小程序中,需要使用Taro.createCanvasContext
|
source: 'url("https://fonts.gstatic.com/s/notosanssc/v39/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaGzjCnYlNbPzT7HEL7j12XCOHJKg4RgZw3nFTvwZ8atTsBvwlvRUk7mYP2g.24.woff2")',
|
||||||
const ctx = Taro.createCanvasContext('shareCardCanvas')
|
desc: { style: 'normal', weight: '700' },
|
||||||
console.log('Canvas上下文创建成功:', ctx)
|
success: () => {
|
||||||
drawShareCard(ctx)
|
console.log('===Noto Sans SC 远程字体加载成功')
|
||||||
}, 500)
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -666,10 +694,12 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
|||||||
<View className={`share-card-canvas ${className}`}>
|
<View className={`share-card-canvas ${className}`}>
|
||||||
<Canvas
|
<Canvas
|
||||||
canvasId="shareCardCanvas"
|
canvasId="shareCardCanvas"
|
||||||
|
id="shareCardCanvas"
|
||||||
|
type="2d"
|
||||||
style={{
|
style={{
|
||||||
width: `${canvasWidth}px`,
|
width: `${canvasWidth}px`,
|
||||||
height: `${canvasHeight}px`,
|
height: `${canvasHeight}px`,
|
||||||
position: 'absolute', // 绝对定位避免影响布局
|
// position: 'absolute', // 绝对定位避免影响布局
|
||||||
// top: '-9999px', // 移出可视区域
|
// top: '-9999px', // 移出可视区域
|
||||||
// left: '-9999px'
|
// left: '-9999px'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "./index.scss";
|
|||||||
import { EditModal } from "@/components";
|
import { EditModal } from "@/components";
|
||||||
import { UserService, PickerOption } from "@/services/userService";
|
import { UserService, PickerOption } from "@/services/userService";
|
||||||
import { PopupPicker } from "@/components/Picker/index";
|
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 { UserInfoType } from "@/services/userService";
|
||||||
import {
|
import {
|
||||||
useCities,
|
useCities,
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "@/store/pickerOptionsStore";
|
} from "@/store/pickerOptionsStore";
|
||||||
import { formatNtrpDisplay } from "@/utils/helper";
|
import { formatNtrpDisplay } from "@/utils/helper";
|
||||||
import { useGlobalState } from "@/store/global";
|
import { useGlobalState } from "@/store/global";
|
||||||
import evaluateService from "@/services/evaluateService";
|
|
||||||
|
|
||||||
// 用户信息接口
|
// 用户信息接口
|
||||||
// export interface UserInfo {
|
// export interface UserInfo {
|
||||||
@@ -83,9 +82,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const nickname_change_status = useNicknameChangeStatus();
|
const nickname_change_status = useNicknameChangeStatus();
|
||||||
const { setShowGuideBar } = useGlobalState();
|
const { setShowGuideBar } = useGlobalState();
|
||||||
const { updateUserInfo, updateNickname } = useUserActions();
|
const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
|
||||||
const ntrpLevels = useNtrpLevels();
|
const ntrpLevels = useNtrpLevels();
|
||||||
const [ntrpTested, setNtrpTested] = useState(false);
|
// 使用全局状态中的测试结果,避免重复调用接口
|
||||||
|
const lastTestResult = useLastTestResult();
|
||||||
|
|
||||||
// 使用 useRef 记录上一次的 user_info,只在真正变化时打印
|
// 使用 useRef 记录上一次的 user_info,只在真正变化时打印
|
||||||
const prevUserInfoRef = useRef<Partial<UserInfoType>>();
|
const prevUserInfoRef = useRef<Partial<UserInfoType>>();
|
||||||
@@ -98,15 +98,14 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
|||||||
console.log("UserInfoCard 用户信息变化:", user_info);
|
console.log("UserInfoCard 用户信息变化:", user_info);
|
||||||
prevUserInfoRef.current = user_info;
|
prevUserInfoRef.current = user_info;
|
||||||
}
|
}
|
||||||
const getLastResult = async () => {
|
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
|
||||||
// 获取测试结果
|
if (!lastTestResult && user_info?.id) {
|
||||||
const res = await evaluateService.getLastResult();
|
fetchLastTestResult();
|
||||||
if (res.code === 0) {
|
}
|
||||||
setNtrpTested(res.data.has_test_in_last_month);
|
}, [user_info?.id, lastTestResult, fetchLastTestResult]);
|
||||||
}
|
|
||||||
};
|
// 从全局状态中获取测试状态
|
||||||
getLastResult();
|
const ntrpTested = lastTestResult?.has_test_in_last_month || false;
|
||||||
}, [user_info]);
|
|
||||||
|
|
||||||
// 编辑个人简介弹窗状态
|
// 编辑个人简介弹窗状态
|
||||||
const [edit_modal_visible, setEditModalVisible] = useState(false);
|
const [edit_modal_visible, setEditModalVisible] = useState(false);
|
||||||
|
|||||||
@@ -69,4 +69,5 @@ export default {
|
|||||||
ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
|
ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
|
||||||
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
|
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
|
||||||
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
|
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
|
||||||
|
ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import ListCard from "@/components/ListCard";
|
|||||||
import ListLoadError from "@/components/ListLoadError";
|
import ListLoadError from "@/components/ListLoadError";
|
||||||
import ListCardSkeleton from "@/components/ListCardSkeleton";
|
import ListCardSkeleton from "@/components/ListCardSkeleton";
|
||||||
import { useReachBottom } from "@tarojs/taro";
|
import { useReachBottom } from "@tarojs/taro";
|
||||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
|
||||||
import { setStorage, getStorage } from "@/store/storage";
|
|
||||||
import { NTRPTestEntryCard } from "@/components";
|
import { NTRPTestEntryCard } from "@/components";
|
||||||
import { EvaluateScene } from "@/store/evaluateStore";
|
import { EvaluateScene } from "@/store/evaluateStore";
|
||||||
import evaluateService from "@/services/evaluateService";
|
|
||||||
import { waitForAuthInit } from "@/utils/authInit";
|
import { waitForAuthInit } from "@/utils/authInit";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { useRef, useEffect, useState, useMemo } from "react";
|
import { useRef, useEffect, useState, useMemo } from "react";
|
||||||
@@ -40,10 +38,11 @@ const ListContainer = (props) => {
|
|||||||
|
|
||||||
const [showNumber, setShowNumber] = useState(0);
|
const [showNumber, setShowNumber] = useState(0);
|
||||||
const [showSkeleton, setShowSkeleton] = useState(false);
|
const [showSkeleton, setShowSkeleton] = useState(false);
|
||||||
const [hasTestInLastMonth, setHasTestInLastMonth] = useState(false);
|
|
||||||
|
|
||||||
const userInfo = useUserInfo();
|
const userInfo = useUserInfo();
|
||||||
const { fetchUserInfo } = useUserActions();
|
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
|
||||||
|
// 使用全局状态中的测试结果,避免重复调用接口
|
||||||
|
const lastTestResult = useLastTestResult();
|
||||||
|
|
||||||
useReachBottom(() => {
|
useReachBottom(() => {
|
||||||
// 加载更多方法
|
// 加载更多方法
|
||||||
@@ -100,17 +99,21 @@ const ListContainer = (props) => {
|
|||||||
// 先等待静默登录完成
|
// 先等待静默登录完成
|
||||||
await waitForAuthInit();
|
await waitForAuthInit();
|
||||||
// 然后再获取用户信息
|
// 然后再获取用户信息
|
||||||
if (!userInfo.id) {
|
const userInfoId = userInfo && 'id' in userInfo ? userInfo.id : null;
|
||||||
|
if (!userInfoId) {
|
||||||
await fetchUserInfo();
|
await fetchUserInfo();
|
||||||
|
return; // 等待下一次 useEffect 触发(此时 userInfo.id 已有值)
|
||||||
}
|
}
|
||||||
// 获取测试结果
|
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
|
||||||
const res = await evaluateService.getLastResult();
|
if (!lastTestResult) {
|
||||||
if (res.code === 0) {
|
await fetchLastTestResult();
|
||||||
setHasTestInLastMonth(res.data.has_test_in_last_month);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
init();
|
init();
|
||||||
}, [evaluateFlag, userInfo.id]);
|
}, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]);
|
||||||
|
|
||||||
|
// 从全局状态中获取测试状态
|
||||||
|
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ListLoadError reload={reload} />;
|
return <ListLoadError reload={reload} />;
|
||||||
|
|||||||
@@ -23,15 +23,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
border-radius: 12px;
|
overflow: scroll;
|
||||||
border: 0.5px solid rgba(255, 255, 255, 0.08);
|
border: 0.5px solid rgba(255, 255, 255, 0.08);
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
overflow: hidden;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
// background: #536272;
|
border-radius: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-family: "Quicksand";
|
font-family: "Quicksand";
|
||||||
// opacity: 0;
|
|
||||||
animation: intro 0.3s ease-in forwards;
|
animation: intro 0.3s ease-in forwards;
|
||||||
|
|
||||||
@keyframes intro {
|
@keyframes intro {
|
||||||
@@ -52,25 +50,22 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-top-left-radius: 12px;
|
||||||
background: #7b828b;
|
border-top-right-radius: 12px;
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
overflow: scroll;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day {
|
.day {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
// padding-bottom: 6px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 700;
|
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;
|
border-radius: 12px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
// background: #4d5865;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
|
||||||
border: 0.5px solid rgba(255, 255, 255, 0.08);
|
border: 0.5px solid rgba(255, 255, 255, 0.08);
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
overflow: scroll;
|
||||||
animation: intro 0.3s ease-in forwards;
|
animation: intro 0.3s ease-in forwards;
|
||||||
|
|
||||||
@keyframes intro {
|
@keyframes intro {
|
||||||
|
|||||||
@@ -156,10 +156,24 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
// border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(255, 255, 255, 0.16);
|
// background: rgba(255, 255, 255, 0.16);
|
||||||
|
overflow: scroll;
|
||||||
padding: 12px 0 12px 15px;
|
padding: 12px 0 12px 15px;
|
||||||
box-sizing: border-box;
|
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 {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -59,9 +59,13 @@
|
|||||||
|
|
||||||
&-scroll {
|
&-scroll {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: calc(100% - 116px);
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
&.withApplication {
|
||||||
|
width: calc(100% - 116px);
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -80,9 +84,23 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 0.5px solid rgba(255, 255, 255, 0.2);
|
// border: 0.5px solid rgba(255, 255, 255, 0.2);
|
||||||
background: rgba(255, 255, 255, 0.16);
|
// background: rgba(255, 255, 255, 0.16);
|
||||||
flex: 0 0 auto;
|
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 {
|
.participants-list-item-avatar {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React, { useRef } from "react";
|
|||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import classnames from "classnames";
|
||||||
import img from "@/config/images";
|
import img from "@/config/images";
|
||||||
import { useUserInfo } from "@/store/userStore";
|
import { useUserInfo } from "@/store/userStore";
|
||||||
import { formatNtrpDisplay, toast, navto } from "@/utils/helper";
|
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 { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService";
|
||||||
import OrderService from "@/services/orderService";
|
import OrderService from "@/services/orderService";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
@@ -183,18 +184,18 @@ export default function Participants(props) {
|
|||||||
if (!user_action_status) {
|
if (!user_action_status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const priceStrArr = price?.toString().split(".") ?? [];
|
// const priceStrArr = price?.toString().split(".") ?? [];
|
||||||
const displayPrice = is_organizer ? (
|
// const displayPrice = is_organizer ? (
|
||||||
<>
|
// <>
|
||||||
<Text className={styles.integer}>0</Text>
|
// <Text className={styles.integer}>0</Text>
|
||||||
{/* <Text className={styles.decimalPart}>.00</Text> */}
|
// {/* <Text className={styles.decimalPart}>.00</Text> */}
|
||||||
</>
|
// </>
|
||||||
) : (
|
// ) : (
|
||||||
<>
|
// <>
|
||||||
<Text className={styles.integer}>{priceStrArr[0]}</Text>
|
// <Text className={styles.integer}>{priceStrArr[0]}</Text>
|
||||||
<Text className={styles.decimalPart}>.{priceStrArr[1]}</Text>
|
// <Text className={styles.decimalPart}>.{priceStrArr[1]}</Text>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
// user_action_status.can_assess = true;
|
// user_action_status.can_assess = true;
|
||||||
// user_action_status.can_join = false;
|
// user_action_status.can_join = false;
|
||||||
// console.log(user_action_status, "user_action");
|
// console.log(user_action_status, "user_action");
|
||||||
@@ -323,7 +324,10 @@ export default function Participants(props) {
|
|||||||
{/* participants list */}
|
{/* participants list */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refresherBackground="#FAFAFA"
|
refresherBackground="#FAFAFA"
|
||||||
className={styles["participants-list-scroll"]}
|
className={classnames(
|
||||||
|
styles["participants-list-scroll"],
|
||||||
|
showApplicationEntry ? styles.withApplication : ""
|
||||||
|
)}
|
||||||
scrollX
|
scrollX
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
// gap: 12px;
|
// gap: 12px;
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
// border: 1px solid rgba(0, 0, 0, 0.06);
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
// background: #fff;
|
// background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
// font-style: italic;
|
// font-style: italic;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-radius: 16px 0 0 16px;
|
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%);
|
background: linear-gradient(95deg, #fff 20.85%, #eaeaea 73.29%);
|
||||||
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
@@ -169,16 +169,15 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
border-radius: 0 16px 16px 0;
|
border-radius: 0 16px 16px 0;
|
||||||
border-top: 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-right: 2px solid rgba(255, 255, 255, 0.06);
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.06);
|
// border-bottom: 2px solid rgba(255, 255, 255, 0.06);
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
223.82% 178.84% at -64.5% 0%,
|
223.82% 178.84% at -64.5% 0%,
|
||||||
#525252 16.88%,
|
#525252 16.88%,
|
||||||
#222 54.19%,
|
#222 54.19%,
|
||||||
#000 100%
|
#000 100%
|
||||||
);
|
);
|
||||||
// box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
|
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService";
|
|||||||
import { GameManagePopup, NTRPEvaluatePopup } from "@/components";
|
import { GameManagePopup, NTRPEvaluatePopup } from "@/components";
|
||||||
import { useUserInfo } from "@/store/userStore";
|
import { useUserInfo } from "@/store/userStore";
|
||||||
import img from "@/config/images";
|
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 { toast, navto } from "@/utils/helper";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useDictionaryStore } from "@/store/dictionaryStore";
|
|||||||
import { saveImage, navigateTo } from "@/utils";
|
import { saveImage, navigateTo } from "@/utils";
|
||||||
|
|
||||||
export interface ListPageContentProps {
|
export interface ListPageContentProps {
|
||||||
|
isActive?: boolean; // 是否处于激活状态(当前显示的页面)
|
||||||
onNavStateChange?: (state: {
|
onNavStateChange?: (state: {
|
||||||
isShowInputCustomerNavBar?: boolean;
|
isShowInputCustomerNavBar?: boolean;
|
||||||
isDistanceFilterVisible?: boolean;
|
isDistanceFilterVisible?: boolean;
|
||||||
@@ -26,6 +27,7 @@ export interface ListPageContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListPageContent: React.FC<ListPageContentProps> = ({
|
const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||||
|
isActive = true,
|
||||||
onNavStateChange,
|
onNavStateChange,
|
||||||
onScrollToTop: _onScrollToTop,
|
onScrollToTop: _onScrollToTop,
|
||||||
scrollToTopTrigger,
|
scrollToTopTrigger,
|
||||||
@@ -53,6 +55,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
initialFilterSearch,
|
initialFilterSearch,
|
||||||
loadMoreMatches,
|
loadMoreMatches,
|
||||||
fetchGetGamesCount,
|
fetchGetGamesCount,
|
||||||
|
refreshBothLists,
|
||||||
updateDistanceQuickFilter,
|
updateDistanceQuickFilter,
|
||||||
getCities,
|
getCities,
|
||||||
getCityQrCode,
|
getCityQrCode,
|
||||||
@@ -86,6 +89,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
const [showSearchBar, setShowSearchBar] = useState(true);
|
const [showSearchBar, setShowSearchBar] = useState(true);
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
// 记录上一次加载数据时的城市,用于检测城市变化
|
||||||
|
const lastLoadedAreaRef = useRef<[string, string] | null>(null);
|
||||||
|
const prevIsActiveRef = useRef(isActive);
|
||||||
|
|
||||||
// 处理距离筛选显示/隐藏
|
// 处理距离筛选显示/隐藏
|
||||||
const handleDistanceFilterVisibleChange = useCallback(
|
const handleDistanceFilterVisibleChange = useCallback(
|
||||||
@@ -200,11 +206,83 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
getCityQrCode();
|
getCityQrCode();
|
||||||
getDistricts(); // 新增:获取行政区列表
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 地址发生变化或不一致,重新加载数据和球局数量
|
||||||
|
// 先调用列表接口,然后在列表接口完成后调用数量接口
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (refreshBothLists) {
|
||||||
|
await refreshBothLists();
|
||||||
|
}
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
if (fetchGetGamesCount) {
|
||||||
|
await fetchGetGamesCount();
|
||||||
|
}
|
||||||
|
// 数据加载完成后,更新记录的城市(记录为上一次在列表页加载数据时的城市)
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (pageOption?.page === 1 && matches?.length > 0) {
|
if (pageOption?.page === 1 && matches?.length > 0) {
|
||||||
@@ -237,8 +315,14 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
console.error("更新用户位置失败:", error);
|
console.error("更新用户位置失败:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchGetGamesCount();
|
// 先调用列表接口
|
||||||
getMatchesData();
|
await getMatchesData();
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
await fetchGetGamesCount();
|
||||||
|
// 初始数据加载完成后,记录当前城市
|
||||||
|
if (area && isActive) {
|
||||||
|
lastLoadedAreaRef.current = [...area] as [string, string];
|
||||||
|
}
|
||||||
return location;
|
return location;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import React from "react";
|
||||||
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
||||||
import { EmptyState } from "@/components";
|
import { EmptyState } from "@/components";
|
||||||
import SubscribeNotificationTip from "@/components/SubscribeNotificationTip";
|
import SubscribeNotificationTip from "@/components/SubscribeNotificationTip";
|
||||||
@@ -25,7 +26,11 @@ interface MessageItem {
|
|||||||
|
|
||||||
type MessageCategory = "comment" | "follow";
|
type MessageCategory = "comment" | "follow";
|
||||||
|
|
||||||
const MessagePageContent = () => {
|
interface MessagePageContentProps {
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessagePageContent: React.FC<MessagePageContentProps> = ({ isActive = true }) => {
|
||||||
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ const MessagePageContent = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [reachedBottom, setReachedBottom] = useState(false);
|
const [reachedBottom, setReachedBottom] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
|
||||||
|
|
||||||
// 从 store 获取红点信息
|
// 从 store 获取红点信息
|
||||||
const reddotInfo = useReddotInfo();
|
const reddotInfo = useReddotInfo();
|
||||||
@@ -58,10 +64,14 @@ const MessagePageContent = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 只有当页面激活且未加载过数据时才加载接口
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getNoticeList();
|
if (isActive && !hasLoaded) {
|
||||||
fetchReddotInfo();
|
getNoticeList();
|
||||||
}, []);
|
fetchReddotInfo();
|
||||||
|
setHasLoaded(true);
|
||||||
|
}
|
||||||
|
}, [isActive, hasLoaded]);
|
||||||
|
|
||||||
const filteredMessages = messageList;
|
const filteredMessages = messageList;
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ import ListContainer from "@/container/listContainer";
|
|||||||
import { TennisMatch } from "@/../types/list/types";
|
import { TennisMatch } from "@/../types/list/types";
|
||||||
import { NTRPTestEntryCard } from "@/components";
|
import { NTRPTestEntryCard } from "@/components";
|
||||||
import { EvaluateScene } from "@/store/evaluateStore";
|
import { EvaluateScene } from "@/store/evaluateStore";
|
||||||
import { useUserInfo } from "@/store/userStore";
|
import { useUserInfo, useUserActions } from "@/store/userStore";
|
||||||
import { usePickerOption } from "@/store/pickerOptionsStore";
|
import { usePickerOption } from "@/store/pickerOptionsStore";
|
||||||
import { useGlobalState } from "@/store/global";
|
import { useGlobalState } from "@/store/global";
|
||||||
|
|
||||||
const MyselfPageContent: React.FC = () => {
|
interface MyselfPageContentProps {
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }) => {
|
||||||
const pickerOption = usePickerOption();
|
const pickerOption = usePickerOption();
|
||||||
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
||||||
|
const { fetchUserInfo } = useUserActions();
|
||||||
|
|
||||||
const instance = (Taro as any).getCurrentInstance();
|
const instance = (Taro as any).getCurrentInstance();
|
||||||
const user_id = instance.router?.params?.userid || "";
|
const user_id = instance.router?.params?.userid || "";
|
||||||
@@ -29,6 +34,7 @@ const MyselfPageContent: React.FC = () => {
|
|||||||
const [active_tab, setActiveTab] = useState<"hosted" | "participated">(
|
const [active_tab, setActiveTab] = useState<"hosted" | "participated">(
|
||||||
"hosted"
|
"hosted"
|
||||||
);
|
);
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
|
||||||
|
|
||||||
const [collapseProfile, setCollapseProfile] = useState(false);
|
const [collapseProfile, setCollapseProfile] = useState(false);
|
||||||
|
|
||||||
@@ -37,6 +43,16 @@ const MyselfPageContent: React.FC = () => {
|
|||||||
pickerOption.getProfessions();
|
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;
|
const { useDidShow } = Taro as any;
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
// 确保从编辑页面返回时刷新数据
|
// 确保从编辑页面返回时刷新数据
|
||||||
@@ -92,11 +108,20 @@ const MyselfPageContent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [active_tab, user_info, classifyGameRecords]);
|
}, [active_tab, user_info, classifyGameRecords]);
|
||||||
|
|
||||||
|
// 只有当页面激活且未加载过数据时才加载接口
|
||||||
useEffect(() => {
|
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();
|
load_game_data();
|
||||||
}
|
}
|
||||||
}, [loading, load_game_data]);
|
}, [active_tab, isActive, hasLoaded, loading, load_game_data, user_info]);
|
||||||
|
|
||||||
const handle_follow = async () => {
|
const handle_follow = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ const MainPage: React.FC = () => {
|
|||||||
if (code === currentTab) {
|
if (code === currentTab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换标签时关闭城市选择器
|
||||||
|
if (isCityPickerVisible) {
|
||||||
|
setIsCityPickerVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
setCurrentTab(code as TabType);
|
setCurrentTab(code as TabType);
|
||||||
// 切换标签时滚动到顶部
|
// 切换标签时滚动到顶部
|
||||||
(Taro as any).pageScrollTo({
|
(Taro as any).pageScrollTo({
|
||||||
@@ -103,7 +109,7 @@ const MainPage: React.FC = () => {
|
|||||||
duration: 300,
|
duration: 300,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[currentTab]
|
[currentTab, isCityPickerVisible]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理发布菜单显示/隐藏
|
// 处理发布菜单显示/隐藏
|
||||||
@@ -230,6 +236,7 @@ const MainPage: React.FC = () => {
|
|||||||
className={`tab-content ${currentTab === "list" ? "active" : ""}`}
|
className={`tab-content ${currentTab === "list" ? "active" : ""}`}
|
||||||
>
|
>
|
||||||
<ListPageContent
|
<ListPageContent
|
||||||
|
isActive={currentTab === "list"}
|
||||||
onNavStateChange={handleListNavStateChange}
|
onNavStateChange={handleListNavStateChange}
|
||||||
onScrollToTop={scrollToTop}
|
onScrollToTop={scrollToTop}
|
||||||
scrollToTopTrigger={listPageScrollToTopTrigger}
|
scrollToTopTrigger={listPageScrollToTopTrigger}
|
||||||
@@ -243,14 +250,14 @@ const MainPage: React.FC = () => {
|
|||||||
<View
|
<View
|
||||||
className={`tab-content ${currentTab === "message" ? "active" : ""}`}
|
className={`tab-content ${currentTab === "message" ? "active" : ""}`}
|
||||||
>
|
>
|
||||||
<MessagePageContent />
|
<MessagePageContent isActive={currentTab === "message"} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 我的页内容 */}
|
{/* 我的页内容 */}
|
||||||
<View
|
<View
|
||||||
className={`tab-content ${currentTab === "personal" ? "active" : ""}`}
|
className={`tab-content ${currentTab === "personal" ? "active" : ""}`}
|
||||||
>
|
>
|
||||||
<MyselfPageContent />
|
<MyselfPageContent isActive={currentTab === "personal"} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 底部导航栏 */}
|
{/* 底部导航栏 */}
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export enum MATCH_STATUS {
|
|||||||
|
|
||||||
// 是否支持候补
|
// 是否支持候补
|
||||||
export enum IsSubstituteSupported {
|
export enum IsSubstituteSupported {
|
||||||
SUPPORT = '0', // 支持
|
SUPPORT = '1', // 支持
|
||||||
NOTSUPPORT = '1', // 不支持
|
NOTSUPPORT = '0', // 不支持
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateLocationRes {
|
export interface UpdateLocationRes {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
ListState,
|
ListState,
|
||||||
IPayload,
|
IPayload,
|
||||||
} from "../../types/list/types";
|
} from "../../types/list/types";
|
||||||
import { useUser } from "./userStore";
|
|
||||||
|
|
||||||
function translateCityData(dataTree) {
|
function translateCityData(dataTree) {
|
||||||
return dataTree.map((item) => {
|
return dataTree.map((item) => {
|
||||||
@@ -185,7 +184,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
gamesNum: 0,
|
gamesNum: 0,
|
||||||
|
|
||||||
// 组装搜索数据
|
// 组装搜索数据
|
||||||
getSearchParams: (overrideArea?: [string, string]) => {
|
// 注意:始终使用 state.area,不接收 overrideArea 参数,确保参数一致性
|
||||||
|
getSearchParams: () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
||||||
const filterOptions = currentPageState?.filterOptions || {};
|
const filterOptions = currentPageState?.filterOptions || {};
|
||||||
@@ -193,13 +193,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
const distanceQuickFilter = currentPageState?.distanceQuickFilter || {};
|
const distanceQuickFilter = currentPageState?.distanceQuickFilter || {};
|
||||||
const { distanceFilter, order, district } = distanceQuickFilter || {};
|
const { distanceFilter, order, district } = distanceQuickFilter || {};
|
||||||
|
|
||||||
// 优先使用 overrideArea(切换城市时传入),其次使用 area 状态中的省份,最后使用用户信息中的
|
// 始终使用 state.area,确保所有接口使用一致的城市参数
|
||||||
const areaProvince = overrideArea?.at(1) || state.area?.at(1) || "";
|
const areaProvince = state.area?.at(1) || "";
|
||||||
const userInfo = useUser.getState().user as any;
|
const last_location_province = areaProvince;
|
||||||
const userLastLocationProvince = userInfo?.last_location_province || "";
|
|
||||||
// 优先使用切换后的城市,如果没有切换过则使用用户信息中的
|
|
||||||
const last_location_province = areaProvince || userLastLocationProvince;
|
|
||||||
const last_location_city = userInfo?.last_location_city || "";
|
|
||||||
|
|
||||||
// city 参数逻辑:
|
// city 参数逻辑:
|
||||||
// 1. 如果选择了行政区(district 有值),使用行政区的名称(label)
|
// 1. 如果选择了行政区(district 有值),使用行政区的名称(label)
|
||||||
@@ -217,6 +213,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
// 使用 filterOptions 中的 dateRange
|
// 使用 filterOptions 中的 dateRange
|
||||||
const dateRange: [string, string] = filterOptions?.dateRange || defaultDateRange;
|
const dateRange: [string, string] = filterOptions?.dateRange || defaultDateRange;
|
||||||
|
|
||||||
|
// 构建 searchOption
|
||||||
|
// 注意:province 必须从 state.area 获取,不能依赖 filterOptions 中可能存在的旧值
|
||||||
const searchOption: any = {
|
const searchOption: any = {
|
||||||
...filterOptions,
|
...filterOptions,
|
||||||
title: state.searchValue,
|
title: state.searchValue,
|
||||||
@@ -224,7 +222,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
ntrpMax: filterOptions?.ntrp?.[1],
|
ntrpMax: filterOptions?.ntrp?.[1],
|
||||||
dateRange: dateRange, // 确保始终是两个值的数组
|
dateRange: dateRange, // 确保始终是两个值的数组
|
||||||
distanceFilter: distanceFilter,
|
distanceFilter: distanceFilter,
|
||||||
province: last_location_province, // 使用 province 替代 last_location_province
|
// 显式设置 province,确保始终使用 state.area 中的最新值
|
||||||
|
province: last_location_province, // 始终使用 state.area 中的 province,确保城市参数一致
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只在有值时添加 city 参数
|
// 只在有值时添加 city 参数
|
||||||
@@ -337,13 +336,14 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 同时更新两个列表接口(常规列表和智能排序列表)
|
// 同时更新两个列表接口(常规列表和智能排序列表)
|
||||||
refreshBothLists: async (overrideArea?: [string, string]) => {
|
// 注意:不再接收 overrideArea 参数,始终使用 state.area
|
||||||
|
refreshBothLists: async () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const { getSearchParams, setListData } = state;
|
const { getSearchParams, setListData } = state;
|
||||||
const { getGamesList, getGamesIntegrateList } = await import("../services/listApi");
|
const { getGamesList, getGamesIntegrateList } = await import("../services/listApi");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchParams = getSearchParams(overrideArea) || {};
|
const searchParams = getSearchParams() || {};
|
||||||
|
|
||||||
// 调用常规列表接口
|
// 调用常规列表接口
|
||||||
const listParams = {
|
const listParams = {
|
||||||
@@ -408,11 +408,24 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取球局数量
|
// 获取球局数量
|
||||||
fetchGetGamesCount: async (overrideArea?: [string, string]) => {
|
// 注意:必须和 games/integrate_list 使用相同的参数构建逻辑,确保数据一致性
|
||||||
|
// 不再接收 overrideArea 参数,始终使用 state.area
|
||||||
|
fetchGetGamesCount: async () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const { getSearchParams } = state;
|
const { getSearchParams } = state;
|
||||||
const params = getSearchParams(overrideArea) || {};
|
const searchParams = getSearchParams() || {};
|
||||||
console.log("fetchGetGamesCount 参数:", { overrideArea, params: JSON.stringify(params) });
|
|
||||||
|
// 使用和 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 resData = (await getGamesCount(params)) || {};
|
||||||
const gamesNum = resData?.data?.count || 0;
|
const gamesNum = resData?.data?.count || 0;
|
||||||
console.log("fetchGetGamesCount 结果:", gamesNum);
|
console.log("fetchGetGamesCount 结果:", gamesNum);
|
||||||
@@ -528,9 +541,13 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 使用 Promise.resolve 确保状态更新后再调用接口
|
// 使用 Promise.resolve 确保状态更新后再调用接口
|
||||||
Promise.resolve().then(() => {
|
// 先调用列表接口,然后在列表接口完成后调用数量接口
|
||||||
|
Promise.resolve().then(async () => {
|
||||||
const freshState = get(); // 重新获取最新状态
|
const freshState = get(); // 重新获取最新状态
|
||||||
freshState.fetchGetGamesCount();
|
// 先调用列表接口
|
||||||
|
await freshState.getMatchesData();
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
await freshState.fetchGetGamesCount();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -548,15 +565,18 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 使用 Promise.resolve 确保状态更新后再调用接口
|
// 使用 Promise.resolve 确保状态更新后再调用接口
|
||||||
Promise.resolve().then(() => {
|
// 先调用列表接口,然后在列表接口完成后调用数量接口
|
||||||
|
Promise.resolve().then(async () => {
|
||||||
const freshState = get(); // 重新获取最新状态
|
const freshState = get(); // 重新获取最新状态
|
||||||
freshState.getMatchesData();
|
// 先调用列表接口
|
||||||
freshState.fetchGetGamesCount();
|
await freshState.getMatchesData();
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
await freshState.fetchGetGamesCount();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清空综合筛选选项
|
// 清空综合筛选选项
|
||||||
clearFilterOptions: () => {
|
clearFilterOptions: async () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const { getMatchesData, fetchGetGamesCount } = state;
|
const { getMatchesData, fetchGetGamesCount } = state;
|
||||||
|
|
||||||
@@ -565,8 +585,10 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
filterCount: 0,
|
filterCount: 0,
|
||||||
pageOption: defaultPageOption,
|
pageOption: defaultPageOption,
|
||||||
});
|
});
|
||||||
getMatchesData();
|
// 先调用列表接口
|
||||||
fetchGetGamesCount();
|
await getMatchesData();
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
await fetchGetGamesCount();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载更多数据
|
// 加载更多数据
|
||||||
@@ -615,8 +637,10 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
if (!isSearchData) {
|
if (!isSearchData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fetchGetGamesCount();
|
// 先调用列表接口
|
||||||
await getMatchesData();
|
await getMatchesData();
|
||||||
|
// 列表接口完成后,再调用数量接口
|
||||||
|
await fetchGetGamesCount();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新store数据
|
// 更新store数据
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
NicknameChangeStatus,
|
NicknameChangeStatus,
|
||||||
updateNickname as updateNicknameApi,
|
updateNickname as updateNicknameApi,
|
||||||
} from "@/services/userService";
|
} from "@/services/userService";
|
||||||
|
import evaluateService, { LastTimeTestResult } from "@/services/evaluateService";
|
||||||
|
import { useListStore } from "./listStore";
|
||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
user: UserInfoType | {};
|
user: UserInfoType | {};
|
||||||
@@ -15,35 +17,11 @@ export interface UserState {
|
|||||||
nicknameChangeStatus: Partial<NicknameChangeStatus>;
|
nicknameChangeStatus: Partial<NicknameChangeStatus>;
|
||||||
checkNicknameChangeStatus: () => void;
|
checkNicknameChangeStatus: () => void;
|
||||||
updateNickname: (nickname: string) => void;
|
updateNickname: (nickname: string) => void;
|
||||||
|
// NTRP 测试结果缓存
|
||||||
|
lastTestResult: LastTimeTestResult | null;
|
||||||
|
fetchLastTestResult: () => Promise<LastTimeTestResult | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求锁,防止重复请求
|
|
||||||
let fetchingUserInfo = false;
|
|
||||||
let fetchUserInfoPromise: Promise<UserInfoType | undefined> | 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 getTimeNextDate = (time: string) => {
|
||||||
const date = new Date(time);
|
const date = new Date(time);
|
||||||
@@ -56,15 +34,50 @@ const getTimeNextDate = (time: string) => {
|
|||||||
|
|
||||||
export const useUser = create<UserState>()((set) => ({
|
export const useUser = create<UserState>()((set) => ({
|
||||||
user: {},
|
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<UserInfoType>) => {
|
updateUserInfo: async (userInfo: Partial<UserInfoType>) => {
|
||||||
try {
|
try {
|
||||||
// 先更新后端
|
// 先更新后端
|
||||||
await updateUserProfile(userInfo);
|
await updateUserProfile(userInfo);
|
||||||
// 然后立即更新本地状态(乐观更新)
|
// 然后立即更新本地状态(乐观更新)
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
user: { ...state.user, ...userInfo },
|
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的字段时才需要重新获取
|
// 只有在更新头像等需要服务器返回新URL的字段时才需要重新获取
|
||||||
// 如果需要确保数据一致性,可以在特定场景下手动调用 fetchUserInfo
|
// 如果需要确保数据一致性,可以在特定场景下手动调用 fetchUserInfo
|
||||||
@@ -99,6 +112,21 @@ export const useUser = create<UserState>()((set) => ({
|
|||||||
console.error("更新用户昵称失败:", error);
|
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);
|
export const useUserInfo = () => useUser((state) => state.user);
|
||||||
@@ -111,4 +139,7 @@ export const useUserActions = () =>
|
|||||||
updateUserInfo: state.updateUserInfo,
|
updateUserInfo: state.updateUserInfo,
|
||||||
checkNicknameChangeStatus: state.checkNicknameChangeStatus,
|
checkNicknameChangeStatus: state.checkNicknameChangeStatus,
|
||||||
updateNickname: state.updateNickname,
|
updateNickname: state.updateNickname,
|
||||||
|
fetchLastTestResult: state.fetchLastTestResult,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useLastTestResult = () => useUser((state) => state.lastTestResult);
|
||||||
|
|||||||
@@ -43,120 +43,25 @@ const runtime: {
|
|||||||
offscreen?: any
|
offscreen?: any
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// 兼容适配:将微信 CanvasContext 的 set* 方法映射到标准 Canvas 2D 属性
|
// 2D Canvas 字体设置
|
||||||
const polyfillCanvasContext = (ctx: any) => {
|
const setFont2D = (ctx: any, fontSize: number, family?: string, weight?: string) => {
|
||||||
if (ctx && typeof ctx === 'object') {
|
const fam = family || 'Noto Sans SC'
|
||||||
if (typeof ctx.setFillStyle !== 'function') {
|
const wt = weight || '500'
|
||||||
ctx.setFillStyle = (v: any) => { ctx.fillStyle = v }
|
ctx.font = `${wt} ${fontSize}px "${fam}"`
|
||||||
}
|
|
||||||
if (typeof ctx.setStrokeStyle !== 'function') {
|
|
||||||
ctx.setStrokeStyle = (v: any) => { ctx.strokeStyle = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineWidth !== 'function') {
|
|
||||||
ctx.setLineWidth = (v: number) => { ctx.lineWidth = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineCap !== 'function') {
|
|
||||||
ctx.setLineCap = (v: any) => { ctx.lineCap = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setLineJoin !== 'function') {
|
|
||||||
ctx.setLineJoin = (v: any) => { ctx.lineJoin = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setTextAlign !== 'function') {
|
|
||||||
ctx.setTextAlign = (v: any) => { ctx.textAlign = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setTextBaseline !== 'function') {
|
|
||||||
ctx.setTextBaseline = (v: any) => { ctx.textBaseline = v }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setFontSize !== 'function') {
|
|
||||||
ctx.setFontSize = (size: number) => { ctx.font = `${size}px sans-serif` }
|
|
||||||
}
|
|
||||||
if (typeof ctx.setFont !== 'function') {
|
|
||||||
ctx.setFont = (fontStr: string) => { ctx.font = fontStr }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制加粗文字(单行)
|
// 绘制加粗文字(单行,支持可选描边式加粗)
|
||||||
const drawBoldText = (ctx: any, text: string, x: number, y: number, fontSize: number, color: string, fontFamily?: string) => {
|
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)
|
||||||
if (fontFamily) {
|
ctx.fillStyle = color
|
||||||
try {
|
ctx.textAlign = 'left'
|
||||||
// 尝试使用setFont方法(如果支持)
|
ctx.textBaseline = 'top'
|
||||||
ctx.setFont(`${fontSize}px ${fontFamily}`)
|
|
||||||
} catch (error) {
|
|
||||||
// 如果不支持setFont,回退到setFontSize
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.setFontSize(fontSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setFillStyle(color)
|
|
||||||
ctx.setTextAlign('left')
|
|
||||||
ctx.setTextBaseline('top')
|
|
||||||
|
|
||||||
// 绘制加粗效果:多次绘制并偏移
|
|
||||||
ctx.fillText(text, x, y)
|
ctx.fillText(text, x, y)
|
||||||
ctx.fillText(text, x + 1, y)
|
// if (isBold) {
|
||||||
ctx.fillText(text, x, y + 1)
|
// ctx.fillText(text, x + 1, y)
|
||||||
ctx.fillText(text, x + 1, y + 1)
|
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制圆角矩形函数
|
// 绘制圆角矩形函数
|
||||||
@@ -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') => {
|
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)
|
drawRoundedRect(ctx, x, y, width, height, radius)
|
||||||
|
|
||||||
// 绘制边框
|
// 绘制边框
|
||||||
ctx.setStrokeStyle(borderColor)
|
ctx.strokeStyle = borderColor
|
||||||
ctx.setLineWidth(1 * dpr)
|
ctx.lineWidth = 1 * dpr
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
const textCenterX = x + width / 2
|
const textCenterX = x + width / 2
|
||||||
const textCenterY = y + height / 2
|
const textCenterY = y + height / 2
|
||||||
|
|
||||||
ctx.setFillStyle(textColor)
|
ctx.fillStyle = textColor
|
||||||
ctx.setTextAlign('center')
|
ctx.textAlign = 'center'
|
||||||
ctx.setTextBaseline('middle')
|
ctx.textBaseline = 'middle'
|
||||||
ctx.setFontSize(fontSize)
|
setFont2D(ctx, fontSize)
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(textCenterX, textCenterY)
|
ctx.translate(textCenterX, textCenterY)
|
||||||
@@ -201,49 +106,31 @@ const drawLabel = (ctx: any, x: number, y: number, width: number, height: number
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载图片 - 微信小程序版本
|
// 工具函数 - OffscreenCanvas 下加载图片(使用 offscreen.createImage)
|
||||||
// const loadImage = (src: string): Promise<string> => {
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// Taro.getImageInfo({
|
|
||||||
// src: src,
|
|
||||||
// success: (res) => resolve(res.path),
|
|
||||||
// fail: reject
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 工具函数 - OffscreenCanvas 下加载图片(从 runtime.offscreen 读取)
|
|
||||||
const loadImage = (src: string): Promise<any> => {
|
const loadImage = (src: string): Promise<any> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.getImageInfo({
|
try {
|
||||||
src,
|
const off = runtime.offscreen
|
||||||
success: (res) => {
|
if (!off || typeof off.createImage !== 'function') {
|
||||||
try {
|
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
||||||
// @ts-ignore - createImage 为小程序 OffscreenCanvas 能力
|
}
|
||||||
const off = runtime.offscreen
|
const img = off.createImage()
|
||||||
if (!off || typeof off.createImage !== 'function') {
|
img.onload = () => resolve(img)
|
||||||
throw new Error('OffscreenCanvas 未初始化或不支持 createImage')
|
img.onerror = reject
|
||||||
}
|
img.src = src
|
||||||
const img = off.createImage()
|
} catch (e) {
|
||||||
img.onload = () => resolve(img)
|
reject(e)
|
||||||
img.onerror = reject
|
}
|
||||||
img.src = res.path
|
|
||||||
} catch (e) {
|
|
||||||
reject(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: reject
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制SVG路径到Canvas
|
// 绘制SVG路径到Canvas
|
||||||
const drawSVGPathToCanvas = (ctx: any) => {
|
const drawSVGPathToCanvas = (ctx: any) => {
|
||||||
// 设置绘制样式
|
// 设置绘制样式
|
||||||
ctx.setStrokeStyle('#00E5AD');
|
ctx.strokeStyle = '#00E5AD';
|
||||||
ctx.setLineWidth(scale * 3 * dpr);
|
ctx.lineWidth = scale * 3 * dpr;
|
||||||
ctx.setLineCap('round');
|
ctx.lineCap = 'round';
|
||||||
ctx.setLineJoin('round');
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
@@ -296,7 +183,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 1. 先绘制白色圆角矩形背景
|
// 1. 先绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
// 使用更精确的圆角矩形绘制
|
// 使用更精确的圆角矩形绘制
|
||||||
@@ -385,7 +272,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
ctx.rotate((rotation * Math.PI) / 180)
|
ctx.rotate((rotation * Math.PI) / 180)
|
||||||
|
|
||||||
// 绘制白色圆角矩形背景
|
// 绘制白色圆角矩形背景
|
||||||
ctx.setFillStyle('#FFFFFF')
|
ctx.fillStyle = '#FFFFFF'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
const rectX = -playerImgSize / 2
|
const rectX = -playerImgSize / 2
|
||||||
@@ -427,7 +314,7 @@ const drawVenueImages = async (ctx: any, venueImageConfig: any) => {
|
|||||||
const imgY = -playerImgSize / 2 + padding
|
const imgY = -playerImgSize / 2 + padding
|
||||||
const imgSize = playerImgSize - padding * 2
|
const imgSize = playerImgSize - padding * 2
|
||||||
|
|
||||||
ctx.setFillStyle('#E0E0E0')
|
ctx.fillStyle = '#E0E0E0'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
const imgRadius = borderRadius - padding
|
const imgRadius = borderRadius - padding
|
||||||
|
|
||||||
@@ -471,8 +358,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
console.log('开始绘制分享卡片...')
|
console.log('开始绘制分享卡片...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先对 2D 上下文做一次 API 兼容处理
|
|
||||||
polyfillCanvasContext(ctx)
|
|
||||||
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
// 设置Canvas的实际尺寸(使用dpr确保高清显示)
|
||||||
const canvasWidthPx = canvasWidth * dpr
|
const canvasWidthPx = canvasWidth * dpr
|
||||||
const canvasHeightPx = canvasHeight * 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)
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeightPx)
|
||||||
gradient.addColorStop(0, '#BFFFEF')
|
gradient.addColorStop(0, '#BFFFEF')
|
||||||
gradient.addColorStop(1, '#F2FFFC')
|
gradient.addColorStop(1, '#F2FFFC')
|
||||||
ctx.setFillStyle(gradient)
|
ctx.fillStyle = gradient
|
||||||
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
ctx.fillRect(0, 0, canvasWidthPx, canvasHeightPx)
|
||||||
console.log('背景绘制完成')
|
console.log('背景绘制完成')
|
||||||
|
|
||||||
// 绘制背景条纹 已完成
|
// 绘制背景条纹 已完成
|
||||||
ctx.setStrokeStyle('rgba(0, 0, 0, 0.03)')
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.03)'
|
||||||
ctx.setLineWidth(2)
|
ctx.lineWidth = 2
|
||||||
for (let i = 0; i < canvasWidthPx; i += 4) {
|
for (let i = 0; i < canvasWidthPx; i += 4) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(i, 0)
|
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 nicknameX = avatarX + avatarSize + 8 * dpr // 距离头像8px
|
||||||
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
const nicknameY = avatarY + (avatarSize - 18 * dpr) / 2 // 与头像水平居中对齐
|
||||||
const nicknameFontSize = scale * 18 * dpr
|
const nicknameFontSize = scale * 18 * dpr
|
||||||
drawText(ctx, data.userNickname, nicknameX, nicknameY, 200 * dpr, nicknameFontSize, '#000000', true, '"Noto Sans SC"')
|
drawBoldText(ctx, data.userNickname, nicknameX, nicknameY, nicknameFontSize, '#000000', 'Noto Sans SC', '900')
|
||||||
|
|
||||||
// 绘制"邀你加入球局"文案
|
// 绘制"邀你加入球局"文案
|
||||||
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
const inviteX = scale * 35 * dpr // 距离画布左侧35px
|
||||||
@@ -540,12 +425,12 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const inviteFontSize = scale * 44 * dpr
|
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 qiuJuX = inviteX + ctx.measureText('邀你加入').width + 4 * dpr
|
||||||
const qiuJuFontSize = scale * 44 * 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)
|
drawSVGPathToCanvas(ctx)
|
||||||
@@ -607,7 +492,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
|||||||
const danDaFontSize = scale * 22 * dpr
|
const danDaFontSize = scale * 22 * dpr
|
||||||
// 根据内容动态计算标签宽度(左右内边距)
|
// 根据内容动态计算标签宽度(左右内边距)
|
||||||
const danDaPaddingX = scale * 16 * dpr
|
const danDaPaddingX = scale * 16 * dpr
|
||||||
ctx.setFontSize(danDaFontSize)
|
setFont2D(ctx, danDaFontSize)
|
||||||
const danDaTextWidth = ctx.measureText(data.gameType).width
|
const danDaTextWidth = ctx.measureText(data.gameType).width
|
||||||
const danDaWidth = danDaTextWidth + danDaPaddingX * 2
|
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 skillFontSize = scale * 22 * dpr
|
||||||
// 根据内容动态计算技能标签宽度
|
// 根据内容动态计算技能标签宽度
|
||||||
const skillPaddingX = scale * 20 * dpr
|
const skillPaddingX = scale * 20 * dpr
|
||||||
ctx.setFontSize(skillFontSize)
|
setFont2D(ctx, skillFontSize)
|
||||||
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
const skillTextWidth = ctx.measureText(data.skillLevel).width
|
||||||
const skillWidth = skillTextWidth + skillPaddingX * 2
|
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`)
|
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
||||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
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
|
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 locationInfoY = infoStartY + infoSpacing * 2
|
||||||
const locationFontSize = scale * 22 * dpr
|
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`)
|
||||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
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 {
|
try {
|
||||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||||
|
|||||||
@@ -111,11 +111,11 @@ export interface ListActions {
|
|||||||
getSearchHistory: () => Promise<void>;
|
getSearchHistory: () => Promise<void>;
|
||||||
clearHistory: () => void;
|
clearHistory: () => void;
|
||||||
searchSuggestion: (val: string) => Promise<void>;
|
searchSuggestion: (val: string) => Promise<void>;
|
||||||
getSearchParams: (overrideArea?: [string, string]) => Record<string, any>;
|
getSearchParams: () => Record<string, any>;
|
||||||
loadMoreMatches: () => Promise<void>;
|
loadMoreMatches: () => Promise<void>;
|
||||||
initialFilterSearch: (isSearchData?: boolean) => void;
|
initialFilterSearch: (isSearchData?: boolean) => void;
|
||||||
setListData: (payload: IPayload) => void;
|
setListData: (payload: IPayload) => void;
|
||||||
fetchGetGamesCount: (overrideArea?: [string, string]) => Promise<void>;
|
fetchGetGamesCount: () => Promise<void>;
|
||||||
getCurrentPageState: () => { currentPageState: any; currentPageKey: string };
|
getCurrentPageState: () => { currentPageState: any; currentPageKey: string };
|
||||||
updateCurrentPageState: (payload: Record<string, any>) => void;
|
updateCurrentPageState: (payload: Record<string, any>) => void;
|
||||||
updateDistanceQuickFilter: (payload: Record<string, any>) => void;
|
updateDistanceQuickFilter: (payload: Record<string, any>) => void;
|
||||||
@@ -123,7 +123,7 @@ export interface ListActions {
|
|||||||
getCityQrCode: () => Promise<void>;
|
getCityQrCode: () => Promise<void>;
|
||||||
getDistricts: () => Promise<BubbleOption[]>; // 新增:获取行政区
|
getDistricts: () => Promise<BubbleOption[]>; // 新增:获取行政区
|
||||||
updateArea: (payload: [string, string]) => void;
|
updateArea: (payload: [string, string]) => void;
|
||||||
refreshBothLists: (overrideArea?: [string, string]) => Promise<void>;
|
refreshBothLists: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPayload {
|
export interface IPayload {
|
||||||
|
|||||||
Reference in New Issue
Block a user