11 Commits

26 changed files with 1137 additions and 844 deletions

2
.gitignore vendored
View File

@@ -8,4 +8,6 @@ node_modules/
src/config/env.ts
.vscode
*.http
.cursor
.codewiz

View File

@@ -22,7 +22,7 @@ export interface EnvConfig {
const baseConfig = {
apiBaseURL: "https://tennis.bimwe.com",
ossBaseURL: "https://bimwe-oss.oss-cn-shanghai.aliyuncs.com",
ossBaseURL: "https://bimwe.oss-cn-shanghai.aliyuncs.com",
appid: "wx815b533167eb7b53", // 测试号
timeout: 15000,
enableLog: true,

128
config/env.ts Normal file
View File

@@ -0,0 +1,128 @@
import Taro from '@tarojs/taro'
// 环境类型
export type EnvType = 'development' | 'production'
// 环境配置接口
export interface EnvConfig {
name: string
apiBaseURL: string
timeout: number
enableLog: boolean
enableMock: boolean
// 客服配置
customerService: {
corpId: string
serviceUrl: string
phoneNumber?: string
email?: string
}
}
// 各环境配置
const envConfigs: Record<EnvType, EnvConfig> = {
// 开发环境
development: {
name: '开发环境',
// apiBaseURL: 'https://tennis.bimwe.com',
apiBaseURL: 'http://localhost:9098',
timeout: 15000,
enableLog: true,
enableMock: false,
// 客服配置
customerService: {
corpId: 'ww51fc969e8b76af82', // 企业ID
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
}
},
// 生产环境1
// production: {
// name: '生产环境1',
// apiBaseURL: 'https://tennis.bimwe.com',
// timeout: 10000,
// enableLog: false,
// enableMock: false,
// // 客服配置
// customerService: {
// corpId: 'ww51fc969e8b76af82', // 企业ID
// serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
// }
// },
// 生产环境2
production: {
name: '生产环境2',
apiBaseURL: 'https://youchang.qiongjingtiyu.com',
timeout: 10000,
enableLog: false,
enableMock: false,
// 客服配置
customerService: {
corpId: 'ww9a2d9a5d9410c664', // 企业ID
serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd355e162e0390684',
}
}
}
// 获取当前环境
export const getCurrentEnv = (): EnvType => {
// 在小程序环境中,使用默认逻辑判断环境
// 可以根据实际需要配置不同的判断逻辑
// 可以根据实际部署情况添加更多判断逻辑
// 比如通过 Taro.getEnv() 获取当前平台环境
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {
return 'production'
} else {
return 'development'
}
}
// 获取当前环境配置
export const getCurrentConfig = (): EnvConfig => {
const env = getCurrentEnv()
return envConfigs[env]
}
// 获取指定环境配置
export const getEnvConfig = (env: EnvType): EnvConfig => {
return envConfigs[env]
}
// 是否为开发环境
export const isDevelopment = (): boolean => {
return getCurrentEnv() === 'development'
}
// 是否为生产环境
export const isProduction = (): boolean => {
return getCurrentEnv() === 'production'
}
// 环境配置调试信息
export const getEnvInfo = () => {
const config = getCurrentConfig()
return {
env: getCurrentEnv(),
config,
taroEnv: Taro.getEnv(),
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
Taro.getEnv() === Taro.ENV_TYPE.WEB ? 'Web' :
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
}
}
// 导出当前环境配置(方便直接使用)
export default getCurrentConfig()

View File

@@ -3,6 +3,12 @@
.common-popup {
position: fixed;
z-index: 9999 !important;
padding: 0;
box-sizing: border-box;
max-height: calc(100vh - 10px);
display: flex;
flex-direction: column;
background-color: theme.$page-background-color;
&:global(.nut-popup-bottom.nut-popup-round) {
border-radius: 20px 20px 0 0 !important;
}
@@ -32,12 +38,7 @@
}
}
padding: 0;
box-sizing: border-box;
max-height: calc(100vh - 10px);
display: flex;
flex-direction: column;
background-color: theme.$page-background-color;
// .common-popup__header {
// padding: 12px 16px;

View File

@@ -13,7 +13,9 @@
align-items: center;
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
@@ -32,7 +34,9 @@
padding-top: 24px;
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
@@ -48,8 +52,10 @@
align-items: center;
.tips {
color: rgba(60, 60, 67, 0.60);
font-feature-settings: 'liga' off, 'clig' off;
color: rgba(60, 60, 67, 0.6);
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
@@ -62,13 +68,15 @@
margin-top: 8px;
padding: 8px;
border-radius: 4px;
background: #F0F0F0;
background: #f0f0f0;
.input {
width: 100%;
&:placeholder-shown {
color: rgba(60, 60, 67, 0.30);
font-feature-settings: 'liga' off, 'clig' off;
color: rgba(60, 60, 67, 0.3);
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
@@ -84,11 +92,12 @@
justify-content: space-between;
align-items: center;
height: 44px;
border-top: 0.5px solid #CECECE;
background: #FFF;
border-top: 0.5px solid #cecece;
background: #fff;
margin-top: 2px;
.confirm, .cancel {
.confirm,
.cancel {
width: 50%;
height: 44px;
display: flex;
@@ -96,7 +105,9 @@
align-items: center;
color: #000;
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;

View File

@@ -186,10 +186,11 @@ export default forwardRef(function GameManagePopup(props, ref) {
.some((item) => item.user.id === userInfo.id);
const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes(
detail.match_status
detail.match_status,
);
const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
// const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
const beforeStart = dayjs(detail.start_time).isAfter(dayjs());
const hasOtherParticiappants = (detail.participants || [])
.filter((item) => item.status === "joined")
@@ -207,7 +208,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
style={{ minHeight: "unset" }}
>
<View className={styles.container}>
{!inTwoHours && !hasOtherParticiappants && (
{!finished && !hasOtherParticiappants && beforeStart && (
<View className={styles.button} onClick={handleEditGame}>
</View>
@@ -217,12 +218,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
</View>
)}
{!inTwoHours && !hasOtherParticiappants && (
{!finished && beforeStart && (
<View className={styles.button} onClick={handleCancelGame}>
</View>
)}
{hasJoin && (
{!finished && beforeStart && hasJoin && (
<View className={styles.button} onClick={handleQuitGame}>
退
</View>

View File

@@ -5,8 +5,9 @@ import img from "../../config/images";
import { ListCardProps } from "../../../types/list/types";
import { formatGameTime, calculateDuration } from "@/utils/timeUtils";
import { navigateTo } from "@/utils/navigation";
import images from '@/config/images'
import images from "@/config/images";
import "./index.scss";
import { OSS_BASE } from "@/config/api";
const ListCard: React.FC<ListCardProps> = ({
id,
@@ -45,7 +46,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image"
mode="aspectFill"
lazyLoad
defaultSource={require("@/static/emptyStatus/publish-empty-card.png")}
defaultSource={`${OSS_BASE}/front/ball/images/publish-empty-card.svg`}
/>
);
};
@@ -67,7 +68,9 @@ const ListCard: React.FC<ListCardProps> = ({
const containerWidthPx = screenWidth - 130;
// 计算固定信息宽度
const extraInfo = `${court_type ? `${court_type}` : ''}${distance_km ? `${distance_km}km` : ''}`;
const extraInfo = `${court_type ? `${court_type}` : ""}${
distance_km ? `${distance_km}km` : ""
}`;
// 估算字符宽度(基于 12px 字体)
const getTextWidth = (text: string) => {
@@ -98,7 +101,9 @@ const ListCard: React.FC<ListCardProps> = ({
let currentWidth = 0;
for (let i = 0; i < location.length; i++) {
const char = location[i];
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 12 : 6;
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)
? 12
: 6;
if (currentWidth + charWidth > availableWidth) {
break;
}
@@ -106,7 +111,7 @@ const ListCard: React.FC<ListCardProps> = ({
maxChars++;
}
return location.slice(0, maxChars) + '...';
return location.slice(0, maxChars) + "...";
}, [location, court_type, distance_km]);
// 根据图片数量决定展示样式
@@ -220,9 +225,10 @@ const ListCard: React.FC<ListCardProps> = ({
</Text>
</View>
<View className="tag ntprTag">
<Image src={images.ICON_LIST_NTPR} className='ntprIcon' />
<Image src={images.ICON_LIST_NTPR} className="ntprIcon" />
<Text className="tag-text">
{Number(skill_level_min)?.toFixed(1)} - {Number(skill_level_max)?.toFixed(1)}
{Number(skill_level_min)?.toFixed(1)} -{" "}
{Number(skill_level_max)?.toFixed(1)}
</Text>
{/* 分割线 */}
<View className="typeLine" />
@@ -251,13 +257,8 @@ const ListCard: React.FC<ListCardProps> = ({
/>
{/* <Text className="smoothTitle">{game_type}</Text> */}
</View>
{
venue_description && (<View className="line" />)
}
{
venue_description &&
(
{venue_description && <View className="line" />}
{venue_description && (
<View className="localAreaContainer">
<View className="localAreaTitle">:</View>
<View className="localAreaWrapper">
@@ -265,8 +266,7 @@ const ListCard: React.FC<ListCardProps> = ({
<Text className="localAreaText">{venue_description}</Text>
</View>
</View>
)
}
)}
</View>
)}
</View>

View File

@@ -24,7 +24,6 @@ const ListLoadError = (props: IProps) => {
wrapperHeight = "",
width = "",
height = "",
scale = "",
} = props;
const handleReload = () => {
reload && typeof reload === "function" && reload();
@@ -34,7 +33,7 @@ const ListLoadError = (props: IProps) => {
<View className={styles.listLoadError} style={{ height: wrapperHeight }}>
<Image
className={styles.listLoadErrorImg}
style={{ width, height, transform: `scale(${scale})` }}
style={{ width, height }}
src={errorImg ? img[errorImg] : img.ICON_LIST_LOAD_ERROR}
/>
{text && <Text className={styles.listLoadErrorText}>{text}</Text>}

View File

@@ -29,18 +29,24 @@ export interface RadarChartV2Ref {
}) => Promise<string>;
}
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref) => {
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>(
(props, ref) => {
const { data } = props;
const maxValue = 100;
const levels = 5;
// 在 exportCanvasV2 中绘制雷达图的函数
function drawRadarChart(ctx: CanvasRenderingContext2D, radarX: number, radarY: number, radarSize: number) {
function drawRadarChart(
ctx: CanvasRenderingContext2D,
radarX: number,
radarY: number,
radarSize: number,
) {
// 雷达图中心点位置radarSize 已经是2倍图尺寸
const center = {
x: radarX + radarSize / 2,
y: radarY + radarSize / 2
y: radarY + radarSize / 2,
};
// 计算实际半径radarSize 是直径,半径是直径的一半)
@@ -48,9 +54,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 启用抗锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.imageSmoothingQuality = "high";
ctx.lineCap = "round";
ctx.lineJoin = "round";
// 解析数据
const { texts, vals } = data.reduce(
@@ -61,7 +67,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
vals: [...res.vals, val],
};
},
{ texts: [], vals: [] }
{ texts: [], vals: [] },
);
// === 绘制圆形网格 ===
@@ -148,25 +154,39 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
}
// 获取图片信息(宽高)
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
function getImageInfo(
src: string,
): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
(Taro as any).getImageInfo({
src,
success: (res: any) => resolve({ width: res.width, height: res.height }),
success: (res: any) =>
resolve({ width: res.width, height: res.height }),
fail: reject,
});
});
}
// 绘制圆角矩形
function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) {
function roundRect(
ctx: any,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height,
);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
@@ -187,8 +207,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
useImperativeHandle(ref, () => ({
// 生成原始雷达图(已废弃,现在直接在 exportCanvasV2 中绘制)
generateImage: () =>
Promise.resolve(""),
generateImage: () => Promise.resolve(""),
// 生成完整图片(包含标题、雷达图、底部文字和二维码)
generateFullImage: async (options: {
@@ -229,7 +248,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 启用抗锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.imageSmoothingQuality = "high";
// 绘制背景 - 使用 share_bg.png 背景图,撑满整个画布(从 OSS 动态加载)
try {
@@ -253,18 +272,28 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
if (options.avatarUrl) {
try {
const avatarSize = 43.46 * scale; // 设计稿头像尺寸
const avatarImg = await loadImage(canvas, options.avatarUrl);
const avatarImg = await loadImage(
canvas,
options.avatarUrl,
);
const avatarInfo = await getImageInfo(options.avatarUrl);
// 头像区域总宽度(头像 + 装饰图片重叠部分)
const avatarWrapWidth = 84.7 * scale; // 设计稿 Frame 1912055063 宽度
const avatarX = sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarX =
sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarY = currentY;
// 绘制头像圆形背景
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.arc(
avatarX + avatarSize / 2,
avatarY + avatarSize / 2,
avatarSize / 2,
0,
Math.PI * 2,
);
ctx.fillStyle = "#FFFFFF";
ctx.fill();
ctx.strokeStyle = "#EFEFEF";
@@ -273,7 +302,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 计算头像绘制尺寸,保持宽高比
const innerSize = avatarSize - 1.94 * scale; // 内部可用尺寸
const avatarAspectRatio = avatarInfo.width / avatarInfo.height;
const avatarAspectRatio =
avatarInfo.width / avatarInfo.height;
let drawWidth = innerSize;
let drawHeight = innerSize;
let drawX = avatarX + 0.97 * scale;
@@ -292,9 +322,21 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 绘制头像(圆形裁剪)
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 0.97 * scale, 0, Math.PI * 2);
ctx.arc(
avatarX + avatarSize / 2,
avatarY + avatarSize / 2,
avatarSize / 2 - 0.97 * scale,
0,
Math.PI * 2,
);
ctx.clip();
ctx.drawImage(avatarImg, drawX, drawY, drawWidth, drawHeight);
ctx.drawImage(
avatarImg,
drawX,
drawY,
drawWidth,
drawHeight,
);
ctx.restore();
// 绘制装饰图片DocCopy- 在头像右侧
@@ -317,7 +359,14 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const borderRadius = 9.66 * scale; // 设计稿圆角
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius);
roundRect(
ctx,
-addonSize / 2,
-addonSize / 2,
addonSize,
addonSize,
borderRadius,
);
ctx.fill();
// 添加渐变背景色
@@ -334,7 +383,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const docSize = 26.18 * scale; // 设计稿内部图片尺寸
const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度
ctx.rotate(docRotation);
ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize);
ctx.drawImage(
docCopyImg,
-docSize / 2,
-docSize / 2,
docSize,
docSize,
);
ctx.restore();
} catch (error) {
console.error("Failed to load docCopy image:", error);
@@ -409,7 +464,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const qrX = 276 * scale; // 设计稿二维码 x 位置
const qrY = 523 * scale; // 设计稿二维码 y 位置
const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!";
const bottomTextContent =
options.bottomText ||
"长按识别二维码,快来加入,有你就有场!";
// 绘制底部文字 - 设计稿fontSize: 12, fontWeight: 400, line-height: 1.52倍图
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
@@ -458,7 +515,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const iconImg = await loadImage(canvas, shareLogoSvg);
// 图标位置:文字顶部上方 iconSize + gap
const iconY = textY - iconSize - iconGap;
ctx.drawImage(iconImg, topTitleX, iconY, 235 * scale, iconSize);
ctx.drawImage(
iconImg,
topTitleX,
iconY,
235 * scale,
iconSize,
);
} catch (error) {
console.error("Failed to load icon:", error);
}
@@ -468,7 +531,6 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.fillText(lineText, textX, textY + index * lineHeight);
});
// 绘制二维码 - 设计稿位置(带白色背景、边框、阴影和圆角)
if (options.qrCodeUrl) {
@@ -517,10 +579,23 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 绘制二维码图片(在圆角矩形内)
ctx.save();
// 创建圆角裁剪区域
roundRect(ctx, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize, borderRadius - borderWidth);
roundRect(
ctx,
qrInnerX,
qrInnerY,
qrInnerSize,
qrInnerSize,
borderRadius - borderWidth,
);
ctx.clip();
// 绘制二维码图片
ctx.drawImage(qrImg, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize);
ctx.drawImage(
qrImg,
qrInnerX,
qrInnerY,
qrInnerSize,
qrInnerSize,
);
ctx.restore();
// 恢复上下文状态
@@ -533,8 +608,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 导出图片
Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
fileType: "png",
quality: 0.7,
success: (res) => {
resolve(res.tempFilePath);
},
@@ -556,13 +631,19 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
<Canvas
type="2d"
id="exportCanvasV2"
style={{ position: "fixed", top: "-9999px", left: "-9999px", width: "700px", height: "1200px" }}
style={{
position: "fixed",
top: "-9999px",
left: "-9999px",
width: "700px",
height: "1200px",
}}
/>
</View>
);
});
},
);
RadarChartV2.displayName = "RadarChartV2";
export default RadarChartV2;

View File

@@ -6,7 +6,11 @@ import "./index.scss";
import { EditModal } from "@/components";
import { UserService, PickerOption } from "@/services/userService";
import { PopupPicker } from "@/components/Picker/index";
import { useUserActions, useNicknameChangeStatus, useLastTestResult } from "@/store/userStore";
import {
useUserActions,
useNicknameChangeStatus,
useLastTestResult,
} from "@/store/userStore";
import { UserInfoType } from "@/services/userService";
import {
useCities,
@@ -82,7 +86,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
}) => {
const nickname_change_status = useNicknameChangeStatus();
const { setShowGuideBar } = useGlobalState();
const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
const { updateUserInfo, updateNickname, fetchLastTestResult } =
useUserActions();
const ntrpLevels = useNtrpLevels();
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
@@ -129,6 +134,7 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
location_picker_visible,
ntrp_picker_visible,
occupation_picker_visible,
edit_modal_visible,
];
const allPickersClosed = visibles.every((item) => !item);
// 所有选择器都关闭时,显示 GuideBar否则隐藏
@@ -138,6 +144,7 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
location_picker_visible,
ntrp_picker_visible,
occupation_picker_visible,
edit_modal_visible,
]);
// 职业数据
@@ -295,8 +302,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
// 处理地区选择
const handle_location_change = (e: any) => {
const [country, province, city] = e;
handle_field_edit({ country, province, city });
const [province, city, district] = e;
handle_field_edit({ province, city, district });
};
// 处理NTRP水平选择
@@ -307,8 +314,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
// 处理职业选择
const handle_occupation_change = (e: any) => {
const [country, province, city] = e;
handle_field_edit("occupation", `${country} ${province} ${city}`);
const [firstVal, secondVal, thirdVal] = e;
handle_field_edit("occupation", `${firstVal} ${secondVal} ${thirdVal}`);
};
const handle_edit_modal_cancel = () => {
// 关闭编辑弹窗时显示 GuideBar
@@ -565,12 +572,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<Text></Text>
</View>
) : null}
{user_info.country || user_info.province || user_info.city ? (
{user_info.province || user_info.city || user_info.district ? (
<View
className="tag_item"
onClick={() => editable && handle_open_edit_modal("location")}
>
<Text className="tag_text">{`${user_info.province}${user_info.city}`}</Text>
<Text className="tag_text">{`${user_info.city}${user_info.district}`}</Text>
</View>
) : is_current_user ? (
<View
@@ -665,8 +672,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
visible={location_picker_visible}
setvisible={setLocationPickerVisible}
value={
form_data.country
? [form_data.country, form_data.province, form_data.city]
form_data.province
? [form_data.province, form_data.city, form_data.district]
: getDefaultOption(cities)
}
onChange={handle_location_change}
@@ -678,15 +685,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
showHeader={true}
title="选择 NTRP 自评水平"
ntrpTested={ntrpTested}
options={ntrpLevels.map((level) => ({
text: level,
value: level,
}))}
options={ntrpLevels}
type="ntrp"
img={user_info.avatar_url || ""}
visible={ntrp_picker_visible}
setvisible={setNtrpPickerVisible}
value={[form_data.ntrp_level || "2.5"]}
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]}
onChange={handle_ntrp_level_change}
/>
)}

View File

@@ -1,74 +1,75 @@
import { OSS_BASE } from "@/config/api";
export default {
ICON_REMOVE: require('@/static/publishBall/icon-remove.svg'),
ICON_UPLOAD: require('@/static/publishBall/icon-upload.svg'),
ICON_LOCATION: require('@/static/publishBall/icon-location.svg'),
ICON_GAMEPLAY: require('@/static/publishBall/icon-gameplay.svg'),
ICON_PERSONAL: require('@/static/publishBall/icon-personal.svg'),
ICON_CHANGDA: require('@/static/publishBall/icon-changda.svg'),
ICON_COST: require('@/static/publishBall/icon-cost.svg'),
ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.svg'),
ICON_FILTER: require('@/static/list/icon-filter.svg'),
ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'),
ICON_SEARCH: require('@/static/list/icon-search.svg'),
ICON_PLAY: require('@/static/list/icon-play.svg'),
ICON_SITE: require('@/static/list/icon-site.svg'),
ICON_ARROW_DOWN: require('@/static/list/icon-arrow-down.svg'),
ICON_MENU_ITEM_SELECTED: require('@/static/list/icon-menu-item-selected.svg'),
ICON_ARROW_DOWN_WHITE: require('@/static/list/icon-arrow-down-white.svg'),
ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'),
ICON_ARROW_LEFT: require('@/static/detail/icon-arrow-left.svg'),
ICON_LOGO_GO: require('@/static/detail/icon-logo-go.svg'),
ICON_MAP: require('@/static/publishBall/icon-map.svg'),
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'),
ICON_MAP_SEARCH: require('@/static/publishBall/icon-map-search.svg'),
ICON_HEART_CIRCLE: require('@/static/publishBall/icon-heartcircle.png'),
ICON_ADD: require('@/static/publishBall/icon-add.svg'),
ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
ICON_DELETE: require('@/static/publishBall/icon-delete.svg'),
ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'),
ICON_PLUS: require('@/static/publishBall/icon-plus.svg'),
ICON_GROUP: require('@/static/publishBall/icon-group.svg'),
ICON_PERSON: require('@/static/publishBall/icon-person.svg'),
ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'),
ICON_CIRCLE_UNSELECT: require('@/static/publishBall/icon-circle-unselect.svg'),
ICON_CIRCLE_SELECT: require('@/static/publishBall/icon-circle-select-ring.svg'),
ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'),
ICON_LOGO: require('@/static/logo.svg'),
ICON_CHANGE: require('@/static/list/icon-change.svg'),
ICON_DETAIL_MAP: require('@/static/detail/icon-map.svg'),
ICON_DETAIL_ARROW_RIGHT: require('@/static/detail/icon-arrow-right.svg'),
ICON_DETAIL_NOTICE: require('@/static/detail/icon-notice.svg'),
ICON_DETAIL_APPLICATION_ADD: require('@/static/detail/icon-application-add.svg'),
ICON_DETAIL_COMMENT: require('@/static/detail/icon-comment.svg'),
ICON_DETAIL_COMMENT_LIGHT: require('@/static/detail/icon-comment-light.svg'),
ICON_DETAIL_SHARE: require('@/static/detail/icon-share-light.svg'),
ICON_GUIDE_BAR_PUBLISH: require('@/static/common/guide-bar-publish.svg'),
ICON_NAVIGATOR_BACK: require('@/static/common/navigator-back.svg'),
ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'),
ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'),
ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'),
ICON_LIST_EMPTY: require('@/static/emptyStatus/publish-empty.png'),
ICON_LIST_EMPTY_CARD: require('@/static/emptyStatus/publish-empty-card.png'),
ICON_LIST_SEARCH_SEARCH: require('@/static/search/icon-search.svg'),
ICON_LIST_SEARCH_BACK: require('@/static/search/icon-back.svg'),
ICON_LIST_SEARCH_CLEAR: require('@/static/search/icon-search-clear.svg'),
ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'),
ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'),
ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'),
ICON_IMPORTANT_BTN: require('@/static/publishBall/icon-important-btn.svg'),
ICON_IMPORTANT_BLACK: require('@/static/publishBall/icon-important-black.svg'),
ICON_ARROW_RIGHT_WHITE: require('@/static/publishBall/icon-arrow-right-white.svg'),
ICON_ARROW_RIGHT_BLACK: require('@/static/publishBall/icon-arrow-right-black.svg'),
ICON_EXAMINATION: require('@/static/userInfo/examination.svg'),
ICON_ARROW_GREEN: require('@/static/userInfo/arrow-green.svg'),
ICON_COPY: require('@/static/publishBall/icon-copy.svg'),
ICON_UPLOAD_IMG: require('@/static/publishBall/icon-upload-img.svg'),
ICON_UPLOAD_SUCCESS: require('@/static/publishBall/icon-upload-success.svg'),
ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
ICON_RELOCATE: require('@/static/list/icon-relocate.svg'),
}
ICON_REMOVE: require("@/static/publishBall/icon-remove.svg"),
ICON_UPLOAD: require("@/static/publishBall/icon-upload.svg"),
ICON_LOCATION: require("@/static/publishBall/icon-location.svg"),
ICON_GAMEPLAY: require("@/static/publishBall/icon-gameplay.svg"),
ICON_PERSONAL: require("@/static/publishBall/icon-personal.svg"),
ICON_CHANGDA: require("@/static/publishBall/icon-changda.svg"),
ICON_COST: require("@/static/publishBall/icon-cost.svg"),
ICON_TIPS: require("@/static/publishBall/icon-tips.svg"),
ICON_ARROW_RIGHT: require("@/static/publishBall/icon-arrow-right.svg"),
ICON_FILTER: require("@/static/list/icon-filter.svg"),
ICON_FILTER_SELECTED: require("@/static/list/icon-filter-selected.svg"),
ICON_SEARCH: require("@/static/list/icon-search.svg"),
ICON_PLAY: require("@/static/list/icon-play.svg"),
ICON_SITE: require("@/static/list/icon-site.svg"),
ICON_ARROW_DOWN: require("@/static/list/icon-arrow-down.svg"),
ICON_MENU_ITEM_SELECTED: require("@/static/list/icon-menu-item-selected.svg"),
ICON_ARROW_DOWN_WHITE: require("@/static/list/icon-arrow-down-white.svg"),
ICON_LIST_RIGHT_ARROW: require("@/static/list/icon-list-right-arrow.svg"),
ICON_ARROW_LEFT: require("@/static/detail/icon-arrow-left.svg"),
ICON_LOGO_GO: require("@/static/detail/icon-logo-go.svg"),
ICON_MAP: require("@/static/publishBall/icon-map.svg"),
ICON_STADIUM: require("@/static/publishBall/icon-stadium.svg"),
ICON_ARRORW_SMALL: require("@/static/publishBall/icon-arrow-small.svg"),
ICON_MAP_SEARCH: require("@/static/publishBall/icon-map-search.svg"),
ICON_HEART_CIRCLE: require("@/static/publishBall/icon-heartcircle.png"),
ICON_ADD: require("@/static/publishBall/icon-add.svg"),
ICON_COPY: require("@/static/publishBall/icon-arrow-right.svg"),
ICON_DELETE: require("@/static/publishBall/icon-delete.svg"),
ICON_RIGHT_MAX: require("@/static/publishBall/icon-right-max.svg"),
ICON_PLUS: require("@/static/publishBall/icon-plus.svg"),
ICON_GROUP: require("@/static/publishBall/icon-group.svg"),
ICON_PERSON: require("@/static/publishBall/icon-person.svg"),
ICON_PUBLISH: require("@/static/publishBall/icon-publish.png"),
ICON_CIRCLE_UNSELECT: require("@/static/publishBall/icon-circle-unselect.svg"),
ICON_CIRCLE_SELECT: require("@/static/publishBall/icon-circle-select-ring.svg"),
ICON_CIRCLE_SELECT_ARROW: require("@/static/publishBall/icon-circle-select-arrow.svg"),
ICON_LOGO: require("@/static/logo.svg"),
ICON_CHANGE: require("@/static/list/icon-change.svg"),
ICON_DETAIL_MAP: require("@/static/detail/icon-map.svg"),
ICON_DETAIL_ARROW_RIGHT: require("@/static/detail/icon-arrow-right.svg"),
ICON_DETAIL_NOTICE: require("@/static/detail/icon-notice.svg"),
ICON_DETAIL_APPLICATION_ADD: require("@/static/detail/icon-application-add.svg"),
ICON_DETAIL_COMMENT: require("@/static/detail/icon-comment.svg"),
ICON_DETAIL_COMMENT_LIGHT: require("@/static/detail/icon-comment-light.svg"),
ICON_DETAIL_SHARE: require("@/static/detail/icon-share-light.svg"),
ICON_GUIDE_BAR_PUBLISH: require("@/static/common/guide-bar-publish.svg"),
ICON_NAVIGATOR_BACK: require("@/static/common/navigator-back.svg"),
ICON_LIST_PLAYING_GAME: require("@/static/list/icon-paying-game.svg"),
ICON_LIST_LOAD_ERROR: require("@/static/list/icon-load-error.svg"),
ICON_LIST_RELOAD: require("@/static/list/icon-reload.svg"),
ICON_LIST_EMPTY: require("@/static/emptyStatus/publish-empty.png"),
ICON_LIST_EMPTY_CARD: `${OSS_BASE}/front/ball/images/publish-empty-card.svg`,
ICON_LIST_SEARCH_SEARCH: require("@/static/search/icon-search.svg"),
ICON_LIST_SEARCH_BACK: require("@/static/search/icon-back.svg"),
ICON_LIST_SEARCH_CLEAR: require("@/static/search/icon-search-clear.svg"),
ICON_LIST_SEARCH_CLEAR_HISTORY: require("@/static/search/icon-clear-history.svg"),
ICON_LIST_SEARCH_SUGGESTION: require("@/static/search/icon-search-suggestion.svg"),
ICON_LIST_INPUT_LOGO: require("@/static/list/icon-input-logo.svg"),
ICON_IMPORTANT_BTN: require("@/static/publishBall/icon-important-btn.svg"),
ICON_IMPORTANT_BLACK: require("@/static/publishBall/icon-important-black.svg"),
ICON_ARROW_RIGHT_WHITE: require("@/static/publishBall/icon-arrow-right-white.svg"),
ICON_ARROW_RIGHT_BLACK: require("@/static/publishBall/icon-arrow-right-black.svg"),
ICON_EXAMINATION: require("@/static/userInfo/examination.svg"),
ICON_ARROW_GREEN: require("@/static/userInfo/arrow-green.svg"),
ICON_COPY: require("@/static/publishBall/icon-copy.svg"),
ICON_UPLOAD_IMG: require("@/static/publishBall/icon-upload-img.svg"),
ICON_UPLOAD_SUCCESS: require("@/static/publishBall/icon-upload-success.svg"),
ICON_CLOSE: require("@/static/publishBall/icon-close.svg"),
ICON_LIST_NTPR: require("@/static/list/ntpr.svg"),
ICON_LIST_CHANGDA: require("@/static/list/icon-changda.svg"),
ICON_LIST_CHANGDA_QIuju: require("@/static/list/changdaqiuju.png"),
ICON_RELOCATE: require("@/static/list/icon-relocate.svg"),
};

View File

@@ -40,7 +40,7 @@ function isFull(counts) {
function matchNtrpRequestment(
target?: string,
min?: string,
max?: string
max?: string,
): boolean {
// 目标值为空或 undefined
if (!target?.trim()) return true;
@@ -110,7 +110,7 @@ export default function Participants(props) {
user_action_status;
const showApplicationEntry =
[can_pay, can_substitute, is_substituting, waiting_start].every(
(item) => !item
(item) => !item,
) &&
can_join &&
dayjs(start_time).isAfter(dayjs());
@@ -138,7 +138,7 @@ export default function Participants(props) {
Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent(
fullPath
fullPath,
)}`,
});
}
@@ -153,7 +153,7 @@ export default function Participants(props) {
const matchNtrpReq = matchNtrpRequestment(
userInfo?.ntrp_level,
skill_level_min,
skill_level_max
skill_level_max,
);
function handleSelfEvaluate() {
@@ -180,7 +180,7 @@ export default function Participants(props) {
}
function generateTextAndAction(
user_action_status: null | { [key: string]: boolean }
user_action_status: null | { [key: string]: boolean },
):
| undefined
| { text: string | React.FC; action?: () => void; available?: boolean } {
@@ -259,7 +259,7 @@ export default function Participants(props) {
const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) {
navto(
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`,
);
}
}),
@@ -296,10 +296,11 @@ export default function Participants(props) {
const { action = () => {} } = generateTextAndAction(user_action_status)!;
const leftCount = max_participants - participant_count;
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0);
const leftSubstituteCount =
(max_substitute_players || 0) - (substitute_count || 0);
const showSubstituteApplicationEntry =
[can_pay, can_join, is_substituting, waiting_start].every(
(item) => !item
(item) => !item,
) &&
can_substitute &&
dayjs(start_time).isAfter(dayjs());
@@ -336,7 +337,7 @@ export default function Participants(props) {
refresherBackground="#FAFAFA"
className={classnames(
styles["participants-list-scroll"],
showApplicationEntry ? styles.withApplication : ""
showApplicationEntry ? styles.withApplication : "",
)}
scrollX
>
@@ -377,14 +378,14 @@ export default function Participants(props) {
src={avatar_url}
onClick={handleViewUserInfo.bind(
null,
participant_user_id
participant_user_id,
)}
/>
<Text className={styles["participants-list-item-name"]}>
{nickname || "未知"}
</Text>
<Text className={styles["participants-list-item-level"]}>
{displayNtrp}
NTRP {displayNtrp}
</Text>
<Text className={styles["participants-list-item-role"]}>
{role}
@@ -400,12 +401,17 @@ export default function Participants(props) {
)}
</View>
{/* 候补区域 */}
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && (
{max_substitute_players > 0 &&
(substitute_count > 0 || showSubstituteApplicationEntry) && (
<View className={styles["detail-page-content-participants"]}>
<View className={styles["participants-title"]}>
<Text></Text>
<Text>·</Text>
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text>
<Text>
{leftSubstituteCount > 0
? `剩余空位 ${leftSubstituteCount}`
: "已满员"}
</Text>
</View>
<View className={styles["participants-list"]}>
{/* 候补申请入口 */}
@@ -420,7 +426,9 @@ export default function Participants(props) {
className={styles["participants-list-application-icon"]}
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className={styles["participants-list-application-text"]}>
<Text
className={styles["participants-list-application-text"]}
>
</Text>
</View>
@@ -430,7 +438,7 @@ export default function Participants(props) {
refresherBackground="#FAFAFA"
className={classnames(
styles["participants-list-scroll"],
showSubstituteApplicationEntry ? styles.withApplication : ""
showSubstituteApplicationEntry ? styles.withApplication : "",
)}
scrollX
>
@@ -438,7 +446,8 @@ export default function Participants(props) {
className={styles["participants-list-scroll-content"]}
style={{
width: `${
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8
Math.max(substitute_members.length, 1) * 103 +
(Math.max(substitute_members.length, 1) - 1) * 8
}px`,
}}
>
@@ -471,13 +480,15 @@ export default function Participants(props) {
src={avatar_url}
onClick={handleViewUserInfo.bind(
null,
substitute_user_id
substitute_user_id,
)}
/>
<Text className={styles["participants-list-item-name"]}>
{nickname || "未知"}
</Text>
<Text className={styles["participants-list-item-level"]}>
<Text
className={styles["participants-list-item-level"]}
>
{displayNtrp}
</Text>
<Text className={styles["participants-list-item-role"]}>

View File

@@ -25,9 +25,10 @@ dayjs.locale("zh-cn");
// 分享弹窗
export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const [visible, setVisible] = useState(false);
const [publishFlag, setPublishFlag] = useState(false);
const [shareImageUrl, setShareImageUrl] = useState("");
const { fetchUserInfo } = useUserActions();
const publishFlag = from === "publish";
// const posterRef = useRef();
const { max_participants, participant_count } = detail || {};
@@ -57,18 +58,20 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
}
useImperativeHandle(ref, () => ({
show: async (publish_flag = false) => {
setPublishFlag(publish_flag);
if (publish_flag) {
try {
const url = await generateShareImageUrl();
setShareImageUrl(url);
} catch (e) {}
}
show: async () => {
setVisible(true);
},
}));
useEffect(() => {
if (from === "publish") {
generateShareImageUrl().then((url) => {
setShareImageUrl(url);
setVisible(true);
});
}
}, [from]);
async function generateShareImageUrl() {
const {
play_type,
@@ -106,7 +109,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
// console.log(res, "res");
return {
title: detail.title,
imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg",
imageUrl: url,
path: `/game_pages/detail/index?id=${id}&from=share`,
};
});
@@ -183,7 +186,6 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
function onClose() {
setVisible(false);
setPublishFlag(false);
}
return (

View File

@@ -54,12 +54,6 @@ function Index() {
await waitForAuthInit();
// 然后再获取用户信息
await fetchUserInfo();
await delay(1000);
if (from === "publish") {
handleShare(true);
}
};
init();
}, []);
@@ -126,8 +120,12 @@ function Index() {
}
}
function handleShare(flag = false) {
sharePopupRef.current.show(flag);
function handleShare() {
if (!detail.id) {
toast("球局未加载完成,请稍后再试");
return false;
}
sharePopupRef.current.show();
}
const handleJoinGame = async () => {
@@ -293,6 +291,7 @@ function Index() {
currentUserInfo={myInfo}
/>
{/* share popup */}
{detail.id && myInfo.id && (
<SharePopup
ref={sharePopupRef}
id={id as string}
@@ -300,6 +299,7 @@ function Index() {
detail={detail}
userInfo={myInfo}
/>
)}
</View>
</ScrollView>
);

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { View, Text, Button, Image } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
import { GeneralNavbar } from "@/components";
import {
wechat_auth_login,
save_login_state,
@@ -171,6 +172,8 @@ const LoginPage: React.FC = () => {
<View className="bg_overlay"></View>
</View>
<GeneralNavbar title="" showBack={true} showAvatar={false} onBack={handle_return_home} />
{/* 主要内容 */}
<View className="login_main_content">
{/* 品牌区域 */}
@@ -216,9 +219,9 @@ const LoginPage: React.FC = () => {
<Text className="button_text"></Text>
</Button>
<View className="return_home_button link_button" onClick={handle_return_home}>
{/* <View className="return_home_button link_button" onClick={handle_return_home}>
<Text className="button_text">返回首页</Text>
</View>
</View> */}
{/* 用户协议复选框 */}
<View className="terms_checkbox_section">

View File

@@ -16,7 +16,9 @@ interface MyselfPageContentProps {
isActive?: boolean;
}
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }) => {
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
isActive = true,
}) => {
const pickerOption = usePickerOption();
const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
@@ -65,20 +67,22 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
game_records: TennisMatch[]
): { notEndGames: TennisMatch[]; finishedGames: TennisMatch[] } => {
const now = new Date().getTime();
return game_records.reduce(
(result, cur) => {
let { end_time } = cur;
end_time = end_time.replace(/\s/, "T");
new Date(end_time).getTime() > now
? result.notEndGames.push(cur)
: result.finishedGames.push(cur);
return result;
},
{
notEndGames: [] as TennisMatch[],
finishedGames: [] as TennisMatch[],
// 使用for
const notEndGames: TennisMatch[] = [];
const finishedGames: TennisMatch[] = [];
for (const game of game_records) {
const { end_time } = game;
const end_time_str = end_time.replace(/\s/, "T");
new Date(end_time_str).getTime() > now
? notEndGames.push(game)
: finishedGames.push(game);
}
);
console.log("notEndGames", notEndGames);
return { notEndGames, finishedGames };
},
[]
);
@@ -95,6 +99,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
} else {
games_data = await UserService.get_participated_games(user_info.id);
}
const sorted_games = games_data.sort((a, b) => {
return (
new Date(a.original_start_time.replace(/\s/, "T")).getTime() -
@@ -102,6 +108,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
);
});
const { notEndGames, finishedGames } = classifyGameRecords(sorted_games);
console.log("notEndGames", notEndGames);
set_game_records(notEndGames);
setEndedGameRecords(finishedGames);
} catch (error) {
@@ -250,16 +258,14 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
<View className={styles.gameTabsSection}>
<View className={styles.tabContainer}>
<View
className={`${styles.tabItem} ${
active_tab === "hosted" ? styles.active : ""
className={`${styles.tabItem} ${active_tab === "hosted" ? styles.active : ""
}`}
onClick={() => setActiveTab("hosted")}
>
<Text className={styles.tabText}></Text>
</View>
<View
className={`${styles.tabItem} ${
active_tab === "participated" ? styles.active : ""
className={`${styles.tabItem} ${active_tab === "participated" ? styles.active : ""
}`}
onClick={() => setActiveTab("participated")}
>
@@ -288,9 +294,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
overflow: "hidden",
}}
listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="320px"
listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
listLoadErrorWidth="410px"
listLoadErrorHeight="185px"
defaultShowNum={3}
/>
</ScrollView>
@@ -312,9 +317,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
collapse={true}
style={{ paddingBottom: "90px", overflow: "hidden" }}
listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="320px"
listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
listLoadErrorWidth="410px"
listLoadErrorHeight="185px"
defaultShowNum={3}
/>
</ScrollView>

View File

@@ -249,7 +249,7 @@ const CommentReply = () => {
<View className="comment-left">
<Image
className="user-avatar"
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
src={item.user_avatar }
mode="aspectFill"
onClick={(e) => handleUserClick(e, item.user_id)}
/>

View File

@@ -16,7 +16,7 @@ import { useGlobalState } from "@/store/global";
import { delay, getCurrentFullPath } from "@/utils";
import { formatNtrpDisplay } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import httpService from "@/services/httpService";
// import httpService from "@/services/httpService";
import DetailService from "@/services/detailService";
import { OSS_BASE } from "@/config/api";
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";

View File

@@ -72,6 +72,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
}, ref) => {
const [openPicker, setOpenPicker] = useState(false); //为了解决上传图片时按钮样式问题
const [scrollTop, setScrollTop] = useState(0);
const [isTextareaFocused, setIsTextareaFocused] = useState(false); // 记录 TextareaTag 是否 focus
const { getDictionaryValue } = useDictionaryActions()
const court_type = getDictionaryValue('court_type') || []
const court_surface = getDictionaryValue('court_surface') || []
@@ -199,12 +200,12 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
}
}
// 当键盘显示触发 changeTextarea
// 当键盘显示且 TextareaTag 当前 focus 时才触发 changeTextarea
useEffect(() => {
if (isKeyboardVisible) {
if (isKeyboardVisible && isTextareaFocused) {
changeTextarea(true)
}
}, [isKeyboardVisible])
}, [isKeyboardVisible, isTextareaFocused])
const changePicker = (value:boolean) => {
setOpenPicker(value);
@@ -269,11 +270,15 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<TextareaTag
value={formData[item.prop]}
onChange={(value) => {
//changeTextarea(true)
updateFormData(item.prop, value)
}}
// onBlur={() => changeTextarea(false)}
onFocus={() => changeTextarea(true)}
onBlur={() => {
setIsTextareaFocused(false)
}}
onFocus={() => {
setIsTextareaFocused(true)
changeTextarea(true)
}}
placeholder='有其他场地信息可备注'
options={(item.options || []).map((o) => ({ label: o, value: o }))}
/>

View File

@@ -151,6 +151,7 @@ interface BackendGameData {
longitude: string;
venue_type: string;
surface_type: string;
distance_km: string;
};
participants: {
user: {
@@ -206,7 +207,7 @@ export class UserService {
latitude = parseFloat(game.venue_dtl.latitude) || latitude;
longitude = parseFloat(game.venue_dtl.longitude) || longitude;
}
const distance = this.calculate_distance(latitude, longitude);
// 处理地点信息 - 优先使用venue_dtl中的信息
let location = game.location_name || game.location || "未知地点";
@@ -227,7 +228,7 @@ export class UserService {
original_start_time: game.start_time,
end_time: game.end_time || "",
location: location,
distance_km: parseFloat(distance.replace("km", "")) || 0,
distance_km: game.venue_dtl?.distance_km ,
current_players: registered_count,
max_players: max_count,
skill_level_min: parseInt(game.skill_level_min) || 0,
@@ -303,20 +304,7 @@ export class UserService {
return `${date_str} ${time_str}`;
}
// 计算距离(模拟实现,实际需要根据用户位置计算)
private static calculate_distance(
latitude: number,
longitude: number
): string {
if (latitude === 0 && longitude === 0) {
return "未知距离";
}
// 这里应该根据用户当前位置计算实际距离
// 暂时返回模拟距离
const distances = ["1.2km", "2.5km", "3.8km", "5.1km", "7.3km"];
return distances[Math.floor(Math.random() * distances.length)];
}
// 获取用户信息
static async get_user_info(user_id?: string): Promise<UserInfo> {
try {
@@ -359,8 +347,6 @@ export class UserService {
last_location_province: userData.last_location_province || "",
last_location_city: userData.last_location_city || "",
};
} else {
throw new Error(response.message || "获取用户信息失败");
}
@@ -747,7 +733,7 @@ export const updateUserLocation = async (
const response = await httpService.post("/user/update_location", {
latitude,
longitude,
force
force,
});
return response;
} catch (error) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -5,7 +5,7 @@ import { UserService } from "@/services/userService";
export interface PickerOptionState {
cities: any[];
professions: any[];
ntrpLevels: string[];
ntrpLevels: any[];
getCities: () => Promise<any>;
getProfessions: () => Promise<any>;
}
@@ -13,7 +13,40 @@ export interface PickerOptionState {
export const usePickerOption = create<PickerOptionState>((set) => ({
cities: [],
professions: [],
ntrpLevels: ["1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5", "4.5+"],
ntrpLevels: [
{
text: "1.5",
value: "1.5",
},
{
text: "2.0",
value: "2.0",
},
{
text: "2.5",
value: "2.5",
},
{
text: "3.0",
value: "3.0",
},
{
text: "3.5",
value: "3.5",
},
{
text: "4.0",
value: "4.0",
},
{
text: "4.5",
value: "4.5",
},
{
text: "4.5+",
value: "4.5+",
},
],
getCities: async () => {
try {
const res = await UserService.getCities();

View File

@@ -8,7 +8,9 @@ import {
NicknameChangeStatus,
updateNickname as updateNicknameApi,
} from "@/services/userService";
import evaluateService, { LastTimeTestResult } from "@/services/evaluateService";
import evaluateService, {
LastTimeTestResult,
} from "@/services/evaluateService";
import { useListStore } from "./listStore";
export interface UserState {
@@ -23,7 +25,6 @@ export interface UserState {
fetchLastTestResult: () => Promise<LastTimeTestResult | null>;
}
const getTimeNextDate = (time: string) => {
const date = new Date(time);
date.setDate(date.getDate() + 1);
@@ -51,8 +52,6 @@ export const useUser = create<UserState>()((set) => ({
const cachedCity = (Taro as any).getStorageSync?.(CITY_CACHE_KEY);
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
// 如果有缓存的城市,使用缓存,不更新 area
console.log("[userStore] 检测到缓存的城市,使用缓存,不更新 area");
@@ -66,7 +65,10 @@ export const useUser = create<UserState>()((set) => ({
// 只有当 area 不存在时才使用用户信息中的位置
if (!currentArea) {
const newArea: [string, string] = [userData.last_location_province||"", userData.last_location_city||""];
const newArea: [string, string] = [
userData.last_location_province || "",
userData.last_location_city || "",
];
listStore.updateArea(newArea);
// 保存到缓存
useUser.getState().updateCache(newArea);
@@ -102,8 +104,14 @@ export const useUser = create<UserState>()((set) => ({
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 || "", userInfo.last_location_city || ""];
if (
!currentArea ||
currentArea[1] !== userInfo.last_location_province
) {
const newArea: [string, string] = [
userInfo.last_location_province || "",
userInfo.last_location_city || "",
];
listStore.updateArea(newArea);
}
}
@@ -127,7 +135,10 @@ export const useUser = create<UserState>()((set) => ({
// 如果已经有状态数据且不是强制更新,跳过,避免重复调用
if (!force) {
const currentState = useUser.getState();
if (currentState.nicknameChangeStatus && Object.keys(currentState.nicknameChangeStatus).length > 0) {
if (
currentState.nicknameChangeStatus &&
Object.keys(currentState.nicknameChangeStatus).length > 0
) {
return;
}
}

View File

@@ -44,6 +44,7 @@ const EditProfilePage: React.FC = () => {
country: info?.country ?? "",
province: info?.province ?? "",
city: info?.city ?? "",
district: info?.district ?? "",
};
};
const [form_data, setFormData] = useState(getInitialFormData());
@@ -85,6 +86,7 @@ const EditProfilePage: React.FC = () => {
country: info?.country ?? "",
province: info?.province ?? "",
city: info?.city ?? "",
district: info?.district ?? "",
});
}
@@ -358,11 +360,11 @@ const EditProfilePage: React.FC = () => {
});
return;
}
const [country, province, city] = e;
const [province, city, district] = e;
handle_field_edit({
country: String(country ?? ""),
province: String(province ?? ""),
city: String(city ?? ""),
district: String(district ?? ""),
});
};
@@ -660,15 +662,17 @@ const EditProfilePage: React.FC = () => {
<View className="item_right">
<Text
className={`item_value ${
form_data.country ||
form_data.province ||
form_data.city
form_data.city ||
form_data.district
? ""
: "placehoder"
}`}
>
{form_data.country || form_data.province || form_data.city
? `${form_data.country} ${form_data.province} ${form_data.city}`
{form_data.province ||
form_data.city ||
form_data.district
? `${form_data.province} ${form_data.city} ${form_data.district}`
: "选择所在地区"}
</Text>
<Image
@@ -885,8 +889,8 @@ const EditProfilePage: React.FC = () => {
visible={location_picker_visible}
setvisible={setLocationPickerVisible}
value={
form_data.country
? [form_data.country, form_data.province, form_data.city]
form_data.province
? [form_data.province, form_data.city, form_data.district]
: getDefaultOption(cities)
}
onChange={handle_location_change}
@@ -899,15 +903,12 @@ const EditProfilePage: React.FC = () => {
title="选择 NTRP 自评水平"
confirmText="保存"
ntrpTested={ntrpTested}
options={ntrpLevels.map((level) => ({
text: level,
value: level,
}))}
options={ntrpLevels}
type="ntrp"
// img={(user_info as UserInfoType)?.avatar_url}
visible={ntrp_picker_visible}
setvisible={setNtrpPickerVisible}
value={[form_data.ntrp_level || "2.5"]}
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]}
onChange={handle_ntrp_level_change}
/>
)}

View File

@@ -329,9 +329,8 @@ const OtherUserPage: React.FC = () => {
overflow: "hidden",
}}
listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="320px"
listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
listLoadErrorWidth="410px"
listLoadErrorHeight="185px"
defaultShowNum={3}
/>
</ScrollView>
@@ -375,9 +374,8 @@ const OtherUserPage: React.FC = () => {
collapse={true}
style={{ paddingBottom: "90px", overflow: "hidden" }}
listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="320px"
listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
listLoadErrorWidth="410px"
listLoadErrorHeight="185px"
defaultShowNum={3}
/>
</ScrollView>

View File

@@ -35,12 +35,21 @@ export function base64ToTempFilePath(base64Data: string): Promise<string> {
}
interface TaroGetImageInfo {
getImageInfo(option: {
src: string;
success?: (res: { width: number; height: number }) => void;
fail?: (err: unknown) => void;
}): void;
}
/** 获取图片宽高 */
function getImageWh(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
Taro.getImageInfo({
return new Promise((resolve, reject) => {
(Taro as TaroGetImageInfo).getImageInfo({
src,
success: ({ width, height }) => resolve({ width, height }),
fail: (e) => reject(e),
});
});
}
@@ -282,7 +291,9 @@ function drawTextWrap(
/** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!");
const dpr = Taro.getWindowInfo().pixelRatio;
// const dpr = Taro.getWindowInfo().pixelRatio;
const dpr = 1;
// console.log(dpr, 'dpr')
const width = 600;
const height = 1000;
@@ -433,7 +444,7 @@ export async function generatePosterImage(data: any): Promise<string> {
const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
quality: 0.7,
});
return tempFilePath;
}