CheckoutModal.tsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. import { Image } from "expo-image";
  2. import { useRouter } from "expo-router";
  3. import React, {
  4. forwardRef,
  5. useImperativeHandle,
  6. useRef,
  7. useState,
  8. } from "react";
  9. import {
  10. ActivityIndicator,
  11. Alert,
  12. AppState,
  13. Dimensions,
  14. Modal,
  15. ScrollView,
  16. StyleSheet,
  17. Text,
  18. TouchableOpacity,
  19. View,
  20. } from "react-native";
  21. import { applyOrder, getApplyResult, previewOrder } from "@/services/award";
  22. import Alipay from "expo-native-alipay";
  23. import {
  24. LotteryResultModal,
  25. LotteryResultModalRef,
  26. } from "./LotteryResultModal";
  27. const { width: SCREEN_WIDTH } = Dimensions.get("window");
  28. // 等级配置
  29. const LEVEL_MAP: Record<string, { title: string; color: string }> = {
  30. A: { title: "超神款", color: "#FF4444" },
  31. B: { title: "欧皇款", color: "#FF9600" },
  32. C: { title: "隐藏款", color: "#9B59B6" },
  33. D: { title: "普通款", color: "#666666" },
  34. };
  35. interface CheckoutModalProps {
  36. data: any;
  37. poolId: string;
  38. onSuccess: (param: { num: number; tradeNo: string }) => void;
  39. boxNumber?: string;
  40. }
  41. export interface CheckoutModalRef {
  42. show: (
  43. num: number,
  44. preview: any,
  45. boxNum?: string,
  46. seatNumbers?: number[],
  47. packFlag?: boolean,
  48. ) => void;
  49. showFreedom: () => void;
  50. close: () => void;
  51. }
  52. interface LotteryItem {
  53. id: string;
  54. name: string;
  55. cover: string;
  56. level: string;
  57. spu?: { marketPrice: number };
  58. }
  59. // 自由购买数量选项
  60. const FREEDOM_NUMS = [10, 20, 30, 40, 50];
  61. export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
  62. ({ data, poolId, onSuccess, boxNumber }, ref) => {
  63. const router = useRouter();
  64. const lotteryResultRef = useRef<LotteryResultModalRef>(null);
  65. const [visible, setVisible] = useState(false);
  66. const [num, setNum] = useState(1);
  67. const [checked, setChecked] = useState(true);
  68. const [loading, setLoading] = useState(false);
  69. const [freedomNum, setFreedomNum] = useState(10);
  70. const [freedomSelectVisible, setFreedomSelectVisible] = useState(false);
  71. // 预览数据
  72. const [coin, setCoin] = useState<number | null>(null);
  73. const [couponAmount, setCouponAmount] = useState<number | null>(null);
  74. const [lastPrice, setLastPrice] = useState<number | null>(null);
  75. const [magic, setMagic] = useState<any>(null);
  76. const [cash, setCash] = useState<any>(null);
  77. const [cashChecked, setCashChecked] = useState(false);
  78. // 盒子相关
  79. const [boxNum, setBoxNum] = useState<string | undefined>(boxNumber);
  80. const [seatNumbers, setSeatNumbers] = useState<number[] | undefined>();
  81. const [packFlag, setPackFlag] = useState<boolean | undefined>();
  82. // 抽奖结果
  83. const [resultVisible, setResultVisible] = useState(false);
  84. const [resultLoading, setResultLoading] = useState(false);
  85. const [resultList, setResultList] = useState<LotteryItem[]>([]);
  86. // 设置预览数据
  87. const setPreviewData = (previewData: any) => {
  88. setCoin(previewData.magicAmount || null);
  89. setCouponAmount(previewData.couponAmount || null);
  90. setLastPrice(previewData.paymentAmount);
  91. setMagic(previewData.magic || null);
  92. if (
  93. previewData.cash &&
  94. previewData.cash.balance > previewData.paymentAmount
  95. ) {
  96. setCash(previewData.cash);
  97. setCashChecked(true);
  98. } else {
  99. setCash(previewData.cash || null);
  100. setCashChecked(false);
  101. }
  102. };
  103. // ... (Imports handled via separate edit or assume existing)
  104. const [payConfig, setPayConfig] = useState<any>(null);
  105. const [paymentMethod, setPaymentMethod] = useState<"ALIPAY">("ALIPAY");
  106. // ...
  107. useImperativeHandle(ref, () => ({
  108. show: (
  109. n: number,
  110. previewData: any = {},
  111. bNum?: string,
  112. seats?: number[],
  113. pack?: boolean,
  114. ) => {
  115. setNum(n);
  116. setBoxNum(bNum);
  117. setSeatNumbers(seats);
  118. setPackFlag(pack || undefined);
  119. setPreviewData(previewData);
  120. setVisible(true);
  121. fetchPayConfig();
  122. },
  123. showFreedom: () => {
  124. setFreedomNum(10);
  125. setFreedomSelectVisible(true);
  126. },
  127. close: () => {
  128. setVisible(false);
  129. setFreedomSelectVisible(false);
  130. setResultVisible(false);
  131. },
  132. }));
  133. const handleFreedomSelect = async (selectedNum: number) => {
  134. setFreedomSelectVisible(false);
  135. setLoading(true);
  136. try {
  137. const preview = await previewOrder(poolId, selectedNum);
  138. if (preview) {
  139. setNum(selectedNum);
  140. setFreedomNum(selectedNum);
  141. setPreviewData(preview);
  142. setVisible(true);
  143. fetchPayConfig();
  144. }
  145. } catch (error: any) {
  146. Alert.alert("提示", error?.message || "获取订单信息失败");
  147. } finally {
  148. setLoading(false);
  149. }
  150. };
  151. const close = () => {
  152. setVisible(false);
  153. setFreedomSelectVisible(false);
  154. };
  155. const closeResult = () => {
  156. setResultVisible(false);
  157. setResultList([]);
  158. onSuccess({ tradeNo: "", num });
  159. };
  160. const fetchPayConfig = async () => {
  161. try {
  162. const res = await import("@/services/user").then((m) =>
  163. m.getParamConfig("wxpay_alipay"),
  164. );
  165. if (res && res.data) {
  166. setPayConfig(JSON.parse(res.data));
  167. }
  168. } catch (e) {
  169. console.log("Fetch Pay Config Error", e);
  170. }
  171. };
  172. // ...
  173. const pay = async () => {
  174. if (loading) return;
  175. if (!checked) {
  176. Alert.alert("提示", "请同意《宝箱服务协议》");
  177. return;
  178. }
  179. setLoading(true);
  180. try {
  181. let paymentType = "";
  182. // Prioritize Wallet if checked
  183. if (cashChecked) {
  184. paymentType = "WALLET";
  185. } else {
  186. // 默认只支持支付宝
  187. paymentType = "ALIPAY_APP";
  188. }
  189. const payNum = packFlag ? 1 : num;
  190. console.log("Submit Order Params:", {
  191. poolId,
  192. quantity: payNum,
  193. paymentType,
  194. boxNum,
  195. seatNumbers,
  196. packFlag,
  197. payConfig,
  198. });
  199. const res = await applyOrder(
  200. poolId,
  201. payNum,
  202. paymentType,
  203. boxNum,
  204. seatNumbers,
  205. packFlag,
  206. );
  207. console.log("Apply Order Result:", res);
  208. if (!res) {
  209. Alert.alert("提示", "订单创建失败");
  210. return;
  211. }
  212. if (res.paySuccess) {
  213. // Direct Success (Wallet)
  214. handleSuccess(res.bizTradeNo || res.tradeNo);
  215. } else if (res.payInfo) {
  216. // Handle Native Payment
  217. handleNativePay(
  218. res.payInfo,
  219. paymentType,
  220. res.bizTradeNo || res.tradeNo,
  221. );
  222. } else {
  223. Alert.alert("提示", res?.message || "支付失败,请重试");
  224. }
  225. } catch (error: any) {
  226. Alert.alert("支付失败", error?.message || "请稍后重试");
  227. } finally {
  228. setLoading(false);
  229. }
  230. };
  231. const [verifyLoading, setVerifyLoading] = useState(false);
  232. const isNavigatingRef = useRef(false);
  233. const handleSuccess = (tradeNo: string, inventoryList?: any[]) => {
  234. setVerifyLoading(false);
  235. setVisible(false);
  236. if (isNavigatingRef.current) return;
  237. isNavigatingRef.current = true;
  238. router.replace({
  239. pathname: "/treasure-hunt/happy-spin" as any,
  240. params: {
  241. tradeNo,
  242. num,
  243. poolId,
  244. ...(inventoryList
  245. ? { prefetchedResults: JSON.stringify(inventoryList) }
  246. : {}),
  247. },
  248. });
  249. onSuccess({ tradeNo, num });
  250. };
  251. const handleNativePay = async (
  252. payInfo: string,
  253. type: string,
  254. tradeNo: string,
  255. ) => {
  256. if (type === "ALIPAY" || type.includes("ALIPAY")) {
  257. let appStateSub: any = null;
  258. let isResolved = false;
  259. let pollingStarted = false;
  260. try {
  261. Alipay.setAlipayScheme("alipay2021005175632205");
  262. // Watch for app returning to foreground as a fallback
  263. appStateSub = AppState.addEventListener(
  264. "change",
  265. async (nextAppState) => {
  266. if (nextAppState === "active" && !isResolved && !pollingStarted) {
  267. pollingStarted = true;
  268. const loadingTimer = setTimeout(() => {
  269. if (!isResolved) setVerifyLoading(true);
  270. }, 800);
  271. let attempts = 0;
  272. const pollInterval = setInterval(async () => {
  273. if (isResolved) {
  274. clearInterval(pollInterval);
  275. return;
  276. }
  277. attempts++;
  278. try {
  279. const res = await getApplyResult(tradeNo);
  280. // 关键修复:await 返回后再次检查 isResolved,防止竞态
  281. if (isResolved) {
  282. clearInterval(pollInterval);
  283. return;
  284. }
  285. if (
  286. res?.paySuccess ||
  287. (res?.inventoryList && res.inventoryList.length > 0)
  288. ) {
  289. isResolved = true;
  290. clearInterval(pollInterval);
  291. clearTimeout(loadingTimer);
  292. setVerifyLoading(false);
  293. handleSuccess(tradeNo, res?.inventoryList);
  294. }
  295. } catch (e) {
  296. console.log("Fallback poll failed", e);
  297. }
  298. if (attempts >= 15) {
  299. clearInterval(pollInterval);
  300. clearTimeout(loadingTimer);
  301. setVerifyLoading(false);
  302. }
  303. }, 300);
  304. }
  305. },
  306. );
  307. const result = await Alipay.pay(payInfo);
  308. if (isResolved) return; // if fallback already worked
  309. isResolved = true;
  310. setVerifyLoading(false);
  311. console.log("Alipay Result:", result);
  312. const status = result?.resultStatus;
  313. if (status === "9000") {
  314. handleSuccess(tradeNo);
  315. } else if (status === "6001") {
  316. Alert.alert("提示", "用户取消支付");
  317. } else {
  318. // Also poll server just in case the status is wrong but it succeeded
  319. try {
  320. setVerifyLoading(true);
  321. const res = await getApplyResult(tradeNo);
  322. if (
  323. res?.paySuccess ||
  324. (res?.inventoryList && res.inventoryList.length > 0)
  325. ) {
  326. handleSuccess(tradeNo);
  327. return;
  328. }
  329. } catch (ignore) {
  330. } finally {
  331. setVerifyLoading(false);
  332. }
  333. Alert.alert("支付中断", `状态码: ${status}`);
  334. }
  335. } catch (e: any) {
  336. if (isResolved) return;
  337. isResolved = true;
  338. setVerifyLoading(false);
  339. console.log("Alipay Error:", e);
  340. Alert.alert("支付异常", e.message || "调用支付宝失败");
  341. } finally {
  342. setVerifyLoading(false);
  343. if (appStateSub) {
  344. appStateSub.remove();
  345. }
  346. }
  347. } else {
  348. Alert.alert("提示", "微信支付暂未实现");
  349. }
  350. };
  351. // 获取抽奖结果(10发以下用弹窗)
  352. const fetchLotteryResult = async (tradeNo: string) => {
  353. setResultLoading(true);
  354. setResultVisible(true);
  355. setResultList([]);
  356. let attempts = 0;
  357. const maxAttempts = 5;
  358. const poll = async () => {
  359. try {
  360. const res = await getApplyResult(tradeNo);
  361. if (res?.inventoryList && res.inventoryList.length > 0) {
  362. setResultList(res.inventoryList);
  363. setResultLoading(false);
  364. // 不在这里调用 onSuccess,等用户关闭弹窗时再调用
  365. } else if (attempts < maxAttempts) {
  366. attempts++;
  367. setTimeout(poll, 1000);
  368. } else {
  369. setResultLoading(false);
  370. Alert.alert("提示", "获取结果超时,请在仓库中查看");
  371. }
  372. } catch {
  373. if (attempts < maxAttempts) {
  374. attempts++;
  375. setTimeout(poll, 1000);
  376. } else {
  377. setResultLoading(false);
  378. Alert.alert("提示", "获取结果失败,请在仓库中查看");
  379. }
  380. }
  381. };
  382. poll();
  383. };
  384. const displayPrice = lastPrice ?? (data?.price || 0) * num;
  385. return (
  386. <>
  387. {/* 10发以上的全屏抽奖结果弹窗 */}
  388. <LotteryResultModal
  389. ref={lotteryResultRef}
  390. onClose={() => {
  391. // 抽奖结果弹窗关闭后刷新数据
  392. onSuccess({ tradeNo: "", num });
  393. }}
  394. onGoStore={() => {
  395. onSuccess({ tradeNo: "", num });
  396. router.replace("/cloud-warehouse" as any);
  397. }}
  398. />
  399. {/* 自由购买数量选择弹窗 */}
  400. <Modal
  401. visible={freedomSelectVisible}
  402. transparent
  403. animationType="fade"
  404. onRequestClose={() => setFreedomSelectVisible(false)}
  405. >
  406. <View style={styles.overlay}>
  407. <TouchableOpacity
  408. style={styles.mask}
  409. activeOpacity={1}
  410. onPress={() => setFreedomSelectVisible(false)}
  411. />
  412. <View style={styles.freedomContainer}>
  413. <View style={styles.header}>
  414. <Text style={styles.title}>购买多盒</Text>
  415. <TouchableOpacity
  416. onPress={() => setFreedomSelectVisible(false)}
  417. style={styles.closeBtn}
  418. >
  419. <Text style={styles.closeText}>×</Text>
  420. </TouchableOpacity>
  421. </View>
  422. <View style={styles.freedomContent}>
  423. <View style={styles.freedomBtnList}>
  424. {FREEDOM_NUMS.map((item) => (
  425. <TouchableOpacity
  426. key={item}
  427. style={[
  428. styles.freedomBtn,
  429. freedomNum === item && styles.freedomBtnActive,
  430. ]}
  431. onPress={() => setFreedomNum(item)}
  432. >
  433. <Text
  434. style={[
  435. styles.freedomBtnText,
  436. freedomNum === item && styles.freedomBtnTextActive,
  437. ]}
  438. >
  439. {item}
  440. <Text style={styles.freedomUnit}>盒</Text>
  441. </Text>
  442. {freedomNum === item && (
  443. <Text style={styles.checkIcon}>✓</Text>
  444. )}
  445. </TouchableOpacity>
  446. ))}
  447. </View>
  448. <TouchableOpacity
  449. style={[
  450. styles.freedomSubmitBtn,
  451. loading && styles.payBtnDisabled,
  452. ]}
  453. onPress={() => handleFreedomSelect(freedomNum)}
  454. disabled={loading}
  455. >
  456. {loading ? (
  457. <ActivityIndicator color="#fff" size="small" />
  458. ) : (
  459. <Text style={styles.freedomSubmitText}>
  460. 确认 ¥{(data?.price || 0) * freedomNum}
  461. </Text>
  462. )}
  463. </TouchableOpacity>
  464. </View>
  465. </View>
  466. </View>
  467. </Modal>
  468. {/* 支付确认弹窗 */}
  469. <Modal
  470. visible={visible}
  471. transparent
  472. animationType="slide"
  473. onRequestClose={close}
  474. >
  475. <View style={styles.overlay}>
  476. <TouchableOpacity
  477. style={styles.mask}
  478. activeOpacity={1}
  479. onPress={close}
  480. />
  481. <View style={styles.container}>
  482. <View style={styles.header}>
  483. <Text style={styles.title}>{data?.name}</Text>
  484. <TouchableOpacity onPress={close} style={styles.closeBtn}>
  485. <Text style={styles.closeText}>×</Text>
  486. </TouchableOpacity>
  487. </View>
  488. <View style={styles.content}>
  489. <View style={styles.row}>
  490. <Text style={styles.label}>购买件数</Text>
  491. <Text style={styles.priceText}>
  492. ¥{data?.price} x {num}
  493. </Text>
  494. </View>
  495. <View style={styles.row}>
  496. <Text style={styles.label}>优惠券</Text>
  497. <Text
  498. style={[
  499. styles.valueText,
  500. couponAmount ? styles.themeColor : {},
  501. ]}
  502. >
  503. {couponAmount
  504. ? `已使用优惠¥${couponAmount}`
  505. : "暂无优惠券可选"}
  506. </Text>
  507. </View>
  508. {magic && magic.balance > 0 && (
  509. <View style={styles.row}>
  510. <View style={styles.rowLeft}>
  511. <Text style={styles.label}>果实</Text>
  512. <Text style={styles.balanceText}>
  513. (剩余:{magic.balance})
  514. </Text>
  515. </View>
  516. <Text style={styles.balanceText}>
  517. 已抵扣 <Text style={styles.themeColor}>¥{coin || 0}</Text>
  518. </Text>
  519. </View>
  520. )}
  521. {cash && (
  522. <View style={styles.row}>
  523. <View style={styles.rowLeft}>
  524. <Text style={styles.label}>钱包支付</Text>
  525. <Text style={styles.themeColor}>
  526. (余额:¥{cash.balance})
  527. </Text>
  528. </View>
  529. <TouchableOpacity
  530. style={[styles.radio, cashChecked && styles.radioChecked]}
  531. onPress={() => setCashChecked(!cashChecked)}
  532. >
  533. {cashChecked && <View style={styles.radioInner} />}
  534. </TouchableOpacity>
  535. </View>
  536. )}
  537. {/* Payment Methods Section */}
  538. {(!cashChecked ||
  539. (cash &&
  540. cash.balance < (lastPrice || (data?.price || 0) * num))) &&
  541. payConfig ? (
  542. <View style={styles.paymentSection}>
  543. <Text style={styles.sectionTitle}>支付方式</Text>
  544. {payConfig?.alipay?.enabled ? (
  545. <TouchableOpacity
  546. style={styles.payOption}
  547. onPress={() => setPaymentMethod("ALIPAY")}
  548. >
  549. <View style={styles.rowLeft}>
  550. <Text style={styles.payLabel}>支付宝支付</Text>
  551. </View>
  552. <View
  553. style={[
  554. styles.radio,
  555. paymentMethod === "ALIPAY" && styles.radioChecked,
  556. ]}
  557. >
  558. {paymentMethod === "ALIPAY" ? (
  559. <View style={styles.radioInner} />
  560. ) : null}
  561. </View>
  562. </TouchableOpacity>
  563. ) : null}
  564. </View>
  565. ) : null}
  566. <View style={styles.agreementRow}>
  567. <View style={styles.agreementLeft}>
  568. <Text style={styles.agreementText}>
  569. 我已满18周岁,已阅读并同意
  570. <Text style={styles.link}>《宝箱服务协议》</Text>
  571. </Text>
  572. <Text style={styles.tips}>
  573. 宝箱商品存在概率性,请谨慎消费
  574. </Text>
  575. </View>
  576. <TouchableOpacity
  577. style={[styles.radio, checked && styles.radioChecked]}
  578. onPress={() => setChecked(!checked)}
  579. >
  580. {checked && <View style={styles.radioInner} />}
  581. </TouchableOpacity>
  582. </View>
  583. </View>
  584. <View style={styles.footer}>
  585. <View style={styles.priceInfo}>
  586. <Text style={styles.totalLabel}>实付:</Text>
  587. <Text style={styles.totalPrice}>
  588. ¥{displayPrice.toFixed(2)}
  589. </Text>
  590. </View>
  591. <TouchableOpacity
  592. style={[styles.payBtn, loading && styles.payBtnDisabled]}
  593. onPress={pay}
  594. disabled={loading}
  595. >
  596. {loading ? (
  597. <ActivityIndicator color="#fff" size="small" />
  598. ) : (
  599. <Text style={styles.payBtnText}>立即支付</Text>
  600. )}
  601. </TouchableOpacity>
  602. </View>
  603. </View>
  604. </View>
  605. </Modal>
  606. {/* 抽奖结果弹窗 */}
  607. <Modal
  608. visible={resultVisible}
  609. transparent
  610. animationType="fade"
  611. onRequestClose={closeResult}
  612. >
  613. <View style={styles.resultOverlay}>
  614. <View style={styles.resultContainer}>
  615. <View style={styles.resultHeader}>
  616. <Text style={styles.resultTitle}>🎉 恭喜您获得 🎉</Text>
  617. <TouchableOpacity
  618. onPress={closeResult}
  619. style={styles.resultCloseBtn}
  620. >
  621. <Text style={styles.closeText}>×</Text>
  622. </TouchableOpacity>
  623. </View>
  624. {resultLoading ? (
  625. <View style={styles.resultLoading}>
  626. <ActivityIndicator size="large" color="#ff9600" />
  627. <Text style={styles.resultLoadingText}>正在开启宝箱...</Text>
  628. </View>
  629. ) : (
  630. <ScrollView
  631. style={styles.resultScroll}
  632. showsVerticalScrollIndicator={false}
  633. >
  634. <View style={styles.resultList}>
  635. {resultList.map((item, index) => (
  636. <View key={item.id || index} style={styles.resultItem}>
  637. <View
  638. style={[
  639. styles.levelBadge,
  640. {
  641. backgroundColor:
  642. LEVEL_MAP[item.level]?.color || "#666",
  643. },
  644. ]}
  645. >
  646. <Text style={styles.levelText}>
  647. {LEVEL_MAP[item.level]?.title || item.level}
  648. </Text>
  649. </View>
  650. <View style={styles.resultImageBox}>
  651. <Image
  652. source={{ uri: item.cover }}
  653. style={styles.resultImage}
  654. contentFit="contain"
  655. />
  656. </View>
  657. <Text style={styles.resultName} numberOfLines={2}>
  658. {item.name}
  659. </Text>
  660. {item.spu?.marketPrice && (
  661. <Text style={styles.resultPrice}>
  662. 参考价:¥{item.spu.marketPrice}
  663. </Text>
  664. )}
  665. </View>
  666. ))}
  667. </View>
  668. </ScrollView>
  669. )}
  670. <View style={styles.resultFooter}>
  671. <TouchableOpacity
  672. style={styles.resultBtn}
  673. onPress={closeResult}
  674. >
  675. <Text style={styles.resultBtnText}>继续抽奖</Text>
  676. </TouchableOpacity>
  677. </View>
  678. </View>
  679. </View>
  680. </Modal>
  681. {/* 支付结果轮询确认加载中 */}
  682. <Modal
  683. visible={verifyLoading}
  684. transparent
  685. animationType="fade"
  686. onRequestClose={() => {}}
  687. >
  688. <View style={styles.verifyLoadingOverlay}>
  689. <View style={styles.verifyLoadingBox}>
  690. <ActivityIndicator size="large" color="#ff9600" />
  691. <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
  692. </View>
  693. </View>
  694. </Modal>
  695. </>
  696. );
  697. },
  698. );
  699. const styles = StyleSheet.create({
  700. verifyLoadingOverlay: {
  701. ...StyleSheet.absoluteFillObject,
  702. backgroundColor: "rgba(0,0,0,0.4)",
  703. justifyContent: "center",
  704. alignItems: "center",
  705. zIndex: 9999,
  706. },
  707. verifyLoadingBox: {
  708. backgroundColor: "rgba(0,0,0,0.8)",
  709. padding: 20,
  710. borderRadius: 12,
  711. alignItems: "center",
  712. },
  713. verifyLoadingText: {
  714. color: "#fff",
  715. marginTop: 10,
  716. fontSize: 14,
  717. },
  718. overlay: {
  719. flex: 1,
  720. backgroundColor: "rgba(0,0,0,0.5)",
  721. justifyContent: "flex-end",
  722. },
  723. mask: { flex: 1 },
  724. container: {
  725. backgroundColor: "#fff",
  726. borderTopLeftRadius: 20,
  727. borderTopRightRadius: 20,
  728. paddingBottom: 34,
  729. },
  730. header: {
  731. flexDirection: "row",
  732. alignItems: "center",
  733. justifyContent: "center",
  734. padding: 15,
  735. borderBottomWidth: 1,
  736. borderBottomColor: "#eee",
  737. position: "relative",
  738. },
  739. title: { fontSize: 16, fontWeight: "600", color: "#333" },
  740. closeBtn: { position: "absolute", right: 15, top: 10 },
  741. closeText: { fontSize: 24, color: "#999" },
  742. content: { padding: 15 },
  743. row: {
  744. flexDirection: "row",
  745. justifyContent: "space-between",
  746. alignItems: "center",
  747. paddingVertical: 10,
  748. },
  749. rowLeft: { flexDirection: "row", alignItems: "center", flex: 1 },
  750. label: { fontSize: 14, color: "#333" },
  751. priceText: { fontSize: 14, color: "#ff9600", fontWeight: "600" },
  752. valueText: { fontSize: 12, color: "#999" },
  753. themeColor: { color: "#ff9600" },
  754. balanceText: { fontSize: 12, color: "#999", marginLeft: 5 },
  755. radio: {
  756. width: 20,
  757. height: 20,
  758. borderRadius: 10,
  759. borderWidth: 2,
  760. borderColor: "#ddd",
  761. justifyContent: "center",
  762. alignItems: "center",
  763. },
  764. radioChecked: { borderColor: "#ff9600" },
  765. radioInner: {
  766. width: 10,
  767. height: 10,
  768. borderRadius: 5,
  769. backgroundColor: "#ff9600",
  770. },
  771. agreementRow: {
  772. flexDirection: "row",
  773. justifyContent: "space-between",
  774. alignItems: "flex-start",
  775. paddingVertical: 10,
  776. marginTop: 10,
  777. },
  778. agreementLeft: { flex: 1, marginRight: 10 },
  779. agreementText: { fontSize: 12, color: "#333", lineHeight: 18 },
  780. link: { color: "#ff9600" },
  781. tips: { fontSize: 11, color: "#999", marginTop: 5 },
  782. footer: {
  783. flexDirection: "row",
  784. alignItems: "center",
  785. justifyContent: "space-between",
  786. paddingHorizontal: 15,
  787. paddingTop: 15,
  788. borderTopWidth: 1,
  789. borderTopColor: "#eee",
  790. },
  791. priceInfo: { flexDirection: "row", alignItems: "center" },
  792. totalLabel: { fontSize: 14, color: "#333" },
  793. totalPrice: { fontSize: 20, color: "#ff9600", fontWeight: "bold" },
  794. payBtn: {
  795. backgroundColor: "#ff9600",
  796. paddingHorizontal: 30,
  797. paddingVertical: 12,
  798. borderRadius: 25,
  799. },
  800. payBtnDisabled: { opacity: 0.6 },
  801. payBtnText: { color: "#fff", fontSize: 16, fontWeight: "600" },
  802. // 自由购买弹窗
  803. freedomContainer: {
  804. backgroundColor: "#fff",
  805. borderTopLeftRadius: 20,
  806. borderTopRightRadius: 20,
  807. paddingBottom: 34,
  808. },
  809. freedomContent: { padding: 20 },
  810. freedomBtnList: {
  811. flexDirection: "row",
  812. flexWrap: "wrap",
  813. justifyContent: "space-between",
  814. },
  815. freedomBtn: {
  816. width: "48%",
  817. height: 50,
  818. backgroundColor: "#fff",
  819. borderRadius: 8,
  820. justifyContent: "center",
  821. alignItems: "center",
  822. marginBottom: 12,
  823. borderWidth: 1,
  824. borderColor: "#eee",
  825. position: "relative",
  826. },
  827. freedomBtnActive: { backgroundColor: "#F1423D", borderColor: "#F1423D" },
  828. freedomBtnText: { fontSize: 18, fontWeight: "bold", color: "#333" },
  829. freedomBtnTextActive: { color: "#fff" },
  830. freedomUnit: { fontSize: 12, fontWeight: "500" },
  831. checkIcon: {
  832. position: "absolute",
  833. bottom: 0,
  834. right: 0,
  835. backgroundColor: "#fff",
  836. color: "#F1423D",
  837. fontSize: 12,
  838. paddingHorizontal: 6,
  839. paddingVertical: 2,
  840. borderTopLeftRadius: 8,
  841. borderBottomRightRadius: 8,
  842. },
  843. freedomSubmitBtn: {
  844. backgroundColor: "#ff9600",
  845. height: 50,
  846. borderRadius: 25,
  847. justifyContent: "center",
  848. alignItems: "center",
  849. marginTop: 20,
  850. },
  851. freedomSubmitText: { color: "#fff", fontSize: 16, fontWeight: "600" },
  852. // 抽奖结果弹窗
  853. resultOverlay: {
  854. flex: 1,
  855. backgroundColor: "rgba(0,0,0,0.7)",
  856. justifyContent: "center",
  857. alignItems: "center",
  858. padding: 20,
  859. },
  860. resultContainer: {
  861. width: "100%",
  862. maxHeight: "80%",
  863. backgroundColor: "#fff",
  864. borderRadius: 16,
  865. overflow: "hidden",
  866. },
  867. resultHeader: {
  868. alignItems: "center",
  869. padding: 20,
  870. backgroundColor: "#ff9600",
  871. position: "relative",
  872. },
  873. resultTitle: {
  874. fontSize: 20,
  875. fontWeight: "bold",
  876. color: "#fff",
  877. },
  878. resultCloseBtn: {
  879. position: "absolute",
  880. right: 15,
  881. top: 15,
  882. width: 30,
  883. height: 30,
  884. backgroundColor: "rgba(255,255,255,0.3)",
  885. borderRadius: 15,
  886. justifyContent: "center",
  887. alignItems: "center",
  888. },
  889. resultLoading: {
  890. padding: 60,
  891. alignItems: "center",
  892. },
  893. resultLoadingText: {
  894. marginTop: 15,
  895. fontSize: 14,
  896. color: "#666",
  897. },
  898. resultScroll: {
  899. maxHeight: 400,
  900. },
  901. resultList: {
  902. flexDirection: "row",
  903. flexWrap: "wrap",
  904. padding: 10,
  905. justifyContent: "space-between",
  906. },
  907. resultItem: {
  908. width: (SCREEN_WIDTH - 80) / 2,
  909. backgroundColor: "#f9f9f9",
  910. borderRadius: 10,
  911. padding: 10,
  912. marginBottom: 10,
  913. alignItems: "center",
  914. },
  915. levelBadge: {
  916. paddingHorizontal: 10,
  917. paddingVertical: 3,
  918. borderRadius: 10,
  919. marginBottom: 8,
  920. },
  921. levelText: {
  922. color: "#fff",
  923. fontSize: 11,
  924. fontWeight: "bold",
  925. },
  926. resultImageBox: {
  927. width: "100%",
  928. aspectRatio: 1,
  929. backgroundColor: "#fff",
  930. borderRadius: 8,
  931. overflow: "hidden",
  932. },
  933. resultImage: {
  934. width: "100%",
  935. height: "100%",
  936. },
  937. resultName: {
  938. fontSize: 12,
  939. color: "#333",
  940. textAlign: "center",
  941. marginTop: 8,
  942. lineHeight: 16,
  943. },
  944. resultPrice: {
  945. fontSize: 10,
  946. color: "#999",
  947. marginTop: 4,
  948. },
  949. resultFooter: {
  950. padding: 15,
  951. borderTopWidth: 1,
  952. borderTopColor: "#eee",
  953. },
  954. resultBtn: {
  955. backgroundColor: "#ff9600",
  956. height: 46,
  957. borderRadius: 23,
  958. justifyContent: "center",
  959. alignItems: "center",
  960. },
  961. resultBtnText: {
  962. color: "#fff",
  963. fontSize: 16,
  964. fontWeight: "600",
  965. },
  966. paymentSection: {
  967. marginTop: 20,
  968. borderTopWidth: 1,
  969. borderTopColor: "#f0f0f0",
  970. paddingTop: 10,
  971. },
  972. sectionTitle: {
  973. fontSize: 14,
  974. fontWeight: "bold",
  975. marginBottom: 10,
  976. color: "#333",
  977. },
  978. payOption: {
  979. flexDirection: "row",
  980. justifyContent: "space-between",
  981. alignItems: "center",
  982. paddingVertical: 12,
  983. borderBottomWidth: 1,
  984. borderBottomColor: "#f9f9f9",
  985. },
  986. payLabel: {
  987. fontSize: 14,
  988. color: "#333",
  989. marginLeft: 10,
  990. },
  991. });