CheckoutModal.tsx 29 KB

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