| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- 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.acefig.com/kai_xin_ma_te/resource/magic/lottery.mp3";
- const DEFAULT_JACKPOT_VIDEO =
- "https://cdn.acefig.com/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);
- const isFinishedRef = useRef(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 (isFinishedRef.current) return;
- isFinishedRef.current = true;
- 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: "/happy-spin/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%",
- },
- });
|