index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. import { Image } from 'expo-image';
  2. import { useLocalSearchParams, useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useRef, useState } from 'react';
  4. import {
  5. ActivityIndicator,
  6. Animated,
  7. Dimensions,
  8. ImageBackground,
  9. ScrollView,
  10. StatusBar,
  11. StyleSheet,
  12. Text,
  13. TouchableOpacity,
  14. View,
  15. } from 'react-native';
  16. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  17. import { Images } from '@/constants/images';
  18. import {
  19. getPoolDetail,
  20. poolIn,
  21. poolOut,
  22. previewOrder,
  23. } from '@/services/award';
  24. import { CheckoutModal } from './components/CheckoutModal';
  25. import { ProductList } from './components/ProductList';
  26. import { RecordModal } from './components/RecordModal';
  27. import { RuleModal } from './components/RuleModal';
  28. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  29. interface PoolData {
  30. id: string;
  31. name: string;
  32. cover: string;
  33. price: number;
  34. specialPrice?: number;
  35. specialPriceFive?: number;
  36. demoFlag?: number;
  37. luckGoodsList: ProductItem[];
  38. luckGoodsLevelProbabilityList?: any[];
  39. }
  40. interface ProductItem {
  41. id: string;
  42. name: string;
  43. cover: string;
  44. level: string;
  45. probability: number;
  46. price?: number;
  47. }
  48. export default function AwardDetailScreen() {
  49. const { poolId } = useLocalSearchParams<{ poolId: string }>();
  50. const router = useRouter();
  51. const insets = useSafeAreaInsets();
  52. const [loading, setLoading] = useState(true);
  53. const [data, setData] = useState<PoolData | null>(null);
  54. const [products, setProducts] = useState<ProductItem[]>([]);
  55. const [currentIndex, setCurrentIndex] = useState(0);
  56. const [scrollTop, setScrollTop] = useState(0);
  57. const checkoutRef = useRef<any>(null);
  58. const recordRef = useRef<any>(null);
  59. const ruleRef = useRef<any>(null);
  60. const floatAnim = useRef(new Animated.Value(0)).current;
  61. useEffect(() => {
  62. Animated.loop(
  63. Animated.sequence([
  64. Animated.timing(floatAnim, { toValue: 10, duration: 1500, useNativeDriver: true }),
  65. Animated.timing(floatAnim, { toValue: -10, duration: 1500, useNativeDriver: true }),
  66. ])
  67. ).start();
  68. }, []);
  69. const loadData = useCallback(async () => {
  70. if (!poolId) return;
  71. setLoading(true);
  72. try {
  73. const detail = await getPoolDetail(poolId);
  74. if (detail) {
  75. setData(detail);
  76. setProducts(detail.luckGoodsList || []);
  77. }
  78. } catch (error) {
  79. console.error('加载数据失败:', error);
  80. }
  81. setLoading(false);
  82. }, [poolId]);
  83. useEffect(() => {
  84. loadData();
  85. if (poolId) poolIn(poolId);
  86. return () => {
  87. if (poolId) poolOut(poolId);
  88. };
  89. }, [poolId]);
  90. const handlePay = async (num: number) => {
  91. if (!poolId || !data) return;
  92. try {
  93. const preview = await previewOrder(poolId, num);
  94. if (preview) checkoutRef.current?.show(num, preview);
  95. } catch (error) {
  96. console.error('预览订单失败:', error);
  97. }
  98. };
  99. const handleSuccess = () => {
  100. setTimeout(() => loadData(), 500);
  101. };
  102. const handlePrev = () => {
  103. if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
  104. };
  105. const handleNext = () => {
  106. if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
  107. };
  108. const getLevelName = (level: string) => {
  109. const map: Record<string, string> = { A: '超神款', B: '欧皇款', C: '隐藏款', D: '普通款' };
  110. return map[level] || level;
  111. };
  112. const ignoreRatio0 = (val: number) => {
  113. const str = String(val);
  114. const match = str.match(/^(\d+\.?\d*?)0*$/);
  115. return match ? match[1].replace(/\.$/, '') : str;
  116. };
  117. if (loading) {
  118. return (
  119. <View style={styles.loadingContainer}>
  120. <ActivityIndicator size="large" color="#fff" />
  121. </View>
  122. );
  123. }
  124. if (!data) {
  125. return (
  126. <View style={styles.loadingContainer}>
  127. <Text style={styles.errorText}>奖池不存在</Text>
  128. <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
  129. <Text style={styles.backBtn2Text}>返回</Text>
  130. </TouchableOpacity>
  131. </View>
  132. );
  133. }
  134. const currentProduct = products[currentIndex];
  135. const headerBg = scrollTop > 0 ? '#333' : 'transparent';
  136. return (
  137. <View style={styles.container}>
  138. <StatusBar barStyle="light-content" />
  139. <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
  140. {/* 顶部导航 */}
  141. <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
  142. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  143. <Text style={styles.backText}>{'<'}</Text>
  144. </TouchableOpacity>
  145. <Text style={styles.headerTitle} numberOfLines={1}>{data.name}</Text>
  146. <View style={styles.placeholder} />
  147. </View>
  148. <ScrollView
  149. style={styles.scrollView}
  150. showsVerticalScrollIndicator={false}
  151. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  152. scrollEventThrottle={16}
  153. >
  154. {/* 主商品展示区域 */}
  155. <ImageBackground
  156. source={{ uri: Images.box.detail.mainGoodsSection }}
  157. style={styles.mainGoodsSection}
  158. resizeMode="cover"
  159. >
  160. <View style={{ height: 72 + insets.top }} />
  161. {/* 商品轮播区域 */}
  162. <View style={styles.mainSwiper}>
  163. {currentProduct && (
  164. <>
  165. <Animated.View style={[styles.productImageBox, { transform: [{ translateY: floatAnim }] }]}>
  166. <Image source={{ uri: currentProduct.cover }} style={styles.productImage} contentFit="contain" />
  167. </Animated.View>
  168. {/* 等级信息 */}
  169. <ImageBackground
  170. source={{ uri: Images.box.detail.detailsBut }}
  171. style={styles.detailsBut}
  172. resizeMode="contain"
  173. >
  174. <View style={styles.detailsText}>
  175. <Text style={styles.levelText}>{getLevelName(currentProduct.level)}</Text>
  176. <Text style={styles.probabilityText}>
  177. ({ignoreRatio0(currentProduct.probability * 100)}%)
  178. </Text>
  179. </View>
  180. </ImageBackground>
  181. {/* 商品名称 */}
  182. <ImageBackground source={{ uri: Images.box.detail.nameBg }} style={styles.goodsNameBg} resizeMode="contain">
  183. <Text style={styles.goodsNameText} numberOfLines={6}>{currentProduct.name}</Text>
  184. </ImageBackground>
  185. </>
  186. )}
  187. {/* 左右切换按钮 */}
  188. {currentIndex > 0 && (
  189. <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
  190. <Image source={{ uri: Images.box.detail.left }} style={styles.arrowImg} contentFit="contain" />
  191. </TouchableOpacity>
  192. )}
  193. {currentIndex < products.length - 1 && (
  194. <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
  195. <Image source={{ uri: Images.box.detail.right }} style={styles.arrowImg} contentFit="contain" />
  196. </TouchableOpacity>
  197. )}
  198. </View>
  199. {/* 左侧装饰 */}
  200. <Image source={{ uri: Images.box.detail.positionBgleftBg }} style={styles.positionBgleftBg} contentFit="contain" />
  201. {/* 右侧装饰 */}
  202. <Image source={{ uri: Images.box.detail.positionBgRightBg }} style={styles.positionBgRightBg} contentFit="contain" />
  203. </ImageBackground>
  204. {/* 底部装饰文字 */}
  205. <Image source={{ uri: Images.box.detail.mainGoodsSectionBtext }} style={styles.mainGoodsSectionBtext} contentFit="cover" />
  206. {/* 侧边按钮 - 规则 */}
  207. <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
  208. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
  209. <Text style={styles.positionButText}>规则</Text>
  210. </ImageBackground>
  211. </TouchableOpacity>
  212. {/* 侧边按钮 - 记录 */}
  213. <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
  214. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
  215. <Text style={styles.positionButText}>记录</Text>
  216. </ImageBackground>
  217. </TouchableOpacity>
  218. {/* 侧边按钮 - 仓库 */}
  219. <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store' as any)}>
  220. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
  221. <Text style={styles.positionButTextR}>仓库</Text>
  222. </ImageBackground>
  223. </TouchableOpacity>
  224. {/* 商品列表 */}
  225. <ProductList products={products} levelList={data.luckGoodsLevelProbabilityList} poolId={poolId!} price={data.price} />
  226. <View style={{ height: 150 }} />
  227. </ScrollView>
  228. {/* 底部购买栏 */}
  229. <ImageBackground source={{ uri: Images.box.detail.boxDetailBott }} style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]} resizeMode="cover">
  230. <View style={styles.bottomBtns}>
  231. <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(1)} activeOpacity={0.8}>
  232. <ImageBackground source={{ uri: Images.common.butBgV }} style={styles.btnBg} resizeMode="contain">
  233. <Text style={styles.btnText}>购买一盒</Text>
  234. <Text style={styles.btnPrice}>(¥{data.specialPrice || data.price})</Text>
  235. </ImageBackground>
  236. </TouchableOpacity>
  237. <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(5)} activeOpacity={0.8}>
  238. <ImageBackground source={{ uri: Images.common.butBgL }} style={styles.btnBg} resizeMode="contain">
  239. <Text style={styles.btnText}>购买五盒</Text>
  240. <Text style={styles.btnPrice}>(¥{data.specialPriceFive || data.price * 5})</Text>
  241. </ImageBackground>
  242. </TouchableOpacity>
  243. <TouchableOpacity style={styles.btnItem} onPress={() => checkoutRef.current?.showFreedom()} activeOpacity={0.8}>
  244. <ImageBackground source={{ uri: Images.common.butBgH }} style={styles.btnBg} resizeMode="contain">
  245. <Text style={styles.btnText}>购买多盒</Text>
  246. </ImageBackground>
  247. </TouchableOpacity>
  248. </View>
  249. </ImageBackground>
  250. </ImageBackground>
  251. <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} onSuccess={handleSuccess} />
  252. <RecordModal ref={recordRef} poolId={poolId!} />
  253. <RuleModal ref={ruleRef} />
  254. </View>
  255. );
  256. }
  257. const styles = StyleSheet.create({
  258. container: { flex: 1, backgroundColor: '#1a1a2e' },
  259. background: { flex: 1 },
  260. loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
  261. errorText: { color: '#999', fontSize: 16 },
  262. backBtn2: { marginTop: 20, backgroundColor: '#ff6600', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
  263. backBtn2Text: { color: '#fff', fontSize: 14 },
  264. // 顶部导航
  265. header: {
  266. flexDirection: 'row',
  267. alignItems: 'center',
  268. justifyContent: 'space-between',
  269. paddingHorizontal: 10,
  270. paddingBottom: 10,
  271. position: 'absolute',
  272. top: 0,
  273. left: 0,
  274. right: 0,
  275. zIndex: 100,
  276. },
  277. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  278. backText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  279. headerTitle: { color: '#fff', fontSize: 15, fontWeight: 'bold', flex: 1, textAlign: 'center', width: 250 },
  280. placeholder: { width: 40 },
  281. scrollView: { flex: 1 },
  282. // 主商品展示区域
  283. mainGoodsSection: {
  284. width: SCREEN_WIDTH,
  285. height: 504,
  286. position: 'relative',
  287. },
  288. mainSwiper: {
  289. position: 'relative',
  290. width: '100%',
  291. height: 375,
  292. alignItems: 'center',
  293. justifyContent: 'center',
  294. marginTop: -50,
  295. },
  296. productImageBox: {
  297. width: 200,
  298. height: 300,
  299. justifyContent: 'center',
  300. alignItems: 'center',
  301. },
  302. productImage: {
  303. width: 200,
  304. height: 300,
  305. },
  306. detailsBut: {
  307. width: 120,
  308. height: 45,
  309. justifyContent: 'center',
  310. alignItems: 'center',
  311. marginTop: -30,
  312. },
  313. detailsText: {
  314. flexDirection: 'row',
  315. alignItems: 'center',
  316. },
  317. levelText: {
  318. fontSize: 14,
  319. color: '#FBC400',
  320. fontWeight: 'bold',
  321. },
  322. probabilityText: {
  323. fontSize: 10,
  324. color: '#FBC400',
  325. marginLeft: 3,
  326. },
  327. goodsNameBg: {
  328. position: 'absolute',
  329. left: 47,
  330. top: 53,
  331. width: 43,
  332. height: 100,
  333. paddingTop: 8,
  334. justifyContent: 'flex-start',
  335. alignItems: 'center',
  336. },
  337. goodsNameText: {
  338. fontSize: 12,
  339. fontWeight: 'bold',
  340. color: '#000',
  341. writingDirection: 'ltr',
  342. width: 20,
  343. textAlign: 'center',
  344. },
  345. prevBtn: { position: 'absolute', left: 35, top: '40%' },
  346. nextBtn: { position: 'absolute', right: 35, top: '40%' },
  347. arrowImg: { width: 33, height: 38 },
  348. // 装饰图片
  349. positionBgleftBg: {
  350. position: 'absolute',
  351. left: 0,
  352. top: 225,
  353. width: 32,
  354. height: 188,
  355. },
  356. positionBgRightBg: {
  357. position: 'absolute',
  358. right: 0,
  359. top: 225,
  360. width: 32,
  361. height: 188,
  362. },
  363. mainGoodsSectionBtext: {
  364. width: SCREEN_WIDTH,
  365. height: 74,
  366. marginTop: -10,
  367. },
  368. // 侧边按钮
  369. positionBut: {
  370. position: 'absolute',
  371. zIndex: 10,
  372. width: 35,
  373. height: 34,
  374. },
  375. positionButBg: {
  376. width: 35,
  377. height: 34,
  378. justifyContent: 'center',
  379. alignItems: 'center',
  380. },
  381. positionButText: {
  382. fontSize: 12,
  383. fontWeight: 'bold',
  384. color: '#fff',
  385. transform: [{ rotate: '14deg' }],
  386. textShadowColor: '#000',
  387. textShadowOffset: { width: 1, height: 1 },
  388. textShadowRadius: 1,
  389. },
  390. positionButTextR: {
  391. fontSize: 12,
  392. fontWeight: 'bold',
  393. color: '#fff',
  394. transform: [{ rotate: '-16deg' }],
  395. textShadowColor: '#000',
  396. textShadowOffset: { width: 1, height: 1 },
  397. textShadowRadius: 1,
  398. },
  399. positionRule: { top: 256, left: 0 },
  400. positionRecord: { top: 300, left: 0 },
  401. positionStore: { top: 256, right: 0 },
  402. // 底部购买栏
  403. bottomBar: {
  404. position: 'absolute',
  405. bottom: 0,
  406. left: 0,
  407. right: 0,
  408. height: 69,
  409. paddingHorizontal: 5,
  410. },
  411. bottomBtns: {
  412. flexDirection: 'row',
  413. height: 64,
  414. alignItems: 'center',
  415. justifyContent: 'space-around',
  416. },
  417. btnItem: {
  418. flex: 1,
  419. marginHorizontal: 6,
  420. },
  421. btnBg: {
  422. width: '100%',
  423. height: 54,
  424. justifyContent: 'center',
  425. alignItems: 'center',
  426. },
  427. btnText: {
  428. fontSize: 14,
  429. fontWeight: 'bold',
  430. color: '#fff',
  431. },
  432. btnPrice: {
  433. fontSize: 9,
  434. color: '#fff',
  435. },
  436. });