| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- 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<any[]>([]);
- const [pool, setPool] = useState<any[]>([]);
- const [loading, setLoading] = useState(true);
- const [sound, setSound] = useState<Audio.Sound>();
- const [animationFinished, setAnimationFinished] = useState(false);
-
- // Timer Ref for cleanup
- const timerRef = useRef<any>(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 (
- <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}>
- <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 ? 200 : 540,
- top: num === 1 ? -35 : -10,
- }
- ]}
- contentFit="contain"
- />
- </View>
- )}
- </View>
- <View style={styles.bottom}>
- <TouchableOpacity
- style={styles.skipBtn}
- onPress={handleSkip}
- >
- <Text style={styles.skipText}>
- {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: 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,
- },
- });
|