CheckoutModal.tsx 26 KB

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