index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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)}%)
  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.positionService]} onPress={() => kefuRef.current?.open()}>
  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. <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store' as any)}>
  235. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
  236. <Text style={styles.positionButTextR}>仓库</Text>
  237. </ImageBackground>
  238. </TouchableOpacity>
  239. {/* 商品列表 */}
  240. <ProductList products={products} levelList={data.luckGoodsLevelProbabilityList} poolId={poolId!} price={data.price} />
  241. {/* 说明文字 */}
  242. <ExplainSection poolId={poolId!} />
  243. <View style={{ height: 150 }} />
  244. </ScrollView>
  245. {/* 底部购买栏 */}
  246. <ImageBackground source={{ uri: Images.box.detail.boxDetailBott }} style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]} resizeMode="cover">
  247. <View style={styles.bottomBtns}>
  248. <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(1)} activeOpacity={0.8}>
  249. <ImageBackground source={{ uri: Images.common.butBgV }} style={styles.btnBg} resizeMode="contain">
  250. <Text style={styles.btnText}>购买一盒</Text>
  251. <Text style={styles.btnPrice}>(¥{data.specialPrice || data.price})</Text>
  252. </ImageBackground>
  253. </TouchableOpacity>
  254. <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(5)} activeOpacity={0.8}>
  255. <ImageBackground source={{ uri: Images.common.butBgL }} style={styles.btnBg} resizeMode="contain">
  256. <Text style={styles.btnText}>购买五盒</Text>
  257. <Text style={styles.btnPrice}>(¥{data.specialPriceFive || data.price * 5})</Text>
  258. </ImageBackground>
  259. </TouchableOpacity>
  260. <TouchableOpacity style={styles.btnItem} onPress={() => checkoutRef.current?.showFreedom()} activeOpacity={0.8}>
  261. <ImageBackground source={{ uri: Images.common.butBgH }} style={styles.btnBg} resizeMode="contain">
  262. <Text style={styles.btnText}>购买多盒</Text>
  263. </ImageBackground>
  264. </TouchableOpacity>
  265. </View>
  266. </ImageBackground>
  267. </ImageBackground>
  268. <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} onSuccess={handleSuccess} />
  269. <RecordModal ref={recordRef} poolId={poolId!} />
  270. <RuleModal ref={ruleRef} />
  271. </View>
  272. );
  273. }
  274. const styles = StyleSheet.create({
  275. container: { flex: 1, backgroundColor: '#1a1a2e' },
  276. background: { flex: 1 },
  277. loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
  278. errorText: { color: '#999', fontSize: 16 },
  279. backBtn2: { marginTop: 20, backgroundColor: '#ff6600', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
  280. backBtn2Text: { color: '#fff', fontSize: 14 },
  281. // 顶部导航
  282. header: {
  283. flexDirection: 'row',
  284. alignItems: 'center',
  285. justifyContent: 'space-between',
  286. paddingHorizontal: 10,
  287. paddingBottom: 10,
  288. position: 'absolute',
  289. top: 0,
  290. left: 0,
  291. right: 0,
  292. zIndex: 100,
  293. },
  294. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  295. backText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  296. headerTitle: { color: '#fff', fontSize: 15, fontWeight: 'bold', flex: 1, textAlign: 'center', width: 250 },
  297. placeholder: { width: 40 },
  298. scrollView: { flex: 1 },
  299. // 主商品展示区域
  300. mainGoodsSection: {
  301. width: SCREEN_WIDTH,
  302. height: 504,
  303. position: 'relative',
  304. },
  305. mainSwiper: {
  306. position: 'relative',
  307. width: '100%',
  308. height: 375,
  309. alignItems: 'center',
  310. justifyContent: 'center',
  311. marginTop: -50,
  312. },
  313. productImageBox: {
  314. width: 200,
  315. height: 300,
  316. justifyContent: 'center',
  317. alignItems: 'center',
  318. },
  319. productImage: {
  320. width: 200,
  321. height: 300,
  322. },
  323. detailsBut: {
  324. width: 120,
  325. height: 45,
  326. justifyContent: 'center',
  327. alignItems: 'center',
  328. marginTop: -30,
  329. },
  330. detailsText: {
  331. flexDirection: 'row',
  332. alignItems: 'center',
  333. },
  334. levelText: {
  335. fontSize: 14,
  336. color: '#FBC400',
  337. fontWeight: 'bold',
  338. },
  339. probabilityText: {
  340. fontSize: 10,
  341. color: '#FBC400',
  342. marginLeft: 3,
  343. },
  344. goodsNameBg: {
  345. position: 'absolute',
  346. left: 47,
  347. top: 53,
  348. width: 43,
  349. height: 100,
  350. paddingTop: 8,
  351. justifyContent: 'flex-start',
  352. alignItems: 'center',
  353. },
  354. goodsNameText: {
  355. fontSize: 12,
  356. fontWeight: 'bold',
  357. color: '#000',
  358. writingDirection: 'ltr',
  359. width: 20,
  360. textAlign: 'center',
  361. },
  362. prevBtn: { position: 'absolute', left: 35, top: '40%' },
  363. nextBtn: { position: 'absolute', right: 35, top: '40%' },
  364. arrowImg: { width: 33, height: 38 },
  365. // 装饰图片
  366. positionBgleftBg: {
  367. position: 'absolute',
  368. left: 0,
  369. top: 225,
  370. width: 32,
  371. height: 188,
  372. },
  373. positionBgRightBg: {
  374. position: 'absolute',
  375. right: 0,
  376. top: 225,
  377. width: 32,
  378. height: 188,
  379. },
  380. mainGoodsSectionBtext: {
  381. width: SCREEN_WIDTH,
  382. height: 74,
  383. marginTop: -10,
  384. },
  385. // 侧边按钮
  386. positionBut: {
  387. position: 'absolute',
  388. zIndex: 10,
  389. width: 35,
  390. height: 34,
  391. },
  392. positionButBg: {
  393. width: 35,
  394. height: 34,
  395. justifyContent: 'center',
  396. alignItems: 'center',
  397. },
  398. positionButText: {
  399. fontSize: 12,
  400. fontWeight: 'bold',
  401. color: '#fff',
  402. transform: [{ rotate: '14deg' }],
  403. textShadowColor: '#000',
  404. textShadowOffset: { width: 1, height: 1 },
  405. textShadowRadius: 1,
  406. },
  407. positionButTextR: {
  408. fontSize: 12,
  409. fontWeight: 'bold',
  410. color: '#fff',
  411. transform: [{ rotate: '-16deg' }],
  412. textShadowColor: '#000',
  413. textShadowOffset: { width: 1, height: 1 },
  414. textShadowRadius: 1,
  415. },
  416. positionRule: { top: 256, left: 0 },
  417. positionRecord: { top: 300, left: 0 },
  418. positionStore: { top: 256, right: 0 },
  419. // 底部购买栏
  420. bottomBar: {
  421. position: 'absolute',
  422. bottom: 0,
  423. left: 0,
  424. right: 0,
  425. height: 69,
  426. paddingHorizontal: 5,
  427. },
  428. bottomBtns: {
  429. flexDirection: 'row',
  430. height: 64,
  431. alignItems: 'center',
  432. justifyContent: 'space-around',
  433. },
  434. btnItem: {
  435. flex: 1,
  436. marginHorizontal: 6,
  437. },
  438. btnBg: {
  439. width: '100%',
  440. height: 54,
  441. justifyContent: 'center',
  442. alignItems: 'center',
  443. },
  444. btnText: {
  445. fontSize: 14,
  446. fontWeight: 'bold',
  447. color: '#fff',
  448. },
  449. btnPrice: {
  450. fontSize: 9,
  451. color: '#fff',
  452. },
  453. });