feat: 申请加入、退款政策、支付倒计时、详情页发布者信息展示
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
&-message {
|
||||
@@ -41,7 +42,11 @@
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px; /* 184.615% */
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
&-stats {
|
||||
@@ -65,6 +70,7 @@
|
||||
|
||||
.organizer-actions {
|
||||
display: flex;
|
||||
flex: 0 0 103px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
|
||||
@@ -44,7 +44,9 @@ function genRecommendGames(games, location, avatar) {
|
||||
checkedApplications: current_players,
|
||||
levelRequirements:
|
||||
skill_level_max !== skill_level_min
|
||||
? `${formatNtrpDisplay(skill_level_min) || "-"}-${formatNtrpDisplay(skill_level_max) || "-"}`
|
||||
? `${formatNtrpDisplay(skill_level_min) || "-"}-${
|
||||
formatNtrpDisplay(skill_level_max) || "-"
|
||||
}`
|
||||
: skill_level_min === "1"
|
||||
? "无要求"
|
||||
: `${formatNtrpDisplay(skill_level_min)}以上`,
|
||||
@@ -125,7 +127,9 @@ export default function OrganizerInfo(props) {
|
||||
styles["organizer-avatar-name-message-stats-separator"]
|
||||
}
|
||||
/>
|
||||
<Text>NTRP {ntrp_level ? formatNtrpDisplay(ntrp_level) : "初学者"}</Text>
|
||||
<Text>
|
||||
NTRP {ntrp_level ? formatNtrpDisplay(ntrp_level) : "初学者"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles["organizer-actions"]}>
|
||||
|
||||
@@ -1,19 +1,91 @@
|
||||
import React, { useRef } from "react";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { View, Text, Image, ScrollView } from "@tarojs/components";
|
||||
import dayjs from "dayjs";
|
||||
import img from "@/config/images";
|
||||
import { formatNtrpDisplay } from "@/utils/helper";
|
||||
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 {
|
||||
participant_count,
|
||||
max_participants,
|
||||
user_action_status = {},
|
||||
start_time,
|
||||
} = detail;
|
||||
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 =
|
||||
@@ -22,91 +94,322 @@ export default function Participants(props) {
|
||||
) &&
|
||||
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(
|
||||
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: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>已加入</Text>
|
||||
</>
|
||||
),
|
||||
action: () => toast("您已参与了本次活动"),
|
||||
};
|
||||
} else if (is_substituting) {
|
||||
return {
|
||||
text: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>已加入候补</Text>
|
||||
</>
|
||||
),
|
||||
action: () => toast("您已加入候补,候补失败会全额退款~"),
|
||||
};
|
||||
} else if (can_pay) {
|
||||
return {
|
||||
text: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>继续支付</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: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>立即加入</Text>
|
||||
</>
|
||||
),
|
||||
available: false,
|
||||
action: () =>
|
||||
toast("您当前不符合此球局NTRP水平要求,去看看其他活动吧~"),
|
||||
};
|
||||
} else if (can_substitute) {
|
||||
return {
|
||||
text: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>我要候补</Text>
|
||||
</>
|
||||
),
|
||||
action: checkPhoneAndExecute(handleJoinGame),
|
||||
};
|
||||
} else if (can_join) {
|
||||
return {
|
||||
text: () => {
|
||||
return (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>立即加入</Text>
|
||||
</>
|
||||
);
|
||||
},
|
||||
action: checkPhoneAndExecute(handleJoinGame),
|
||||
};
|
||||
} else if (can_assess) {
|
||||
return {
|
||||
text: () => (
|
||||
<>
|
||||
<Image className={styles.crrrencySymbol} src={RMB_ICON} />
|
||||
{displayPrice}
|
||||
<Text className={styles.btnText}>立即加入</Text>
|
||||
</>
|
||||
),
|
||||
action: checkPhoneAndExecute(handleSelfEvaluate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: "球局无法加入",
|
||||
available: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||||
|
||||
const leftCount = max_participants - participant_count;
|
||||
|
||||
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={() => {
|
||||
handleJoinGame();
|
||||
}}
|
||||
>
|
||||
<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 className={styles["participants-list-scroll"]} 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 className={styles["detail-page-content-participants"]}>
|
||||
<View className={styles["participants-title"]}>
|
||||
<Text>参与者</Text>
|
||||
<Text>·</Text>
|
||||
<Text>{leftCount > 0 ? `剩余空位 ${leftCount}` : "已满员"}</Text>
|
||||
</View>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</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 className={styles["participants-list-scroll"]} 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>
|
||||
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user