CheckoutModal.tsx 28 KB

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