CheckoutModal.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  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. useImperativeHandle(ref, () => ({
  86. show: (n: number, previewData: any = {}, bNum?: string, seats?: number[], pack?: boolean) => {
  87. setNum(n);
  88. setBoxNum(bNum);
  89. setSeatNumbers(seats);
  90. setPackFlag(pack || undefined);
  91. setPreviewData(previewData);
  92. setVisible(true);
  93. },
  94. showFreedom: () => {
  95. setFreedomNum(10);
  96. setFreedomSelectVisible(true);
  97. },
  98. close: () => {
  99. setVisible(false);
  100. setFreedomSelectVisible(false);
  101. setResultVisible(false);
  102. },
  103. }));
  104. const handleFreedomSelect = async (selectedNum: number) => {
  105. setFreedomSelectVisible(false);
  106. setLoading(true);
  107. try {
  108. const preview = await previewOrder(poolId, selectedNum);
  109. if (preview) {
  110. setNum(selectedNum);
  111. setFreedomNum(selectedNum);
  112. setPreviewData(preview);
  113. setVisible(true);
  114. }
  115. } catch (error: any) {
  116. Alert.alert('提示', error?.message || '获取订单信息失败');
  117. } finally {
  118. setLoading(false);
  119. }
  120. };
  121. const close = () => {
  122. setVisible(false);
  123. setFreedomSelectVisible(false);
  124. };
  125. const closeResult = () => {
  126. setResultVisible(false);
  127. setResultList([]);
  128. onSuccess({ tradeNo: '', num });
  129. };
  130. // 支付
  131. const pay = async () => {
  132. if (loading) return;
  133. if (!checked) {
  134. Alert.alert('提示', '请同意《宝箱服务协议》');
  135. return;
  136. }
  137. setLoading(true);
  138. try {
  139. let paymentType = '';
  140. // Prioritize Wallet if checked
  141. if (cashChecked) {
  142. paymentType = 'WALLET';
  143. } else {
  144. // Default to Alipay for now as per branch logic
  145. paymentType = 'ALIPAY_APP';
  146. }
  147. // If wallet is insufficient and logic requires mixed payment, it might be more complex
  148. // strictly following branch logic: if cashChecked use Wallet, else Alipay
  149. const payNum = packFlag ? 1 : num;
  150. const res = await applyOrder(poolId, payNum, paymentType, boxNum, seatNumbers, packFlag);
  151. // Handle Alipay Response
  152. if (res?.payInfo) {
  153. Alipay.setAlipayScheme('alipay2021005175632205');
  154. const result = await Alipay.pay(res.payInfo);
  155. console.log('Alipay Result:', result);
  156. const resultStatus = result?.resultStatus;
  157. if (resultStatus === '9000') {
  158. // Payment Success
  159. // Maybe verify payment here if needed, or just assume success
  160. const tradeNo = res.bizTradeNo || res.tradeNo; // Assuming tradeNo is in applyOrder response
  161. setVisible(false);
  162. router.push({
  163. pathname: '/lottery' as any,
  164. params: { tradeNo, num, poolId }
  165. });
  166. onSuccess({ tradeNo, num });
  167. } else {
  168. Alert.alert('提示', '支付未完成');
  169. }
  170. return;
  171. }
  172. if (res?.paySuccess || res?.bizTradeNo || res?.tradeNo) {
  173. const tradeNo = res.bizTradeNo || res.tradeNo;
  174. setVisible(false);
  175. // Navigation to Lottery Animation
  176. router.push({
  177. pathname: '/lottery' as any,
  178. params: { tradeNo, num, poolId }
  179. });
  180. // Trigger success callback (e.g. to refresh pool data)
  181. onSuccess({ tradeNo, num });
  182. } else {
  183. Alert.alert('提示', res?.message || '支付失败,请重试');
  184. }
  185. } catch (error: any) {
  186. console.error('Pay error:', error);
  187. Alert.alert('支付失败', error?.message || '请稍后重试');
  188. } finally {
  189. setLoading(false);
  190. }
  191. };
  192. // 获取抽奖结果(10发以下用弹窗)
  193. const fetchLotteryResult = async (tradeNo: string) => {
  194. setResultLoading(true);
  195. setResultVisible(true);
  196. setResultList([]);
  197. let attempts = 0;
  198. const maxAttempts = 5;
  199. const poll = async () => {
  200. try {
  201. const res = await getApplyResult(tradeNo);
  202. if (res?.inventoryList && res.inventoryList.length > 0) {
  203. setResultList(res.inventoryList);
  204. setResultLoading(false);
  205. // 不在这里调用 onSuccess,等用户关闭弹窗时再调用
  206. } else if (attempts < maxAttempts) {
  207. attempts++;
  208. setTimeout(poll, 1000);
  209. } else {
  210. setResultLoading(false);
  211. if (typeof window !== 'undefined') {
  212. Alert.alert('提示', '获取结果超时,请在仓库中查看');
  213. }
  214. }
  215. } catch {
  216. if (attempts < maxAttempts) {
  217. attempts++;
  218. setTimeout(poll, 1000);
  219. } else {
  220. setResultLoading(false);
  221. if (typeof window !== 'undefined') {
  222. Alert.alert('提示', '获取结果失败,请在仓库中查看');
  223. }
  224. }
  225. }
  226. };
  227. poll();
  228. };
  229. const displayPrice = lastPrice ?? (data?.price || 0) * num;
  230. return (
  231. <>
  232. {/* 10发以上的全屏抽奖结果弹窗 */}
  233. <LotteryResultModal
  234. ref={lotteryResultRef}
  235. onClose={() => {
  236. // 抽奖结果弹窗关闭后刷新数据
  237. onSuccess({ tradeNo: '', num });
  238. }}
  239. onGoStore={() => {
  240. onSuccess({ tradeNo: '', num });
  241. router.replace('/store' as any);
  242. }}
  243. />
  244. {/* 自由购买数量选择弹窗 */}
  245. <Modal visible={freedomSelectVisible} transparent animationType="fade" onRequestClose={() => setFreedomSelectVisible(false)}>
  246. <View style={styles.overlay}>
  247. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setFreedomSelectVisible(false)} />
  248. <View style={styles.freedomContainer}>
  249. <View style={styles.header}>
  250. <Text style={styles.title}>购买多盒</Text>
  251. <TouchableOpacity onPress={() => setFreedomSelectVisible(false)} style={styles.closeBtn}>
  252. <Text style={styles.closeText}>×</Text>
  253. </TouchableOpacity>
  254. </View>
  255. <View style={styles.freedomContent}>
  256. <View style={styles.freedomBtnList}>
  257. {FREEDOM_NUMS.map((item) => (
  258. <TouchableOpacity
  259. key={item}
  260. style={[styles.freedomBtn, freedomNum === item && styles.freedomBtnActive]}
  261. onPress={() => setFreedomNum(item)}
  262. >
  263. <Text style={[styles.freedomBtnText, freedomNum === item && styles.freedomBtnTextActive]}>
  264. {item}<Text style={styles.freedomUnit}>盒</Text>
  265. </Text>
  266. {freedomNum === item && <Text style={styles.checkIcon}>✓</Text>}
  267. </TouchableOpacity>
  268. ))}
  269. </View>
  270. <TouchableOpacity
  271. style={[styles.freedomSubmitBtn, loading && styles.payBtnDisabled]}
  272. onPress={() => handleFreedomSelect(freedomNum)}
  273. disabled={loading}
  274. >
  275. {loading ? (
  276. <ActivityIndicator color="#fff" size="small" />
  277. ) : (
  278. <Text style={styles.freedomSubmitText}>确认 ¥{(data?.price || 0) * freedomNum}</Text>
  279. )}
  280. </TouchableOpacity>
  281. </View>
  282. </View>
  283. </View>
  284. </Modal>
  285. {/* 支付确认弹窗 */}
  286. <Modal visible={visible} transparent animationType="slide" onRequestClose={close}>
  287. <View style={styles.overlay}>
  288. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={close} />
  289. <View style={styles.container}>
  290. <View style={styles.header}>
  291. <Text style={styles.title}>{data?.name}</Text>
  292. <TouchableOpacity onPress={close} style={styles.closeBtn}>
  293. <Text style={styles.closeText}>×</Text>
  294. </TouchableOpacity>
  295. </View>
  296. <View style={styles.content}>
  297. <View style={styles.row}>
  298. <Text style={styles.label}>购买件数</Text>
  299. <Text style={styles.priceText}>¥{data?.price} x {num}</Text>
  300. </View>
  301. <View style={styles.row}>
  302. <Text style={styles.label}>优惠券</Text>
  303. <Text style={[styles.valueText, couponAmount ? styles.themeColor : {}]}>
  304. {couponAmount ? `已使用优惠¥${couponAmount}` : '暂无优惠券可选'}
  305. </Text>
  306. </View>
  307. {magic && magic.balance > 0 && (
  308. <View style={styles.row}>
  309. <View style={styles.rowLeft}>
  310. <Text style={styles.label}>果实</Text>
  311. <Text style={styles.balanceText}>(剩余:{magic.balance})</Text>
  312. </View>
  313. <Text style={styles.balanceText}>
  314. 已抵扣 <Text style={styles.themeColor}>¥{coin || 0}</Text>
  315. </Text>
  316. </View>
  317. )}
  318. {cash && (
  319. <View style={styles.row}>
  320. <View style={styles.rowLeft}>
  321. <Text style={styles.label}>钱包支付</Text>
  322. <Text style={styles.themeColor}>(余额:¥{cash.balance})</Text>
  323. </View>
  324. <TouchableOpacity
  325. style={[styles.radio, cashChecked && styles.radioChecked]}
  326. onPress={() => setCashChecked(!cashChecked)}
  327. >
  328. {cashChecked && <View style={styles.radioInner} />}
  329. </TouchableOpacity>
  330. </View>
  331. )}
  332. <View style={styles.agreementRow}>
  333. <View style={styles.agreementLeft}>
  334. <View style={styles.agreementTextContainer}>
  335. <Text style={styles.agreementText}>我已满18周岁,已阅读并同意</Text>
  336. <TouchableOpacity onPress={() => {
  337. setVisible(false);
  338. router.push({ pathname: '/agreement', params: { type: 'magic.html' } });
  339. }}>
  340. <Text style={styles.link}>《宝箱服务协议》</Text>
  341. </TouchableOpacity>
  342. </View>
  343. <Text style={styles.tips}>宝箱商品存在概率性,请谨慎消费</Text>
  344. </View>
  345. <TouchableOpacity
  346. style={[styles.radio, checked && styles.radioChecked]}
  347. onPress={() => setChecked(!checked)}
  348. >
  349. {checked && <View style={styles.radioInner} />}
  350. </TouchableOpacity>
  351. </View>
  352. </View>
  353. <View style={styles.footer}>
  354. <View style={styles.priceInfo}>
  355. <Text style={styles.totalLabel}>实付:</Text>
  356. <Text style={styles.totalPrice}>¥{displayPrice.toFixed(2)}</Text>
  357. </View>
  358. <TouchableOpacity
  359. style={[styles.payBtn, loading && styles.payBtnDisabled]}
  360. onPress={pay}
  361. disabled={loading}
  362. >
  363. {loading ? <ActivityIndicator color="#fff" size="small" /> : <Text style={styles.payBtnText}>立即支付</Text>}
  364. </TouchableOpacity>
  365. </View>
  366. </View>
  367. </View>
  368. </Modal>
  369. {/* 抽奖结果弹窗 */}
  370. <Modal visible={resultVisible} transparent animationType="fade" onRequestClose={closeResult}>
  371. <View style={styles.resultOverlay}>
  372. <View style={styles.resultContainer}>
  373. <View style={styles.resultHeader}>
  374. <Text style={styles.resultTitle}>🎉 恭喜您获得 🎉</Text>
  375. <TouchableOpacity onPress={closeResult} style={styles.resultCloseBtn}>
  376. <Text style={styles.closeText}>×</Text>
  377. </TouchableOpacity>
  378. </View>
  379. {resultLoading ? (
  380. <View style={styles.resultLoading}>
  381. <ActivityIndicator size="large" color="#ff9600" />
  382. <Text style={styles.resultLoadingText}>正在开启宝箱...</Text>
  383. </View>
  384. ) : (
  385. <ScrollView style={styles.resultScroll} showsVerticalScrollIndicator={false}>
  386. <View style={styles.resultList}>
  387. {resultList.map((item, index) => (
  388. <View key={item.id || index} style={styles.resultItem}>
  389. <View style={[styles.levelBadge, { backgroundColor: LEVEL_MAP[item.level]?.color || '#666' }]}>
  390. <Text style={styles.levelText}>{LEVEL_MAP[item.level]?.title || item.level}</Text>
  391. </View>
  392. <View style={styles.resultImageBox}>
  393. <Image source={{ uri: item.cover }} style={styles.resultImage} contentFit="contain" />
  394. </View>
  395. <Text style={styles.resultName} numberOfLines={2}>{item.name}</Text>
  396. {item.spu?.marketPrice && (
  397. <Text style={styles.resultPrice}>参考价:¥{item.spu.marketPrice}</Text>
  398. )}
  399. </View>
  400. ))}
  401. </View>
  402. </ScrollView>
  403. )}
  404. <View style={styles.resultFooter}>
  405. <TouchableOpacity style={styles.resultBtn} onPress={closeResult}>
  406. <Text style={styles.resultBtnText}>继续抽奖</Text>
  407. </TouchableOpacity>
  408. </View>
  409. </View>
  410. </View>
  411. </Modal>
  412. </>
  413. );
  414. }
  415. );
  416. const styles = StyleSheet.create({
  417. overlay: {
  418. flex: 1,
  419. backgroundColor: 'rgba(0,0,0,0.5)',
  420. justifyContent: 'flex-end',
  421. },
  422. mask: { flex: 1 },
  423. container: {
  424. backgroundColor: '#fff',
  425. borderTopLeftRadius: 20,
  426. borderTopRightRadius: 20,
  427. paddingBottom: 34,
  428. },
  429. header: {
  430. flexDirection: 'row',
  431. alignItems: 'center',
  432. justifyContent: 'center',
  433. padding: 15,
  434. borderBottomWidth: 1,
  435. borderBottomColor: '#eee',
  436. position: 'relative',
  437. },
  438. title: { fontSize: 16, fontWeight: '600', color: '#333' },
  439. closeBtn: { position: 'absolute', right: 15, top: 10 },
  440. closeText: { fontSize: 24, color: '#999' },
  441. content: { padding: 15 },
  442. row: {
  443. flexDirection: 'row',
  444. justifyContent: 'space-between',
  445. alignItems: 'center',
  446. paddingVertical: 10,
  447. },
  448. rowLeft: { flexDirection: 'row', alignItems: 'center', flex: 1 },
  449. label: { fontSize: 14, color: '#333' },
  450. priceText: { fontSize: 14, color: '#ff9600', fontWeight: '600' },
  451. valueText: { fontSize: 12, color: '#999' },
  452. themeColor: { color: '#ff9600' },
  453. balanceText: { fontSize: 12, color: '#999', marginLeft: 5 },
  454. radio: {
  455. width: 20,
  456. height: 20,
  457. borderRadius: 10,
  458. borderWidth: 2,
  459. borderColor: '#ddd',
  460. justifyContent: 'center',
  461. alignItems: 'center',
  462. },
  463. radioChecked: { borderColor: '#ff9600' },
  464. radioInner: { width: 10, height: 10, borderRadius: 5, backgroundColor: '#ff9600' },
  465. agreementRow: {
  466. flexDirection: 'row',
  467. justifyContent: 'space-between',
  468. alignItems: 'flex-start',
  469. paddingVertical: 10,
  470. marginTop: 10,
  471. },
  472. agreementLeft: { flex: 1, marginRight: 10 },
  473. agreementTextContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap' },
  474. agreementText: { fontSize: 12, color: '#333', lineHeight: 18 },
  475. link: { color: '#ff9600' },
  476. tips: { fontSize: 11, color: '#999', marginTop: 5 },
  477. footer: {
  478. flexDirection: 'row',
  479. alignItems: 'center',
  480. justifyContent: 'space-between',
  481. paddingHorizontal: 15,
  482. paddingTop: 15,
  483. borderTopWidth: 1,
  484. borderTopColor: '#eee',
  485. },
  486. priceInfo: { flexDirection: 'row', alignItems: 'center' },
  487. totalLabel: { fontSize: 14, color: '#333' },
  488. totalPrice: { fontSize: 20, color: '#ff9600', fontWeight: 'bold' },
  489. payBtn: {
  490. backgroundColor: '#ff9600',
  491. paddingHorizontal: 30,
  492. paddingVertical: 12,
  493. borderRadius: 25,
  494. },
  495. payBtnDisabled: { opacity: 0.6 },
  496. payBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  497. // 自由购买弹窗
  498. freedomContainer: {
  499. backgroundColor: '#fff',
  500. borderTopLeftRadius: 20,
  501. borderTopRightRadius: 20,
  502. paddingBottom: 34,
  503. },
  504. freedomContent: { padding: 20 },
  505. freedomBtnList: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
  506. freedomBtn: {
  507. width: '48%',
  508. height: 50,
  509. backgroundColor: '#fff',
  510. borderRadius: 8,
  511. justifyContent: 'center',
  512. alignItems: 'center',
  513. marginBottom: 12,
  514. borderWidth: 1,
  515. borderColor: '#eee',
  516. position: 'relative',
  517. },
  518. freedomBtnActive: { backgroundColor: '#F1423D', borderColor: '#F1423D' },
  519. freedomBtnText: { fontSize: 18, fontWeight: 'bold', color: '#333' },
  520. freedomBtnTextActive: { color: '#fff' },
  521. freedomUnit: { fontSize: 12, fontWeight: '500' },
  522. checkIcon: {
  523. position: 'absolute',
  524. bottom: 0,
  525. right: 0,
  526. backgroundColor: '#fff',
  527. color: '#F1423D',
  528. fontSize: 12,
  529. paddingHorizontal: 6,
  530. paddingVertical: 2,
  531. borderTopLeftRadius: 8,
  532. borderBottomRightRadius: 8,
  533. },
  534. freedomSubmitBtn: {
  535. backgroundColor: '#ff9600',
  536. height: 50,
  537. borderRadius: 25,
  538. justifyContent: 'center',
  539. alignItems: 'center',
  540. marginTop: 20,
  541. },
  542. freedomSubmitText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  543. // 抽奖结果弹窗
  544. resultOverlay: {
  545. flex: 1,
  546. backgroundColor: 'rgba(0,0,0,0.7)',
  547. justifyContent: 'center',
  548. alignItems: 'center',
  549. padding: 20,
  550. },
  551. resultContainer: {
  552. width: '100%',
  553. maxHeight: '80%',
  554. backgroundColor: '#fff',
  555. borderRadius: 16,
  556. overflow: 'hidden',
  557. },
  558. resultHeader: {
  559. alignItems: 'center',
  560. padding: 20,
  561. backgroundColor: '#ff9600',
  562. position: 'relative',
  563. },
  564. resultTitle: {
  565. fontSize: 20,
  566. fontWeight: 'bold',
  567. color: '#fff',
  568. },
  569. resultCloseBtn: {
  570. position: 'absolute',
  571. right: 15,
  572. top: 15,
  573. width: 30,
  574. height: 30,
  575. backgroundColor: 'rgba(255,255,255,0.3)',
  576. borderRadius: 15,
  577. justifyContent: 'center',
  578. alignItems: 'center',
  579. },
  580. resultLoading: {
  581. padding: 60,
  582. alignItems: 'center',
  583. },
  584. resultLoadingText: {
  585. marginTop: 15,
  586. fontSize: 14,
  587. color: '#666',
  588. },
  589. resultScroll: {
  590. maxHeight: 400,
  591. },
  592. resultList: {
  593. flexDirection: 'row',
  594. flexWrap: 'wrap',
  595. padding: 10,
  596. justifyContent: 'space-between',
  597. },
  598. resultItem: {
  599. width: (SCREEN_WIDTH - 80) / 2,
  600. backgroundColor: '#f9f9f9',
  601. borderRadius: 10,
  602. padding: 10,
  603. marginBottom: 10,
  604. alignItems: 'center',
  605. },
  606. levelBadge: {
  607. paddingHorizontal: 10,
  608. paddingVertical: 3,
  609. borderRadius: 10,
  610. marginBottom: 8,
  611. },
  612. levelText: {
  613. color: '#fff',
  614. fontSize: 11,
  615. fontWeight: 'bold',
  616. },
  617. resultImageBox: {
  618. width: '100%',
  619. aspectRatio: 1,
  620. backgroundColor: '#fff',
  621. borderRadius: 8,
  622. overflow: 'hidden',
  623. },
  624. resultImage: {
  625. width: '100%',
  626. height: '100%',
  627. },
  628. resultName: {
  629. fontSize: 12,
  630. color: '#333',
  631. textAlign: 'center',
  632. marginTop: 8,
  633. lineHeight: 16,
  634. },
  635. resultPrice: {
  636. fontSize: 10,
  637. color: '#999',
  638. marginTop: 4,
  639. },
  640. resultFooter: {
  641. padding: 15,
  642. borderTopWidth: 1,
  643. borderTopColor: '#eee',
  644. },
  645. resultBtn: {
  646. backgroundColor: '#ff9600',
  647. height: 46,
  648. borderRadius: 23,
  649. justifyContent: 'center',
  650. alignItems: 'center',
  651. },
  652. resultBtnText: {
  653. color: '#fff',
  654. fontSize: 16,
  655. fontWeight: '600',
  656. },
  657. });