| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- import { Images } from '@/constants/images';
- import ServiceWallet from '@/services/wallet';
- import Service from '@/services/weal';
- import { Ionicons } from '@expo/vector-icons';
- import { Image } from 'expo-image';
- import { useRouter } from 'expo-router';
- import React, { useEffect, useRef, useState } from 'react';
- import { Alert, Dimensions, ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
- import Animated, {
- cancelAnimation,
- Easing,
- useAnimatedStyle,
- useSharedValue,
- withRepeat,
- withSequence,
- withTiming
- } from 'react-native-reanimated';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { CatchRuleModal, CatchRuleModalRef } from './components/CatchRuleModal';
- import { DollPrizeModal, DollPrizeModalRef } from './components/DollPrizeModal';
- import { DollResultModal, DollResultModalRef } from './components/DollResultModal';
- import { LackMolibModal, LackMolibModalRef } from './components/LackMolibModal';
- import { PressSureModal, PressSureModalRef } from './components/PressSureModal';
- import { WinRecordModal, WinRecordModalRef } from './components/WinRecordModal';
- const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
- const BALL_COUNT = 20; // Optimized count
- const BALL_SIZE = 66;
- const BALL_CONTAINER_WIDTH = 275;
- const BALL_CONTAINER_HEIGHT = 278;
- // Separate Ball Component for Performance
- const Ball = React.memo(({ index }: { index: number }) => {
- const x = useSharedValue(Math.random() * (BALL_CONTAINER_WIDTH - BALL_SIZE));
- const y = useSharedValue(Math.random() * (BALL_CONTAINER_HEIGHT - BALL_SIZE));
- const rotate = useSharedValue(Math.random() * 360);
- useEffect(() => {
- // Generate a sequence of random moves to simulate "chaos" on UI thread
- const xMoves = Array.from({ length: 10 }).map(() =>
- withTiming(Math.random() * (BALL_CONTAINER_WIDTH - BALL_SIZE), {
- duration: 2000 + Math.random() * 1500,
- easing: Easing.linear
- })
- );
- const yMoves = Array.from({ length: 10 }).map(() =>
- withTiming(Math.random() * (BALL_CONTAINER_HEIGHT - BALL_SIZE), {
- duration: 2000 + Math.random() * 1500,
- easing: Easing.linear
- })
- );
- const rMoves = Array.from({ length: 10 }).map(() =>
- withTiming(Math.random() * 360, {
- duration: 2000 + Math.random() * 1500,
- easing: Easing.linear
- })
- );
- // @ts-ignore: spread argument for withSequence
- x.value = withRepeat(withSequence(...xMoves), -1, true);
- // @ts-ignore
- y.value = withRepeat(withSequence(...yMoves), -1, true);
- // @ts-ignore
- rotate.value = withRepeat(withSequence(...rMoves), -1, true);
- return () => {
- cancelAnimation(x);
- cancelAnimation(y);
- cancelAnimation(rotate);
- };
- }, []);
- const animatedStyle = useAnimatedStyle(() => {
- return {
- transform: [
- { translateX: x.value },
- { translateY: y.value },
- { rotate: `${rotate.value}deg` } // Reanimated handles string interpolation
- ]
- };
- });
- return (
- <Animated.View style={[styles.ball, animatedStyle]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollBall }} style={styles.fullSize} />
- </Animated.View>
- );
- });
- export default function CatchDollScreen() {
- const router = useRouter();
- const insets = useSafeAreaInsets();
- // State
- const [molibi, setMolibi] = useState(0);
- const [luckWheelGoodsList, setLuckWheelGoodsList] = useState<any[]>([]);
- const [loading, setLoading] = useState(false);
- const [lotteryFlag, setLotteryFlag] = useState(false);
- // Reanimated Values for interactions
- const switchRotateVal = useSharedValue(0);
- const dropScale = useSharedValue(0);
- const dropOpacity = useSharedValue(0);
- // Modals
- const ruleRef = useRef<CatchRuleModalRef>(null);
- const winRecordRef = useRef<WinRecordModalRef>(null);
- const resultRef = useRef<DollResultModalRef>(null);
- const prizeRef = useRef<DollPrizeModalRef>(null);
- const pressSureRef = useRef<PressSureModalRef>(null);
- const lackMolibRef = useRef<LackMolibModalRef>(null);
- useEffect(() => {
- initData();
- }, []);
- const initData = async () => {
- getMolibi();
- getDetail();
- };
- const getMolibi = async () => {
- const res = await ServiceWallet.info('MAGIC_POWER_COIN');
- if (res) {
- setMolibi(res.balance);
- }
- };
- const getDetail = async () => {
- const res = await Service.catchDollDetail();
- if (res.code == 0) {
- setLuckWheelGoodsList(res.data.luckWheelGoodsList);
- } else {
- Alert.alert('提示', res.msg);
- }
- };
- const handleBack = () => router.back();
- const handlePress1 = () => {
- if (lotteryFlag) {
- Alert.alert('提示', '请不要重复点击');
- return;
- }
- if (molibi === 0) {
- lackMolibRef.current?.show();
- } else {
- pressSureRef.current?.show(1);
- }
- };
- const handlePress5 = () => {
- if (lotteryFlag) {
- Alert.alert('提示', '请不要重复点击');
- return;
- }
- if (molibi < 5) {
- lackMolibRef.current?.show();
- } else {
- pressSureRef.current?.show(5);
- }
- };
- const onConfirmPress = (quantity: number) => {
- playLottery(quantity);
- };
- const playLottery = async (quantity: number) => {
- setLotteryFlag(true);
- const res = await Service.dollLottery({ quantity });
- if (res.code == 0) {
- getMolibi();
- // Animate Switch
- switchRotateVal.value = withSequence(
- withTiming(90, { duration: 500, easing: Easing.inOut(Easing.ease) }),
- withTiming(0, { duration: 500, easing: Easing.inOut(Easing.ease) })
- );
- setTimeout(() => {
- // Animate Drop
- dropOpacity.value = 1;
- dropScale.value = 1;
- dropOpacity.value = withTiming(0, { duration: 2000 });
- dropScale.value = withTiming(0, { duration: 2000 });
- }, 800);
- setTimeout(() => {
- resultRef.current?.show(res.data);
- setLotteryFlag(false);
- }, 2000);
- } else {
- Alert.alert('提示', res.msg);
- setLotteryFlag(false);
- }
- };
- const switchStyle = useAnimatedStyle(() => ({
- transform: [{ rotate: `${switchRotateVal.value}deg` }]
- }));
- const dropStyle = useAnimatedStyle(() => ({
- opacity: dropOpacity.value,
- transform: [{ scale: dropScale.value }]
- }));
- return (
- <ImageBackground source={{ uri: Images.common.commonBg }} style={styles.container}>
- {/* Header */}
- <View style={[styles.header, { paddingTop: insets.top, minHeight: 44 + insets.top }]}>
- <TouchableOpacity onPress={handleBack} style={styles.backBtn}>
- <Ionicons name="chevron-back" size={24} color="#fff" />
- </TouchableOpacity>
- <Text style={styles.title}>扭蛋机</Text>
- </View>
- {/* Fixed Record Button */}
- <TouchableOpacity onPress={() => winRecordRef.current?.show()} style={styles.recordBtn}>
- <ImageBackground source={{ uri: Images.welfare.qijiWelfareRecordBg }} style={styles.recordBg}>
- <Text style={styles.recordText}>中奖记录</Text>
- </ImageBackground>
- </TouchableOpacity>
- <ScrollView contentContainerStyle={styles.scrollContent}>
- <View style={styles.content}>
- {/* Rule Button */}
- <TouchableOpacity onPress={() => ruleRef.current?.show()} style={styles.ruleBtn}>
- <ImageBackground source={{ uri: Images.welfare.catchDollRule }} style={styles.fullSize} />
- </TouchableOpacity>
- {/* Machine */}
- <View style={styles.machineBox}>
- <ImageBackground source={{ uri: Images.welfare.qijiWelfareDollBox }} style={styles.machineBg} resizeMode="stretch">
- {/* Prizes Scroll */}
- <View style={styles.prizesScrollBox}>
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
- {luckWheelGoodsList.map((item, index) => (
- <View key={index} style={styles.prizeItem}>
- <Image source={{ uri: item.cover }} style={styles.prizeImg} />
- </View>
- ))}
- </ScrollView>
- </View>
- {/* Molibi Count & Add */}
- <View style={styles.molibiBox}>
- <Text style={styles.molibiText}>源力币:<Text style={styles.molibiNum}>{molibi}</Text> 个</Text>
- <TouchableOpacity onPress={() => router.push('/box' as any)} style={styles.addMolibiBtn}>
- <Image source={{ uri: Images.welfare.molibiBoxBtn }} style={styles.fullSize} resizeMode="contain" />
- </TouchableOpacity>
- </View>
- {/* Switch */}
- <Animated.View style={[styles.switchBox, switchStyle]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollBi1 }} style={styles.fullSize} />
- </Animated.View>
- <Animated.View style={[styles.switchBoxRight, switchStyle]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollBi5 }} style={styles.fullSize} />
- </Animated.View>
- {/* Balls */}
- <View style={styles.ballsContainer}>
- {Array.from({ length: BALL_COUNT }).map((_, index) => (
- <Ball key={index} index={index} />
- ))}
- </View>
- {/* Dropping Ball */}
- <Animated.View style={[styles.droppingBall, dropStyle]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollBall }} style={styles.fullSize} />
- </Animated.View>
- {/* Opening hole image */}
- <Image source={{ uri: Images.welfare.opening }} style={styles.opening} />
- {/* Buttons */}
- <TouchableOpacity onPress={handlePress1} style={[styles.playBtn, styles.playBtn1]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollOne }} style={styles.fullSize} resizeMode="contain" />
- </TouchableOpacity>
- <TouchableOpacity onPress={handlePress5} style={[styles.playBtn, styles.playBtn5]}>
- <Image source={{ uri: Images.welfare.qijiWelfareDollFive }} style={styles.fullSize} resizeMode="contain" />
- </TouchableOpacity>
- </ImageBackground>
- </View>
- </View>
- </ScrollView>
- {/* Modals */}
- <CatchRuleModal ref={ruleRef} />
- <WinRecordModal ref={winRecordRef} />
- <DollResultModal ref={resultRef} />
- <DollPrizeModal ref={prizeRef} />
- <PressSureModal ref={pressSureRef} onPress={onConfirmPress} />
- <LackMolibModal ref={lackMolibRef} />
- </ImageBackground>
- );
- }
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- width: '100%',
- height: '100%',
- },
- header: {
- // height: 44, // Removing fixed height to allow dynamic override
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- zIndex: 100,
- },
- backBtn: {
- position: 'absolute',
- left: 10,
- bottom: 10,
- zIndex: 101,
- },
- title: {
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold',
- },
- scrollContent: {
- flexGrow: 1,
- paddingBottom: 50,
- paddingTop: 40, // Push entire content down to avoid header overlap
- },
- content: {
- width: '100%',
- alignItems: 'center',
- position: 'relative',
- height: 750,
- },
- fullSize: {
- width: '100%',
- height: '100%',
- },
- ruleBtn: {
- position: 'absolute',
- left: 13,
- top: 422,
- zIndex: 10,
- width: 62,
- height: 20,
- },
- recordBtn: {
- position: 'absolute',
- right: 0,
- top: 200, // Fixed position from top of screen
- zIndex: 99,
- },
- recordBg: {
- width: 26,
- height: 80, // Increased from 70 to fit 4 chars
- justifyContent: 'center',
- alignItems: 'center',
- paddingTop: 5, // Reduced padding slightly
- },
- recordText: {
- fontSize: 12,
- color: '#fff',
- fontWeight: 'bold',
- width: 12,
- textAlign: 'center',
- textShadowColor: '#6C3200',
- textShadowOffset: { width: 1, height: 1 },
- textShadowRadius: 1,
- },
- machineBox: {
- marginTop: 20, // Reverted to 20, spacing handled by scrollContent padding
- width: '100%',
- alignItems: 'center',
- },
- machineBg: {
- width: '100%',
- height: 706,
- position: 'relative',
- // resizeMode: 'stretch' // Applied in component prop
- },
- prizesScrollBox: {
- width: 250,
- alignSelf: 'center',
- marginTop: 110,
- height: 50,
- },
- prizeItem: {
- width: 46,
- height: 46,
- borderRadius: 4,
- backgroundColor: '#ADAEF6',
- borderWidth: 2.5,
- borderColor: '#8687E4',
- marginRight: 5,
- justifyContent: 'center',
- alignItems: 'center',
- },
- prizeImg: {
- width: '100%',
- height: '100%',
- },
- molibiBox: {
- position: 'absolute',
- top: 465,
- left: 42,
- width: 120,
- height: 67,
- backgroundColor: '#1E1C5B',
- borderRadius: 8,
- alignItems: 'center',
- paddingTop: 5,
- },
- molibiText: {
- color: '#7982CB',
- fontSize: 12,
- },
- molibiNum: {
- color: '#FF8400',
- fontSize: 18,
- fontWeight: '400',
- },
- addMolibiBtn: {
- width: 105,
- height: 30,
- marginTop: 5,
- },
- opening: {
- position: 'absolute',
- top: 455,
- right: 42,
- width: 133,
- height: 82,
- },
- switchBox: {
- position: 'absolute',
- top: 560,
- left: 42,
- width: 65,
- height: 65,
- zIndex: 3,
- },
- switchBoxRight: { // switchBox5
- position: 'absolute',
- top: 560,
- right: 42,
- width: 65,
- height: 65,
- zIndex: 3,
- },
- ballsContainer: {
- position: 'absolute',
- top: 125,
- left: 43,
- width: 275,
- height: 278,
- zIndex: 9,
- overflow: 'hidden',
- },
- ball: {
- width: BALL_SIZE,
- height: BALL_SIZE,
- position: 'absolute',
- left: 0,
- top: 0,
- },
- droppingBall: {
- position: 'absolute',
- top: 475,
- right: 87,
- width: 49,
- height: 48,
- zIndex: 3,
- },
- playBtn: {
- position: 'absolute',
- bottom: 90, // Match Vue: 179rpx / 2 = 89.5
- zIndex: 10,
- width: 73,
- height: 50,
- },
- playBtn1: {
- left: 98,
- },
- playBtn5: {
- right: 98,
- }
- });
|