Files
mini-programs/src/game_pages/detail/components/StickyBottom/index.tsx
2026-03-20 23:24:22 +08:00

417 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from "react";
import Taro from "@tarojs/taro";
import classnames from "classnames";
import dayjs from "dayjs";
import { debounce } from "@tarojs/runtime";
import { Text, View, Image } from "@tarojs/components";
import OrderService from "@/services/orderService";
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService";
import { GameManagePopup, NTRPEvaluatePopup } from "@/components";
import { useUserInfo } from "@/store/userStore";
import img from "@/config/images";
// import RMB_ICON from "@/static/detail/rmb.svg";
import { toast, navto } from "@/utils/helper";
import styles from "./index.module.scss";
function isFull(counts) {
const {
max_players,
current_players,
max_substitute_players,
current_substitute_count,
is_substitute_supported,
} = counts;
if (
current_players >= max_players &&
is_substitute_supported === IsSubstituteSupported.NOTSUPPORT
) {
return true;
} else if (
current_players >= max_players &&
is_substitute_supported === IsSubstituteSupported.SUPPORT
) {
return max_substitute_players === current_substitute_count;
}
return false;
}
function RmbIcon() {
return <Text className={styles.rmbSymbol}>¥</Text>;
}
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 StickyButton(props) {
const {
handleShare,
handleJoinGame,
detail,
onStatusChange,
handleAddComment,
getCommentCount,
currentUserInfo,
} = props;
const [commentCount, setCommentCount] = useState(0);
const userInfo = useUserInfo();
const ntrpRef = useRef<{
show: (evaluateCallback: EvaluateCallback) => void;
}>({ show: () => {} });
const {
id,
price,
user_action_status,
match_status,
start_time,
end_time,
is_organizer,
skill_level_max,
skill_level_min,
} = detail || {};
const { ntrp_level } = currentUserInfo || {};
// 检查手机号绑定的包装函数
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,
);
const gameManageRef = useRef();
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();
},
});
}
useEffect(() => {
getCommentCount?.((count) => {
setCommentCount(count);
});
}, [getCommentCount]);
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} /> */}
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
),
action: () => toast("您已参与了本次活动"),
};
} else if (is_substituting) {
return {
text: () => (
<>
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
),
action: () => toast("您已加入候补,候补失败会全额退款~"),
};
} else if (can_pay) {
return {
text: () => (
<>
<RmbIcon />
{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: () => (
<>
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
),
available: false,
action: () =>
toast("您当前不符合此球局NTRP水平要求去看看其他活动吧"),
};
} else if (can_substitute) {
return {
text: () => (
<>
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
),
action: checkPhoneAndExecute(handleJoinGame),
};
} else if (can_join) {
return {
text: () => {
return (
<>
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
);
},
action: checkPhoneAndExecute(handleJoinGame),
};
} else if (can_assess) {
return {
text: () => (
<>
<RmbIcon />
{displayPrice}
<Text className={styles.btnText}></Text>
</>
),
action: checkPhoneAndExecute(handleSelfEvaluate),
};
}
return {
text: "球局无法加入",
available: false,
};
}
if (!user_action_status) {
return "";
}
const {
text,
available = true,
action = () => {},
} = generateTextAndAction(user_action_status)!;
let ActionText: React.FC | string = text;
if (typeof ActionText === "string") {
ActionText = () => {
return <Text className={styles.btnText}>{text as string}</Text>;
};
}
const debounceAction = debounce(action, 300);
return (
<>
<View className={styles["sticky-bottom-bar"]}>
<View className={styles["sticky-bottom-bar-share-and-comment"]}>
<View
className={styles["sticky-bottom-bar-share"]}
onClick={() => handleShare()}
>
<Image
className={styles["sticky-bottom-bar-share-icon"]}
src={img.ICON_DETAIL_SHARE}
/>
<Text className={styles["sticky-bottom-bar-share-text"]}></Text>
</View>
<View
className={styles["sticky-bottom-bar-share-and-comment-separator"]}
/>
<View
className={styles["sticky-bottom-bar-comment"]}
onClick={() => {
// Taro.showToast({ title: "To be continued", icon: "none" });
handleAddComment();
}}
>
<Image
className={styles["sticky-bottom-bar-comment-icon"]}
src={img.ICON_DETAIL_COMMENT_LIGHT}
/>
<Text className={styles["sticky-bottom-bar-comment-text"]}>
{commentCount > 0 ? commentCount : "评论"}
</Text>
</View>
</View>
<View
className={classnames(
styles["detail-main-action"],
available ? "" : styles.disabled,
)}
>
<View
style={is_organizer ? {} : { margin: "auto" }}
className={styles["sticky-bottom-bar-join-game"]}
onClick={debounceAction}
>
<ActionText />
</View>
{is_organizer && (
<View
className={styles.game_manage}
onClick={() => {
gameManageRef.current.show(detail, onStatusChange);
}}
>
</View>
)}
</View>
</View>
<GameManagePopup ref={gameManageRef} />
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
</>
);
}