index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import { Audio, AVPlaybackStatus, ResizeMode, Video } 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 {
  6. Dimensions,
  7. StyleSheet,
  8. Text,
  9. TouchableOpacity,
  10. View,
  11. } from "react-native";
  12. import { useSafeAreaInsets } from "react-native-safe-area-context";
  13. import LotteryGrid from "./components/LotteryGrid";
  14. import LotteryReel from "./components/LotteryReel";
  15. import { Images } from "@/constants/images";
  16. import services from "@/services/api";
  17. const { width: SCREEN_WIDTH } = Dimensions.get("window");
  18. // Sound URL from Uniapp config
  19. const SOUND_URL =
  20. "https://cdn.acefig.com/kai_xin_ma_te/resource/magic/lottery.mp3";
  21. const DEFAULT_JACKPOT_VIDEO =
  22. "https://cdn.acefig.com/kai_xin_ma_te/supermart/box/lottery/jackpot.mp4";
  23. export default function LotteryScreen() {
  24. const router = useRouter();
  25. const params = useLocalSearchParams();
  26. const insets = useSafeAreaInsets();
  27. const num = Number(params.num) || 1;
  28. const isGrid = num >= 10;
  29. const tradeNo = params.tradeNo as string;
  30. const poolId = params.poolId as string;
  31. const [results, setResults] = useState<any[]>([]);
  32. const [pool, setPool] = useState<any[]>([]);
  33. const [loading, setLoading] = useState(true);
  34. const [sound, setSound] = useState<Audio.Sound>();
  35. const [animationFinished, setAnimationFinished] = useState(false);
  36. // Video state
  37. const [videoVisible, setVideoVisible] = useState(false);
  38. const [videoUrl, setVideoUrl] = useState("");
  39. const videoRef = useRef<Video>(null);
  40. // Timer Ref for cleanup
  41. const timerRef = useRef<any>(null);
  42. // Layout calculations
  43. const singleItemWidth = 240;
  44. const itemWidth = num === 1 ? singleItemWidth : 72;
  45. const maskWidth = (SCREEN_WIDTH - (num === 1 ? singleItemWidth : 72)) / 2;
  46. const padding = SCREEN_WIDTH > 375 ? 32 : 0;
  47. // Dynamic styles
  48. const singleReelHeight = singleItemWidth * 1.35 + 20; // 324 + 20
  49. useEffect(() => {
  50. init();
  51. return () => {
  52. sound?.unloadAsync();
  53. if (timerRef.current) clearTimeout(timerRef.current);
  54. };
  55. }, []);
  56. // ... (rest of the file)
  57. // Update render styles inline or via StyleSheet
  58. // I will update the stylesheet usage in return and the StyleSheet definition below
  59. const init = async () => {
  60. try {
  61. console.log(
  62. "LotteryScreen Init: num",
  63. num,
  64. "tradeNo",
  65. tradeNo,
  66. "poolId",
  67. poolId,
  68. );
  69. await Promise.all([loadData() /*, playSound()*/]);
  70. } catch (e) {
  71. console.error("LotteryScreen Init Error", e);
  72. } finally {
  73. setLoading(false);
  74. }
  75. };
  76. const playSound = async () => {
  77. try {
  78. const { sound } = await Audio.Sound.createAsync({ uri: SOUND_URL });
  79. setSound(sound);
  80. await sound.playAsync();
  81. } catch (error) {
  82. console.log("Error playing sound", error);
  83. }
  84. };
  85. const loadData = async () => {
  86. if (tradeNo) {
  87. try {
  88. const res: any = await services.award.getApplyResult(tradeNo);
  89. const list = res?.data?.inventoryList || res?.inventoryList || [];
  90. setResults(list);
  91. // 2. Fetch Pool (Goods) if not passed
  92. if (poolId && !isGrid) {
  93. const detailRes = await services.award.getPoolDetail(poolId);
  94. const goods = detailRes?.luckGoodsList || [];
  95. setPool(goods);
  96. } else if (!isGrid) {
  97. setPool(list);
  98. }
  99. // Auto show result after animation
  100. if (timerRef.current) clearTimeout(timerRef.current);
  101. // Check for Level A video
  102. const hasLevelA = list.some((item: any) =>
  103. ["A", "a"].includes(item.level),
  104. );
  105. // Also check res.video if available
  106. const playVideoUrl =
  107. res.video || (hasLevelA ? DEFAULT_JACKPOT_VIDEO : "");
  108. if (playVideoUrl) {
  109. console.log("Found Level A, preparing video:", playVideoUrl);
  110. setVideoUrl(playVideoUrl);
  111. setVideoVisible(true);
  112. // Do NOT set timer here, wait for video to finish
  113. } else {
  114. if (!isGrid) {
  115. // Reel mode uses timer
  116. timerRef.current = setTimeout(() => {
  117. handleFinish(list);
  118. }, 2800);
  119. }
  120. }
  121. // Grid mode handles finish via callback
  122. } catch (error) {
  123. console.error("Load Data Error", error);
  124. // Safety fallback?
  125. }
  126. }
  127. };
  128. const handleFinish = (finalResults?: any[]) => {
  129. if (timerRef.current) {
  130. clearTimeout(timerRef.current);
  131. timerRef.current = null;
  132. }
  133. if (isGrid) {
  134. // Stop sound and mark finished, stay on page
  135. sound?.stopAsync();
  136. setAnimationFinished(true);
  137. return;
  138. }
  139. // Navigate to result modal/screen
  140. const data = finalResults || results;
  141. // We can navigate to a result page or show a local modal components
  142. // Assuming we have a result route
  143. router.replace({
  144. pathname: "/happy-spin/result" as any,
  145. params: {
  146. results: JSON.stringify(data),
  147. poolId,
  148. },
  149. });
  150. };
  151. const handleSkip = () => {
  152. if (videoVisible) {
  153. setVideoVisible(false);
  154. handleFinish();
  155. return;
  156. }
  157. if (isGrid) {
  158. if (animationFinished) {
  159. // User clicked "Claim Prize", exit
  160. router.back();
  161. } else {
  162. // User clicked "Skip Animation", finish immediately
  163. // Ideally LotteryGrid should expose a "finish" method or we force state
  164. // But for now calling handleFinish stops navigation
  165. handleFinish();
  166. // Note: This doesn't force cards to flip instantly unless we implement that prop in LotteryGrid
  167. }
  168. } else {
  169. sound?.stopAsync();
  170. handleFinish();
  171. }
  172. };
  173. if (loading)
  174. return (
  175. <View style={styles.container}>
  176. <Image
  177. source={{ uri: Images.mine.kaixinMineBg }}
  178. style={styles.bg}
  179. contentFit="cover"
  180. />
  181. </View>
  182. );
  183. return (
  184. <View style={styles.container}>
  185. <Stack.Screen options={{ headerShown: false }} />
  186. <Image
  187. source={{ uri: Images.mine.kaixinMineBg }}
  188. style={styles.bg}
  189. contentFit="cover"
  190. />
  191. <View style={styles.maskPage} />
  192. <View style={[styles.wrapper, { paddingTop: padding }]}>
  193. {isGrid ? (
  194. <LotteryGrid results={results} onFinish={() => handleFinish()} />
  195. ) : (
  196. <View style={styles.reelsContainer}>
  197. {/* Left Column Masks */}
  198. <View style={[styles.maskLeft, { width: maskWidth }]} />
  199. {/* Reels */}
  200. <View style={num === 1 ? styles.height1 : styles.height5}>
  201. {results.map((item, index) => (
  202. <View
  203. key={index}
  204. style={[
  205. styles.reelRow,
  206. num === 1 && { height: singleReelHeight, marginBottom: 0 },
  207. ]}
  208. >
  209. <LotteryReel
  210. key={`reel-${index}-${pool.length}-${results.length}`}
  211. pool={pool.length > 0 ? pool : results}
  212. result={item}
  213. width={SCREEN_WIDTH}
  214. itemWidth={itemWidth}
  215. index={index}
  216. delay={index * 30} // Very fast stagger
  217. duration={2000} // Faster spin (was 3000)
  218. />
  219. </View>
  220. ))}
  221. </View>
  222. {/* Right Column Masks */}
  223. <View style={[styles.maskRight, { width: maskWidth }]} />
  224. {/* Middle Frame */}
  225. <Image
  226. source={{
  227. uri:
  228. num === 1
  229. ? Images.resource.lottery_middle_s
  230. : Images.resource.lottery_middle_l,
  231. }}
  232. style={[
  233. styles.middleImage,
  234. {
  235. height: num === 1 ? 360 : 540,
  236. top: num === 1 ? -20 : -10,
  237. },
  238. ]}
  239. contentFit="contain"
  240. />
  241. </View>
  242. )}
  243. </View>
  244. {/* Video Overlay */}
  245. {videoVisible && videoUrl ? (
  246. <View style={styles.videoContainer}>
  247. <Video
  248. ref={videoRef}
  249. key={videoUrl}
  250. source={{ uri: videoUrl }}
  251. style={styles.video}
  252. resizeMode={ResizeMode.COVER}
  253. shouldPlay
  254. isLooping={false}
  255. useNativeControls={false}
  256. onLoad={() => {
  257. videoRef.current?.playAsync();
  258. }}
  259. onPlaybackStatusUpdate={(status: AVPlaybackStatus) => {
  260. if (status.isLoaded && status.didJustFinish) {
  261. setVideoVisible(false);
  262. handleFinish();
  263. }
  264. }}
  265. onError={() => {
  266. setVideoVisible(false);
  267. handleFinish();
  268. }}
  269. />
  270. </View>
  271. ) : null}
  272. <View style={styles.bottom}>
  273. <TouchableOpacity style={styles.skipBtn} onPress={handleSkip}>
  274. <Text style={styles.skipText}>
  275. {videoVisible
  276. ? "跳过动画"
  277. : isGrid && animationFinished
  278. ? "收下奖品"
  279. : "跳过动画"}
  280. </Text>
  281. </TouchableOpacity>
  282. </View>
  283. </View>
  284. );
  285. }
  286. const styles = StyleSheet.create({
  287. container: {
  288. flex: 1,
  289. backgroundColor: "#222335",
  290. },
  291. bg: {
  292. position: "absolute",
  293. width: "100%",
  294. height: "100%",
  295. zIndex: -1,
  296. },
  297. maskPage: {
  298. ...StyleSheet.absoluteFillObject,
  299. backgroundColor: "rgba(0,0,0,0.4)",
  300. zIndex: 0,
  301. },
  302. wrapper: {
  303. flex: 1,
  304. // justifyContent: 'center',
  305. alignItems: "center",
  306. marginTop: 0, // Remove fixed margin, let flex center it or use justify center if needed
  307. justifyContent: "center", // Center vertically
  308. },
  309. reelsContainer: {
  310. position: "relative",
  311. alignItems: "center",
  312. justifyContent: "center",
  313. // backgroundColor: 'rgba(255,0,0,0.1)', // Debug
  314. },
  315. reelRow: {
  316. overflow: "hidden",
  317. width: SCREEN_WIDTH,
  318. alignItems: "center",
  319. justifyContent: "center",
  320. marginBottom: 12, // Gap between rows
  321. height: 94, // Height of one row (card height + tiny padding)
  322. },
  323. height1: {
  324. height: 360, // Accommodate larger card (approx 325px height)
  325. justifyContent: "center",
  326. },
  327. height5: {
  328. height: 540, // 5 rows * (~100)
  329. justifyContent: "center",
  330. paddingVertical: 10,
  331. },
  332. maskLeft: {
  333. position: "absolute",
  334. left: 0,
  335. top: 0,
  336. bottom: 0,
  337. backgroundColor: "rgba(0,0,0,0.5)",
  338. zIndex: 10,
  339. },
  340. maskRight: {
  341. position: "absolute",
  342. right: 0,
  343. top: 0,
  344. bottom: 0,
  345. backgroundColor: "rgba(0,0,0,0.5)",
  346. zIndex: 10,
  347. },
  348. middleImage: {
  349. position: "absolute",
  350. width: SCREEN_WIDTH,
  351. zIndex: 20,
  352. left: 0,
  353. // The image frame should be centered over the reels
  354. // top is handled dynamically or via flex centering in parent if possible
  355. },
  356. bottom: {
  357. position: "absolute",
  358. bottom: 50,
  359. width: "100%",
  360. alignItems: "center",
  361. zIndex: 60,
  362. },
  363. skipBtn: {
  364. backgroundColor: "rgba(255,255,255,0.2)",
  365. paddingHorizontal: 30,
  366. paddingVertical: 10,
  367. borderRadius: 32,
  368. borderWidth: 1,
  369. borderColor: "rgba(255,255,255,0.4)",
  370. },
  371. skipText: {
  372. color: "#fff",
  373. fontSize: 14,
  374. },
  375. videoContainer: {
  376. ...StyleSheet.absoluteFillObject,
  377. backgroundColor: "#000",
  378. zIndex: 50,
  379. },
  380. video: {
  381. width: "100%",
  382. height: "100%",
  383. },
  384. });