index.tsx 16 KB

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