index.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import { Audio } from 'expo-av';
  2. import { Image } from 'expo-image';
  3. import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
  4. import React, { useEffect, useRef, useState } from 'react';
  5. import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  6. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  7. import LotteryReel from './components/LotteryReel';
  8. import { Images } from '@/constants/images';
  9. import services from '@/services/api';
  10. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  11. // Sound URL from Uniapp config
  12. const SOUND_URL = 'https://cdn.acetoys.cn/kai_xin_ma_te/resource/magic/lottery.mp3';
  13. export default function LotteryScreen() {
  14. const router = useRouter();
  15. const params = useLocalSearchParams();
  16. const insets = useSafeAreaInsets();
  17. const num = Number(params.num) || 1;
  18. const tradeNo = params.tradeNo as string;
  19. const poolId = params.poolId as string;
  20. const [results, setResults] = useState<any[]>([]);
  21. const [pool, setPool] = useState<any[]>([]);
  22. const [loading, setLoading] = useState(true);
  23. const [sound, setSound] = useState<Audio.Sound>();
  24. // Timer Ref for cleanup
  25. const timerRef = useRef<any>(null);
  26. // Layout calculations
  27. const itemWidth = (num === 1) ? 94 : 72; // Adjusted to 72 to match mask calculation exactly (was 71)
  28. // Uniapp width logic:
  29. // width() { return (screenWidth - (this.num == 1 ? 94 : 72)) / 2 }
  30. const maskWidth = (SCREEN_WIDTH - ((num === 1) ? 94 : 72)) / 2;
  31. const padding = SCREEN_WIDTH > 375 ? 32 : 0;
  32. useEffect(() => {
  33. init();
  34. return () => {
  35. sound?.unloadAsync();
  36. if (timerRef.current) clearTimeout(timerRef.current);
  37. };
  38. }, []);
  39. const init = async () => {
  40. try {
  41. console.log('LotteryScreen Init: num', num, 'tradeNo', tradeNo, 'poolId', poolId);
  42. await Promise.all([loadData(), playSound()]);
  43. } catch (e) {
  44. console.error('LotteryScreen Init Error', e);
  45. } finally {
  46. setLoading(false);
  47. }
  48. }
  49. const playSound = async () => {
  50. try {
  51. const { sound } = await Audio.Sound.createAsync(
  52. { uri: SOUND_URL }
  53. );
  54. setSound(sound);
  55. await sound.playAsync();
  56. } catch (error) {
  57. console.log('Error playing sound', error);
  58. }
  59. };
  60. const loadData = async () => {
  61. if (tradeNo) {
  62. try {
  63. const res: any = await services.award.getApplyResult(tradeNo);
  64. const list = res?.data?.inventoryList || res?.inventoryList || [];
  65. setResults(list);
  66. // 2. Fetch Pool (Goods) if not passed
  67. // In a real app we might pass this via context or params if small
  68. // For now, fetch detail to get the goods list
  69. if (poolId) {
  70. const detailRes = await services.award.getPoolDetail(poolId);
  71. const goods = detailRes?.luckGoodsList || [];
  72. setPool(goods);
  73. } else {
  74. // Fallback if no poolId, just use result as pool (not ideal as it's small)
  75. setPool(list);
  76. }
  77. // Auto show result after animation
  78. if (timerRef.current) clearTimeout(timerRef.current);
  79. timerRef.current = setTimeout(() => {
  80. handleFinish(list);
  81. }, 2800); // 2000ms duration + buffer
  82. } catch (error) {
  83. console.error('Load Data Error', error);
  84. // Safety fallback?
  85. }
  86. }
  87. };
  88. const handleFinish = (finalResults?: any[]) => {
  89. if (timerRef.current) {
  90. clearTimeout(timerRef.current);
  91. timerRef.current = null;
  92. }
  93. // Navigate to result modal/screen
  94. const data = finalResults || results;
  95. // We can navigate to a result page or show a local modal components
  96. // Assuming we have a result route
  97. router.replace({
  98. pathname: '/lottery/result' as any,
  99. params: {
  100. results: JSON.stringify(data),
  101. poolId
  102. }
  103. });
  104. };
  105. const handleSkip = () => {
  106. sound?.stopAsync();
  107. handleFinish();
  108. };
  109. if (loading) return (
  110. <View style={styles.container}>
  111. <Image source={{ uri: Images.mine.kaixinMineBg }} style={styles.bg} contentFit="cover" />
  112. </View>
  113. );
  114. return (
  115. <View style={styles.container}>
  116. <Stack.Screen options={{ headerShown: false }} />
  117. <Image source={{ uri: Images.mine.kaixinMineBg }} style={styles.bg} contentFit="cover" />
  118. <View style={styles.maskPage} />
  119. <View style={[styles.wrapper, { paddingTop: padding }]}>
  120. <View style={styles.reelsContainer}>
  121. {/* Left Column Masks */}
  122. <View style={[styles.maskLeft, { width: maskWidth }]} />
  123. {/* Reels */}
  124. <View style={num === 1 ? styles.height1 : styles.height5}>
  125. {results.map((item, index) => (
  126. <View key={index} style={styles.reelRow}>
  127. <LotteryReel
  128. key={`reel-${index}-${pool.length}-${results.length}`}
  129. pool={pool.length > 0 ? pool : results}
  130. result={item}
  131. width={SCREEN_WIDTH}
  132. itemWidth={itemWidth}
  133. index={index}
  134. delay={index * 30} // Very fast stagger
  135. duration={2000} // Faster spin (was 3000)
  136. />
  137. </View>
  138. ))}
  139. </View>
  140. {/* Right Column Masks */}
  141. <View style={[styles.maskRight, { width: maskWidth }]} />
  142. {/* Middle Frame */}
  143. <Image
  144. source={{ uri: num === 1 ? Images.resource.lottery_middle_s : Images.resource.lottery_middle_l }}
  145. style={[
  146. styles.middleImage,
  147. {
  148. height: num === 1 ? 200 : 540,
  149. top: num === 1 ? -35 : -10,
  150. }
  151. ]}
  152. contentFit="contain"
  153. />
  154. </View>
  155. </View>
  156. <View style={styles.bottom}>
  157. <TouchableOpacity style={styles.skipBtn} onPress={handleSkip}>
  158. <Text style={styles.skipText}>跳过动画</Text>
  159. </TouchableOpacity>
  160. </View>
  161. </View>
  162. );
  163. }
  164. const styles = StyleSheet.create({
  165. container: {
  166. flex: 1,
  167. backgroundColor: '#222335',
  168. },
  169. bg: {
  170. position: 'absolute',
  171. width: '100%',
  172. height: '100%',
  173. zIndex: -1,
  174. },
  175. maskPage: {
  176. ...StyleSheet.absoluteFillObject,
  177. backgroundColor: 'rgba(0,0,0,0.4)',
  178. zIndex: 0,
  179. },
  180. wrapper: {
  181. flex: 1,
  182. // justifyContent: 'center',
  183. alignItems: 'center',
  184. marginTop: 100, // Adjust based on visual
  185. },
  186. reelsContainer: {
  187. position: 'relative',
  188. alignItems: 'center',
  189. justifyContent: 'center',
  190. // backgroundColor: 'rgba(255,0,0,0.1)', // Debug
  191. },
  192. reelRow: {
  193. overflow: 'hidden',
  194. width: SCREEN_WIDTH,
  195. alignItems: 'center',
  196. justifyContent: 'center',
  197. marginBottom: 12, // Gap between rows
  198. height: 94, // Height of one row (card height + tiny padding)
  199. },
  200. height1: {
  201. height: 130, // 1 row + gaps
  202. justifyContent: 'center',
  203. },
  204. height5: {
  205. height: 540, // 5 rows * (~100)
  206. justifyContent: 'center',
  207. paddingVertical: 10,
  208. },
  209. maskLeft: {
  210. position: 'absolute',
  211. left: 0,
  212. top: 0,
  213. bottom: 0,
  214. backgroundColor: 'rgba(0,0,0,0.5)',
  215. zIndex: 10,
  216. },
  217. maskRight: {
  218. position: 'absolute',
  219. right: 0,
  220. top: 0,
  221. bottom: 0,
  222. backgroundColor: 'rgba(0,0,0,0.5)',
  223. zIndex: 10,
  224. },
  225. middleImage: {
  226. position: 'absolute',
  227. width: SCREEN_WIDTH,
  228. zIndex: 20,
  229. left: 0,
  230. // The image frame should be centered over the reels
  231. // top is handled dynamically or via flex centering in parent if possible
  232. },
  233. bottom: {
  234. position: 'absolute',
  235. bottom: 50,
  236. width: '100%',
  237. alignItems: 'center',
  238. zIndex: 30,
  239. },
  240. skipBtn: {
  241. backgroundColor: 'rgba(255,255,255,0.2)',
  242. paddingHorizontal: 30,
  243. paddingVertical: 10,
  244. borderRadius: 32,
  245. borderWidth: 1,
  246. borderColor: 'rgba(255,255,255,0.4)',
  247. },
  248. skipText: {
  249. color: '#fff',
  250. fontSize: 14,
  251. },
  252. });