This commit is contained in:
筱野
2025-12-10 22:14:15 +08:00
6 changed files with 196 additions and 51 deletions

View File

@@ -46,8 +46,7 @@ function genRefundNotice(refund_policy) {
}; };
} }
function renderCancelContent(checkOrderInfo) { function renderCancelContent(refund_policy = []) {
const { refund_policy = [] } = checkOrderInfo;
const current = dayjs(); const current = dayjs();
const policyList = [ const policyList = [
{ {
@@ -65,7 +64,6 @@ function renderCancelContent(checkOrderInfo) {
}; };
}), }),
]; ];
console.log("policyList", policyList);
const targetIndex = policyList.findIndex((item) => item.beforeCurrent); const targetIndex = policyList.findIndex((item) => item.beforeCurrent);
const { notice } = genRefundNotice(refund_policy); const { notice } = genRefundNotice(refund_policy);
return ( return (
@@ -107,7 +105,7 @@ export type RefundRef = {
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) { export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [checkOrderInfo, setCheckOrderInfo] = useState({}); const [refundPolicy, setRefundPolicy] = useState([]);
const [orderData, setOrderData] = useState({}); const [orderData, setOrderData] = useState({});
const onDown = useRef<((result: boolean) => void) | null>(null); const onDown = useRef<((result: boolean) => void) | null>(null);
@@ -116,11 +114,10 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
})); }));
async function onShow(orderItem, onFinish: (result: boolean) => void) { async function onShow(orderItem, onFinish: (result: boolean) => void) {
const { game_info } = orderItem; const { refund_policy } = orderItem;
onDown.current = onFinish; onDown.current = onFinish;
setOrderData(orderItem); setOrderData(orderItem);
const res = await orderService.getCheckOrderInfo(game_info.id); setRefundPolicy(refund_policy);
setCheckOrderInfo(res.data);
setVisible(true); setVisible(true);
} }
@@ -172,7 +169,7 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
onClick={onClose} onClick={onClose}
/> />
</View> </View>
{renderCancelContent(checkOrderInfo)} {renderCancelContent(refundPolicy)}
<Button className={styles.action} onClick={handleConfirmQuit}> <Button className={styles.action} onClick={handleConfirmQuit}>
退 退
</Button> </Button>

View File

@@ -73,6 +73,7 @@ export default function Participants(props) {
}>({ show: () => {} }); }>({ show: () => {} });
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const participants = detail.participants || []; const participants = detail.participants || [];
const substitute_members = detail.substitute_members || [];
// const participants = Array(10) // const participants = Array(10)
// .fill(0) // .fill(0)
// .map((_, index) => ({ // .map((_, index) => ({
@@ -92,6 +93,8 @@ export default function Participants(props) {
const { const {
participant_count, participant_count,
max_participants, max_participants,
substitute_count,
max_substitute_players,
user_action_status = {}, user_action_status = {},
start_time, start_time,
price, price,
@@ -293,6 +296,13 @@ export default function Participants(props) {
const { action = () => {} } = generateTextAndAction(user_action_status)!; const { action = () => {} } = generateTextAndAction(user_action_status)!;
const leftCount = max_participants - participant_count; 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 ( return (
<> <>
@@ -389,6 +399,98 @@ export default function Participants(props) {
"" ""
)} )}
</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 /> <NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
</> </>
); );

View File

@@ -79,7 +79,8 @@ function Index() {
}); });
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化) // 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
await fetchDetail(); // 注意:这里不调用 fetchDetail避免与 useDidShow 中的调用重复
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
if (from === "publish") { if (from === "publish") {
handleShare(true); handleShare(true);
} }

View File

