import { Audio } from 'expo-av'; import { Image } from 'expo-image'; import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import LotteryGrid from './components/LotteryGrid'; import LotteryReel from './components/LotteryReel'; import { Images } from '@/constants/images'; import services from '@/services/api'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); // Sound URL from Uniapp config const SOUND_URL = 'https://cdn.acetoys.cn/kai_xin_ma_te/resource/magic/lottery.mp3'; export default function LotteryScreen() { const router = useRouter(); const params = useLocalSearchParams(); const insets = useSafeAreaInsets(); const num = Number(params.num) || 1; const isGrid = num >= 10; const tradeNo = params.tradeNo as string; const poolId = params.poolId as string; const [results, setResults] = useState([]); const [pool, setPool] = useState([]); const [loading, setLoading] = useState(true); const [sound, setSound] = useState(); const [animationFinished, setAnimationFinished] = useState(false); // Timer Ref for cleanup const timerRef = useRef(null); // Layout calculations const itemWidth = (num === 1) ? 94 : 72; // Adjusted to 72 to match mask calculation exactly (was 71) // Uniapp width logic: // width() { return (screenWidth - (this.num == 1 ? 94 : 72)) / 2 } const maskWidth = (SCREEN_WIDTH - ((num === 1) ? 94 : 72)) / 2; const padding = SCREEN_WIDTH > 375 ? 32 : 0; useEffect(() => { init(); return () => { sound?.unloadAsync(); if (timerRef.current) clearTimeout(timerRef.current); }; }, []); const init = async () => { try { console.log('LotteryScreen Init: num', num, 'tradeNo', tradeNo, 'poolId', poolId); await Promise.all([loadData() /*, playSound()*/]); } catch (e) { console.error('LotteryScreen Init Error', e); } finally { setLoading(false); } } const playSound = async () => { try { const { sound } = await Audio.Sound.createAsync( { uri: SOUND_URL } ); setSound(sound); await sound.playAsync(); } catch (error) { console.log('Error playing sound', error); } }; const loadData = async () => { if (tradeNo) { try { const res: any = await services.award.getApplyResult(tradeNo); const list = res?.data?.inventoryList || res?.inventoryList || []; setResults(list); // 2. Fetch Pool (Goods) if not passed if (poolId && !isGrid) { const detailRes = await services.award.getPoolDetail(poolId); const goods = detailRes?.luckGoodsList || []; setPool(goods); } else if (!isGrid) { setPool(list); } // Auto show result after animation if (timerRef.current) clearTimeout(timerRef.current); if (!isGrid) { // Reel mode uses timer timerRef.current = setTimeout(() => { handleFinish(list); }, 2800); } // Grid mode handles finish via callback } catch (error) { console.error('Load Data Error', error); // Safety fallback? } } }; const handleFinish = (finalResults?: any[]) => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } if (isGrid) { // Stop sound and mark finished, stay on page sound?.stopAsync(); setAnimationFinished(true); return; } // Navigate to result modal/screen const data = finalResults || results; // We can navigate to a result page or show a local modal components // Assuming we have a result route router.replace({ pathname: '/lottery/result' as any, params: { results: JSON.stringify(data), poolId } }); }; const handleSkip = () => { if (isGrid) { if (animationFinished) { // User clicked "Claim Prize", exit router.back(); } else { // User clicked "Skip Animation", finish immediately // Ideally LotteryGrid should expose a "finish" method or we force state // But for now calling handleFinish stops navigation handleFinish(); // Note: This doesn't force cards to flip instantly unless we implement that prop in LotteryGrid } } else { sound?.stopAsync(); handleFinish(); } }; if (loading) return ( ); return ( {isGrid ? ( handleFinish()} /> ) : ( {/* Left Column Masks */} {/* Reels */} {results.map((item, index) => ( 0 ? pool : results} result={item} width={SCREEN_WIDTH} itemWidth={itemWidth} index={index} delay={index * 30} // Very fast stagger duration={2000} // Faster spin (was 3000) /> ))} {/* Right Column Masks */} {/* Middle Frame */} )} {isGrid && animationFinished ? '收下奖品' : '跳过动画'} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#222335', }, bg: { position: 'absolute', width: '100%', height: '100%', zIndex: -1, }, maskPage: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)', zIndex: 0, }, wrapper: { flex: 1, // justifyContent: 'center', alignItems: 'center', marginTop: 100, // Adjust based on visual }, reelsContainer: { position: 'relative', alignItems: 'center', justifyContent: 'center', // backgroundColor: 'rgba(255,0,0,0.1)', // Debug }, reelRow: { overflow: 'hidden', width: SCREEN_WIDTH, alignItems: 'center', justifyContent: 'center', marginBottom: 12, // Gap between rows height: 94, // Height of one row (card height + tiny padding) }, height1: { height: 130, // 1 row + gaps justifyContent: 'center', }, height5: { height: 540, // 5 rows * (~100) justifyContent: 'center', paddingVertical: 10, }, maskLeft: { position: 'absolute', left: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 10, }, maskRight: { position: 'absolute', right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 10, }, middleImage: { position: 'absolute', width: SCREEN_WIDTH, zIndex: 20, left: 0, // The image frame should be centered over the reels // top is handled dynamically or via flex centering in parent if possible }, bottom: { position: 'absolute', bottom: 50, width: '100%', alignItems: 'center', zIndex: 30, }, skipBtn: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 30, paddingVertical: 10, borderRadius: 32, borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', }, skipText: { color: '#fff', fontSize: 14, }, });