CheckoutModal.tsx 27 KB

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