index.tsx 15 KB

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