CheckoutModal.tsx 31 KB

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