498 lines
16 KiB
TypeScript
498 lines
16 KiB
TypeScript
import React, { useRef } from "react";
|
||
import Taro from "@tarojs/taro";
|
||
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
||
import dayjs from "dayjs";
|
||
import classnames from "classnames";
|
||
import img from "@/config/images";
|
||
import { useUserInfo } from "@/store/userStore";
|
||
import { formatNtrpDisplay, toast, navto } from "@/utils/helper";
|
||
// import RMB_ICON from "@/static/detail/rmb.svg";
|
||
import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService";
|
||
import OrderService from "@/services/orderService";
|
||
import styles from "./index.module.scss";
|
||
import NTRPEvaluatePopup from "@/components/NTRPEvaluatePopup";
|
||
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
|
||
|
||
function isFull(counts) {
|
||
const {
|
||
max_players,
|
||
current_players,
|
||
max_substitute_players,
|
||
current_substitute_count,
|
||
is_substitute_supported,
|
||
} = counts;
|
||
|
||
if (
|
||
max_players === current_players &&
|
||
is_substitute_supported === IsSubstituteSupported.NOTSUPPORT
|
||
) {
|
||
return true;
|
||
} else if (
|
||
max_players === current_players &&
|
||
is_substitute_supported === IsSubstituteSupported.SUPPORT
|
||
) {
|
||
return max_substitute_players === current_substitute_count;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function matchNtrpRequestment(
|
||
target?: string,
|
||
min?: string,
|
||
max?: string
|
||
): boolean {
|
||
// 目标值为空或 undefined
|
||
if (!target?.trim()) return true;
|
||
|
||
// 提取目标值中的第一个数字
|
||
const match = target.match(/-?\d+(\.\d+)?/);
|
||
if (!match) return true;
|
||
|
||
const value = parseFloat(match[0]);
|
||
const minNum = min !== undefined ? parseFloat(min) : undefined;
|
||
const maxNum = max !== undefined ? parseFloat(max) : undefined;
|
||
|
||
// min 和 max 都未定义 → 直接通过
|
||
if (minNum === undefined && maxNum === undefined) return true;
|
||
|
||
// min = max 或只有一边 undefined → 参考值判断,包含端点
|
||
if (minNum === undefined || maxNum === undefined || minNum === maxNum) {
|
||
return value >= (minNum ?? maxNum!);
|
||
}
|
||
|
||
// 正常区间判断,包含端点
|
||
return value >= minNum && value <= maxNum;
|
||
}
|
||
|
||
// 参与者
|
||
export default function Participants(props) {
|
||
const { detail = {}, handleJoinGame, handleViewUserInfo } = props;
|
||
const ntrpRef = useRef<{
|
||
show: (evaluateCallback: EvaluateCallback) => void;
|
||
}>({ show: () => {} });
|
||
const userInfo = useUserInfo();
|
||
const participants = detail.participants || [];
|
||
const substitute_members = detail.substitute_members || [];
|
||
// const participants = Array(10)
|
||
// .fill(0)
|
||
// .map((_, index) => ({
|
||
// id: 337 + index,
|
||
// join_time: "2025-12-06 11:06:24",
|
||
// status: "joined",
|
||
// is_organizer: true,
|
||
// user: {
|
||
// id: 18,
|
||
// nickname: "小猫开刀削面店往猫毛里面下面条",
|
||
// avatar_url:
|
||
// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/d284060f-248b-4d58-a153-4d37c0ca77c8.jpg",
|
||
// phone: "18513125687",
|
||
// ntrp_level: "1.5",
|
||
// },
|
||
// }));
|
||
const {
|
||
participant_count,
|
||
max_participants,
|
||
substitute_count,
|
||
max_substitute_players,
|
||
user_action_status = {},
|
||
start_time,
|
||
price,
|
||
ntrp_level,
|
||
skill_level_min,
|
||
skill_level_max,
|
||
is_organizer,
|
||
match_status,
|
||
end_time,
|
||
id,
|
||
} = detail || {};
|
||
const { can_join, can_pay, can_substitute, is_substituting, waiting_start } =
|
||
user_action_status;
|
||
const showApplicationEntry =
|
||
[can_pay, can_substitute, is_substituting, waiting_start].every(
|
||
(item) => !item
|
||
) &&
|
||
can_join &&
|
||
dayjs(start_time).isAfter(dayjs());
|
||
|
||
// 检查手机号绑定的包装函数
|
||
const checkPhoneAndExecute = (action: () => void) => {
|
||
return () => {
|
||
if (!userInfo?.phone) {
|
||
Taro.showModal({
|
||
title: "提示",
|
||
content: "该功能需要绑定手机号",
|
||
confirmText: "去绑定",
|
||
cancelText: "取消",
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
const currentPath = Taro.getCurrentInstance().router?.path || "";
|
||
const currentParams =
|
||
Taro.getCurrentInstance().router?.params || {};
|
||
const queryString = Object.keys(currentParams)
|
||
.map((key) => `${key}=${currentParams[key]}`)
|
||
.join("&");
|
||
const fullPath = queryString
|
||
? `${currentPath}?${queryString}`
|
||
: currentPath;
|
||
|
||
Taro.navigateTo({
|
||
url: `/login_pages/index/index?redirect=${encodeURIComponent(
|
||
fullPath
|
||
)}`,
|
||
});
|
||
}
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
action();
|
||
};
|
||
};
|
||
|
||
const matchNtrpReq = matchNtrpRequestment(
|
||
userInfo?.ntrp_level,
|
||
skill_level_min,
|
||
skill_level_max
|
||
);
|
||
|
||
function handleSelfEvaluate() {
|
||
ntrpRef?.current?.show({
|
||
type: EvaluateScene.detail,
|
||
next: ({ flag, score }) => {
|
||
if (!matchNtrpRequestment(score, skill_level_min, skill_level_max)) {
|
||
toast("您当前不符合此球局NTRP水平要求,去看看其他活动吧~");
|
||
return;
|
||
}
|
||
if (flag) {
|
||
Taro.navigateTo({
|
||
url: `/order_pages/orderDetail/index?gameId=${id}`,
|
||
});
|
||
return;
|
||
}
|
||
Taro.redirectTo({ url: `/order_pages/orderDetail/index?gameId=${id}` });
|
||
},
|
||
onCancel: () => {
|
||
// Taro.redirectTo({ url: `/game_pages/detail/index?id=${id}` });
|
||
Taro.navigateBack();
|
||
},
|
||
});
|
||
}
|
||
|
||
function generateTextAndAction(
|
||
user_action_status: null | { [key: string]: boolean }
|
||
):
|
||
| undefined
|
||
| { text: string | React.FC; action?: () => void; available?: boolean } {
|
||
if (!user_action_status) {
|
||
return;
|
||
}
|
||
// const priceStrArr = price?.toString().split(".") ?? [];
|
||
// const displayPrice = is_organizer ? (
|
||
// <>
|
||
// <Text className={styles.integer}>0</Text>
|
||
// {/* <Text className={styles.decimalPart}>.00</Text> */}
|
||
// </>
|
||
// ) : (
|
||
// <>
|
||
// <Text className={styles.integer}>{priceStrArr[0]}</Text>
|
||
// <Text className={styles.decimalPart}>.{priceStrArr[1]}</Text>
|
||
// </>
|
||
// );
|
||
// user_action_status.can_assess = true;
|
||
// user_action_status.can_join = false;
|
||
// console.log(user_action_status, "user_action");
|
||
const {
|
||
can_assess,
|
||
can_join,
|
||
can_substitute,
|
||
can_pay,
|
||
is_substituting,
|
||
waiting_start,
|
||
} = user_action_status || {};
|
||
|
||
if (MATCH_STATUS.CANCELED === match_status) {
|
||
return {
|
||
text: "活动已取消",
|
||
available: false,
|
||
action: () => toast("活动已取消,去看看其他活动吧~"),
|
||
};
|
||
} else if (MATCH_STATUS.FINISHED === match_status) {
|
||
return {
|
||
text: "活动已结束",
|
||
available: false,
|
||
action: () => toast("活动已结束,去看看其他活动吧~"),
|
||
};
|
||
} else if (dayjs(end_time).isBefore(dayjs())) {
|
||
return {
|
||
text: "活动已结束",
|
||
available: false,
|
||
action: () => toast("活动已结束,去看看其他活动吧~"),
|
||
};
|
||
} else if (dayjs(start_time).isBefore(dayjs())) {
|
||
return {
|
||
text: "活动已开始",
|
||
available: false,
|
||
action: () => toast("活动已开始,去看看其他活动吧~"),
|
||
};
|
||
} else if (isFull(detail)) {
|
||
return {
|
||
text: "活动已满员",
|
||
available: false,
|
||
action: () => toast("活动已满员,去看看其他活动吧~"),
|
||
};
|
||
}
|
||
if (waiting_start) {
|
||
return {
|
||
text: "已加入",
|
||
action: () => toast("您已参与了本次活动"),
|
||
};
|
||
} else if (is_substituting) {
|
||
return {
|
||
text: "已加入候补",
|
||
action: () => toast("您已加入候补,候补失败会全额退款~"),
|
||
};
|
||
} else if (can_pay) {
|
||
return {
|
||
text: "继续支付",
|
||
action: checkPhoneAndExecute(async () => {
|
||
const res = await OrderService.getUnpaidOrder(id);
|
||
if (res.code === 0) {
|
||
navto(
|
||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
|
||
);
|
||
}
|
||
}),
|
||
};
|
||
} else if (!matchNtrpReq) {
|
||
return {
|
||
text: "立即加入1",
|
||
available: false,
|
||
action: () =>
|
||
toast("您当前不符合此球局NTRP水平要求,去看看其他活动吧~"),
|
||
};
|
||
} else if (can_substitute) {
|
||
return {
|
||
text: "我要候补",
|
||
action: checkPhoneAndExecute(handleJoinGame),
|
||
};
|
||
} else if (can_join) {
|
||
return {
|
||
text: "立即加入2",
|
||
action: checkPhoneAndExecute(handleJoinGame),
|
||
};
|
||
} else if (can_assess) {
|
||
return {
|
||
text: "立即加入3",
|
||
action: checkPhoneAndExecute(handleSelfEvaluate),
|
||
};
|
||
}
|
||
return {
|
||
text: "球局无法加入",
|
||
available: false,
|
||
};
|
||
}
|
||
|
||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||
|
||
const leftCount = max_participants - participant_count;
|
||
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0);
|
||
const showSubstituteApplicationEntry =
|
||
[can_pay, can_join, is_substituting, waiting_start].every(
|
||
(item) => !item
|
||
) &&
|
||
can_substitute &&
|
||
dayjs(start_time).isAfter(dayjs());
|
||
|
||
return (
|
||
<>
|
||
<View className={styles["detail-page-content-participants"]}>
|
||
<View className={styles["participants-title"]}>
|
||
<Text>参与者</Text>
|
||
<Text>·</Text>
|
||
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"}</Text>
|
||
</View>
|
||
{participant_count > 0 || showApplicationEntry ? (
|
||
<View className={styles["participants-list"]}>
|
||
{/* application */}
|
||
{showApplicationEntry && (
|
||
<View
|
||
className={styles["participants-list-application"]}
|
||
onClick={() => {
|
||
action?.();
|
||
}}
|
||
>
|
||
<Image
|
||
className={styles["participants-list-application-icon"]}
|
||
src={img.ICON_DETAIL_APPLICATION_ADD}
|
||
/>
|
||
<Text className={styles["participants-list-application-text"]}>
|
||
申请加入
|
||
</Text>
|
||
</View>
|
||
)}
|
||
{/* participants list */}
|
||
<ScrollView
|
||
refresherBackground="#FAFAFA"
|
||
className={classnames(
|
||
styles["participants-list-scroll"],
|
||
showApplicationEntry ? styles.withApplication : ""
|
||
)}
|
||
scrollX
|
||
>
|
||
<View
|
||
className={styles["participants-list-scroll-content"]}
|
||
style={{
|
||
width: `${
|
||
participants.length * 103 + (participants.length - 1) * 8
|
||
}px`,
|
||
}}
|
||
>
|
||
{participants.map((participant) => {
|
||
const {
|
||
is_organizer,
|
||
user: {
|
||
avatar_url,
|
||
nickname,
|
||
level,
|
||
ntrp_level,
|
||
id: participant_user_id,
|
||
},
|
||
} = participant;
|
||
const role = is_organizer ? "组织者" : "参与者";
|
||
// 优先使用 ntrp_level,如果没有则使用 level
|
||
const ntrpValue = ntrp_level || level;
|
||
// 格式化显示 NTRP,如果没有值则显示"初学者"
|
||
const displayNtrp = ntrpValue
|
||
? formatNtrpDisplay(ntrpValue)
|
||
: "初学者";
|
||
return (
|
||
<View
|
||
key={participant.id}
|
||
className={styles["participants-list-item"]}
|
||
>
|
||
<Image
|
||
className={styles["participants-list-item-avatar"]}
|
||
mode="aspectFill"
|
||
src={avatar_url}
|
||
onClick={handleViewUserInfo.bind(
|
||
null,
|
||
participant_user_id
|
||
)}
|
||
/>
|
||
<Text className={styles["participants-list-item-name"]}>
|
||
{nickname || "未知"}
|
||
</Text>
|
||
<Text className={styles["participants-list-item-level"]}>
|
||
{displayNtrp}
|
||
</Text>
|
||
<Text className={styles["participants-list-item-role"]}>
|
||
{role}
|
||
</Text>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
) : (
|
||
""
|
||
)}
|
||
</View>
|
||
{/* 候补区域 */}
|
||
{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>
|
||
</View>
|
||
<View className={styles["participants-list"]}>
|
||
{/* 候补申请入口 */}
|
||
{showSubstituteApplicationEntry && (
|
||
<View
|
||
className={styles["participants-list-application"]}
|
||
onClick={() => {
|
||
action?.();
|
||
}}
|
||
>
|
||
<Image
|
||
className={styles["participants-list-application-icon"]}
|
||
src={img.ICON_DETAIL_APPLICATION_ADD}
|
||
/>
|
||
<Text className={styles["participants-list-application-text"]}>
|
||
申请候补
|
||
</Text>
|
||
</View>
|
||
)}
|
||
{/* 候补成员列表 */}
|
||
<ScrollView
|
||
refresherBackground="#FAFAFA"
|
||
className={classnames(
|
||
styles["participants-list-scroll"],
|
||
showSubstituteApplicationEntry ? styles.withApplication : ""
|
||
)}
|
||
scrollX
|
||
>
|
||
<View
|
||
className={styles["participants-list-scroll-content"]}
|
||
style={{
|
||
width: `${
|
||
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8
|
||
}px`,
|
||
}}
|
||
>
|
||
{substitute_members.map((substitute) => {
|
||
const {
|
||
is_organizer,
|
||
user: {
|
||
avatar_url,
|
||
nickname,
|
||
level,
|
||
ntrp_level,
|
||
id: substitute_user_id,
|
||
},
|
||
} = substitute;
|
||
const role = is_organizer ? "组织者" : "参与者";
|
||
// 优先使用 ntrp_level,如果没有则使用 level
|
||
const ntrpValue = ntrp_level || level;
|
||
// 格式化显示 NTRP,如果没有值则显示"初学者"
|
||
const displayNtrp = ntrpValue
|
||
? formatNtrpDisplay(ntrpValue)
|
||
: "初学者";
|
||
return (
|
||
<View
|
||
key={substitute.id}
|
||
className={styles["participants-list-item"]}
|
||
>
|
||
<Image
|
||
className={styles["participants-list-item-avatar"]}
|
||
mode="aspectFill"
|
||
src={avatar_url}
|
||
onClick={handleViewUserInfo.bind(
|
||
null,
|
||
substitute_user_id
|
||
)}
|
||
/>
|
||
<Text className={styles["participants-list-item-name"]}>
|
||
{nickname || "未知"}
|
||
</Text>
|
||
<Text className={styles["participants-list-item-level"]}>
|
||
{displayNtrp}
|
||
</Text>
|
||
<Text className={styles["participants-list-item-role"]}>
|
||
{role}
|
||
</Text>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
</View>
|
||
)}
|
||
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
|
||
</>
|
||
);
|
||
}
|