@@ -380,8 +380,13 @@ function OrderMsg(props) {
wechat_contact, wechat_contact,
price, price,
} = detail; } = detail;
const { order_no } = orderDetail; const { order_no, registrant_phone: registrant_phone_from_order } =
const { order_info: { registrant_phone } = {} } = checkOrderInfo; orderDetail;
const {
order_info: { registrant_phone: registrant_phone_from_check_order } = {},
} = checkOrderInfo || {};
const registrant_phone =
registrant_phone_from_order || registrant_phone_from_check_order;
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const startDate = startTime.format("YYYY年M月D日"); const startDate = startTime.format("YYYY年M月D日");
@@ -402,13 +407,11 @@ function OrderMsg(props) {
}, },
{ {
title: "报名人电话", title: "报名人电话",
// content: registrant_phone,
content: registrant_phone ? ( content: registrant_phone ? (
<Text <Text
selectable={true} // 支持长按复制 selectable={true} // 支持长按复制
style={{ style={{
color: "#007AFF", color: "#007AFF",
// textDecoration: "underline",
cursor: "pointer", cursor: "pointer",
}} }}
onClick={() => { onClick={() => {
@@ -427,7 +430,6 @@ function OrderMsg(props) {
}, },
{ {
title: "组织人电话", title: "组织人电话",
// content: wechat_contact,
content: content:
wechat_contact && isPhoneNumber(wechat_contact) ? ( wechat_contact && isPhoneNumber(wechat_contact) ? (
<Text <Text
@@ -489,8 +491,7 @@ function OrderMsg(props) {
} }
function RefundPolicy(props) { function RefundPolicy(props) {
const { checkOrderInfo } = props; const { refund_policy = [] } = props;
const { refund_policy = [] } = checkOrderInfo;
const current = dayjs(); const current = dayjs();
const policyList = [ const policyList = [
{ {
@@ -563,7 +564,7 @@ const OrderCheck = () => {
const [id, gameId] = [Number(stringId), Number(stringGameId)]; const [id, gameId] = [Number(stringId), Number(stringGameId)];
const [detail, setDetail] = useState<GameData | {}>({}); const [detail, setDetail] = useState<GameData | {}>({});
const [location, setLocation] = useState<number[]>([0, 0]); const [location, setLocation] = useState<number[]>([0, 0]);
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({}); const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes>();
const [orderDetail, setOrderDetail] = useState({}); const [orderDetail, setOrderDetail] = useState({});
const { paying, setPaying } = useOrder(); const { paying, setPaying } = useOrder();
@@ -584,11 +585,11 @@ const OrderCheck = () => {
if (res.code === 0) { if (res.code === 0) {
gameDetail = res.data; gameDetail = res.data;
} }
checkOrder(gameId);
} }
if (gameDetail.id) {
setDetail(gameDetail); setDetail(gameDetail);
onInit(gameDetail.id); const location = await getCurrentLocation();
} setLocation([location.latitude, location.longitude]);
} }
async function checkOrder(gid) { async function checkOrder(gid) {
@@ -596,12 +597,6 @@ const OrderCheck = () => {
setCheckOrderInfo(orderRes.data); setCheckOrderInfo(orderRes.data);
} }
async function onInit(gid) {
checkOrder(gid);
const location = await getCurrentLocation();
setLocation([location.latitude, location.longitude]);
}
async function getPaymentParams() { async function getPaymentParams() {
// 检查登录状态和手机号(创建订单前检查) // 检查登录状态和手机号(创建订单前检查)
if (!requireLoginWithPhone()) { if (!requireLoginWithPhone()) {
@@ -626,10 +621,6 @@ const OrderCheck = () => {
return; // 未登录或未绑定手机号,已跳转到登录页 return; // 未登录或未绑定手机号,已跳转到登录页
} }
setPaying(true); setPaying(true);
Taro.showLoading({
title: "支付中...",
mask: true,
});
let payment_params = {}; let payment_params = {};
try { try {
@@ -641,7 +632,6 @@ const OrderCheck = () => {
}); });
} }
await payOrder(payment_params); await payOrder(payment_params);
Taro.hideLoading();
Taro.showToast({ Taro.showToast({
title: "支付成功", title: "支付成功",
icon: "success", icon: "success",
@@ -655,7 +645,6 @@ const OrderCheck = () => {
// delta: 1, // delta: 1,
// }); // });
} catch (error) { } catch (error) {
Taro.hideLoading();
Taro.showToast({ Taro.showToast({
title: error.message, title: error.message,
icon: "none", icon: "none",
@@ -712,20 +701,25 @@ const OrderCheck = () => {
checkOrderInfo={checkOrderInfo} checkOrderInfo={checkOrderInfo}
/> />
{/* Refund policy */} {/* Refund policy */}
<RefundPolicy checkOrderInfo={checkOrderInfo} /> <RefundPolicy
refund_policy={
checkOrderInfo?.refund_policy || orderDetail?.refund_policy || []
}
/>
{/* Disclaimer */} {/* Disclaimer */}
<Disclaimer /> <Disclaimer />
{(!id || {(!id ||
(order_status === OrderStatus.PENDING && (order_status === OrderStatus.PENDING &&
cancel_type === CancelType.NONE)) && cancel_type === CancelType.NONE)) && (
!paying && (
<Button <Button
className={styles.payButton} className={styles.payButton}
disabled={paying} disabled={paying}
onClick={handlePay} onClick={handlePay}
loading={paying}
> >
{order_status === OrderStatus.PENDING ? "继续" : "确认"} {paying
? "支付中..."
: `${order_status === OrderStatus.PENDING ? "继续" : "确认"}支付`}
</Button> </Button>
)} )}
</View> </View>

View File

@@ -15,7 +15,7 @@ export interface UserState {
fetchUserInfo: () => Promise<UserInfoType | undefined>; fetchUserInfo: () => Promise<UserInfoType | undefined>;
updateUserInfo: (userInfo: Partial<UserInfoType>) => void; updateUserInfo: (userInfo: Partial<UserInfoType>) => void;
nicknameChangeStatus: Partial<NicknameChangeStatus>; nicknameChangeStatus: Partial<NicknameChangeStatus>;
checkNicknameChangeStatus: () => void; checkNicknameChangeStatus: (force?: boolean) => void;
updateNickname: (nickname: string) => void; updateNickname: (nickname: string) => void;
// NTRP 测试结果缓存 // NTRP 测试结果缓存
lastTestResult: LastTimeTestResult | null; lastTestResult: LastTimeTestResult | null;
@@ -32,6 +32,10 @@ const getTimeNextDate = (time: string) => {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };
// 请求锁,避免重复调用
let isFetchingLastTestResult = false;
let isCheckingNicknameStatus = false;
export const useUser = create<UserState>()((set) => ({ export const useUser = create<UserState>()((set) => ({
user: {}, user: {},
fetchUserInfo: async () => { fetchUserInfo: async () => {
@@ -87,8 +91,20 @@ export const useUser = create<UserState>()((set) => ({
} }
}, },
nicknameChangeStatus: {}, nicknameChangeStatus: {},
checkNicknameChangeStatus: async () => { checkNicknameChangeStatus: async (force = false) => {
// 如果正在请求中,直接返回,避免重复调用
if (isCheckingNicknameStatus) {
return;
}
// 如果已经有状态数据且不是强制更新,跳过,避免重复调用
if (!force) {
const currentState = useUser.getState();
if (currentState.nicknameChangeStatus && Object.keys(currentState.nicknameChangeStatus).length > 0) {
return;
}
}
try { try {
isCheckingNicknameStatus = true;
const res = await checkNicknameChangeStatusApi(); const res = await checkNicknameChangeStatusApi();
const { next_period_start_time } = res.data; const { next_period_start_time } = res.data;
set({ set({
@@ -99,12 +115,15 @@ export const useUser = create<UserState>()((set) => ({
}); });
} catch (error) { } catch (error) {
console.error("检查昵称变更状态失败:", error); console.error("检查昵称变更状态失败:", error);
} finally {
isCheckingNicknameStatus = false;
} }
}, },
updateNickname: async (nickname) => { updateNickname: async (nickname) => {
try { try {
await updateNicknameApi(nickname); await updateNicknameApi(nickname);
await useUser.getState().checkNicknameChangeStatus(); // 更新昵称后强制更新状态
await useUser.getState().checkNicknameChangeStatus(true);
set((state) => ({ set((state) => ({
user: { ...state.user, nickname }, user: { ...state.user, nickname },
})); }));
@@ -115,7 +134,27 @@ export const useUser = create<UserState>()((set) => ({
// NTRP 测试结果缓存 // NTRP 测试结果缓存
lastTestResult: null, lastTestResult: null,
fetchLastTestResult: async () => { fetchLastTestResult: async () => {
// 如果已经有缓存数据,直接返回,避免重复调用
const currentState = useUser.getState();
if (currentState.lastTestResult) {
return currentState.lastTestResult;
}
// 如果正在请求中,等待请求完成,避免重复调用
if (isFetchingLastTestResult) {
// 等待请求完成,最多等待 3 秒
let waitCount = 0;
while (isFetchingLastTestResult && waitCount < 30) {
await new Promise((resolve) => setTimeout(resolve, 100));
waitCount++;
const state = useUser.getState();
if (state.lastTestResult) {
return state.lastTestResult;
}
}
return null;
}
try { try {
isFetchingLastTestResult = true;
const res = await evaluateService.getLastResult(); const res = await evaluateService.getLastResult();
if (res.code === 0) { if (res.code === 0) {
set({ lastTestResult: res.data }); set({ lastTestResult: res.data });
@@ -125,6 +164,8 @@ export const useUser = create<UserState>()((set) => ({
} catch (error) { } catch (error) {
console.error("获取NTRP测试结果失败:", error); console.error("获取NTRP测试结果失败:", error);
return null; return null;
} finally {
isFetchingLastTestResult = false;
} }
}, },
})); }));

View File

@@ -6,18 +6,28 @@ export function getOrderStatus(orderData) {
if (!order_no) { if (!order_no) {
return 'none' return 'none'
} }
const { start_time } = game_info const { start_time } = game_info || {}
if (!start_time) { console.log('活动开始时间未找到, start_time: ', start_time); }
const unPay = order_status === OrderStatus.PENDING && ([CancelType.NONE].includes(cancel_type)); const unPay = order_status === OrderStatus.PENDING && ([CancelType.NONE].includes(cancel_type));
const refund = [RefundStatus.SUCCESS].includes(refund_status); const refund = [RefundStatus.SUCCESS].includes(refund_status);
const refunding = [RefundStatus.PENDING].includes(refund_status); const refunding = [RefundStatus.PENDING].includes(refund_status);
const expired = const expired =
order_status === OrderStatus.FINISHED; order_status === OrderStatus.FINISHED;
const frozen = dayjs().add(2, 'h').isAfter(dayjs(start_time)) const frozen = dayjs().isAfter(dayjs(start_time))
const canceled = [CancelType.TIMEOUT, CancelType.USER].includes(cancel_type); const canceled = [CancelType.TIMEOUT, CancelType.USER].includes(cancel_type);
return unPay ? 'unpay' : refund ? 'refund' : canceled ? 'canceled' : expired ? 'expired' : refunding ? 'refunding' : frozen ? 'start' : 'progress' // return unPay ? 'unpay' : refund ? 'refund' : canceled ? 'canceled' : expired ? 'expired' : refunding ? 'refunding' : is_substitute_order ? 'progress' : frozen ? 'start' : 'progress'
if (unPay) return 'unpay';
if (refund) return 'refund';
if (canceled) return 'canceled';
if (expired) return 'expired';
if (refunding) return 'refunding';
if (frozen) return 'start';
// if (is_substitute_order) return 'progress';
return 'progress';
} }
// scene: list、detail // scene: list、detail