|
@@ -0,0 +1,277 @@
|
|
|
|
|
+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 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 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>();
|
|
|
|
|
+
|
|
|
|
|
+ // 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
|
|
|
|
|
+ // In a real app we might pass this via context or params if small
|
|
|
|
|
+ // For now, fetch detail to get the goods list
|
|
|
|
|
+ if (poolId) {
|
|
|
|
|
+ const detailRes = await services.award.getPoolDetail(poolId);
|
|
|
|
|
+ const goods = detailRes?.luckGoodsList || [];
|
|
|
|
|
+ setPool(goods);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fallback if no poolId, just use result as pool (not ideal as it's small)
|
|
|
|
|
+ setPool(list);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Auto show result after animation
|
|
|
|
|
+ if (timerRef.current) clearTimeout(timerRef.current);
|
|
|
|
|
+ timerRef.current = setTimeout(() => {
|
|
|
|
|
+ handleFinish(list);
|
|
|
|
|
+ }, 2800); // 2000ms duration + buffer
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Load Data Error', error);
|
|
|
|
|
+ // Safety fallback?
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleFinish = (finalResults?: any[]) => {
|
|
|
|
|
+ if (timerRef.current) {
|
|
|
|
|
+ clearTimeout(timerRef.current);
|
|
|
|
|
+ timerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 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 = () => {
|
|
|
|
|
+ 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 }]}>
|
|
|
|
|
+ <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}>跳过动画</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,
|
|
|
|
|
+ },
|
|
|
|
|
+});
|