zbb 3 mesiacov pred
rodič
commit
64272648bc

+ 383 - 293
app/weal/catchDoll.tsx

@@ -1,315 +1,397 @@
+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, { useCallback, useEffect, useRef, useState } from 'react';
-import {
-    Animated,
-    Dimensions,
-    ImageBackground,
-    ScrollView,
-    StatusBar,
-    StyleSheet,
-    Text,
-    TouchableOpacity,
-    View,
-} from 'react-native';
+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);
 
-import { get } from '@/services/http';
-
-const { width: SCREEN_WIDTH } = Dimensions.get('window');
-const CDN_BASE = 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart';
-
-const catchDollImages = {
-  bg: `${CDN_BASE}/common/commonBg.png`,
-  dollBox: `${CDN_BASE}/welfare/qijiWelfareDollBox.png`,
-  dollBall: `${CDN_BASE}/welfare/qijiWelfareDollBall.png`,
-  dollOne: `${CDN_BASE}/welfare/qijiWelfareDollOne.png`,
-  dollFive: `${CDN_BASE}/welfare/qijiWelfareDollFive.png`,
-  recordBg: `${CDN_BASE}/welfare/qijiWelfareRecordBg.png`,
-  ruleBtn: `${CDN_BASE}/welfare/catchDollRule.png`,
-  molibiBoxBtn: `${CDN_BASE}/welfare/molibiBoxBtn.png`,
-  opening: `${CDN_BASE}/welfare/opening.png`,
-  dollBi1: `${CDN_BASE}/welfare/qijiWelfareDollBi1.png`,
-  dollBi5: `${CDN_BASE}/welfare/qijiWelfareDollBi5.png`,
-};
-
-interface GoodsItem {
-  id: string;
-  cover: string;
-  name: string;
-}
+  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();
-  const [goodsList, setGoodsList] = useState<GoodsItem[]>([]);
+
+  // State
   const [molibi, setMolibi] = useState(0);
-  const [balls] = useState(() =>
-    Array.from({ length: 50 }, (_, i) => ({
-      id: i,
-      bottom: Math.random() * 80,
-      left: Math.random() * 172,
-    }))
-  );
+  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);
 
