CheckoutModal.tsx 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  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) => {
  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: { tradeNo, num, poolId },
  241. });
  242. onSuccess({ tradeNo, num });
  243. };
  244. const handleNativePay = async (
  245. payInfo: string,
  246. type: string,
  247. tradeNo: string,
  248. ) => {
  249. if (type === "ALIPAY" || type.includes("ALIPAY")) {
  250. let appStateSub: any = null;
  251. let isResolved = false;
  252. let pollingStarted = false;
  253. try {
  254. Alipay.setAlipayScheme("alipay2021005175632205");
  255. // Watch for app returning to foreground as a fallback
  256. appStateSub = AppState.addEventListener(
  257. "change",
  258. async (nextAppState) => {
  259. if (nextAppState === "active" && !isResolved && !pollingStarted) {
  260. pollingStarted = true;
  261. const loadingTimer = setTimeout(() => {
  262. if (!isResolved) setVerifyLoading(true);
  263. }, 800);
  264. let attempts = 0;
  265. const pollInterval = setInterval(async () => {
  266. if (isResolved) {
  267. clearInterval(pollInterval);
  268. return;
  269. }
  270. attempts++;
  271. try {
  272. const res = await getApplyResult(tradeNo);
  273. // 关键修复:await 返回后再次检查 isResolved,防止竞态
  274. if (isResolved) {
  275. clearInterval(pollInterval);
  276. return;
  277. }
  278. if (
  279. res?.paySuccess ||
  280. (res?.inventoryList && res.inventoryList.length > 0)
  281. ) {
  282. isResolved = true;
  283. clearInterval(pollInterval);
  284. clearTimeout(loadingTimer);
  285. setVerifyLoading(false);
  286. handleSuccess(tradeNo);
  287. }
  288. } catch (e) {
  289. console.log("Fallback poll failed", e);
  290. }
  291. if (attempts >= 15) {
  292. clearInterval(pollInterval);
  293. clearTimeout(loadingTimer);
  294. setVerifyLoading(false);
  295. }
  296. }, 300);
  297. }
  298. },
  299. );
  300. const result = await Alipay.pay(payInfo);
  301. if (isResolved) return; // if fallback already worked
  302. isResolved = true;
  303. setVerifyLoading(false);
  304. console.log("Alipay Result:", result);
  305. const status = result?.resultStatus;
  306. if (status === "9000") {
  307. handleSuccess(tradeNo);
  308. } else if (status === "6001") {
  309. Alert.alert("提示", "用户取消支付");
  310. } else {
  311. // Also poll server just in case the status is wrong but it succeeded
  312. try {
  313. setVerifyLoading(true);
  314. const res = await getApplyResult(tradeNo);
  315. if (
  316. res?.paySuccess ||
  317. (res?.inventoryList && res.inventoryList.length > 0)
  318. ) {
  319. handleSuccess(tradeNo);
  320. return;
  321. }
  322. } catch (ignore) {
  323. } finally {
  324. setVerifyLoading(false);
  325. }
  326. Alert.alert("支付中断", `状态码: ${status}`);
  327. }
  328. } catch (e: any) {
  329. if (isResolved) return;
  330. isResolved = true;
  331. setVerifyLoading(false);
  332. console.log("Alipay Error:", e);
  333. Alert.alert("支付异常", e.message || "调用支付宝失败");
  334. } finally {
  335. setVerifyLoading(false);
  336. if (appStateSub) {
  337. appStateSub.remove();
  338. }
  339. }
  340. } else {
  341. Alert.alert("提示", "微信支付暂未实现");
  342. }
  343. };
  344. // 获取抽奖结果(10发以下用弹窗)
  345. const fetchLotteryResult = async (tradeNo: string) => {
  346. setResultLoading(true);
  347. setResultVisible(true);
  348. setResultList([]);
  349. let attempts = 0;
  350. const maxAttempts = 5;
  351. const poll = async () => {
  352. try {
  353. const res = await getApplyResult(tradeNo);
  354. if (res?.inventoryList && res.inventoryList.length > 0) {
  355. setResultList(res.inventoryList);
  356. setResultLoading(false);
  357. // 不在这里调用 onSuccess,等用户关闭弹窗时再调用
  358. } else if (attempts < maxAttempts) {
  359. attempts++;
  360. setTimeout(poll, 1000);
  361. } else {
  362. setResultLoading(false);
  363. Alert.alert("提示", "获取结果超时,请在仓库中查看");
  364. }
  365. } catch {
  366. if (attempts < maxAttempts) {
  367. attempts++;
  368. setTimeout(poll, 1000);
  369. } else {
  370. setResultLoading(false);
  371. Alert.alert("提示", "获取结果失败,请在仓库中查看");
  372. }
  373. }
  374. };
  375. poll();
  376. };
  377. const displayPrice = lastPrice ?? (data?.price || 0) * num;
  378. return (
  379. <>
  380. {/* 10发以上的全屏抽奖结果弹窗 */}
  381. <LotteryResultModal
  382. ref={lotteryResultRef}
  383. onClose={() => {
  384. // 抽奖结果弹窗关闭后刷新数据
  385. onSuccess({ tradeNo: "", num });
  386. }}
  387. onGoStore={() => {
  388. onSuccess({ tradeNo: "", num });
  389. router.replace("/cloud-warehouse" as any);
  390. }}
  391. />
  392. {/* 自由购买数量选择弹窗 */}
  393. <Modal
  394. visible={freedomSelectVisible}
  395. transparent
  396. animationType="fade"
  397. onRequestClose={() => setFreedomSelectVisible(false)}
  398. >
  399. <View style={styles.overlay}>
  400. <TouchableOpacity
  401. style={styles.mask}
  402. activeOpacity={1}
  403. onPress={() => setFreedomSelectVisible(false)}
  404. />
  405. <View style={styles.freedomContainer}>
  406. <View style={styles.header}>
  407. <Text style={styles.title}>购买多盒</Text>
  408. <TouchableOpacity
  409. onPress={() => setFreedomSelectVisible(false)}
  410. style={styles.closeBtn}
  411. >
  412. <Text style={styles.closeText}>×</Text>
  413. </TouchableOpacity>
  414. </View>
  415. <View style={styles.freedomContent}>
  416. <View style={styles.freedomBtnList}>
  417. {FREEDOM_NUMS.map((item) => (
  418. <TouchableOpacity
  419. key={item}
  420. style={[
  421. styles.freedomBtn,
  422. freedomNum === item && styles.freedomBtnActive,
  423. ]}
  424. onPress={() => setFreedomNum(item)}
  425. >
  426. <Text
  427. style={[
  428. styles.freedomBtnText,
  429. freedomNum === item && styles.freedomBtnTextActive,
  430. ]}
  431. >
  432. {item}
  433. <Text style={styles.freedomUnit}>盒</Text>
  434. </Text>
  435. {freedomNum === item && (
  436. <Text style={styles.checkIcon}>✓</Text>
  437. )}
  438. </TouchableOpacity>
  439. ))}
  440. </View>
  441. <TouchableOpacity
  442. style={[
  443. styles.freedomSubmitBtn,
  444. loading && styles.payBtnDisabled,
  445. ]}
  446. onPress={() => handleFreedomSelect(freedomNum)}
  447. disabled={loading}
  448. >
  449. {loading ? (
  450. <ActivityIndicator color="#fff" size="small" />
  451. ) : (
  452. <Text style={styles.freedomSubmitText}>
  453. 确认 ¥{(data?.price || 0) * freedomNum}
  454. </Text>
  455. )}
  456. </TouchableOpacity>
  457. </View>
  458. </View>
  459. </View>
  460. </Modal>
  461. {/* 支付确认弹窗 */}
  462. <Modal
  463. visible={visible}
  464. transparent
  465. animationType="slide"
  466. onRequestClose={close}
  467. >
  468. <View style={styles.overlay}>
  469. <TouchableOpacity
  470. style={styles.mask}
  471. activeOpacity={1}
  472. onPress={close}
  473. />
  474. <View style={styles.container}>
  475. <View style={styles.header}>
  476. <Text style={styles.title}>{data?.name}</Text>
  477. <TouchableOpacity onPress={close} style={styles.closeBtn}>
  478. <Text style={styles.closeText}>×</Text>
  479. </TouchableOpacity>
  480. </View>
  481. <View style={styles.content}>
  482. <View style={styles.row}>
  483. <Text style={styles.label}>购买件数</Text>
  484. <Text style={styles.priceText}>
  485. ¥{data?.price} x {num}
  486. </Text>
  487. </View>
  488. <View style={styles.row}>
  489. <Text style={styles.label}>优惠券</Text>
  490. <Text
  491. style={[
  492. styles.valueText,
  493. couponAmount ? styles.themeColor : {},
  494. ]}
  495. >
  496. {couponAmount
  497. ? `已使用优惠¥${couponAmount}`
  498. : "暂无优惠券可选"}
  499. </Text>
  500. </View>
  501. {magic && magic.balance > 0 && (
  502. <View style={styles.row}>
  503. <View style={styles.rowLeft}>
  504. <Text style={styles.label}>果实</Text>
  505. <Text style={styles.balanceText}>
  506. (剩余:{magic.balance})
  507. </Text>
  508. </View>
  509. <Text style={styles.balanceText}>
  510. 已抵扣 <Text style={styles.themeColor}>¥{coin || 0}</Text>
  511. </Text>
  512. </View>
  513. )}
  514. {cash && (
  515. <View style={styles.row}>
  516. <View style={styles.rowLeft}>
  517. <Text style={styles.label}>钱包支付</Text>
  518. <Text style={styles.themeColor}>
  519. (余额:¥{cash.balance})
  520. </Text>
  521. </View>
  522. <TouchableOpacity
  523. style={[styles.radio, cashChecked && styles.radioChecked]}
  524. onPress={() => setCashChecked(!cashChecked)}
  525. >
  526. {cashChecked && <View style={styles.radioInner} />}
  527. </TouchableOpacity>
  528. </View>
  529. )}
  530. {/* Payment Methods Section */}
  531. {(!cashChecked ||
  532. (cash &&
  533. cash.balance < (lastPrice || (data?.price || 0) * num))) &&
  534. payConfig ? (
  535. <View style={styles.paymentSection}>
  536. <Text style={styles.sectionTitle}>支付方式</Text>
  537. {payConfig?.alipay?.enabled ? (
  538. <TouchableOpacity
  539. style={styles.payOption}
  540. onPress={() => setPaymentMethod("ALIPAY")}
  541. >
  542. <View style={styles.rowLeft}>
  543. <Text style={styles.payLabel}>支付宝支付</Text>
  544. </View>
  545. <View
  546. style={[
  547. styles.radio,
  548. paymentMethod === "ALIPAY" && styles.radioChecked,
  549. ]}
  550. >
  551. {paymentMethod === "ALIPAY" ? (
  552. <View style={styles.radioInner} />
  553. ) : null}
  554. </View>
  555. </TouchableOpacity>
  556. ) : null}
  557. </View>
  558. ) : null}
  559. <View style={styles.agreementRow}>
  560. <View style={styles.agreementLeft}>
  561. <Text style={styles.agreementText}>
  562. 我已满18周岁,已阅读并同意
  563. <Text style={styles.link}>《宝箱服务协议》</Text>
  564. </Text>
  565. <Text style={styles.tips}>
  566. 宝箱商品存在概率性,请谨慎消费
  567. </Text>
  568. </View>
  569. <TouchableOpacity
  570. style={[styles.radio, checked && styles.radioChecked]}
  571. onPress={() => setChecked(!checked)}
  572. >
  573. {checked && <View style={styles.radioInner} />}
  574. </TouchableOpacity>
  575. </View>
  576. </View>
  577. <View style={styles.footer}>
  578. <View style={styles.priceInfo}>
  579. <Text style={styles.totalLabel}>实付:</Text>
  580. <Text style={styles.totalPrice}>
  581. ¥{displayPrice.toFixed(2)}
  582. </Text>
  583. </View>
  584. <TouchableOpacity
  585. style={[styles.payBtn, loading && styles.payBtnDisabled]}
  586. onPress={pay}
  587. disabled={loading}
  588. >
  589. {loading ? (
  590. <ActivityIndicator color="#fff" size="small" />
  591. ) : (
  592. <Text style={styles.payBtnText}>立即支付</Text>
  593. )}
  594. </TouchableOpacity>
  595. </View>
  596. </View>
  597. </View>
  598. </Modal>
  599. {/* 抽奖结果弹窗 */}
  600. <Modal
  601. visible={resultVisible}
  602. transparent
  603. animationType="fade"
  604. onRequestClose={closeResult}
  605. >
  606. <View style={styles.resultOverlay}>
  607. <View style={styles.resultContainer}>
  608. <View style={styles.resultHeader}>
  609. <Text style={styles.resultTitle}>🎉 恭喜您获得 🎉</Text>
  610. <TouchableOpacity
  611. onPress={closeResult}
  612. style={styles.resultCloseBtn}
  613. >
  614. <Text style={styles.closeText}>×</Text>
  615. </TouchableOpacity>
  616. </View>
  617. {resultLoading ? (
  618. <View style={styles.resultLoading}>
  619. <ActivityIndicator size="large" color="#ff9600" />
  620. <Text style={styles.resultLoadingText}>正在开启宝箱...</Text>
  621. </View>
  622. ) : (
  623. <ScrollView
  624. style={styles.resultScroll}
  625. showsVerticalScrollIndicator={false}
  626. >
  627. <View style={styles.resultList}>
  628. {resultList.map((item, index) => (
  629. <View key={item.id || index} style={styles.resultItem}>
  630. <View
  631. style={[
  632. styles.levelBadge,
  633. {
  634. backgroundColor:
  635. LEVEL_MAP[item.level]?.color || "#666",
  636. },
  637. ]}
  638. >
  639. <Text style={styles.levelText}>
  640. {LEVEL_MAP[item.level]?.title || item.level}
  641. </Text>
  642. </View>
  643. <View style={styles.resultImageBox}>
  644. <Image
  645. source={{ uri: item.cover }}
  646. style={styles.resultImage}
  647. contentFit="contain"
  648. />
  649. </View>
  650. <Text style={styles.resultName} numberOfLines={2}>
  651. {item.name}
  652. </Text>
  653. {item.spu?.marketPrice && (
  654. <Text style={styles.resultPrice}>
  655. 参考价:¥{item.spu.marketPrice}
  656. </Text>
  657. )}
  658. </View>
  659. ))}
  660. </View>
  661. </ScrollView>
  662. )}
  663. <View style={styles.resultFooter}>
  664. <TouchableOpacity
  665. style={styles.resultBtn}
  666. onPress={closeResult}
  667. >
  668. <Text style={styles.resultBtnText}>继续抽奖</Text>
  669. </TouchableOpacity>
  670. </View>
  671. </View>
  672. </View>
  673. </Modal>
  674. {/* 支付结果轮询确认加载中 */}
  675. <Modal
  676. visible={verifyLoading}
  677. transparent
  678. animationType="fade"
  679. onRequestClose={() => {}}
  680. >
  681. <View style={styles.verifyLoadingOverlay}>
  682. <View style={styles.verifyLoadingBox}>
  683. <ActivityIndicator size="large" color="#ff9600" />
  684. <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
  685. </View>
  686. </View>
  687. </Modal>
  688. </>
  689. );
  690. },
  691. );
  692. const styles = StyleSheet.create({
  693. verifyLoadingOverlay: {
  694. ...StyleSheet.absoluteFillObject,
  695. backgroundColor: "rgba(0,0,0,0.4)",
  696. justifyContent: "center",
  697. alignItems: "center",
  698. zIndex: 9999,
  699. },
  700. verifyLoadingBox: {
  701. backgroundColor: "rgba(0,0,0,0.8)",
  702. padding: 20,
  703. borderRadius: 12,
  704. alignItems: "center",
  705. },
  706. verifyLoadingText: {
  707. color: "#fff",
  708. marginTop: 10,
  709. fontSize: 14,
  710. },
  711. overlay: {
  712. flex: 1,
  713. backgroundColor: "rgba(0,0,0,0.5)",
  714. justifyContent: "flex-end",
  715. },
  716. mask: { flex: 1 },
  717. container: {
  718. backgroundColor: "#fff",
  719. borderTopLeftRadius: 20,
  720. borderTopRightRadius: 20,
  721. paddingBottom: 34,
  722. },
  723. header: {
  724. flexDirection: "row",
  725. alignItems: "center",
  726. justifyContent: "center",
  727. padding: 15,
  728. borderBottomWidth: 1,
  729. borderBottomColor: "#eee",
  730. position: "relative",
  731. },
  732. title: { fontSize: 16, fontWeight: "600", color: "#333" },
  733. closeBtn: { position: "absolute", right: 15, top: 10 },
  734. closeText: { fontSize: 24, color: "#999" },
  735. content: { padding: 15 },
  736. row: {
  737. flexDirection: "row",
  738. justifyContent: "space-between",
  739. alignItems: "center",
  740. paddingVertical: 10,
  741. },
  742. rowLeft: { flexDirection: "row", alignItems: "center", flex: 1 },
  743. label: { fontSize: 14, color: "#333" },
  744. priceText: { fontSize: 14, color: "#ff9600", fontWeight: "600" },
  745. valueText: { fontSize: 12, color: "#999" },
  746. themeColor: { color: "#ff9600" },
  747. balanceText: { fontSize: 12, color: "#999", marginLeft: 5 },
  748. radio: {
  749. width: 20,
  750. height: 20,
  751. borderRadius: 10,
  752. borderWidth: 2,
  753. borderColor: "#ddd",
  754. justifyContent: "center",
  755. alignItems: "center",
  756. },
  757. radioChecked: { borderColor: "#ff9600" },
  758. radioInner: {
  759. width: 10,
  760. height: 10,
  761. borderRadius: 5,
  762. backgroundColor: "#ff9600",
  763. },
  764. agreementRow: {
  765. flexDirection: "row",
  766. justifyContent: "space-between",
  767. alignItems: "flex-start",
  768. paddingVertical: 10,
  769. marginTop: 10,
  770. },
  771. agreementLeft: { flex: 1, marginRight: 10 },
  772. agreementText: { fontSize: 12, color: "#333", lineHeight: 18 },
  773. link: { color: "#ff9600" },
  774. tips: { fontSize: 11, color: "#999", marginTop: 5 },
  775. footer: {
  776. flexDirection: "row",
  777. alignItems: "center",
  778. justifyContent: "space-between",
  779. paddingHorizontal: 15,
  780. paddingTop: 15,
  781. borderTopWidth: 1,
  782. borderTopColor: "#eee",
  783. },
  784. priceInfo: { flexDirection: "row", alignItems: "center" },
  785. totalLabel: { fontSize: 14, color: "#333" },
  786. totalPrice: { fontSize: 20, color: "#ff9600", fontWeight: "bold" },
  787. payBtn: {
  788. backgroundColor: "#ff9600",
  789. paddingHorizontal: 30,
  790. paddingVertical: 12,
  791. borderRadius: 25,
  792. },
  793. payBtnDisabled: { opacity: 0.6 },
  794. payBtnText: { color: "#fff", fontSize: 16, fontWeight: "600" },
  795. // 自由购买弹窗
  796. freedomContainer: {
  797. backgroundColor: "#fff",
  798. borderTopLeftRadius: 20,
  799. borderTopRightRadius: 20,
  800. paddingBottom: 34,
  801. },
  802. freedomContent: { padding: 20 },
  803. freedomBtnList: {
  804. flexDirection: "row",
  805. flexWrap: "wrap",
  806. justifyContent: "space-between",
  807. },
  808. freedomBtn: {
  809. width: "48%",
  810. height: 50,
  811. backgroundColor: "#fff",
  812. borderRadius: 8,
  813. justifyContent: "center",
  814. alignItems: "center",
  815. marginBottom: 12,
  816. borderWidth: 1,
  817. borderColor: "#eee",
  818. position: "relative",
  819. },
  820. freedomBtnActive: { backgroundColor: "#F1423D", borderColor: "#F1423D" },
  821. freedomBtnText: { fontSize: 18, fontWeight: "bold", color: "#333" },
  822. freedomBtnTextActive: { color: "#fff" },
  823. freedomUnit: { fontSize: 12, fontWeight: "500" },
  824. checkIcon: {
  825. position: "absolute",
  826. bottom: 0,
  827. right: 0,
  828. backgroundColor: "#fff",
  829. color: "#F1423D",
  830. fontSize: 12,
  831. paddingHorizontal: 6,
  832. paddingVertical: 2,
  833. borderTopLeftRadius: 8,
  834. borderBottomRightRadius: 8,
  835. },
  836. freedomSubmitBtn: {
  837. backgroundColor: "#ff9600",
  838. height: 50,
  839. borderRadius: 25,
  840. justifyContent: "center",
  841. alignItems: "center",
  842. marginTop: 20,
  843. },
  844. freedomSubmitText: { color: "#fff", fontSize: 16, fontWeight: "600" },
  845. // 抽奖结果弹窗
  846. resultOverlay: {
  847. flex: 1,
  848. backgroundColor: "rgba(0,0,0,0.7)",
  849. justifyContent: "center",
  850. alignItems: "center",
  851. padding: 20,
  852. },
  853. resultContainer: {
  854. width: "100%",
  855. maxHeight: "80%",
  856. backgroundColor: "#fff",
  857. borderRadius: 16,
  858. overflow: "hidden",
  859. },
  860. resultHeader: {
  861. alignItems: "center",
  862. padding: 20,
  863. backgroundColor: "#ff9600",
  864. position: "relative",
  865. },
  866. resultTitle: {
  867. fontSize: 20,
  868. fontWeight: "bold",
  869. color: "#fff",
  870. },
  871. resultCloseBtn: {
  872. position: "absolute",
  873. right: 15,
  874. top: 15,
  875. width: 30,
  876. height: 30,
  877. backgroundColor: "rgba(255,255,255,0.3)",
  878. borderRadius: 15,
  879. justifyContent: "center",
  880. alignItems: "center",
  881. },
  882. resultLoading: {
  883. padding: 60,
  884. alignItems: "center",
  885. },
  886. resultLoadingText: {
  887. marginTop: 15,
  888. fontSize: 14,
  889. color: "#666",
  890. },
  891. resultScroll: {
  892. maxHeight: 400,
  893. },
  894. resultList: {
  895. flexDirection: "row",
  896. flexWrap: "wrap",
  897. padding: 10,
  898. justifyContent: "space-between",
  899. },
  900. resultItem: {
  901. width: (SCREEN_WIDTH - 80) / 2,
  902. backgroundColor: "#f9f9f9",
  903. borderRadius: 10,
  904. padding: 10,
  905. marginBottom: 10,
  906. alignItems: "center",
  907. },
  908. levelBadge: {
  909. paddingHorizontal: 10,
  910. paddingVertical: 3,
  911. borderRadius: 10,
  912. marginBottom: 8,
  913. },
  914. levelText: {
  915. color: "#fff",
  916. fontSize: 11,
  917. fontWeight: "bold",
  918. },
  919. resultImageBox: {
  920. width: "100%",
  921. aspectRatio: 1,
  922. backgroundColor: "#fff",
  923. borderRadius: 8,
  924. overflow: "hidden",
  925. },
  926. resultImage: {
  927. width: "100%",
  928. height: "100%",
  929. },
  930. resultName: {
  931. fontSize: 12,
  932. color: "#333",
  933. textAlign: "center",
  934. marginTop: 8,
  935. lineHeight: 16,
  936. },
  937. resultPrice: {
  938. fontSize: 10,
  939. color: "#999",
  940. marginTop: 4,
  941. },
  942. resultFooter: {
  943. padding: 15,
  944. borderTopWidth: 1,
  945. borderTopColor: "#eee",
  946. },
  947. resultBtn: {
  948. backgroundColor: "#ff9600",
  949. height: 46,
  950. borderRadius: 23,
  951. justifyContent: "center",
  952. alignItems: "center",
  953. },
  954. resultBtnText: {
  955. color: "#fff",
  956. fontSize: 16,
  957. fontWeight: "600",
  958. },
  959. paymentSection: {
  960. marginTop: 20,
  961. borderTopWidth: 1,
  962. borderTopColor: "#f0f0f0",
  963. paddingTop: 10,
  964. },
  965. sectionTitle: {
  966. fontSize: 14,
  967. fontWeight: "bold",
  968. marginBottom: 10,
  969. color: "#333",
  970. },
  971. payOption: {
  972. flexDirection: "row",
  973. justifyContent: "space-between",
  974. alignItems: "center",
  975. paddingVertical: 12,
  976. borderBottomWidth: 1,
  977. borderBottomColor: "#f9f9f9",
  978. },
  979. payLabel: {
  980. fontSize: 14,
  981. color: "#333",
  982. marginLeft: 10,
  983. },
  984. });