| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- import { Audio, AVPlaybackStatus, ResizeMode, Video } 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';
- const DEFAULT_JACKPOT_VIDEO = 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart/box/lottery/jackpot.mp4';
- 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<any[]>([]);
- const [pool, setPool] = useState<any[]>([]);
- const [loading, setLoading] = useState(true);
- const [sound, setSound] = useState<Audio.Sound>();
- const [animationFinished, setAnimationFinished] = useState(false);
-
- // Video state
- const [videoVisible, setVideoVisible] = useState(false);
- const [videoUrl, setVideoUrl] = useState('');
- const videoRef = useRef<Video>(null);
-
- // Timer Ref for cleanup
- const timerRef = useRef<any>(null);
- // Layout calculations
- const singleItemWidth = 240;
- const itemWidth = (num === 1) ? singleItemWidth : 72;
- const maskWidth = (SCREEN_WIDTH - ((num === 1) ? singleItemWidth : 72)) / 2;
- const padding = SCREEN_WIDTH > 375 ? 32 : 0;
-
- // Dynamic styles
- const singleReelHeight = singleItemWidth * 1.35 + 20; // 324 + 20
- useEffect(() => {
- init();
- return () => {
- sound?.unloadAsync();
- if (timerRef.current) clearTimeout(timerRef.current);
- };
- }, []);
-
- // ... (rest of the file)
-
- // Update render styles inline or via StyleSheet
- // I will update the stylesheet usage in return and the StyleSheet definition below
- 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);
-
- // Check for Level A video
- const hasLevelA = list.some((item: any) => ['A', 'a'].includes(item.level));
- // Also check res.video if available
- const playVideoUrl = res.video || (hasLevelA ? DEFAULT_JACKPOT_VIDEO : '');
- if (playVideoUrl) {
- console.log('Found Level A, preparing video:', playVideoUrl);
- setVideoUrl(playVideoUrl);
- setVideoVisible(true);
- // Do NOT set timer here, wait for video to finish
- } else {
- 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 (videoVisible) {
- setVideoVisible(false);
- handleFinish();
- return;
- }
- 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 (
- <View style={styles.container}>
- <Image source={{ uri: Images.mine.kaixinMineBg }} style={styles.bg} contentFit="cover" />
- </View>
- );
- return (
- <View style={styles.container}>
- <Stack.Screen options={{ headerShown: false }} />
- <Image source={{ uri: Images.mine.kaixinMineBg }} style={styles.bg} contentFit="cover" />
- <View style={styles.maskPage} />
- <View style={[styles.wrapper, { paddingTop: padding }]}>
- {isGrid ? (
- <LotteryGrid
- results={results}
- onFinish={() => handleFinish()}
- />
- ) : (
- <View style={styles.reelsContainer}>
- {/* Left Column Masks */}
- <View style={[styles.maskLeft, { width: maskWidth }]} />
-
- {/* Reels */}
- <View style={num === 1 ? styles.height1 : styles.height5}>
- {results.map((item, index) => (
- <View
- key={index}
- style={[
- styles.reelRow,
- num === 1 && { height: singleReelHeight, marginBottom: 0 }
- ]}
- >
- <LotteryReel
- key={`reel-${index}-${pool.length}-${results.length}`}
- pool={pool.length > 0 ? pool : results}
- result={item}
- width={SCREEN_WIDTH}
- itemWidth={itemWidth}
- index={index}
- delay={index * 30} // Very fast stagger
- duration={2000} // Faster spin (was 3000)
- />
- </View>
- ))}
- </View>
- {/* Right Column Masks */}
- <View style={[styles.maskRight, { width: maskWidth }]} />
-
- {/* Middle Frame */}
- <Image
- source={{ uri: num === 1 ? Images.resource.lottery_middle_s : Images.resource.lottery_middle_l }}
- style={[
- styles.middleImage,
- {
- height: num === 1 ? 360 : 540,
- top: num === 1 ? -20 : -10,
- }
- ]}
- contentFit="contain"
- />
- </View>
- )}
- </View>
- {/* Video Overlay */}
- {videoVisible && videoUrl ? (
- <View style={styles.videoContainer}>
- <Video
- ref={videoRef}
- key={videoUrl}
- source={{ uri: videoUrl }}
- style={styles.video}
- resizeMode={ResizeMode.COVER}
- shouldPlay
- isLooping={false}
- useNativeControls={false}
- onLoad={() => {
- videoRef.current?.playAsync();
- }}
- onPlaybackStatusUpdate={(status: AVPlaybackStatus) => {
- if (status.isLoaded && status.didJustFinish) {
- setVideoVisible(false);
- handleFinish();
- }
- }}
- onError={() => {
- setVideoVisible(false);
- handleFinish();
- }}
- />
- </View>
- ) : null}
- <View style={styles.bottom}>
- <TouchableOpacity
- style={styles.skipBtn}
- onPress={handleSkip}
- >
- <Text style={styles.skipText}>
- {videoVisible ? '跳过动画' : (isGrid && animationFinished ? '收下奖品' : '跳过动画')}
- </Text>
- </TouchableOpacity>
- </View>
- </View>
- );
- }
- 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: 0, // Remove fixed margin, let flex center it or use justify center if needed
- justifyContent: 'center', // Center vertically
- },
- 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: 360, // Accommodate larger card (approx 325px height)
- 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: 60,
- },
- 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,
- },
- videoContainer: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: '#000',
- zIndex: 50,
- },
- video: {
- width: '100%',
- height: '100%',
- },
- });
|