-  // 动画
-  const ballAnimations = useRef(balls.map(() => new Animated.ValueXY({ x: 0, y: 0 }))).current;
-  const ballRotations = useRef(balls.map(() => new Animated.Value(0))).current;
-
-  const loadData = useCallback(async () => {
-    try {
-      const res = await get('/api/luckWheel/detail');
-      if (res.data) {
-        setGoodsList(res.data.luckWheelGoodsList || []);
-      }
-    } catch (error) {
-      console.error('加载扭蛋机数据失败:', error);
-    }
+  useEffect(() => {
+    initData();
   }, []);
 
-  const loadMolibi = useCallback(async () => {
-    try {
-      const res = await get('/api/wallet/info', { type: 'MAGIC_POWER_COIN' });
-      if (res.data) {
-        setMolibi(res.data.balance || 0);
-      }
-    } catch (error) {
-      console.error('加载源力币失败:', error);
+  const initData = async () => {
+    getMolibi();
+    getDetail();
+  };
+
+  const getMolibi = async () => {
+    const res = await ServiceWallet.info('MAGIC_POWER_COIN');
+    if (res) {
+      setMolibi(res.balance);
     }
-  }, []);
+  };
 
-  useEffect(() => {
-    loadData();
-    loadMolibi();
-  }, [loadData, loadMolibi]);
-
-  const animateBalls = () => {
-    const animations = balls.map((_, i) => {
-      const randomX = (Math.random() - 0.5) * 50;
-      const randomY = (Math.random() - 0.5) * 100;
-      const randomRotate = Math.random() * 360;
-
-      return Animated.parallel([
-        Animated.sequence([
-          Animated.timing(ballAnimations[i], {
-            toValue: { x: randomX, y: randomY },
-            duration: 200,
-            useNativeDriver: true,
-          }),
-          Animated.timing(ballAnimations[i], {
-            toValue: { x: randomX * 0.5, y: randomY * 0.5 },
-            duration: 300,
-            useNativeDriver: true,
-          }),
-          Animated.timing(ballAnimations[i], {
-            toValue: { x: 0, y: 0 },
-            duration: 300,
-            useNativeDriver: true,
-          }),
-        ]),
-        Animated.timing(ballRotations[i], {
-          toValue: randomRotate,
-          duration: 800,
-          useNativeDriver: true,
-        }),
-      ]);
-    });
-
-    Animated.parallel(animations).start(() => {
-      ballRotations.forEach((rot) => rot.setValue(0));
-    });
+  const getDetail = async () => {
+    const res = await Service.catchDollDetail();
+    if (res.code === '0') {
+      setLuckWheelGoodsList(res.data.luckWheelGoodsList);
+    } else {
+      Alert.alert('提示', res.msg);
+    }
   };
 
-  const handlePress = (count: number) => {
-    if (molibi < count) {
-      // TODO: 显示源力币不足弹窗
+  const handleBack = () => router.back();
+
+  const handlePress1 = () => {
+    if (lotteryFlag) {
+      Alert.alert('提示', '请不要重复点击');
       return;
     }
-    animateBalls();
-    // TODO: 调用抽奖接口
+    if (molibi === 0) {
+      lackMolibRef.current?.show();
+    } else {
+      pressSureRef.current?.show(1);
+    }
   };
 
-  return (
-    <View style={styles.container}>
-      <StatusBar barStyle="light-content" />
-      <ImageBackground source={{ uri: catchDollImages.bg }} style={styles.background} resizeMode="cover">
-        {/* 固定头部 */}
-        <View style={[styles.header, { paddingTop: insets.top }]}>
-          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
-            <Text style={styles.backText}>←</Text>
-          </TouchableOpacity>
-          <Text style={styles.title}>扭蛋机</Text>
-          <View style={styles.placeholder} />
-        </View>
+  const handlePress5 = () => {
+    if (lotteryFlag) {
+      Alert.alert('提示', '请不要重复点击');
+      return;
+    }
+    if (molibi < 5) {
+      lackMolibRef.current?.show();
+    } else {
+      pressSureRef.current?.show(5);
+    }
+  };
 
-        {/* 规则按钮 - 固定位置 */}
-        <TouchableOpacity style={[styles.ruleBtn, { top: insets.top + 200 }]}>
-          <Image source={{ uri: catchDollImages.ruleBtn }} style={styles.ruleBtnImg} contentFit="contain" />
-        </TouchableOpacity>
+  const onConfirmPress = (quantity: number) => {
+    playLottery(quantity);
+  };
 
-        {/* 中奖记录按钮 - 固定位置 */}
-        <TouchableOpacity style={[styles.recordBtn, { top: insets.top + 120 }]}>
-          <ImageBackground source={{ uri: catchDollImages.recordBg }} style={styles.recordBtnBg} resizeMode="contain">
-            <Text style={styles.recordText}>中</Text>
-            <Text style={styles.recordText}>奖</Text>
-            <Text style={styles.recordText}>记</Text>
-            <Text style={styles.recordText}>录</Text>
-          </ImageBackground>
+  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>
 
-        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
-          <View style={{ height: insets.top + 36 }} />
-
-          {/* 扭蛋机主体 */}
-          <View style={styles.machineWrapper}>
-            <ImageBackground source={{ uri: catchDollImages.dollBox }} style={styles.machineImg} resizeMode="contain">
-              {/* 奖品列表 */}
-              <View style={styles.goodsListWrapper}>
-                <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.goodsScroll}>
-                  {goodsList.map((item, index) => (
-                    <View key={item.id || index} style={styles.goodsItem}>
-                      <Image source={{ uri: item.cover }} style={styles.goodsImg} contentFit="cover" />
+
+          {/* 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>
 
-              {/* 扭蛋球区域 */}
-              <View style={styles.ballsBox}>
-                {balls.map((ball, i) => (
-                  <Animated.View
-                    key={ball.id}
-                    style={[
-                      styles.ball,
-                      {
-                        bottom: ball.bottom,
-                        left: ball.left,
-                        transform: [
-                          { translateX: ballAnimations[i].x },
-                          { translateY: ballAnimations[i].y },
-                          {
-                            rotate: ballRotations[i].interpolate({
-                              inputRange: [0, 360],
-                              outputRange: ['0deg', '360deg'],
-                            }),
-                          },
-                        ],
-                      },
-                    ]}
-                  >
-                    <Image source={{ uri: catchDollImages.dollBall }} style={styles.ballImg} contentFit="contain" />
-                  </Animated.View>
-                ))}
-              </View>
-
-              {/* 源力币信息框 */}
+              {/* Molibi Count & Add */}
               <View style={styles.molibiBox}>
-                <Text style={styles.molibiLabel}>
-                  源力币:<Text style={styles.molibiNum}>{molibi}</Text> 个
-                </Text>
-                <TouchableOpacity style={styles.molibiBtn} onPress={() => router.push('/award' as any)}>
-                  <Image source={{ uri: catchDollImages.molibiBoxBtn }} style={styles.molibiBtnImg} contentFit="contain" />
+                <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>
 
-              {/* 开口动画区域 */}
-              <View style={styles.openingBox}>
-                <Image source={{ uri: catchDollImages.opening }} style={styles.openingImg} contentFit="contain" />
+              {/* 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>
 
-              {/* 扭蛋把手 */}
-              <TouchableOpacity style={[styles.switchBox, styles.switchBox1]} onPress={() => handlePress(1)}>
-                <Image source={{ uri: catchDollImages.dollBi1 }} style={styles.switchImg} contentFit="contain" />
+              {/* 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 style={[styles.switchBox, styles.switchBox5]} onPress={() => handlePress(5)}>
-                <Image source={{ uri: catchDollImages.dollBi5 }} style={styles.switchImg} contentFit="contain" />
+
+              <TouchableOpacity onPress={handlePress5} style={[styles.playBtn, styles.playBtn5]}>
+                <Image source={{ uri: Images.welfare.qijiWelfareDollFive }} style={styles.fullSize} resizeMode="contain" />
               </TouchableOpacity>
+
             </ImageBackground>
           </View>
 
-          {/* 底部按钮 */}
-          <View style={styles.bottomBtns}>
-            <TouchableOpacity style={styles.submitBtn} onPress={() => handlePress(1)}>
-              <Image source={{ uri: catchDollImages.dollOne }} style={styles.submitBtnImg} contentFit="contain" />
-            </TouchableOpacity>
-            <TouchableOpacity style={styles.submitBtn} onPress={() => handlePress(5)}>
-              <Image source={{ uri: catchDollImages.dollFive }} style={styles.submitBtnImg} contentFit="contain" />
-            </TouchableOpacity>
-          </View>
+        </View>
+      </ScrollView>
 
-          <View style={{ height: 100 }} />
-        </ScrollView>
-      </ImageBackground>
-    </View>
+      {/* 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, backgroundColor: '#1a1a2e' },
-  background: { flex: 1 },
+  container: {
+    flex: 1,
+    width: '100%',
+    height: '100%',
+  },
   header: {
+    // height: 44, // Removing fixed height to allow dynamic override
     flexDirection: 'row',
     alignItems: 'center',
-    justifyContent: 'space-between',
-    paddingHorizontal: 10,
-    paddingBottom: 10,
+    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',
-    top: 0,
-    left: 0,
     right: 0,
-    zIndex: 100,
+    top: 200, // Fixed position from top of screen
+    zIndex: 99,
   },
-  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
-  backText: { color: '#fff', fontSize: 20 },
-  title: { color: '#fff', fontSize: 15, fontWeight: 'bold' },
-  placeholder: { width: 40 },
-  scrollView: { flex: 1 },
-
-  // 规则按钮
-  ruleBtn: { position: 'absolute', left: 13, zIndex: 99 },
-  ruleBtnImg: { width: 62, height: 20 },
-
-  // 中奖记录按钮
-  recordBtn: { position: 'absolute', right: 0, zIndex: 99 },
-  recordBtnBg: {
+  recordBg: {
     width: 26,
-    height: 70,
+    height: 80, // Increased from 70 to fit 4 chars
     justifyContent: 'center',
     alignItems: 'center',
-    paddingVertical: 9,
+    paddingTop: 5, // Reduced padding slightly
   },
   recordText: {
-    color: '#fff',
     fontSize: 12,
+    color: '#fff',
     fontWeight: 'bold',
+    width: 12,
+    textAlign: 'center',
     textShadowColor: '#6C3200',
     textShadowOffset: { width: 1, height: 1 },
     textShadowRadius: 1,
   },
-
-  // 扭蛋机主体
-  machineWrapper: {
-    width: SCREEN_WIDTH,
+  machineBox: {
+    marginTop: 20, // Reverted to 20, spacing handled by scrollContent padding
+    width: '100%',
     alignItems: 'center',
   },
-  machineImg: {
-    width: SCREEN_WIDTH,
-    height: SCREEN_WIDTH * 1.88,
+  machineBg: {
+    width: '100%',
+    height: 706,
     position: 'relative',
+    // resizeMode: 'stretch' // Applied in component prop
   },
-
-  // 奖品列表
-  goodsListWrapper: {
-    position: 'absolute',
-    top: SCREEN_WIDTH * 0.29,
-    left: 0,
-    right: 0,
-    alignItems: 'center',
-  },
-  goodsScroll: {
-    paddingHorizontal: SCREEN_WIDTH * 0.18,
+  prizesScrollBox: {
+    width: 250,
+    alignSelf: 'center',
+    marginTop: 110,
+    height: 50,
   },
-  goodsItem: {
+  prizeItem: {
     width: 46,
     height: 46,
     borderRadius: 4,
@@ -317,88 +399,96 @@ const styles = StyleSheet.create({
     borderWidth: 2.5,
     borderColor: '#8687E4',
     marginRight: 5,
-    overflow: 'hidden',
-  },
-  goodsImg: { width: '100%', height: '100%' },
-
-  // 扭蛋球区域
-  ballsBox: {
-    position: 'absolute',
-    top: SCREEN_WIDTH * 0.33,
-    left: SCREEN_WIDTH * 0.115,
-    width: SCREEN_WIDTH * 0.73,
-    height: SCREEN_WIDTH * 0.74,
-    overflow: 'hidden',
+    justifyContent: 'center',
+    alignItems: 'center',
   },
-  ball: {
-    position: 'absolute',
-    width: 66,
-    height: 66,
+  prizeImg: {
+    width: '100%',
+    height: '100%',
   },
-  ballImg: { width: '100%', height: '100%' },
-
-  // 源力币信息框
   molibiBox: {
     position: 'absolute',
-    top: SCREEN_WIDTH * 1.24,
+    top: 465,
     left: 42,
     width: 120,
     height: 67,
     backgroundColor: '#1E1C5B',
     borderRadius: 8,
-    paddingTop: 5,
     alignItems: 'center',
+    paddingTop: 5,
   },
-  molibiLabel: {
+  molibiText: {
     color: '#7982CB',
     fontSize: 12,
   },
   molibiNum: {
     color: '#FF8400',
     fontSize: 18,
-    fontWeight: 'bold',
+    fontWeight: '400',
   },
-  molibiBtn: {
-    marginTop: 5,
-  },
-  molibiBtnImg: {
+  addMolibiBtn: {
     width: 105,
     height: 30,
+    marginTop: 5,
   },
-
-  // 开口动画区域
-  openingBox: {
+  opening: {
     position: 'absolute',
-    top: SCREEN_WIDTH * 1.21,
+    top: 455,
     right: 42,
     width: 133,
     height: 82,
   },
-  openingImg: { width: '100%', height: '100%' },
-
-  // 扭蛋把手
   switchBox: {
     position: 'absolute',
-    top: SCREEN_WIDTH * 1.49,
+    top: 560,
+    left: 42,
     width: 65,
     height: 65,
+    zIndex: 3,
   },
-  switchBox1: { left: 42 },
-  switchBox5: { right: 42 },
-  switchImg: { width: '100%', height: '100%' },
-
-  // 底部按钮
-  bottomBtns: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    marginTop: -SCREEN_WIDTH * 0.24,
-    paddingHorizontal: 20,
+  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,
   },
-  submitBtn: {
-    marginHorizontal: SCREEN_WIDTH * 0.08,
+  droppingBall: {
+    position: 'absolute',
+    top: 475,
+    right: 87,
+    width: 49,
+    height: 48,
+    zIndex: 3,
   },
-  submitBtnImg: {
+  playBtn: {
+    position: 'absolute',
+    bottom: 90, // Match Vue: 179rpx / 2 = 89.5
+    zIndex: 10,
     width: 73,
     height: 50,
   },
+  playBtn1: {
+    left: 98,
+  },
+  playBtn5: {
+    right: 98,
+  }
 });

+ 104 - 0
app/weal/components/CatchRuleModal.tsx

@@ -0,0 +1,104 @@
+import { Images } from '@/constants/images';
+import { ImageBackground } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface CatchRuleModalRef {
+    show: () => void;
+    close: () => void;
+}
+
+export const CatchRuleModal = forwardRef<CatchRuleModalRef>((_, ref) => {
+    const [visible, setVisible] = useState(false);
+
+    useImperativeHandle(ref, () => ({
+        show: () => setVisible(true),
+        close: () => setVisible(false),
+    }));
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <View style={styles.contentContainer}>
+                    <ImageBackground
+                        source={{ uri: Images.welfare.qijiWelfareDollRule }}
+                        style={styles.bgContainer}
+                        resizeMode="stretch"
+                    >
+                        <View style={styles.titleContainer}>
+                            <Text style={styles.title}>扭蛋机 玩法规则</Text>
+                        </View>
+                        <View style={styles.textContent}>
+                            <Text style={styles.textTitle}>扭蛋机规则</Text>
+                            <Text style={styles.text}>一个源力币可以扭动一次旋钮,按照需求可选择“扭一次”或“扭五次”,扭动后随机掉落捣蛋机内物品。</Text>
+                            <Text style={styles.textTitle}>源力币获取途径</Text>
+                            <Text style={styles.text}>在宝箱中购买超神或欧皇款后,系统自动发放源力市。</Text>
+                            <Text style={styles.text}>(超神款2个,欧皇款1个)</Text>
+                        </View>
+                    </ImageBackground>
+                    <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+                        <Image source={{ uri: Images.mine.dialogClose }} style={styles.closeIcon} />
+                    </TouchableOpacity>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    contentContainer: {
+        width: '100%',
+        alignItems: 'center',
+    },
+    bgContainer: {
+        width: 357, // 714rpx
+        height: 252, // 504rpx
+        paddingTop: 32, // 64rpx
+        alignItems: 'center',
+    },
+    titleContainer: {
+        alignItems: 'center',
+        marginBottom: 10,
+    },
+    title: {
+        fontSize: 18,
+        fontWeight: 'bold',
+        color: 'rgba(255,255,255,0.68)',
+        textShadowColor: '#07004A',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    },
+    textContent: {
+        paddingHorizontal: 15,
+        marginTop: 10,
+        width: '100%',
+    },
+    textTitle: {
+        textAlign: 'center',
+        fontSize: 16,
+        fontWeight: 'bold',
+        color: '#000',
+        marginVertical: 5,
+    },
+    text: {
+        fontSize: 12,
+        color: '#000',
+        lineHeight: 18,
+        marginBottom: 4,
+    },
+    closeBtn: {
+        marginTop: 20,
+    },
+    closeIcon: {
+        width: 30,
+        height: 30,
+    }
+});

+ 129 - 0
app/weal/components/DollPrizeModal.tsx

@@ -0,0 +1,129 @@
+import { Images } from '@/constants/images';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Image, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface DollPrizeModalRef {
+    show: (data: any[]) => void;
+    close: () => void;
+}
+
+export const DollPrizeModal = forwardRef<DollPrizeModalRef>((_, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [prizeList, setPrizeList] = useState<any[]>([]);
+
+    useImperativeHandle(ref, () => ({
+        show: (data) => {
+            setPrizeList(data);
+            setVisible(true);
+        },
+        close: () => setVisible(false),
+    }));
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+                <View style={styles.contentContainer}>
+                    <View style={styles.wrapper}>
+                        <Image source={{ uri: Images.welfare.toys.rewardBg }} style={styles.ruleBg} resizeMode="stretch" />
+                        <View style={styles.prizeContainer}>
+                            <ScrollView contentContainerStyle={styles.scrollContent}>
+                                {prizeList.map((item, index) => (
+                                    <View key={item.spuId || index} style={[styles.prizeItem, (index + 1) % 3 === 0 && styles.noRightMargin]}>
+                                        <View style={styles.prizeImgBox}>
+                                            <Image source={{ uri: item.cover }} style={styles.prizeImg} resizeMode="contain" />
+                                        </View>
+                                        <Text style={styles.prizeText} numberOfLines={1}>{item.name}</Text>
+                                    </View>
+                                ))}
+                            </ScrollView>
+                        </View>
+                    </View>
+                    <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+                        <Image source={{ uri: Images.mine.dialogClose }} style={styles.closeIcon} />
+                    </TouchableOpacity>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    mask: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        right: 0,
+        bottom: 0,
+    },
+    contentContainer: {
+        width: '100%',
+        alignItems: 'center',
+    },
+    wrapper: {
+        width: 326, // 652rpx
+        height: 452, // 904rpx
+        position: 'relative',
+        alignItems: 'center',
+    },
+    ruleBg: {
+        width: '100%',
+        height: '100%',
+        position: 'absolute',
+    },
+    prizeContainer: {
+        position: 'absolute',
+        top: 78.5, // 157rpx
+        left: 27, // 54rpx
+        right: 27, // 54rpx
+        height: 357.5, // 715rpx
+        width: 272, // 544rpx
+    },
+    scrollContent: {
+        flexDirection: 'row',
+        flexWrap: 'wrap',
+        justifyContent: 'flex-start',
+    },
+    prizeItem: {
+        width: 80, // 160rpx
+        height: 116.5, // 233rpx
+        marginRight: 10, // 20rpx
+        marginTop: 11.5, // 23rpx
+        alignItems: 'center',
+    },
+    noRightMargin: {
+        marginRight: 0,
+    },
+    prizeImgBox: {
+        width: 80, // 160rpx
+        height: 100, // 200rpx
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    prizeImg: {
+        width: '100%',
+        height: '100%',
+    },
+    prizeText: {
+        color: '#B58100',
+        marginTop: 6,
+        textAlign: 'center',
+        fontSize: 12,
+        width: '100%',
+    },
+    closeBtn: {
+        marginTop: 15,
+    },
+    closeIcon: {
+        width: 30, // 60rpx
+        height: 30,
+    }
+});

+ 154 - 0
app/weal/components/DollResultModal.tsx

@@ -0,0 +1,154 @@
+import { Images } from '@/constants/images';
+import { ImageBackground } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface DollResultModalRef {
+    show: (data: any[]) => void;
+    close: () => void;
+}
+
+export const DollResultModal = forwardRef<DollResultModalRef>((_, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [prizeResult, setPrizeResult] = useState<any[]>([]);
+
+    useImperativeHandle(ref, () => ({
+        show: (data) => {
+            setPrizeResult(data);
+            setVisible(true);
+        },
+        close: () => setVisible(false),
+    }));
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <View style={styles.contentContainer}>
+                    <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+                        <Image source={{ uri: Images.common.closeBut }} style={styles.closeIcon} />
+                    </TouchableOpacity>
+
+                    <View style={styles.wrapper}>
+                        {prizeResult.length === 1 ? (
+                            <ImageBackground source={{ uri: Images.welfare.resultBg }} style={styles.prizeOneBg} resizeMode="stretch">
+                                <View style={styles.box}>
+                                    <Text style={styles.textOne} numberOfLines={1}>{prizeResult[0].name}</Text>
+                                    <View style={styles.imgOneBox}>
+                                        <Image source={{ uri: prizeResult[0].cover }} style={styles.imgOne} resizeMode="contain" />
+                                    </View>
+                                </View>
+                            </ImageBackground>
+                        ) : (
+                            <View style={styles.prizeFiveContainer}>
+                                {prizeResult.map((item, index) => (
+                                    <ImageBackground key={item.spuId || index} source={{ uri: Images.welfare.resultBg }} style={[styles.prizeItemBg, (index + 1) % 3 === 0 && styles.noRightMargin]} resizeMode="stretch">
+                                        <View style={styles.box}>
+                                            <Text style={styles.textFive} numberOfLines={1}>{item.name}</Text>
+                                            <View style={styles.imgFiveBox}>
+                                                <Image source={{ uri: item.cover }} style={styles.imgFive} resizeMode="contain" />
+                                            </View>
+                                        </View>
+                                    </ImageBackground>
+                                ))}
+                            </View>
+                        )}
+                    </View>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    contentContainer: {
+        width: '100%',
+    },
+    closeBtn: {
+        alignSelf: 'flex-end',
+        marginRight: 30, // Adjust as needed
+        marginBottom: 10,
+    },
+    closeIcon: {
+        width: 30,
+        height: 30,
+    },
+    wrapper: {
+        alignItems: 'center',
+        width: '100%',
+    },
+    prizeOneBg: {
+        width: 293, // 586rpx
+        height: 290, // Approx based on padding
+        paddingTop: 75, // 150rpx
+        paddingBottom: 94, // 188rpx
+        alignItems: 'center',
+        justifyContent: 'center',
+    },
+    box: {
+        height: '100%',
+        alignItems: 'center',
+        width: '100%',
+    },
+    textOne: {
+        color: '#444',
+        fontSize: 15,
+        fontWeight: 'bold',
+        textAlign: 'center',
+        width: '80%',
+    },
+    imgOneBox: {
+        marginTop: 27, // 54rpx
+        width: 150, // 300rpx
+        height: 150,
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    imgOne: {
+        width: '100%',
+        height: '100%',
+    },
+    prizeFiveContainer: {
+        flexDirection: 'row',
+        flexWrap: 'wrap',
+        justifyContent: 'center',
+        width: '100%',
+        paddingHorizontal: 20,
+    },
+    prizeItemBg: {
+        width: 102, // 204rpx
+        height: 130, // 260rpx
+        marginRight: 9, // 18rpx
+        marginBottom: 10, // 20rpx
+        paddingTop: 27, // 54rpx
+        alignItems: 'center',
+    },
+    noRightMargin: {
+        marginRight: 0,
+    },
+    textFive: {
+        color: '#444',
+        fontWeight: 'bold',
+        fontSize: 10,
+        textAlign: 'center',
+        width: '90%',
+    },
+    imgFiveBox: {
+        marginTop: -15, // -30rpx
+        width: 80, // 160rpx
+        height: 80,
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    imgFive: {
+        width: '100%',
+        height: '100%',
+    }
+});

+ 100 - 0
app/weal/components/LackMolibModal.tsx

@@ -0,0 +1,100 @@
+import { Images } from '@/constants/images';
+import { ImageBackground } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface LackMolibModalRef {
+    show: () => void;
+    close: () => void;
+}
+
+export const LackMolibModal = forwardRef<LackMolibModalRef>((_, ref) => {
+    const [visible, setVisible] = useState(false);
+    const router = useRouter();
+
+    useImperativeHandle(ref, () => ({
+        show: () => setVisible(true),
+        close: () => setVisible(false),
+    }));
+
+    const handleSubmit = () => {
+        setVisible(false);
+        // Navigate to box homepage
+        router.push('/box');
+    }
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+                <View style={styles.contentContainer}>
+                    <View style={styles.wrapper}>
+                        <ImageBackground source={{ uri: Images.welfare.welfareDialogMain }} style={styles.main} resizeMode="stretch">
+                            <Text style={styles.imgText}>尊主大人的源力币不够啦, 快去收集源力币吧。</Text>
+                        </ImageBackground>
+                        <TouchableOpacity onPress={handleSubmit}>
+                            <ImageBackground source={{ uri: Images.welfare.welfareDialogSubmit }} style={styles.submitBtn} resizeMode="stretch">
+                                <Text style={styles.btnText}>收集源力币</Text>
+                            </ImageBackground>
+                        </TouchableOpacity>
+                    </View>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    mask: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        right: 0,
+        bottom: 0,
+    },
+    contentContainer: {
+        width: '100%',
+        alignItems: 'center',
+    },
+    wrapper: {
+        marginTop: 33,
+        alignItems: 'center',
+    },
+    main: {
+        width: 357, // 714rpx
+        height: 142, // 284rpx
+        justifyContent: 'center',
+        alignItems: 'center',
+        marginBottom: 17,
+        paddingHorizontal: 25,
+    },
+    imgText: {
+        fontWeight: 'bold',
+        fontSize: 20,
+        color: '#000',
+        textAlign: 'center',
+    },
+    submitBtn: {
+        width: 179, // 358rpx
+        height: 57, // 114rpx
+        justifyContent: 'center',
+        alignItems: 'center',
+        paddingTop: 5,
+    },
+    btnText: {
+        fontSize: 17.5, // 35rpx
+        color: '#fff',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    }
+});

+ 111 - 0
app/weal/components/PressSureModal.tsx

@@ -0,0 +1,111 @@
+import { Images } from '@/constants/images';
+import { ImageBackground } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface PressSureModalRef {
+    show: (type: number) => void;
+    close: () => void;
+}
+
+interface Props {
+    onPress: (type: number) => void;
+}
+
+export const PressSureModal = forwardRef<PressSureModalRef, Props>((props, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [pressType, setPressType] = useState(1);
+
+    useImperativeHandle(ref, () => ({
+        show: (type) => {
+            setPressType(type);
+            setVisible(true);
+        },
+        close: () => setVisible(false),
+    }));
+
+    const handleSubmit = () => {
+        setVisible(false);
+        props.onPress(pressType);
+    }
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <View style={styles.contentContainer}>
+                    <View style={styles.content}>
+                        <ImageBackground source={{ uri: Images.welfare.welfareDialogBg }} style={styles.textBox} resizeMode="stretch">
+                            <Text style={styles.text}>是否扭{pressType === 1 ? '一' : '五'}次?</Text>
+                        </ImageBackground>
+
+                        <View style={styles.btns}>
+                            <TouchableOpacity onPress={() => setVisible(false)}>
+                                <ImageBackground source={{ uri: Images.welfare.welfareDialogSubmit }} style={styles.btn} resizeMode="stretch">
+                                    <Text style={styles.btnText}>取消</Text>
+                                </ImageBackground>
+                            </TouchableOpacity>
+                            <TouchableOpacity onPress={handleSubmit}>
+                                <ImageBackground source={{ uri: Images.welfare.welfareDialogSubmit }} style={styles.btn} resizeMode="stretch">
+                                    <Text style={styles.btnText}>确认</Text>
+                                </ImageBackground>
+                            </TouchableOpacity>
+                        </View>
+                    </View>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    contentContainer: {
+        width: '100%',
+        paddingHorizontal: 0,
+        alignItems: 'center',
+    },
+    content: {
+        width: '100%',
+        paddingHorizontal: 16,
+        paddingBottom: 30,
+    },
+    textBox: {
+        width: '100%',
+        height: 136, // 272rpx
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    text: {
+        fontSize: 24,
+        fontWeight: 'bold',
+        color: '#000',
+    },
+    btns: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        paddingHorizontal: 11,
+        marginTop: 20,
+    },
+    btn: {
+        width: 150, // 300rpx
+        height: 59, // 118rpx
+        justifyContent: 'center',
+        alignItems: 'center',
+        paddingTop: 10,
+    },
+    btnText: {
+        fontSize: 14,
+        color: '#fff',
+        fontWeight: 'bold',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    }
+});

+ 219 - 0
app/weal/components/WinRecordModal.tsx

@@ -0,0 +1,219 @@
+import { Images } from '@/constants/images';
+import Service from '@/services/weal';
+import { ImageBackground } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { FlatList, Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+export interface WinRecordModalRef {
+    show: () => void;
+    close: () => void;
+}
+
+export const WinRecordModal = forwardRef<WinRecordModalRef>((_, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [data, setData] = useState<any[]>([]);
+    const [loading, setLoading] = useState(false);
+    const [isRefreshing, setIsRefreshing] = useState(false);
+    const [current, setCurrent] = useState(1);
+    const size = 20;
+    const [hasMore, setHasMore] = useState(true);
+
+    useImperativeHandle(ref, () => ({
+        show: () => {
+            setVisible(true);
+            refresh();
+        },
+        close: () => setVisible(false),
+    }));
+
+    const getData = async (pageNum: number, isRefresh: boolean = false) => {
+        if (loading) return;
+        setLoading(true);
+        try {
+            const res = await Service.prizeResult({ current: pageNum, size });
+            if (res && res.records) {
+                if (isRefresh) {
+                    setData(res.records);
+                } else {
+                    setData(prev => [...prev, ...res.records]);
+                }
+                if (res.records.length < size) {
+                    setHasMore(false);
+                } else {
+                    setHasMore(true);
+                }
+            }
+        } catch (error) {
+            console.error("Failed to fetch records", error);
+        } finally {
+            setLoading(false);
+            setIsRefreshing(false);
+        }
+    };
+
+    const refresh = () => {
+        setCurrent(1);
+        setIsRefreshing(true);
+        setHasMore(true);
+        getData(1, true);
+    };
+
+    const loadMore = () => {
+        if (!loading && hasMore) {
+            const next = current + 1;
+            setCurrent(next);
+            getData(next);
+        }
+    };
+
+    if (!visible) return null;
+
+    return (
+        <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+            <View style={styles.overlay}>
+                <View style={styles.contentContainer}>
+                    <ImageBackground source={{ uri: Images.mine.dialogContentBg }} style={styles.contentBg} resizeMode="stretch">
+                        <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+                            <Image source={{ uri: Images.common.closeBut }} style={styles.closeIcon} />
+                        </TouchableOpacity>
+
+                        <View style={styles.titleBox}>
+                            <Text style={styles.title}>中奖记录</Text>
+                        </View>
+
+                        <View style={styles.listBox}>
+                            {data.length > 0 ? (
+                                <FlatList
+                                    data={data}
+                                    keyExtractor={(item, index) => index.toString()}
+                                    renderItem={({ item }) => (
+                                        <View style={styles.item}>
+                                            <Image source={{ uri: item.cover }} style={styles.itemImg} />
+                                            <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
+                                            <View style={styles.userInfo}>
+                                                <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.avatarBg} resizeMode="cover">
+                                                    <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
+                                                </ImageBackground>
+                                                <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
+                                            </View>
+                                        </View>
+                                    )}
+                                    onEndReached={loadMore}
+                                    onEndReachedThreshold={0.1}
+                                    refreshing={isRefreshing}
+                                    onRefresh={refresh}
+                                />
+                            ) : (
+                                <View style={styles.emptyContainer}>
+                                    <Text style={styles.emptyText}>暂无记录</Text>
+                                </View>
+                            )}
+                        </View>
+                    </ImageBackground>
+                </View>
+            </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.6)',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    contentContainer: {
+        width: '100%',
+        paddingHorizontal: 10,
+        alignItems: 'center',
+    },
+    contentBg: {
+        width: '100%', // Adjust based on your design, usually full width - padding
+        height: 367, // 734rpx
+        paddingTop: 69, // 138rpx
+        position: 'relative',
+    },
+    closeBtn: {
+        position: 'absolute',
+        right: 15,
+        top: 25,
+        zIndex: 1,
+    },
+    closeIcon: {
+        width: 30,
+        height: 30,
+    },
+    titleBox: {
+        position: 'absolute',
+        top: 5,
+        left: 0,
+        right: 0,
+        alignItems: 'center',
+    },
+    title: {
+        fontSize: 16,
+        fontWeight: 'bold',
+        color: '#fff',
+        marginTop: 28,
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    },
+    listBox: {
+        flex: 1,
+        paddingHorizontal: 20,
+        paddingBottom: 20,
+        marginTop: 20, // push down below header area
+    },
+    item: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        paddingVertical: 10,
+        borderBottomWidth: 1,
+        borderBottomColor: '#999',
+    },
+    itemImg: {
+        width: 50, // 100rpx
+        height: 50,
+        borderRadius: 25,
+        borderWidth: 2,
+        borderColor: '#090909',
+        marginRight: 10,
+    },
+    itemName: {
+        fontSize: 12,
+        color: '#000',
+        fontWeight: 'bold',
+        flex: 1,
+        marginRight: 11,
+    },
+    userInfo: {
+        width: 82, // 164rpx
+        alignItems: 'flex-end',
+    },
+    avatarBg: {
+        width: 32, // 64rpx
+        height: 32,
+        marginBottom: 5,
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    avatar: {
+        width: 32,
+        height: 32,
+        borderRadius: 16,
+    },
+    nickname: {
+        fontSize: 10, // font2
+        color: '#666',
+    },
+    emptyContainer: {
+        flex: 1,
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    emptyText: {
+        color: '#999',
+    }
+});

+ 17 - 0
constants/images.ts

@@ -142,6 +142,22 @@ export const Images = {
     participationIcon: `${CDN_BASE}/welfare/participationIcon.png`,
     official: `${CDN_BASE}/welfare/official.png`,
     mustBe: `${CDN_BASE}/welfare/mustBe.png`,
+    catchDollRule: `${CDN_BASE}/welfare/catchDollRule.png`,
+    qijiWelfareRecordBg: `${CDN_BASE}/welfare/qijiWelfareRecordBg.png`,
+    qijiWelfareDollBox: `${CDN_BASE}/welfare/qijiWelfareDollBox.png`,
+    molibiBoxBtn: `${CDN_BASE}/welfare/molibiBoxBtn.png`,
+    opening: `${CDN_BASE}/welfare/opening.png`,
+    qijiWelfareDollBall: `${CDN_BASE}/welfare/qijiWelfareDollBall.png`,
+    qijiWelfareDollBi1: `${CDN_BASE}/welfare/qijiWelfareDollBi1.png`,
+    qijiWelfareDollBi5: `${CDN_BASE}/welfare/qijiWelfareDollBi5.png`,
+    qijiWelfareDollOne: `${CDN_BASE}/welfare/qijiWelfareDollOne.png`,
+    qijiWelfareDollFive: `${CDN_BASE}/welfare/qijiWelfareDollFive.png`,
+    qijiWelfareDollRule: `${CDN_BASE}/welfare/qijiWelfareDollRule.png`,
+    resultBg: `${CDN_BASE}/welfare/resultBg.png`,
+    resultTit: `${CDN_BASE}/welfare/resultTit.png`,
+    welfareDialogMain: `${CDN_BASE}/welfare/welfareDialogMain.png`,
+    welfareDialogSubmit: `${CDN_BASE}/welfare/welfareDialogSubmit.png`,
+    welfareDialogBg: `${CDN_BASE}/welfare/welfareDialogBg.png`,
     detail: {
       record: `${CDN_BASE}/welfare/detail/record.png`,
       recordIcon: `${CDN_BASE}/welfare/detail/recordIcon.png`,
@@ -170,6 +186,7 @@ export const Images = {
     order1: `${CDN_BASE}/mine/order1.png`,
     order2: `${CDN_BASE}/mine/order2.png`,
     dialogContentBg: `${CDN_BASE}/mine/dialogContentBg.png`,
+    dialogClose: `${CDN_BASE}/mine/dialogClose.png`,
     wallet: `${CDN_BASE}/mine/wallet.png`,
     exchangeIcon: `${CDN_BASE}/mine/exchangeIcon.png`,
     customerService: `${CDN_BASE}/mine/customerService.png`,

+ 7 - 1
services/wallet.ts

@@ -1,4 +1,4 @@
-import { post } from './http';
+import { get, post } from './http';
 
 const apis = {
     PRE_ONE_KEY: '/api/substituteOrder/preOneKeySubmit',
@@ -9,6 +9,12 @@ export const preOneKeySubmit = async () => {
     return res;
 };
 
+export const info = async (type: string, loading = false) => {
+    const res = await get('/api/wallet/getByType', { type }, { loading });
+    return res.data;
+};
+
 export default {
     preOneKeySubmit,
+    info,
 };

+ 14 - 0
services/weal.ts

@@ -92,5 +92,19 @@ export default {
     getDateTimeScope,
     getMyCreateList,
     createRoom,
+
+    // 扭蛋机相关 API
+    catchDollDetail: async () => {
+        const res = await get('/api/activity/dollMachine/detail');
+        return res;
+    },
+    dollLottery: async (params: { quantity: number }) => {
+        const res = await post('/api/activity/dollMachine/participate', params);
+        return res;
+    },
+    prizeResult: async (params: { current: number; size: number }, loading = false) => {
+        const res = await post('/api/activity/dollMachine/pageAllParticipate', params, { loading });
+        return res.data;
+    },
 };