CheckoutModal.tsx 29 KB

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