import { Image } from "expo-image"; import { useRouter } from "expo-router"; import React, { forwardRef, useImperativeHandle, useRef, useState, } from "react"; import { ActivityIndicator, Alert, AppState, Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { applyOrder, getApplyResult, previewOrder } from "@/services/award"; import Alipay from "expo-native-alipay"; import { LotteryResultModal, LotteryResultModalRef, } from "./LotteryResultModal"; const { width: SCREEN_WIDTH } = Dimensions.get("window"); // 等级配置 const LEVEL_MAP: Record = { A: { title: "超神款", color: "#FF4444" }, B: { title: "欧皇款", color: "#FF9600" }, C: { title: "隐藏款", color: "#9B59B6" }, D: { title: "普通款", color: "#666666" }, }; interface CheckoutModalProps { data: any; poolId: string; onSuccess: (param: { num: number; tradeNo: string }) => void; boxNumber?: string; } export interface CheckoutModalRef { show: ( num: number, preview: any, boxNum?: string, seatNumbers?: number[], packFlag?: boolean, ) => void; showFreedom: () => void; close: () => void; } interface LotteryItem { id: string; name: string; cover: string; level: string; spu?: { marketPrice: number }; } // 自由购买数量选项 const FREEDOM_NUMS = [10, 20, 30, 40, 50]; export const CheckoutModal = forwardRef( ({ data, poolId, onSuccess, boxNumber }, ref) => { const router = useRouter(); const lotteryResultRef = useRef(null); const [visible, setVisible] = useState(false); const [num, setNum] = useState(1); const [checked, setChecked] = useState(true); const [loading, setLoading] = useState(false); const [freedomNum, setFreedomNum] = useState(10); const [freedomSelectVisible, setFreedomSelectVisible] = useState(false); // 预览数据 const [coin, setCoin] = useState(null); const [couponAmount, setCouponAmount] = useState(null); const [lastPrice, setLastPrice] = useState(null); const [magic, setMagic] = useState(null); const [cash, setCash] = useState(null); const [cashChecked, setCashChecked] = useState(false); // 盒子相关 const [boxNum, setBoxNum] = useState(boxNumber); const [seatNumbers, setSeatNumbers] = useState(); const [packFlag, setPackFlag] = useState(); // 抽奖结果 const [resultVisible, setResultVisible] = useState(false); const [resultLoading, setResultLoading] = useState(false); const [resultList, setResultList] = useState([]); // 设置预览数据 const setPreviewData = (previewData: any) => { setCoin(previewData.magicAmount || null); setCouponAmount(previewData.couponAmount || null); setLastPrice(previewData.paymentAmount); setMagic(previewData.magic || null); if ( previewData.cash && previewData.cash.balance > previewData.paymentAmount ) { setCash(previewData.cash); setCashChecked(true); } else { setCash(previewData.cash || null); setCashChecked(false); } }; // ... (Imports handled via separate edit or assume existing) const [payConfig, setPayConfig] = useState(null); const [paymentMethod, setPaymentMethod] = useState<"ALIPAY">("ALIPAY"); // ... useImperativeHandle(ref, () => ({ show: ( n: number, previewData: any = {}, bNum?: string, seats?: number[], pack?: boolean, ) => { setNum(n); setBoxNum(bNum); setSeatNumbers(seats); setPackFlag(pack || undefined); setPreviewData(previewData); setVisible(true); fetchPayConfig(); }, showFreedom: () => { setFreedomNum(10); setFreedomSelectVisible(true); }, close: () => { setVisible(false); setFreedomSelectVisible(false); setResultVisible(false); }, })); const handleFreedomSelect = async (selectedNum: number) => { setFreedomSelectVisible(false); setLoading(true); try { const preview = await previewOrder(poolId, selectedNum); if (preview) { setNum(selectedNum); setFreedomNum(selectedNum); setPreviewData(preview); setVisible(true); fetchPayConfig(); } } catch (error: any) { Alert.alert("提示", error?.message || "获取订单信息失败"); } finally { setLoading(false); } }; const close = () => { setVisible(false); setFreedomSelectVisible(false); }; const closeResult = () => { setResultVisible(false); setResultList([]); onSuccess({ tradeNo: "", num }); }; const fetchPayConfig = async () => { try { const res = await import("@/services/user").then((m) => m.getParamConfig("wxpay_alipay"), ); if (res && res.data) { setPayConfig(JSON.parse(res.data)); } } catch (e) { console.log("Fetch Pay Config Error", e); } }; // ... const pay = async () => { if (loading) return; if (!checked) { Alert.alert("提示", "请同意《宝箱服务协议》"); return; } setLoading(true); try { let paymentType = ""; // Prioritize Wallet if checked if (cashChecked) { paymentType = "WALLET"; } else { // 默认只支持支付宝 paymentType = "ALIPAY_APP"; } const payNum = packFlag ? 1 : num; console.log("Submit Order Params:", { poolId, quantity: payNum, paymentType, boxNum, seatNumbers, packFlag, payConfig, }); const res = await applyOrder( poolId, payNum, paymentType, boxNum, seatNumbers, packFlag, ); console.log("Apply Order Result:", res); if (!res) { Alert.alert("提示", "订单创建失败"); return; } if (res.paySuccess) { // Direct Success (Wallet) handleSuccess(res.bizTradeNo || res.tradeNo); } else if (res.payInfo) { // Handle Native Payment handleNativePay( res.payInfo, paymentType, res.bizTradeNo || res.tradeNo, ); } else { Alert.alert("提示", res?.message || "支付失败,请重试"); } } catch (error: any) { Alert.alert("支付失败", error?.message || "请稍后重试"); } finally { setLoading(false); } }; const [verifyLoading, setVerifyLoading] = useState(false); const isNavigatingRef = useRef(false); const handleSuccess = (tradeNo: string) => { setVerifyLoading(false); setVisible(false); if (isNavigatingRef.current) return; isNavigatingRef.current = true; router.replace({ pathname: "/treasure-hunt/happy-spin" as any, params: { tradeNo, num, poolId }, }); onSuccess({ tradeNo, num }); }; const handleNativePay = async ( payInfo: string, type: string, tradeNo: string, ) => { if (type === "ALIPAY" || type.includes("ALIPAY")) { let appStateSub: any = null; let isResolved = false; let pollingStarted = false; try { Alipay.setAlipayScheme("alipay2021005175632205"); // Watch for app returning to foreground as a fallback appStateSub = AppState.addEventListener( "change", async (nextAppState) => { if (nextAppState === "active" && !isResolved && !pollingStarted) { pollingStarted = true; const loadingTimer = setTimeout(() => { if (!isResolved) setVerifyLoading(true); }, 800); let attempts = 0; const pollInterval = setInterval(async () => { if (isResolved) { clearInterval(pollInterval); return; } attempts++; try { const res = await getApplyResult(tradeNo); // 关键修复:await 返回后再次检查 isResolved,防止竞态 if (isResolved) { clearInterval(pollInterval); return; } if ( res?.paySuccess || (res?.inventoryList && res.inventoryList.length > 0) ) { isResolved = true; clearInterval(pollInterval); clearTimeout(loadingTimer); setVerifyLoading(false); handleSuccess(tradeNo); } } catch (e) { console.log("Fallback poll failed", e); } if (attempts >= 15) { clearInterval(pollInterval); clearTimeout(loadingTimer); setVerifyLoading(false); } }, 300); } }, ); const result = await Alipay.pay(payInfo); if (isResolved) return; // if fallback already worked isResolved = true; setVerifyLoading(false); console.log("Alipay Result:", result); const status = result?.resultStatus; if (status === "9000") { handleSuccess(tradeNo); } else if (status === "6001") { Alert.alert("提示", "用户取消支付"); } else { // Also poll server just in case the status is wrong but it succeeded try { setVerifyLoading(true); const res = await getApplyResult(tradeNo); if ( res?.paySuccess || (res?.inventoryList && res.inventoryList.length > 0) ) { handleSuccess(tradeNo); return; } } catch (ignore) { } finally { setVerifyLoading(false); } Alert.alert("支付中断", `状态码: ${status}`); } } catch (e: any) { if (isResolved) return; isResolved = true; setVerifyLoading(false); console.log("Alipay Error:", e); Alert.alert("支付异常", e.message || "调用支付宝失败"); } finally { setVerifyLoading(false); if (appStateSub) { appStateSub.remove(); } } } else { Alert.alert("提示", "微信支付暂未实现"); } }; // 获取抽奖结果(10发以下用弹窗) const fetchLotteryResult = async (tradeNo: string) => { setResultLoading(true); setResultVisible(true); setResultList([]); let attempts = 0; const maxAttempts = 5; const poll = async () => { try { const res = await getApplyResult(tradeNo); if (res?.inventoryList && res.inventoryList.length > 0) { setResultList(res.inventoryList); setResultLoading(false); // 不在这里调用 onSuccess,等用户关闭弹窗时再调用 } else if (attempts < maxAttempts) { attempts++; setTimeout(poll, 1000); } else { setResultLoading(false); Alert.alert("提示", "获取结果超时,请在仓库中查看"); } } catch { if (attempts < maxAttempts) { attempts++; setTimeout(poll, 1000); } else { setResultLoading(false); Alert.alert("提示", "获取结果失败,请在仓库中查看"); } } }; poll(); }; const displayPrice = lastPrice ?? (data?.price || 0) * num; return ( <> {/* 10发以上的全屏抽奖结果弹窗 */} { // 抽奖结果弹窗关闭后刷新数据 onSuccess({ tradeNo: "", num }); }} onGoStore={() => { onSuccess({ tradeNo: "", num }); router.replace("/cloud-warehouse" as any); }} /> {/* 自由购买数量选择弹窗 */} setFreedomSelectVisible(false)} > setFreedomSelectVisible(false)} /> 购买多盒 setFreedomSelectVisible(false)} style={styles.closeBtn} > × {FREEDOM_NUMS.map((item) => ( setFreedomNum(item)} > {item} {freedomNum === item && ( )} ))} handleFreedomSelect(freedomNum)} disabled={loading} > {loading ? ( ) : ( 确认 ¥{(data?.price || 0) * freedomNum} )} {/* 支付确认弹窗 */} {data?.name} × 购买件数 ¥{data?.price} x {num} 优惠券 {couponAmount ? `已使用优惠¥${couponAmount}` : "暂无优惠券可选"} {magic && magic.balance > 0 && ( 果实 (剩余:{magic.balance}) 已抵扣 ¥{coin || 0} )} {cash && ( 钱包支付 (余额:¥{cash.balance}) setCashChecked(!cashChecked)} > {cashChecked && } )} {/* Payment Methods Section */} {(!cashChecked || (cash && cash.balance < (lastPrice || (data?.price || 0) * num))) && payConfig ? ( 支付方式 {payConfig?.alipay?.enabled ? ( setPaymentMethod("ALIPAY")} > 支付宝支付 {paymentMethod === "ALIPAY" ? ( ) : null} ) : null} ) : null} 我已满18周岁,已阅读并同意 《宝箱服务协议》 宝箱商品存在概率性,请谨慎消费 setChecked(!checked)} > {checked && } 实付: ¥{displayPrice.toFixed(2)} {loading ? ( ) : ( 立即支付 )} {/* 抽奖结果弹窗 */} 🎉 恭喜您获得 🎉 × {resultLoading ? ( 正在开启宝箱... ) : ( {resultList.map((item, index) => ( {LEVEL_MAP[item.level]?.title || item.level} {item.name} {item.spu?.marketPrice && ( 参考价:¥{item.spu.marketPrice} )} ))} )} 继续抽奖 {/* 支付结果轮询确认加载中 */} {}} > 正在确认支付结果... ); }, ); const styles = StyleSheet.create({ verifyLoadingOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "center", alignItems: "center", zIndex: 9999, }, verifyLoadingBox: { backgroundColor: "rgba(0,0,0,0.8)", padding: 20, borderRadius: 12, alignItems: "center", }, verifyLoadingText: { color: "#fff", marginTop: 10, fontSize: 14, }, overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "flex-end", }, mask: { flex: 1 }, container: { backgroundColor: "#fff", borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: 34, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "center", padding: 15, borderBottomWidth: 1, borderBottomColor: "#eee", position: "relative", }, title: { fontSize: 16, fontWeight: "600", color: "#333" }, closeBtn: { position: "absolute", right: 15, top: 10 }, closeText: { fontSize: 24, color: "#999" }, content: { padding: 15 }, row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 10, }, rowLeft: { flexDirection: "row", alignItems: "center", flex: 1 }, label: { fontSize: 14, color: "#333" }, priceText: { fontSize: 14, color: "#ff9600", fontWeight: "600" }, valueText: { fontSize: 12, color: "#999" }, themeColor: { color: "#ff9600" }, balanceText: { fontSize: 12, color: "#999", marginLeft: 5 }, radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: "#ddd", justifyContent: "center", alignItems: "center", }, radioChecked: { borderColor: "#ff9600" }, radioInner: { width: 10, height: 10, borderRadius: 5, backgroundColor: "#ff9600", }, agreementRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", paddingVertical: 10, marginTop: 10, }, agreementLeft: { flex: 1, marginRight: 10 }, agreementText: { fontSize: 12, color: "#333", lineHeight: 18 }, link: { color: "#ff9600" }, tips: { fontSize: 11, color: "#999", marginTop: 5 }, footer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 15, paddingTop: 15, borderTopWidth: 1, borderTopColor: "#eee", }, priceInfo: { flexDirection: "row", alignItems: "center" }, totalLabel: { fontSize: 14, color: "#333" }, totalPrice: { fontSize: 20, color: "#ff9600", fontWeight: "bold" }, payBtn: { backgroundColor: "#ff9600", paddingHorizontal: 30, paddingVertical: 12, borderRadius: 25, }, payBtnDisabled: { opacity: 0.6 }, payBtnText: { color: "#fff", fontSize: 16, fontWeight: "600" }, // 自由购买弹窗 freedomContainer: { backgroundColor: "#fff", borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: 34, }, freedomContent: { padding: 20 }, freedomBtnList: { flexDirection: "row", flexWrap: "wrap", justifyContent: "space-between", }, freedomBtn: { width: "48%", height: 50, backgroundColor: "#fff", borderRadius: 8, justifyContent: "center", alignItems: "center", marginBottom: 12, borderWidth: 1, borderColor: "#eee", position: "relative", }, freedomBtnActive: { backgroundColor: "#F1423D", borderColor: "#F1423D" }, freedomBtnText: { fontSize: 18, fontWeight: "bold", color: "#333" }, freedomBtnTextActive: { color: "#fff" }, freedomUnit: { fontSize: 12, fontWeight: "500" }, checkIcon: { position: "absolute", bottom: 0, right: 0, backgroundColor: "#fff", color: "#F1423D", fontSize: 12, paddingHorizontal: 6, paddingVertical: 2, borderTopLeftRadius: 8, borderBottomRightRadius: 8, }, freedomSubmitBtn: { backgroundColor: "#ff9600", height: 50, borderRadius: 25, justifyContent: "center", alignItems: "center", marginTop: 20, }, freedomSubmitText: { color: "#fff", fontSize: 16, fontWeight: "600" }, // 抽奖结果弹窗 resultOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.7)", justifyContent: "center", alignItems: "center", padding: 20, }, resultContainer: { width: "100%", maxHeight: "80%", backgroundColor: "#fff", borderRadius: 16, overflow: "hidden", }, resultHeader: { alignItems: "center", padding: 20, backgroundColor: "#ff9600", position: "relative", }, resultTitle: { fontSize: 20, fontWeight: "bold", color: "#fff", }, resultCloseBtn: { position: "absolute", right: 15, top: 15, width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.3)", borderRadius: 15, justifyContent: "center", alignItems: "center", }, resultLoading: { padding: 60, alignItems: "center", }, resultLoadingText: { marginTop: 15, fontSize: 14, color: "#666", }, resultScroll: { maxHeight: 400, }, resultList: { flexDirection: "row", flexWrap: "wrap", padding: 10, justifyContent: "space-between", }, resultItem: { width: (SCREEN_WIDTH - 80) / 2, backgroundColor: "#f9f9f9", borderRadius: 10, padding: 10, marginBottom: 10, alignItems: "center", }, levelBadge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 10, marginBottom: 8, }, levelText: { color: "#fff", fontSize: 11, fontWeight: "bold", }, resultImageBox: { width: "100%", aspectRatio: 1, backgroundColor: "#fff", borderRadius: 8, overflow: "hidden", }, resultImage: { width: "100%", height: "100%", }, resultName: { fontSize: 12, color: "#333", textAlign: "center", marginTop: 8, lineHeight: 16, }, resultPrice: { fontSize: 10, color: "#999", marginTop: 4, }, resultFooter: { padding: 15, borderTopWidth: 1, borderTopColor: "#eee", }, resultBtn: { backgroundColor: "#ff9600", height: 46, borderRadius: 23, justifyContent: "center", alignItems: "center", }, resultBtnText: { color: "#fff", fontSize: 16, fontWeight: "600", }, paymentSection: { marginTop: 20, borderTopWidth: 1, borderTopColor: "#f0f0f0", paddingTop: 10, }, sectionTitle: { fontSize: 14, fontWeight: "bold", marginBottom: 10, color: "#333", }, payOption: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: "#f9f9f9", }, payLabel: { fontSize: 14, color: "#333", marginLeft: 10, }, });