index.tsx 12 KB

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