Sfoglia il codice sorgente

(1) 19:00-19:30 研究react
(2) 19:30-20:30 宝箱详情制作处理详情进不去,显示异常问题
(3) 20:30- 21:30 支付弹窗不弹,显示不正常支付失败,没有支付结果等问题
(4) 21:30-22:00 房间扭蛋祈福进不去 房间页面的问题处理

zxz 3 mesi fa
parent
commit
b2d7a9e5d7

+ 22 - 14
app/(tabs)/box.tsx

@@ -2,16 +2,16 @@ import { Image } from 'expo-image';
 import { useRouter } from 'expo-router';
 import React, { useCallback, useEffect, useState } from 'react';
 import {
-    ActivityIndicator,
-    FlatList,
-    ImageBackground,
-    RefreshControl,
-    StatusBar,
-    StyleSheet,
-    Text,
-    TextInput,
-    TouchableOpacity,
-    View,
+  ActivityIndicator,
+  FlatList,
+  ImageBackground,
+  RefreshControl,
+  StatusBar,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
@@ -119,15 +119,23 @@ export default function BoxScreen() {
   };
 
   const handleItemPress = (item: PoolItem) => {
-    // 根据类型跳转到不同页面
+    // 检查商品状态
+    if (item.status !== undefined && item.status !== 1) return;
+    
+    console.log('点击商品:', item.id, 'mode:', item.mode, 'type:', item.type);
+    
+    // 根据类型跳转到不同页面 - 按照小程序逻辑
     if (item.type === 7) {
       // 擂台赏跳转到 boxInBox 页面
-      router.push(`/boxInBox?poolId=${item.id}` as any);
+      router.push({ pathname: '/boxInBox', params: { poolId: item.id } } as any);
     } else if (item.mode === 'UNLIMITED') {
-      router.push(`/award-detail?poolId=${item.id}` as any);
+      // 高爆赏/高保赏
+      router.push({ pathname: '/award-detail', params: { poolId: item.id } } as any);
     } else if (item.mode === 'YFS_PRO') {
-      router.push(`/award-detail-yfs?poolId=${item.id}` as any);
+      // 一番赏
+      router.push({ pathname: '/award-detail-yfs', params: { poolId: item.id } } as any);
     } else {
+      // 其他商品
       router.push(`/product/${item.id}` as any);
     }
   };

+ 6 - 4
app/_layout.tsx

@@ -20,10 +20,12 @@ export default function RootLayout() {
           <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
           <Stack.Screen name="login" options={{ headerShown: false }} />
           <Stack.Screen name="product/[id]" options={{ headerShown: false }} />
-          <Stack.Screen name="address/index" options={{ headerShown: false }} />
-          <Stack.Screen name="address/edit" options={{ headerShown: false }} />
-          <Stack.Screen name="orders/index" options={{ headerShown: false }} />
-          <Stack.Screen name="orders/[tradeNo]" options={{ headerShown: false }} />
+          <Stack.Screen name="address" options={{ headerShown: false }} />
+          <Stack.Screen name="orders" options={{ headerShown: false }} />
+          <Stack.Screen name="award-detail" options={{ headerShown: false }} />
+          <Stack.Screen name="award-detail-yfs" options={{ headerShown: false }} />
+          <Stack.Screen name="boxInBox" options={{ headerShown: false }} />
+          <Stack.Screen name="weal" options={{ headerShown: false }} />
           <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
         </Stack>
         <StatusBar style="auto" />

+ 10 - 0
app/address/_layout.tsx

@@ -0,0 +1,10 @@
+import { Stack } from 'expo-router';
+
+export default function AddressLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="index" />
+      <Stack.Screen name="edit" />
+    </Stack>
+  );
+}

+ 9 - 0
app/award-detail-yfs/_layout.tsx

@@ -0,0 +1,9 @@
+import { Stack } from 'expo-router';
+
+export default function AwardDetailYfsLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="index" />
+    </Stack>
+  );
+}

+ 565 - 0
app/award-detail-yfs/index.tsx

@@ -0,0 +1,565 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+    ActivityIndicator,
+    Animated,
+    Dimensions,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { useAuth } from '@/contexts/AuthContext';
+import {
+    getBoxDetail,
+    getNextBox,
+    getPoolDetail,
+    getPreBox,
+    lockBox,
+    poolIn,
+    poolOut,
+    previewOrder,
+    unlockBox,
+} from '@/services/award';
+
+import { CheckoutModal } from '../award-detail/components/CheckoutModal';
+import { ProductList } from '../award-detail/components/ProductList';
+import { RecordModal } from '../award-detail/components/RecordModal';
+import { RuleModal } from '../award-detail/components/RuleModal';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface PoolData {
+  id: string;
+  name: string;
+  cover: string;
+  price: number;
+  specialPrice?: number;
+  specialPriceFive?: number;
+  restrictionQuantity?: number;
+  luckGoodsList: ProductItem[];
+  luckGoodsLevelProbabilityList?: any[];
+}
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  probability: number;
+  price?: number;
+  quantity?: number;
+}
+
+interface BoxData {
+  number: string;
+  leftQuantity: number;
+  leftQuantityA: number;
+  leftQuantityB: number;
+  leftQuantityC: number;
+  leftQuantityD: number;
+  lastNumber: number;
+  lock?: { locker: string; leftTime: number };
+  usedStat?: Record<string, { spuId: string; quantity: number }>;
+  prizeList?: any[];
+}
+
+export default function AwardDetailYfsScreen() {
+  const { poolId } = useLocalSearchParams<{ poolId: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { user } = useAuth();
+
+  const [loading, setLoading] = useState(true);
+  const [data, setData] = useState<PoolData | null>(null);
+  const [products, setProducts] = useState<ProductItem[]>([]);
+  const [box, setBox] = useState<BoxData | null>(null);
+  const [boxNum, setBoxNum] = useState<string>('');
+  const [currentIndex, setCurrentIndex] = useState(0);
+  const [leftTime, setLeftTime] = useState(0);
+  const [probability, setProbability] = useState<any[]>([]);
+  const [scrollTop, setScrollTop] = useState(0);
+
+  const checkoutRef = useRef<any>(null);
+  const recordRef = useRef<any>(null);
+  const ruleRef = useRef<any>(null);
+  const floatAnim = useRef(new Animated.Value(0)).current;
+  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+  useEffect(() => {
+    Animated.loop(
+      Animated.sequence([
+        Animated.timing(floatAnim, { toValue: 10, duration: 1500, useNativeDriver: true }),
+        Animated.timing(floatAnim, { toValue: -10, duration: 1500, useNativeDriver: true }),
+      ])
+    ).start();
+  }, []);
+
+  const loadData = useCallback(async () => {
+    if (!poolId) return;
+    setLoading(true);
+    try {
+      const detail = await getPoolDetail(poolId);
+      if (detail) {
+        setData(detail);
+        setProducts(detail.luckGoodsList || []);
+      }
+    } catch (error) {
+      console.error('加载数据失败:', error);
+    }
+    setLoading(false);
+  }, [poolId]);
+
+  const loadBox = useCallback(
+    async (num?: string) => {
+      if (!poolId) return;
+      try {
+        const res = await getBoxDetail(poolId, num);
+        if (res) handleBoxResult(res);
+      } catch (error) {
+        console.error('加载盒子失败:', error);
+      }
+    },
+    [poolId]
+  );
+
+  const handleBoxResult = (res: any) => {
+    const map: Record<string, any> = {};
+    if (res.usedStat) {
+      res.usedStat.forEach((item: any) => {
+        map[item.spuId] = item;
+      });
+    }
+    res.usedStat = map;
+    setBox(res);
+    setBoxNum(res.number);
+    lockTimeStart(res);
+
+    if (res.leftQuantity <= 0) {
+      setProbability([
+        { level: 'A', probability: 0 },
+        { level: 'B', probability: 0 },
+        { level: 'C', probability: 0 },
+        { level: 'D', probability: 0 },
+      ]);
+    } else {
+      setProbability([
+        { level: 'A', probability: res.leftQuantityA / res.leftQuantity },
+        { level: 'B', probability: res.leftQuantityB / res.leftQuantity },
+        { level: 'C', probability: res.leftQuantityC / res.leftQuantity },
+        { level: 'D', probability: res.leftQuantityD / res.leftQuantity },
+      ]);
+    }
+  };
+
+  const lockTimeStart = (boxData: BoxData) => {
+    lockTimeEnd();
+    if (boxData?.lock) {
+      setLeftTime(boxData.lock.leftTime);
+      timerRef.current = setInterval(() => {
+        setLeftTime((prev) => {
+          if (prev <= 1) {
+            lockTimeEnd();
+            loadBox();
+            return 0;
+          }
+          return prev - 1;
+        });
+      }, 1000);
+    }
+  };
+
+  const lockTimeEnd = () => {
+    if (timerRef.current) {
+      clearInterval(timerRef.current);
+      timerRef.current = null;
+    }
+    setLeftTime(0);
+  };
+
+  useEffect(() => {
+    loadData();
+    loadBox();
+    if (poolId) poolIn(poolId);
+    return () => {
+      if (poolId) poolOut(poolId);
+      lockTimeEnd();
+    };
+  }, [poolId]);
+
+  const handlePay = async (num: number) => {
+    if (!poolId || !data || !box) return;
+    try {
+      const preview = await previewOrder(poolId, num, box.number);
+      if (preview) checkoutRef.current?.show(num, preview, box.number);
+    } catch (error) {
+      console.error('预览订单失败:', error);
+    }
+  };
+
+  const handleSuccess = () => {
+    setTimeout(() => {
+      loadData();
+      loadBox(boxNum);
+    }, 500);
+  };
+
+  const handlePreBox = async () => {
+    if (!poolId || !boxNum || parseInt(boxNum) <= 1) return;
+    try {
+      const res = await getPreBox(poolId, boxNum);
+      if (res) handleBoxResult(res);
+    } catch (error) {
+      console.error('获取上一个盒子失败:', error);
+    }
+  };
+
+  const handleNextBox = async () => {
+    if (!poolId || !box || parseInt(boxNum) >= box.lastNumber) return;
+    try {
+      const res = await getNextBox(poolId, boxNum);
+      if (res) handleBoxResult(res);
+    } catch (error) {
+      console.error('获取下一个盒子失败:', error);
+    }
+  };
+
+  const handleLock = async () => {
+    if (!poolId || !boxNum) return;
+    try {
+      await lockBox(poolId, boxNum);
+      loadBox(boxNum);
+    } catch (error) {
+      console.error('锁定失败:', error);
+    }
+  };
+
+  const handleUnlock = async () => {
+    if (!poolId || !boxNum) return;
+    try {
+      await unlockBox(poolId, boxNum);
+      loadBox(boxNum);
+    } catch (error) {
+      console.error('解锁失败:', error);
+    }
+  };
+
+  const handlePrev = () => {
+    if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
+  };
+  const handleNext = () => {
+    if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
+  };
+
+  const getLevelName = (level: string) => {
+    const map: Record<string, string> = { A: '超神款', B: '欧皇款', C: '隐藏款', D: '普通款' };
+    return map[level] || level;
+  };
+
+  const getLeftNum = (item: ProductItem) => {
+    if (!box?.usedStat || !box.usedStat[item.id]?.quantity) {
+      return item.quantity || 1;
+    }
+    return (item.quantity || 1) - box.usedStat[item.id].quantity;
+  };
+
+  const getProbability = (item: ProductItem) => {
+    if (!box || box.leftQuantity <= 0) return '0';
+    const leftNum = getLeftNum(item);
+    return ((leftNum / box.leftQuantity) * 100).toFixed(2);
+  };
+
+  const leftNum = box?.lock ? (leftTime / box.lock.leftTime) * 100 : 0;
+  const headerBg = scrollTop > 0 ? '#333' : 'transparent';
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!data) {
+    return (
+      <View style={styles.loadingContainer}>
+        <Text style={styles.errorText}>奖池不存在</Text>
+        <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
+          <Text style={styles.backBtn2Text}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+  }
+
+  const currentProduct = products[currentIndex];
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backText}>{'<'}</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle} numberOfLines={1}>
+            {data.name}
+          </Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView
+          style={styles.scrollView}
+          showsVerticalScrollIndicator={false}
+          onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
+          scrollEventThrottle={16}
+        >
+          {/* 主商品展示区域 */}
+          <ImageBackground source={{ uri: Images.box.detail.mainGoodsSection }} style={styles.mainGoodsSection} resizeMode="cover">
+            <View style={{ height: 72 + insets.top }} />
+
+            {/* 商品轮播区域 */}
+            <View style={styles.mainSwiper}>
+              {currentProduct && (
+                <>
+                  <Animated.View style={[styles.productImageBox, { transform: [{ translateY: floatAnim }] }]}>
+                    <Image source={{ uri: currentProduct.cover }} style={styles.productImage} contentFit="contain" />
+                  </Animated.View>
+
+                  {/* 等级信息 */}
+                  <ImageBackground source={{ uri: Images.box.detail.detailsBut }} style={styles.detailsBut} resizeMode="contain">
+                    <View style={styles.detailsText}>
+                      <Text style={styles.levelText}>{getLevelName(currentProduct.level)}</Text>
+                      <Text style={styles.probabilityText}>({getProbability(currentProduct)}%)</Text>
+                    </View>
+                  </ImageBackground>
+
+                  {/* 商品名称 */}
+                  <ImageBackground source={{ uri: Images.box.detail.nameBg }} style={styles.goodsNameBg} resizeMode="contain">
+                    <Text style={styles.goodsNameText} numberOfLines={6}>
+                      {currentProduct.name}
+                    </Text>
+                  </ImageBackground>
+                </>
+              )}
+
+              {/* 左右切换按钮 */}
+              {currentIndex > 0 && (
+                <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
+                  <Image source={{ uri: Images.box.detail.left }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+              {currentIndex < products.length - 1 && (
+                <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
+                  <Image source={{ uri: Images.box.detail.right }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+            </View>
+
+            {/* 左侧装饰 */}
+            <Image source={{ uri: Images.box.detail.positionBgleftBg }} style={styles.positionBgleftBg} contentFit="contain" />
+            {/* 右侧装饰 */}
+            <Image source={{ uri: Images.box.detail.positionBgRightBg }} style={styles.positionBgRightBg} contentFit="contain" />
+          </ImageBackground>
+
+          {/* 底部装饰文字 */}
+          <Image source={{ uri: Images.box.detail.mainGoodsSectionBtext }} style={styles.mainGoodsSectionBtext} contentFit="cover" />
+
+          {/* 侧边按钮 - 规则 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButText}>规则</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 侧边按钮 - 记录 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButText}>记录</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 侧边按钮 - 锁定/解锁 */}
+          {box && !box.lock && (
+            <TouchableOpacity style={[styles.positionBut, styles.positionLock]} onPress={handleLock}>
+              <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+                <Text style={styles.positionButText}>锁定</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          )}
+          {box?.lock && user && box.lock.locker === (user.userId || user.id) && (
+            <TouchableOpacity style={[styles.positionBut, styles.positionLock]} onPress={handleUnlock}>
+              <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+                <Text style={styles.positionButText}>解锁</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          )}
+
+          {/* 侧边按钮 - 仓库 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store' as any)}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButTextR}>仓库</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 侧边按钮 - 刷新 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRefresh]} onPress={() => loadBox(boxNum)}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButTextR}>刷新</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 锁定倒计时 */}
+          {box?.lock && (
+            <View style={styles.lockTimeBox}>
+              <Text style={styles.lockTimeLabel}>剩余时间:</Text>
+              <View style={styles.lockTimeBarBox}>
+                <View style={styles.lockTimeBar}>
+                  <View style={[styles.processBar, { width: `${leftNum}%` }]} />
+                </View>
+                <Text style={[styles.lockTimeText, { left: `${leftNum}%` }]}>{leftTime}</Text>
+              </View>
+            </View>
+          )}
+
+          {/* 标题 */}
+          <View style={styles.productTitleBox}>
+            <Image source={{ uri: Images.box.detail.productTitle }} style={styles.productTitleImg} contentFit="contain" />
+          </View>
+
+          {/* 盒子选择器 */}
+          <ImageBackground source={{ uri: Images.box.detail.firstBoxBg }} style={styles.boxSelector}>
+            <TouchableOpacity style={styles.boxArrowBtn} onPress={handlePreBox} disabled={!boxNum || parseInt(boxNum) <= 1}>
+              <Text style={[styles.boxArrowText, (!boxNum || parseInt(boxNum) <= 1) && styles.disabled]}>上一盒</Text>
+            </TouchableOpacity>
+            <View style={styles.boxInfo}>
+              <Text style={styles.boxNumber}>换盒({boxNum || '-'}/{box?.lastNumber || '-'})</Text>
+            </View>
+            <TouchableOpacity style={styles.boxArrowBtn} onPress={handleNextBox} disabled={!box || parseInt(boxNum) >= box.lastNumber}>
+              <Text style={[styles.boxArrowText, (!box || parseInt(boxNum) >= box.lastNumber) && styles.disabled]}>下一盒</Text>
+            </TouchableOpacity>
+          </ImageBackground>
+
+          {/* 商品列表 */}
+          <ProductList products={products} levelList={probability} poolId={poolId!} price={data.price} />
+
+          <View style={{ height: 150 }} />
+        </ScrollView>
+
+        {/* 底部购买栏 */}
+        <ImageBackground source={{ uri: Images.box.detail.boxDetailBott }} style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]} resizeMode="cover">
+          <View style={styles.bottomBtns}>
+            <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(1)} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgV }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买一盒</Text>
+                <Text style={styles.btnPrice}>(¥{data.specialPrice || data.price})</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(5)} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgL }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买五盒</Text>
+                <Text style={styles.btnPrice}>(¥{data.specialPriceFive || data.price * 5})</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.btnItem} onPress={() => checkoutRef.current?.showFreedom()} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgH }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买多盒</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          </View>
+        </ImageBackground>
+      </ImageBackground>
+
+      <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} boxNumber={boxNum} onSuccess={handleSuccess} />
+      <RecordModal ref={recordRef} poolId={poolId!} />
+      <RuleModal ref={ruleRef} />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
+  errorText: { color: '#999', fontSize: 16 },
+  backBtn2: { marginTop: 20, backgroundColor: '#ff6600', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
+  backBtn2Text: { color: '#fff', fontSize: 14 },
+
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    paddingBottom: 10,
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  headerTitle: { color: '#fff', fontSize: 15, fontWeight: 'bold', flex: 1, textAlign: 'center', width: 250 },
+  placeholder: { width: 40 },
+
+  scrollView: { flex: 1 },
+
+  mainGoodsSection: { width: SCREEN_WIDTH, height: 504, position: 'relative' },
+  mainSwiper: { position: 'relative', width: '100%', height: 375, alignItems: 'center', justifyContent: 'center', marginTop: -50 },
+  productImageBox: { width: 200, height: 300, justifyContent: 'center', alignItems: 'center' },
+  productImage: { width: 200, height: 300 },
+  detailsBut: { width: 120, height: 45, justifyContent: 'center', alignItems: 'center', marginTop: -30 },
+  detailsText: { flexDirection: 'row', alignItems: 'center' },
+  levelText: { fontSize: 14, color: '#FBC400', fontWeight: 'bold' },
+  probabilityText: { fontSize: 10, color: '#FBC400', marginLeft: 3 },
+  goodsNameBg: { position: 'absolute', left: 47, top: 53, width: 43, height: 100, paddingTop: 8, justifyContent: 'flex-start', alignItems: 'center' },
+  goodsNameText: { fontSize: 12, fontWeight: 'bold', color: '#000', width: 20, textAlign: 'center' },
+  prevBtn: { position: 'absolute', left: 35, top: '40%' },
+  nextBtn: { position: 'absolute', right: 35, top: '40%' },
+  arrowImg: { width: 33, height: 38 },
+
+  positionBgleftBg: { position: 'absolute', left: 0, top: 225, width: 32, height: 188 },
+  positionBgRightBg: { position: 'absolute', right: 0, top: 225, width: 32, height: 188 },
+  mainGoodsSectionBtext: { width: SCREEN_WIDTH, height: 74, marginTop: -10 },
+
+  positionBut: { position: 'absolute', zIndex: 10, width: 35, height: 34 },
+  positionButBg: { width: 35, height: 34, justifyContent: 'center', alignItems: 'center' },
+  positionButText: { fontSize: 12, fontWeight: 'bold', color: '#fff', transform: [{ rotate: '14deg' }], textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+  positionButTextR: { fontSize: 12, fontWeight: 'bold', color: '#fff', transform: [{ rotate: '-16deg' }], textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+  positionRule: { top: 256, left: 0 },
+  positionRecord: { top: 300, left: 0 },
+  positionLock: { top: 345, left: 0 },
+  positionStore: { top: 256, right: 0 },
+  positionRefresh: { top: 300, right: 0 },
+
+  lockTimeBox: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#71ccff', padding: 10, marginHorizontal: 10, borderRadius: 8, marginTop: -60 },
+  lockTimeLabel: { color: '#000', fontSize: 12 },
+  lockTimeBarBox: { flex: 1, height: 30, position: 'relative', justifyContent: 'center' },
+  lockTimeBar: { height: 8, backgroundColor: 'rgba(255,255,255,0.6)', borderRadius: 4, overflow: 'hidden' },
+  processBar: { height: '100%', backgroundColor: '#209ae5', borderRadius: 4 },
+  lockTimeText: { position: 'absolute', top: -5, fontSize: 10, backgroundColor: '#000', color: '#fff', paddingHorizontal: 4, borderRadius: 2, marginLeft: -13 },
+
+  productTitleBox: { alignItems: 'center', marginTop: -20, marginBottom: 10 },
+  productTitleImg: { width: 121, height: 29 },
+
+  boxSelector: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginHorizontal: 10, height: 50, paddingHorizontal: 15, marginBottom: 10 },
+  boxArrowBtn: { paddingHorizontal: 10, paddingVertical: 5 },
+  boxArrowText: { color: '#fff', fontSize: 12 },
+  disabled: { opacity: 0.3 },
+  boxInfo: { alignItems: 'center' },
+  boxNumber: { color: '#fff', fontSize: 14, fontWeight: 'bold' },
+
+  bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 69, paddingHorizontal: 5 },
+  bottomBtns: { flexDirection: 'row', height: 64, alignItems: 'center', justifyContent: 'space-around' },
+  btnItem: { flex: 1, marginHorizontal: 6 },
+  btnBg: { width: '100%', height: 54, justifyContent: 'center', alignItems: 'center' },
+  btnText: { fontSize: 14, fontWeight: 'bold', color: '#fff' },
+  btnPrice: { fontSize: 9, color: '#fff' },
+});

+ 9 - 0
app/award-detail/_layout.tsx

@@ -0,0 +1,9 @@
+import { Stack } from 'expo-router';
+
+export default function AwardDetailLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="index" />
+    </Stack>
+  );
+}

+ 631 - 0
app/award-detail/components/CheckoutModal.tsx

@@ -0,0 +1,631 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    Dimensions,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+
+import { applyOrder, getApplyResult, previewOrder } from '@/services/award';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+// 等级配置
+const LEVEL_MAP: Record<string, { title: string; color: string }> = {
+  A: { title: '超神款', color: '#FF4444' },
+  B: { title: '欧皇款', color: '#FF9600' },
+  C: { title: '隐藏款', color: '#9B59B6' },
+  D: { title: '普通款', color: '#666666' },
+};
+
+interface CheckoutModalProps {
+  data: any;
+  poolId: string;
+  onSuccess: (param: { num: number; tradeNo: string }) => void;
+  boxNumber?: string;
+}
+
+export interface CheckoutModalRef {
+  show: (num: number, preview: any, boxNum?: string, seatNumbers?: number[], packFlag?: boolean) => void;
+  showFreedom: () => void;
+  close: () => void;
+}
+
+interface LotteryItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  spu?: { marketPrice: number };
+}
+
+// 自由购买数量选项
+const FREEDOM_NUMS = [10, 20, 30, 40, 50];
+
+export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
+  ({ data, poolId, onSuccess, boxNumber }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [num, setNum] = useState(1);
+    const [checked, setChecked] = useState(true);
+    const [loading, setLoading] = useState(false);
+    const [freedomNum, setFreedomNum] = useState(10);
+    const [freedomSelectVisible, setFreedomSelectVisible] = useState(false);
+
+    // 预览数据
+    const [coin, setCoin] = useState<number | null>(null);
+    const [couponAmount, setCouponAmount] = useState<number | null>(null);
+    const [lastPrice, setLastPrice] = useState<number | null>(null);
+    const [magic, setMagic] = useState<any>(null);
+    const [cash, setCash] = useState<any>(null);
+    const [cashChecked, setCashChecked] = useState(false);
+
+    // 盒子相关
+    const [boxNum, setBoxNum] = useState<string | undefined>(boxNumber);
+    const [seatNumbers, setSeatNumbers] = useState<number[] | undefined>();
+    const [packFlag, setPackFlag] = useState<boolean | undefined>();
+
+    // 抽奖结果
+    const [resultVisible, setResultVisible] = useState(false);
+    const [resultLoading, setResultLoading] = useState(false);
+    const [resultList, setResultList] = useState<LotteryItem[]>([]);
+
+    // 设置预览数据
+    const setPreviewData = (previewData: any) => {
+      setCoin(previewData.magicAmount || null);
+      setCouponAmount(previewData.couponAmount || null);
+      setLastPrice(previewData.paymentAmount);
+      setMagic(previewData.magic || null);
+
+      if (previewData.cash && previewData.cash.balance > previewData.paymentAmount) {
+        setCash(previewData.cash);
+        setCashChecked(true);
+      } else {
+        setCash(previewData.cash || null);
+        setCashChecked(false);
+      }
+    };
+
+    useImperativeHandle(ref, () => ({
+      show: (n: number, previewData: any = {}, bNum?: string, seats?: number[], pack?: boolean) => {
+        setNum(n);
+        setBoxNum(bNum);
+        setSeatNumbers(seats);
+        setPackFlag(pack || undefined);
+        setPreviewData(previewData);
+        setVisible(true);
+      },
+      showFreedom: () => {
+        setFreedomNum(10);
+        setFreedomSelectVisible(true);
+      },
+      close: () => {
+        setVisible(false);
+        setFreedomSelectVisible(false);
+        setResultVisible(false);
+      },
+    }));
+
+    const handleFreedomSelect = async (selectedNum: number) => {
+      setFreedomSelectVisible(false);
+      setLoading(true);
+      try {
+        const preview = await previewOrder(poolId, selectedNum);
+        if (preview) {
+          setNum(selectedNum);
+          setFreedomNum(selectedNum);
+          setPreviewData(preview);
+          setVisible(true);
+        }
+      } catch (error: any) {
+        Alert.alert('提示', error?.message || '获取订单信息失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    const close = () => {
+      setVisible(false);
+      setFreedomSelectVisible(false);
+    };
+
+    const closeResult = () => {
+      setResultVisible(false);
+      setResultList([]);
+      onSuccess({ tradeNo: '', num });
+    };
+
+    // 支付
+    const pay = async () => {
+      if (loading) return;
+      if (!checked) {
+        Alert.alert('提示', '请同意《宝箱服务协议》');
+        return;
+      }
+
+      setLoading(true);
+      try {
+        const paymentType = 'WALLET';
+        const payNum = packFlag ? 1 : num;
+
+        const res = await applyOrder(poolId, payNum, paymentType, boxNum, seatNumbers, packFlag);
+
+        if (res?.paySuccess || res?.bizTradeNo || res?.tradeNo) {
+          const tradeNo = res.bizTradeNo || res.tradeNo;
+          setVisible(false);
+          fetchLotteryResult(tradeNo);
+        } else {
+          Alert.alert('提示', res?.message || '支付失败,请重试');
+        }
+      } catch (error: any) {
+        Alert.alert('支付失败', error?.message || '请稍后重试');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    // 获取抽奖结果
+    const fetchLotteryResult = async (tradeNo: string) => {
+      setResultLoading(true);
+      setResultVisible(true);
+      setResultList([]);
+
+      let attempts = 0;
+      const maxAttempts = 5;
+
+      const poll = async () => {
+        try {
+          const res = await getApplyResult(tradeNo);
+          if (res?.inventoryList && res.inventoryList.length > 0) {
+            setResultList(res.inventoryList);
+            setResultLoading(false);
+          } else if (attempts < maxAttempts) {
+            attempts++;
+            setTimeout(poll, 1000);
+          } else {
+            setResultLoading(false);
+            Alert.alert('提示', '获取结果超时,请在仓库中查看');
+          }
+        } catch {
+          if (attempts < maxAttempts) {
+            attempts++;
+            setTimeout(poll, 1000);
+          } else {
+            setResultLoading(false);
+            Alert.alert('提示', '获取结果失败,请在仓库中查看');
+          }
+        }
+      };
+      poll();
+    };
+
+    const displayPrice = lastPrice ?? (data?.price || 0) * num;
+
+    return (
+      <>
+        {/* 自由购买数量选择弹窗 */}
+        <Modal visible={freedomSelectVisible} transparent animationType="fade" onRequestClose={() => setFreedomSelectVisible(false)}>
+          <View style={styles.overlay}>
+            <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setFreedomSelectVisible(false)} />
+            <View style={styles.freedomContainer}>
+              <View style={styles.header}>
+                <Text style={styles.title}>购买多盒</Text>
+                <TouchableOpacity onPress={() => setFreedomSelectVisible(false)} style={styles.closeBtn}>
+                  <Text style={styles.closeText}>×</Text>
+                </TouchableOpacity>
+              </View>
+              <View style={styles.freedomContent}>
+                <View style={styles.freedomBtnList}>
+                  {FREEDOM_NUMS.map((item) => (
+                    <TouchableOpacity
+                      key={item}
+                      style={[styles.freedomBtn, freedomNum === item && styles.freedomBtnActive]}
+                      onPress={() => setFreedomNum(item)}
+                    >
+                      <Text style={[styles.freedomBtnText, freedomNum === item && styles.freedomBtnTextActive]}>
+                        {item}<Text style={styles.freedomUnit}>盒</Text>
+                      </Text>
+                      {freedomNum === item && <Text style={styles.checkIcon}>✓</Text>}
+                    </TouchableOpacity>
+                  ))}
+                </View>
+                <TouchableOpacity
+                  style={[styles.freedomSubmitBtn, loading && styles.payBtnDisabled]}
+                  onPress={() => handleFreedomSelect(freedomNum)}
+                  disabled={loading}
+                >
+                  {loading ? (
+                    <ActivityIndicator color="#fff" size="small" />
+                  ) : (
+                    <Text style={styles.freedomSubmitText}>确认 ¥{(data?.price || 0) * freedomNum}</Text>
+                  )}
+                </TouchableOpacity>
+              </View>
+            </View>
+          </View>
+        </Modal>
+
+        {/* 支付确认弹窗 */}
+        <Modal visible={visible} transparent animationType="slide" onRequestClose={close}>
+          <View style={styles.overlay}>
+            <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={close} />
+            <View style={styles.container}>
+              <View style={styles.header}>
+                <Text style={styles.title}>{data?.name}</Text>
+                <TouchableOpacity onPress={close} style={styles.closeBtn}>
+                  <Text style={styles.closeText}>×</Text>
+                </TouchableOpacity>
+              </View>
+
+              <View style={styles.content}>
+                <View style={styles.row}>
+                  <Text style={styles.label}>购买件数</Text>
+                  <Text style={styles.priceText}>¥{data?.price} x {num}</Text>
+                </View>
+
+                <View style={styles.row}>
+                  <Text style={styles.label}>优惠券</Text>
+                  <Text style={[styles.valueText, couponAmount ? styles.themeColor : {}]}>
+                    {couponAmount ? `已使用优惠¥${couponAmount}` : '暂无优惠券可选'}
+                  </Text>
+                </View>
+
+                {magic && magic.balance > 0 && (
+                  <View style={styles.row}>
+                    <View style={styles.rowLeft}>
+                      <Text style={styles.label}>果实</Text>
+                      <Text style={styles.balanceText}>(剩余:{magic.balance})</Text>
+                    </View>
+                    <Text style={styles.balanceText}>
+                      已抵扣 <Text style={styles.themeColor}>¥{coin || 0}</Text>
+                    </Text>
+                  </View>
+                )}
+
+                {cash && (
+                  <View style={styles.row}>
+                    <View style={styles.rowLeft}>
+                      <Text style={styles.label}>钱包支付</Text>
+                      <Text style={styles.themeColor}>(余额:¥{cash.balance})</Text>
+                    </View>
+                    <TouchableOpacity
+                      style={[styles.radio, cashChecked && styles.radioChecked]}
+                      onPress={() => setCashChecked(!cashChecked)}
+                    >
+                      {cashChecked && <View style={styles.radioInner} />}
+                    </TouchableOpacity>
+                  </View>
+                )}
+
+                <View style={styles.agreementRow}>
+                  <View style={styles.agreementLeft}>
+                    <Text style={styles.agreementText}>
+                      我已满18周岁,已阅读并同意<Text style={styles.link}>《宝箱服务协议》</Text>
+                    </Text>
+                    <Text style={styles.tips}>宝箱商品存在概率性,请谨慎消费</Text>
+                  </View>
+                  <TouchableOpacity
+                    style={[styles.radio, checked && styles.radioChecked]}
+                    onPress={() => setChecked(!checked)}
+                  >
+                    {checked && <View style={styles.radioInner} />}
+                  </TouchableOpacity>
+                </View>
+              </View>
+
+              <View style={styles.footer}>
+                <View style={styles.priceInfo}>
+                  <Text style={styles.totalLabel}>实付:</Text>
+                  <Text style={styles.totalPrice}>¥{displayPrice.toFixed(2)}</Text>
+                </View>
+                <TouchableOpacity
+                  style={[styles.payBtn, loading && styles.payBtnDisabled]}
+                  onPress={pay}
+                  disabled={loading}
+                >
+                  {loading ? <ActivityIndicator color="#fff" size="small" /> : <Text style={styles.payBtnText}>立即支付</Text>}
+                </TouchableOpacity>
+              </View>
+            </View>
+          </View>
+        </Modal>
+
+        {/* 抽奖结果弹窗 */}
+        <Modal visible={resultVisible} transparent animationType="fade" onRequestClose={closeResult}>
+          <View style={styles.resultOverlay}>
+            <View style={styles.resultContainer}>
+              <View style={styles.resultHeader}>
+                <Text style={styles.resultTitle}>🎉 恭喜您获得 🎉</Text>
+                <TouchableOpacity onPress={closeResult} style={styles.resultCloseBtn}>
+                  <Text style={styles.closeText}>×</Text>
+                </TouchableOpacity>
+              </View>
+
+              {resultLoading ? (
+                <View style={styles.resultLoading}>
+                  <ActivityIndicator size="large" color="#ff9600" />
+                  <Text style={styles.resultLoadingText}>正在开启宝箱...</Text>
+                </View>
+              ) : (
+                <ScrollView style={styles.resultScroll} showsVerticalScrollIndicator={false}>
+                  <View style={styles.resultList}>
+                    {resultList.map((item, index) => (
+                      <View key={item.id || index} style={styles.resultItem}>
+                        <View style={[styles.levelBadge, { backgroundColor: LEVEL_MAP[item.level]?.color || '#666' }]}>
+                          <Text style={styles.levelText}>{LEVEL_MAP[item.level]?.title || item.level}</Text>
+                        </View>
+                        <View style={styles.resultImageBox}>
+                          <Image source={{ uri: item.cover }} style={styles.resultImage} contentFit="contain" />
+                        </View>
+                        <Text style={styles.resultName} numberOfLines={2}>{item.name}</Text>
+                        {item.spu?.marketPrice && (
+                          <Text style={styles.resultPrice}>参考价:¥{item.spu.marketPrice}</Text>
+                        )}
+                      </View>
+                    ))}
+                  </View>
+                </ScrollView>
+              )}
+
+              <View style={styles.resultFooter}>
+                <TouchableOpacity style={styles.resultBtn} onPress={closeResult}>
+                  <Text style={styles.resultBtnText}>继续抽奖</Text>
+                </TouchableOpacity>
+              </View>
+            </View>
+          </View>
+        </Modal>
+      </>
+    );
+  }
+);
+
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  mask: { flex: 1 },
+  container: {
+    backgroundColor: '#fff',
+    borderTopLeftRadius: 20,
+    borderTopRightRadius: 20,
+    paddingBottom: 34,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+    position: 'relative',
+  },
+  title: { fontSize: 16, fontWeight: '600', color: '#333' },
+  closeBtn: { position: 'absolute', right: 15, top: 10 },
+  closeText: { fontSize: 24, color: '#999' },
+  content: { padding: 15 },
+  row: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 10,
+  },
+  rowLeft: { flexDirection: 'row', alignItems: 'center', flex: 1 },
+  label: { fontSize: 14, color: '#333' },
+  priceText: { fontSize: 14, color: '#ff9600', fontWeight: '600' },
+  valueText: { fontSize: 12, color: '#999' },
+  themeColor: { color: '#ff9600' },
+  balanceText: { fontSize: 12, color: '#999', marginLeft: 5 },
+  radio: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 2,
+    borderColor: '#ddd',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  radioChecked: { borderColor: '#ff9600' },
+  radioInner: { width: 10, height: 10, borderRadius: 5, backgroundColor: '#ff9600' },
+  agreementRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'flex-start',
+    paddingVertical: 10,
+    marginTop: 10,
+  },
+  agreementLeft: { flex: 1, marginRight: 10 },
+  agreementText: { fontSize: 12, color: '#333', lineHeight: 18 },
+  link: { color: '#ff9600' },
+  tips: { fontSize: 11, color: '#999', marginTop: 5 },
+  footer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingTop: 15,
+    borderTopWidth: 1,
+    borderTopColor: '#eee',
+  },
+  priceInfo: { flexDirection: 'row', alignItems: 'center' },
+  totalLabel: { fontSize: 14, color: '#333' },
+  totalPrice: { fontSize: 20, color: '#ff9600', fontWeight: 'bold' },
+  payBtn: {
+    backgroundColor: '#ff9600',
+    paddingHorizontal: 30,
+    paddingVertical: 12,
+    borderRadius: 25,
+  },
+  payBtnDisabled: { opacity: 0.6 },
+  payBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
+
+  // 自由购买弹窗
+  freedomContainer: {
+    backgroundColor: '#fff',
+    borderTopLeftRadius: 20,
+    borderTopRightRadius: 20,
+    paddingBottom: 34,
+  },
+  freedomContent: { padding: 20 },
+  freedomBtnList: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
+  freedomBtn: {
+    width: '48%',
+    height: 50,
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginBottom: 12,
+    borderWidth: 1,
+    borderColor: '#eee',
+    position: 'relative',
+  },
+  freedomBtnActive: { backgroundColor: '#F1423D', borderColor: '#F1423D' },
+  freedomBtnText: { fontSize: 18, fontWeight: 'bold', color: '#333' },
+  freedomBtnTextActive: { color: '#fff' },
+  freedomUnit: { fontSize: 12, fontWeight: '500' },
+  checkIcon: {
+    position: 'absolute',
+    bottom: 0,
+    right: 0,
+    backgroundColor: '#fff',
+    color: '#F1423D',
+    fontSize: 12,
+    paddingHorizontal: 6,
+    paddingVertical: 2,
+    borderTopLeftRadius: 8,
+    borderBottomRightRadius: 8,
+  },
+  freedomSubmitBtn: {
+    backgroundColor: '#ff9600',
+    height: 50,
+    borderRadius: 25,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: 20,
+  },
+  freedomSubmitText: { color: '#fff', fontSize: 16, fontWeight: '600' },
+
+  // 抽奖结果弹窗
+  resultOverlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.7)',
+    justifyContent: 'center',
+    alignItems: 'center',
+    padding: 20,
+  },
+  resultContainer: {
+    width: '100%',
+    maxHeight: '80%',
+    backgroundColor: '#fff',
+    borderRadius: 16,
+    overflow: 'hidden',
+  },
+  resultHeader: {
+    alignItems: 'center',
+    padding: 20,
+    backgroundColor: '#ff9600',
+    position: 'relative',
+  },
+  resultTitle: {
+    fontSize: 20,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  resultCloseBtn: {
+    position: 'absolute',
+    right: 15,
+    top: 15,
+    width: 30,
+    height: 30,
+    backgroundColor: 'rgba(255,255,255,0.3)',
+    borderRadius: 15,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  resultLoading: {
+    padding: 60,
+    alignItems: 'center',
+  },
+  resultLoadingText: {
+    marginTop: 15,
+    fontSize: 14,
+    color: '#666',
+  },
+  resultScroll: {
+    maxHeight: 400,
+  },
+  resultList: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    padding: 10,
+    justifyContent: 'space-between',
+  },
+  resultItem: {
+    width: (SCREEN_WIDTH - 80) / 2,
+    backgroundColor: '#f9f9f9',
+    borderRadius: 10,
+    padding: 10,
+    marginBottom: 10,
+    alignItems: 'center',
+  },
+  levelBadge: {
+    paddingHorizontal: 10,
+    paddingVertical: 3,
+    borderRadius: 10,
+    marginBottom: 8,
+  },
+  levelText: {
+    color: '#fff',
+    fontSize: 11,
+    fontWeight: 'bold',
+  },
+  resultImageBox: {
+    width: '100%',
+    aspectRatio: 1,
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    overflow: 'hidden',
+  },
+  resultImage: {
+    width: '100%',
+    height: '100%',
+  },
+  resultName: {
+    fontSize: 12,
+    color: '#333',
+    textAlign: 'center',
+    marginTop: 8,
+    lineHeight: 16,
+  },
+  resultPrice: {
+    fontSize: 10,
+    color: '#999',
+    marginTop: 4,
+  },
+  resultFooter: {
+    padding: 15,
+    borderTopWidth: 1,
+    borderTopColor: '#eee',
+  },
+  resultBtn: {
+    backgroundColor: '#ff9600',
+    height: 46,
+    borderRadius: 23,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  resultBtnText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: '600',
+  },
+});

+ 181 - 0
app/award-detail/components/ProductList.tsx

@@ -0,0 +1,181 @@
+import { Image } from 'expo-image';
+import React from 'react';
+import { ImageBackground, StyleSheet, Text, View } from 'react-native';
+
+import { Images } from '@/constants/images';
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  probability: number;
+  price?: number;
+  quantity?: number;
+}
+
+interface ProductListProps {
+  products: ProductItem[];
+  levelList?: any[];
+  poolId: string;
+  price: number;
+}
+
+const LEVEL_CONFIG: Record<string, { title: string; bg: string }> = {
+  A: { title: '超神款', bg: Images.box.detail.productItemA },
+  B: { title: '欧皇款', bg: Images.box.detail.productItemB },
+  C: { title: '隐藏款', bg: Images.box.detail.productItemC },
+  D: { title: '普通款', bg: Images.box.detail.productItemD },
+};
+
+const ignoreRatio0 = (val: number) => {
+  const str = String(val);
+  const match = str.match(/^(\d+\.?\d*?)0*$/);
+  return match ? match[1].replace(/\.$/, '') : str;
+};
+
+export const ProductList: React.FC<ProductListProps> = ({ products, levelList }) => {
+  // 按等级分组
+  const groupedProducts = products.reduce(
+    (acc, item) => {
+      const level = item.level || 'D';
+      if (!acc[level]) acc[level] = [];
+      acc[level].push(item);
+      return acc;
+    },
+    {} as Record<string, ProductItem[]>
+  );
+
+  // 计算各等级概率
+  const levelProbabilities =
+    levelList?.reduce(
+      (acc, item) => {
+        acc[item.level] = item.probability;
+        return acc;
+      },
+      {} as Record<string, number>
+    ) || {};
+
+  const renderLevelSection = (level: string, items: ProductItem[]) => {
+    const config = LEVEL_CONFIG[level] || { title: level, bg: Images.box.detail.productItemD };
+    const probability = levelProbabilities[level] || items.reduce((sum, i) => sum + (i.probability || 0), 0);
+
+    return (
+      <View key={level} style={styles.levelSection}>
+        {/* 等级标题 */}
+        <ImageBackground source={{ uri: Images.box.detail.levelBoxBg }} style={styles.levelHeader} resizeMode="stretch">
+          <Text style={styles.levelTitle}>{config.title}</Text>
+          <Text style={styles.levelProbability}>概率: {ignoreRatio0(probability * 100)}%</Text>
+        </ImageBackground>
+
+        {/* 商品网格 */}
+        <View style={styles.productGrid}>
+          {items.map((item, index) => (
+            <View key={item.id || index} style={styles.productItem}>
+              <ImageBackground source={{ uri: config.bg }} style={styles.productItemBg} resizeMode="stretch">
+                <View style={styles.productImageBox}>
+                  <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="cover" />
+                </View>
+                <Text style={styles.productName} numberOfLines={2}>
+                  {item.name}
+                </Text>
+                <Text style={styles.productProbability}>概率: {ignoreRatio0((item.probability || 0) * 100)}%</Text>
+              </ImageBackground>
+            </View>
+          ))}
+        </View>
+      </View>
+    );
+  };
+
+  const levelOrder = ['A', 'B', 'C', 'D'];
+
+  return (
+    <View style={styles.container}>
+      {/* 标题 */}
+      <View style={styles.titleBox}>
+        <Image source={{ uri: Images.box.detail.productTitle }} style={styles.titleImg} contentFit="contain" />
+      </View>
+
+      {levelOrder.map((level) => {
+        const items = groupedProducts[level];
+        if (!items || items.length === 0) return null;
+        return renderLevelSection(level, items);
+      })}
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 10,
+    marginTop: -40,
+  },
+  titleBox: {
+    alignItems: 'center',
+    marginBottom: 10,
+  },
+  titleImg: {
+    width: 121,
+    height: 29,
+  },
+  levelSection: {
+    marginBottom: 15,
+  },
+  levelHeader: {
+    height: 40,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    marginBottom: 10,
+  },
+  levelTitle: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  levelProbability: {
+    color: 'rgba(255,255,255,0.8)',
+    fontSize: 12,
+  },
+  productGrid: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    marginHorizontal: -5,
+  },
+  productItem: {
+    width: '33.33%',
+    paddingHorizontal: 5,
+    marginBottom: 10,
+  },
+  productItemBg: {
+    width: '100%',
+    aspectRatio: 0.75,
+    padding: 8,
+    alignItems: 'center',
+  },
+  productImageBox: {
+    width: '100%',
+    aspectRatio: 1,
+    borderRadius: 5,
+    overflow: 'hidden',
+    backgroundColor: 'rgba(255,255,255,0.1)',
+  },
+  productImage: {
+    width: '100%',
+    height: '100%',
+  },
+  productName: {
+    color: '#fff',
+    fontSize: 10,
+    marginTop: 5,
+    textAlign: 'center',
+    height: 26,
+  },
+  productProbability: {
+    color: '#FBC400',
+    fontSize: 9,
+    marginTop: 2,
+  },
+});

+ 214 - 0
app/award-detail/components/RecordModal.tsx

@@ -0,0 +1,214 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    FlatList,
+    Modal,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+
+import { getBuyRecord } from '@/services/award';
+
+interface RecordItem {
+  id: string;
+  nickname: string;
+  avatar: string;
+  goodsName: string;
+  goodsCover: string;
+  level: string;
+  createTime: string;
+}
+
+interface RecordModalProps {
+  poolId: string;
+}
+
+export interface RecordModalRef {
+  show: () => void;
+  close: () => void;
+}
+
+const LEVEL_MAP: Record<string, string> = {
+  A: '超神款',
+  B: '欧皇款',
+  C: '隐藏款',
+  D: '普通款',
+};
+
+export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(
+  ({ poolId }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [loading, setLoading] = useState(false);
+    const [records, setRecords] = useState<RecordItem[]>([]);
+    const [levelFilter, setLevelFilter] = useState<number | undefined>();
+
+    useImperativeHandle(ref, () => ({
+      show: () => {
+        setVisible(true);
+        loadRecords();
+      },
+      close: () => setVisible(false),
+    }));
+
+    const loadRecords = async (level?: number) => {
+      setLoading(true);
+      try {
+        const res = await getBuyRecord(poolId, undefined, level);
+        setRecords(res || []);
+      } catch (error) {
+        console.error('加载记录失败:', error);
+      }
+      setLoading(false);
+    };
+
+    const handleLevelFilter = (level?: number) => {
+      setLevelFilter(level);
+      loadRecords(level);
+    };
+
+    const renderItem = ({ item }: { item: RecordItem }) => (
+      <View style={styles.recordItem}>
+        <Image
+          source={{ uri: item.avatar }}
+          style={styles.avatar}
+          contentFit="cover"
+        />
+        <View style={styles.recordInfo}>
+          <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
+          <Text style={styles.time}>{item.createTime}</Text>
+        </View>
+        <View style={styles.goodsInfo}>
+          <Image
+            source={{ uri: item.goodsCover }}
+            style={styles.goodsImage}
+            contentFit="cover"
+          />
+          <View style={styles.goodsDetail}>
+            <Text style={styles.goodsName} numberOfLines={1}>{item.goodsName}</Text>
+            <Text style={styles.levelText}>{LEVEL_MAP[item.level] || item.level}</Text>
+          </View>
+        </View>
+      </View>
+    );
+
+    const levels = [
+      { label: '全部', value: undefined },
+      { label: '超神款', value: 1 },
+      { label: '欧皇款', value: 2 },
+      { label: '隐藏款', value: 3 },
+      { label: '普通款', value: 4 },
+    ];
+
+    return (
+      <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+        <View style={styles.overlay}>
+          <View style={styles.container}>
+            <View style={styles.header}>
+              <Text style={styles.title}>购买记录</Text>
+              <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+                <Text style={styles.closeText}>×</Text>
+              </TouchableOpacity>
+            </View>
+
+            <View style={styles.filterRow}>
+              {levels.map((item) => (
+                <TouchableOpacity
+                  key={item.label}
+                  style={[styles.filterBtn, levelFilter === item.value && styles.filterBtnActive]}
+                  onPress={() => handleLevelFilter(item.value)}
+                >
+                  <Text style={[styles.filterText, levelFilter === item.value && styles.filterTextActive]}>
+                    {item.label}
+                  </Text>
+                </TouchableOpacity>
+              ))}
+            </View>
+
+            {loading ? (
+              <View style={styles.loadingBox}>
+                <ActivityIndicator size="large" color="#ff6600" />
+              </View>
+            ) : (
+              <FlatList
+                data={records}
+                renderItem={renderItem}
+                keyExtractor={(item, index) => item.id || index.toString()}
+                contentContainerStyle={styles.listContent}
+                ListEmptyComponent={
+                  <View style={styles.emptyBox}>
+                    <Text style={styles.emptyText}>暂无记录</Text>
+                  </View>
+                }
+              />
+            )}
+          </View>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  container: {
+    backgroundColor: '#fff',
+    borderTopLeftRadius: 20,
+    borderTopRightRadius: 20,
+    height: '70%',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  title: { fontSize: 16, fontWeight: '600', color: '#333' },
+  closeBtn: { position: 'absolute', right: 15, top: 10 },
+  closeText: { fontSize: 24, color: '#999' },
+  filterRow: {
+    flexDirection: 'row',
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  filterBtn: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    borderRadius: 15,
+    marginRight: 8,
+    backgroundColor: '#f5f5f5',
+  },
+  filterBtnActive: { backgroundColor: '#ff6600' },
+  filterText: { fontSize: 12, color: '#666' },
+  filterTextActive: { color: '#fff' },
+  listContent: { padding: 15 },
+  recordItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 12,
+    borderBottomWidth: 1,
+    borderBottomColor: '#f5f5f5',
+  },
+  avatar: { width: 40, height: 40, borderRadius: 20 },
+  recordInfo: { flex: 1, marginLeft: 10 },
+  nickname: { fontSize: 14, color: '#333' },
+  time: { fontSize: 11, color: '#999', marginTop: 2 },
+  goodsInfo: { flexDirection: 'row', alignItems: 'center' },
+  goodsImage: { width: 50, height: 50, borderRadius: 5 },
+  goodsDetail: { marginLeft: 8, alignItems: 'flex-end' },
+  goodsName: { fontSize: 12, color: '#333', maxWidth: 80 },
+  levelText: { fontSize: 11, color: '#ff6600', marginTop: 2 },
+  loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
+  emptyBox: { alignItems: 'center', paddingVertical: 50 },
+  emptyText: { color: '#999', fontSize: 14 },
+});

+ 91 - 0
app/award-detail/components/RuleModal.tsx

@@ -0,0 +1,91 @@
+import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
+import {
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+
+import { getParamConfig } from '@/services/user';
+
+export interface RuleModalRef {
+  show: () => void;
+  close: () => void;
+}
+
+export const RuleModal = forwardRef<RuleModalRef>((_, ref) => {
+  const [visible, setVisible] = useState(false);
+  const [rules, setRules] = useState('');
+
+  useImperativeHandle(ref, () => ({
+    show: () => {
+      setVisible(true);
+      loadRules();
+    },
+    close: () => setVisible(false),
+  }));
+
+  const loadRules = async () => {
+    try {
+      const res = await getParamConfig('show_rule');
+      setRules(res?.data || '暂无规则说明');
+    } catch (error) {
+      console.error('加载规则失败:', error);
+    }
+  };
+
+  useEffect(() => {
+    if (visible && !rules) loadRules();
+  }, [visible]);
+
+  return (
+    <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+      <View style={styles.overlay}>
+        <View style={styles.container}>
+          <View style={styles.header}>
+            <Text style={styles.title}>玩法规则</Text>
+            <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+              <Text style={styles.closeText}>×</Text>
+            </TouchableOpacity>
+          </View>
+          <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
+            <Text style={styles.ruleText}>{rules}</Text>
+          </ScrollView>
+        </View>
+      </View>
+    </Modal>
+  );
+});
+
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.6)',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  container: {
+    width: '85%',
+    maxHeight: '70%',
+    backgroundColor: '#fff',
+    borderRadius: 15,
+    overflow: 'hidden',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+    backgroundColor: '#ff6600',
+  },
+  title: { fontSize: 16, fontWeight: '600', color: '#fff' },
+  closeBtn: { position: 'absolute', right: 15, top: 10 },
+  closeText: { fontSize: 24, color: '#fff' },
+  content: { padding: 15 },
+  ruleText: { fontSize: 14, color: '#333', lineHeight: 22 },
+});

+ 5 - 0
app/award-detail/components/index.ts

@@ -0,0 +1,5 @@
+export { CheckoutModal } from './CheckoutModal';
+export { ProductList } from './ProductList';
+export { RecordModal } from './RecordModal';
+export { RuleModal } from './RuleModal';
+

+ 477 - 0
app/award-detail/index.tsx

@@ -0,0 +1,477 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+  ActivityIndicator,
+  Animated,
+  Dimensions,
+  ImageBackground,
+  ScrollView,
+  StatusBar,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import {
+  getPoolDetail,
+  poolIn,
+  poolOut,
+  previewOrder,
+} from '@/services/award';
+
+import { CheckoutModal } from './components/CheckoutModal';
+import { ProductList } from './components/ProductList';
+import { RecordModal } from './components/RecordModal';
+import { RuleModal } from './components/RuleModal';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface PoolData {
+  id: string;
+  name: string;
+  cover: string;
+  price: number;
+  specialPrice?: number;
+  specialPriceFive?: number;
+  demoFlag?: number;
+  luckGoodsList: ProductItem[];
+  luckGoodsLevelProbabilityList?: any[];
+}
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  probability: number;
+  price?: number;
+}
+
+export default function AwardDetailScreen() {
+  const { poolId } = useLocalSearchParams<{ poolId: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+
+  const [loading, setLoading] = useState(true);
+  const [data, setData] = useState<PoolData | null>(null);
+  const [products, setProducts] = useState<ProductItem[]>([]);
+  const [currentIndex, setCurrentIndex] = useState(0);
+  const [scrollTop, setScrollTop] = useState(0);
+
+  const checkoutRef = useRef<any>(null);
+  const recordRef = useRef<any>(null);
+  const ruleRef = useRef<any>(null);
+  const floatAnim = useRef(new Animated.Value(0)).current;
+
+  useEffect(() => {
+    Animated.loop(
+      Animated.sequence([
+        Animated.timing(floatAnim, { toValue: 10, duration: 1500, useNativeDriver: true }),
+        Animated.timing(floatAnim, { toValue: -10, duration: 1500, useNativeDriver: true }),
+      ])
+    ).start();
+  }, []);
+
+  const loadData = useCallback(async () => {
+    if (!poolId) return;
+    setLoading(true);
+    try {
+      const detail = await getPoolDetail(poolId);
+      if (detail) {
+        setData(detail);
+        setProducts(detail.luckGoodsList || []);
+      }
+    } catch (error) {
+      console.error('加载数据失败:', error);
+    }
+    setLoading(false);
+  }, [poolId]);
+
+  useEffect(() => {
+    loadData();
+    if (poolId) poolIn(poolId);
+    return () => {
+      if (poolId) poolOut(poolId);
+    };
+  }, [poolId]);
+
+  const handlePay = async (num: number) => {
+    if (!poolId || !data) return;
+    try {
+      const preview = await previewOrder(poolId, num);
+      if (preview) checkoutRef.current?.show(num, preview);
+    } catch (error) {
+      console.error('预览订单失败:', error);
+    }
+  };
+
+  const handleSuccess = () => {
+    setTimeout(() => loadData(), 500);
+  };
+
+  const handlePrev = () => {
+    if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
+  };
+  const handleNext = () => {
+    if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
+  };
+
+  const getLevelName = (level: string) => {
+    const map: Record<string, string> = { A: '超神款', B: '欧皇款', C: '隐藏款', D: '普通款' };
+    return map[level] || level;
+  };
+
+  const ignoreRatio0 = (val: number) => {
+    const str = String(val);
+    const match = str.match(/^(\d+\.?\d*?)0*$/);
+    return match ? match[1].replace(/\.$/, '') : str;
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  if (!data) {
+    return (
+      <View style={styles.loadingContainer}>
+        <Text style={styles.errorText}>奖池不存在</Text>
+        <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
+          <Text style={styles.backBtn2Text}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+  }
+
+  const currentProduct = products[currentIndex];
+  const headerBg = scrollTop > 0 ? '#333' : 'transparent';
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backText}>{'<'}</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle} numberOfLines={1}>{data.name}</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView
+          style={styles.scrollView}
+          showsVerticalScrollIndicator={false}
+          onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
+          scrollEventThrottle={16}
+        >
+          {/* 主商品展示区域 */}
+          <ImageBackground
+            source={{ uri: Images.box.detail.mainGoodsSection }}
+            style={styles.mainGoodsSection}
+            resizeMode="cover"
+          >
+            <View style={{ height: 72 + insets.top }} />
+
+            {/* 商品轮播区域 */}
+            <View style={styles.mainSwiper}>
+              {currentProduct && (
+                <>
+                  <Animated.View style={[styles.productImageBox, { transform: [{ translateY: floatAnim }] }]}>
+                    <Image source={{ uri: currentProduct.cover }} style={styles.productImage} contentFit="contain" />
+                  </Animated.View>
+
+                  {/* 等级信息 */}
+                  <ImageBackground
+                    source={{ uri: Images.box.detail.detailsBut }}
+                    style={styles.detailsBut}
+                    resizeMode="contain"
+                  >
+                    <View style={styles.detailsText}>
+                      <Text style={styles.levelText}>{getLevelName(currentProduct.level)}</Text>
+                      <Text style={styles.probabilityText}>
+                        ({ignoreRatio0(currentProduct.probability * 100)}%)
+                      </Text>
+                    </View>
+                  </ImageBackground>
+
+                  {/* 商品名称 */}
+                  <ImageBackground source={{ uri: Images.box.detail.nameBg }} style={styles.goodsNameBg} resizeMode="contain">
+                    <Text style={styles.goodsNameText} numberOfLines={6}>{currentProduct.name}</Text>
+                  </ImageBackground>
+                </>
+              )}
+
+              {/* 左右切换按钮 */}
+              {currentIndex > 0 && (
+                <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
+                  <Image source={{ uri: Images.box.detail.left }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+              {currentIndex < products.length - 1 && (
+                <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
+                  <Image source={{ uri: Images.box.detail.right }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+            </View>
+
+            {/* 左侧装饰 */}
+            <Image source={{ uri: Images.box.detail.positionBgleftBg }} style={styles.positionBgleftBg} contentFit="contain" />
+            {/* 右侧装饰 */}
+            <Image source={{ uri: Images.box.detail.positionBgRightBg }} style={styles.positionBgRightBg} contentFit="contain" />
+          </ImageBackground>
+
+          {/* 底部装饰文字 */}
+          <Image source={{ uri: Images.box.detail.mainGoodsSectionBtext }} style={styles.mainGoodsSectionBtext} contentFit="cover" />
+
+          {/* 侧边按钮 - 规则 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButText}>规则</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 侧边按钮 - 记录 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButText}>记录</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 侧边按钮 - 仓库 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store' as any)}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButTextR}>仓库</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 商品列表 */}
+          <ProductList products={products} levelList={data.luckGoodsLevelProbabilityList} poolId={poolId!} price={data.price} />
+
+          <View style={{ height: 150 }} />
+        </ScrollView>
+
+        {/* 底部购买栏 */}
+        <ImageBackground source={{ uri: Images.box.detail.boxDetailBott }} style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]} resizeMode="cover">
+          <View style={styles.bottomBtns}>
+            <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(1)} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgV }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买一盒</Text>
+                <Text style={styles.btnPrice}>(¥{data.specialPrice || data.price})</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.btnItem} onPress={() => handlePay(5)} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgL }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买五盒</Text>
+                <Text style={styles.btnPrice}>(¥{data.specialPriceFive || data.price * 5})</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+            <TouchableOpacity style={styles.btnItem} onPress={() => checkoutRef.current?.showFreedom()} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgH }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>购买多盒</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          </View>
+        </ImageBackground>
+      </ImageBackground>
+
+      <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} onSuccess={handleSuccess} />
+      <RecordModal ref={recordRef} poolId={poolId!} />
+      <RuleModal ref={ruleRef} />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
+  errorText: { color: '#999', fontSize: 16 },
+  backBtn2: { marginTop: 20, backgroundColor: '#ff6600', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
+  backBtn2Text: { color: '#fff', fontSize: 14 },
+
+  // 顶部导航
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    paddingBottom: 10,
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  headerTitle: { color: '#fff', fontSize: 15, fontWeight: 'bold', flex: 1, textAlign: 'center', width: 250 },
+  placeholder: { width: 40 },
+
+  scrollView: { flex: 1 },
+
+  // 主商品展示区域
+  mainGoodsSection: {
+    width: SCREEN_WIDTH,
+    height: 504,
+    position: 'relative',
+  },
+  mainSwiper: {
+    position: 'relative',
+    width: '100%',
+    height: 375,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginTop: -50,
+  },
+  productImageBox: {
+    width: 200,
+    height: 300,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  productImage: {
+    width: 200,
+    height: 300,
+  },
+  detailsBut: {
+    width: 120,
+    height: 45,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: -30,
+  },
+  detailsText: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  levelText: {
+    fontSize: 14,
+    color: '#FBC400',
+    fontWeight: 'bold',
+  },
+  probabilityText: {
+    fontSize: 10,
+    color: '#FBC400',
+    marginLeft: 3,
+  },
+  goodsNameBg: {
+    position: 'absolute',
+    left: 47,
+    top: 53,
+    width: 43,
+    height: 100,
+    paddingTop: 8,
+    justifyContent: 'flex-start',
+    alignItems: 'center',
+  },
+  goodsNameText: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    color: '#000',
+    writingDirection: 'ltr',
+    width: 20,
+    textAlign: 'center',
+  },
+  prevBtn: { position: 'absolute', left: 35, top: '40%' },
+  nextBtn: { position: 'absolute', right: 35, top: '40%' },
+  arrowImg: { width: 33, height: 38 },
+
+  // 装饰图片
+  positionBgleftBg: {
+    position: 'absolute',
+    left: 0,
+    top: 225,
+    width: 32,
+    height: 188,
+  },
+  positionBgRightBg: {
+    position: 'absolute',
+    right: 0,
+    top: 225,
+    width: 32,
+    height: 188,
+  },
+  mainGoodsSectionBtext: {
+    width: SCREEN_WIDTH,
+    height: 74,
+    marginTop: -10,
+  },
+
+  // 侧边按钮
+  positionBut: {
+    position: 'absolute',
+    zIndex: 10,
+    width: 35,
+    height: 34,
+  },
+  positionButBg: {
+    width: 35,
+    height: 34,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  positionButText: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    color: '#fff',
+    transform: [{ rotate: '14deg' }],
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+  positionButTextR: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    color: '#fff',
+    transform: [{ rotate: '-16deg' }],
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+  positionRule: { top: 256, left: 0 },
+  positionRecord: { top: 300, left: 0 },
+  positionStore: { top: 256, right: 0 },
+
+  // 底部购买栏
+  bottomBar: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    height: 69,
+    paddingHorizontal: 5,
+  },
+  bottomBtns: {
+    flexDirection: 'row',
+    height: 64,
+    alignItems: 'center',
+    justifyContent: 'space-around',
+  },
+  btnItem: {
+    flex: 1,
+    marginHorizontal: 6,
+  },
+  btnBg: {
+    width: '100%',
+    height: 54,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  btnText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  btnPrice: {
+    fontSize: 9,
+    color: '#fff',
+  },
+});

+ 10 - 0
app/boxInBox/_layout.tsx

@@ -0,0 +1,10 @@
+import { Stack } from 'expo-router';
+
+export default function BoxInBoxLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="index" />
+      <Stack.Screen name="boxList" />
+    </Stack>
+  );
+}

+ 147 - 0
app/boxInBox/boxList.tsx

@@ -0,0 +1,147 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { get, post } from '@/services/http';
+
+interface BoxItem {
+  id: string;
+  boxName: string;
+  boxCover: string;
+}
+
+const getMyBoxes = async () => {
+  const res = await get('/api/nestedBox/myBoxes');
+  return res.data;
+};
+
+const openBoxes = async (boxIds: string) => {
+  const res = await post('/api/nestedBox/openBox', { boxIds });
+  return res;
+};
+
+export default function BoxListScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<BoxItem[]>([]);
+  const [selectedIds, setSelectedIds] = useState<string[]>([]);
+
+  const loadData = async () => {
+    setLoading(true);
+    try {
+      const res = await getMyBoxes();
+      setList(res || []);
+    } catch (error) {
+      console.error('加载宝箱列表失败:', error);
+    }
+    setLoading(false);
+  };
+
+  useEffect(() => {
+    loadData();
+  }, []);
+
+  const toggleSelect = (id: string) => {
+    setSelectedIds(prev => 
+      prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
+    );
+  };
+
+  const selectAll = () => {
+    if (selectedIds.length === list.length) {
+      setSelectedIds([]);
+    } else {
+      setSelectedIds(list.map(item => item.id));
+    }
+  };
+
+  const handleOpen = async () => {
+    if (selectedIds.length === 0) {
+      Alert.alert('提示', '请先选择宝箱');
+      return;
+    }
+    try {
+      const res = await openBoxes(selectedIds.join(','));
+      if (res.code === 0) {
+        Alert.alert('成功', '开启成功');
+        setSelectedIds([]);
+        loadData();
+      }
+    } catch (error) {
+      console.error('开启失败:', error);
+    }
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity onPress={() => router.back()}>
+          <Text style={styles.backText}>←</Text>
+        </TouchableOpacity>
+        <Text style={styles.title}>宝箱</Text>
+        <View style={{ width: 40 }} />
+      </View>
+      {loading ? (
+        <ActivityIndicator size="large" color="#fff" style={{ marginTop: 50 }} />
+      ) : (
+        <ScrollView style={styles.content}>
+          <View style={styles.grid}>
+            {list.map(item => (
+              <TouchableOpacity
+                key={item.id}
+                style={[styles.item, selectedIds.includes(item.id) && styles.itemSelected]}
+                onPress={() => toggleSelect(item.id)}
+              >
+                <Image source={{ uri: item.boxCover }} style={styles.itemImage} contentFit="cover" />
+                <Text style={styles.itemName} numberOfLines={1}>{item.boxName}</Text>
+                {selectedIds.includes(item.id) && (
+                  <View style={styles.checkMark}><Text style={styles.checkText}>✓</Text></View>
+                )}
+              </TouchableOpacity>
+            ))}
+          </View>
+        </ScrollView>
+      )}
+      <View style={[styles.footer, { paddingBottom: insets.bottom + 10 }]}>
+        <TouchableOpacity style={styles.selectAllBtn} onPress={selectAll}>
+          <Text style={styles.btnText}>{selectedIds.length === list.length ? '取消全选' : '全选'}</Text>
+        </TouchableOpacity>
+        <TouchableOpacity style={styles.openBtn} onPress={handleOpen}>
+          <Text style={styles.btnText}>开启宝箱 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}</Text>
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15, paddingBottom: 10 },
+  backText: { color: '#fff', fontSize: 24 },
+  title: { color: '#fff', fontSize: 18, fontWeight: 'bold' },
+  content: { flex: 1, padding: 10 },
+  grid: { flexDirection: 'row', flexWrap: 'wrap' },
+  item: { width: '33%', padding: 5, position: 'relative' },
+  itemSelected: { opacity: 0.7 },
+  itemImage: { width: '100%', aspectRatio: 1, borderRadius: 8 },
+  itemName: { color: '#fff', fontSize: 12, textAlign: 'center', marginTop: 5 },
+  checkMark: { position: 'absolute', top: 10, right: 10, backgroundColor: '#ff6600', width: 24, height: 24, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
+  checkText: { color: '#fff', fontSize: 14 },
+  footer: { flexDirection: 'row', paddingHorizontal: 15, paddingTop: 10, backgroundColor: 'rgba(0,0,0,0.8)' },
+  selectAllBtn: { flex: 1, backgroundColor: '#666', paddingVertical: 12, borderRadius: 8, marginRight: 10, alignItems: 'center' },
+  openBtn: { flex: 2, backgroundColor: '#ff6600', paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
+  btnText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+});

+ 173 - 0
app/boxInBox/components/BoxPopup.tsx

@@ -0,0 +1,173 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+import { Images } from '@/constants/images';
+
+interface BoxItem {
+  boxNumber: number;
+  leftQuantity: number;
+  quantity: number;
+  isCompleted: boolean;
+}
+
+interface BoxPopupProps {
+  onSelect: (item: BoxItem) => void;
+}
+
+export interface BoxPopupRef {
+  open: (list: BoxItem[]) => void;
+  close: () => void;
+}
+
+export const BoxPopup = forwardRef<BoxPopupRef, BoxPopupProps>(({ onSelect }, ref) => {
+  const [visible, setVisible] = useState(false);
+  const [list, setList] = useState<BoxItem[]>([]);
+
+  useImperativeHandle(ref, () => ({
+    open: (data: BoxItem[]) => {
+      setList(data);
+      setVisible(true);
+    },
+    close: () => setVisible(false),
+  }));
+
+  const handleSelect = (item: BoxItem) => {
+    onSelect(item);
+    setVisible(false);
+  };
+
+  return (
+    <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+      <View style={styles.overlay}>
+        <View style={styles.container}>
+          {/* 标题区域 */}
+          <View style={styles.titleSection}>
+            <View style={styles.titleBg}>
+              <Text style={styles.titleText}>选择盒子</Text>
+            </View>
+            <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+              <Text style={styles.closeBtnText}>×</Text>
+            </TouchableOpacity>
+          </View>
+
+          {/* 盒子列表 */}
+          <View style={styles.content}>
+            <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+              <View style={styles.grid}>
+                {list.map((item, index) => (
+                  <TouchableOpacity key={index} style={styles.item} onPress={() => handleSelect(item)}>
+                    <View style={styles.imgBox}>
+                      <Image
+                        source={{ uri: item.isCompleted ? Images.box.detail.packagingBox2 : Images.box.detail.packagingBox1 }}
+                        style={styles.boxImg}
+                        contentFit="contain"
+                      />
+                    </View>
+                    <View style={styles.numBadge}>
+                      <Text style={styles.numText}>{item.boxNumber}箱</Text>
+                    </View>
+                    <Text style={styles.remaining}>
+                      {item.leftQuantity}/{item.quantity}
+                    </Text>
+                  </TouchableOpacity>
+                ))}
+              </View>
+            </ScrollView>
+          </View>
+        </View>
+      </View>
+    </Modal>
+  );
+});
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  container: {
+    backgroundColor: '#ffc901',
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    maxHeight: '80%',
+  },
+  titleSection: {
+    position: 'relative',
+    alignItems: 'center',
+    paddingVertical: 15,
+  },
+  titleBg: {
+    backgroundColor: '#ff8c16',
+    paddingHorizontal: 40,
+    paddingVertical: 10,
+    borderRadius: 20,
+  },
+  titleText: {
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  closeBtn: {
+    position: 'absolute',
+    right: 15,
+    top: 10,
+    width: 30,
+    height: 30,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  closeBtnText: {
+    fontSize: 24,
+    color: '#333',
+    fontWeight: 'bold',
+  },
+  content: {
+    backgroundColor: '#fff',
+    marginHorizontal: 10,
+    marginBottom: 20,
+    borderRadius: 13,
+    borderWidth: 1,
+    borderColor: '#000',
+    maxHeight: 470,
+  },
+  scrollView: {
+    padding: 10,
+  },
+  grid: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+  },
+  item: {
+    width: '25%',
+    alignItems: 'center',
+    marginBottom: 15,
+  },
+  imgBox: {
+    width: 54,
+    height: 54,
+  },
+  boxImg: {
+    width: '100%',
+    height: '100%',
+  },
+  numBadge: {
+    backgroundColor: '#959595',
+    borderRadius: 15,
+    paddingHorizontal: 10,
+    paddingVertical: 2,
+    marginTop: 5,
+  },
+  numText: {
+    color: '#fff',
+    fontSize: 12,
+    fontWeight: '500',
+  },
+  remaining: {
+    fontSize: 12,
+    color: '#999',
+    fontWeight: 'bold',
+    marginTop: 3,
+  },
+});

+ 617 - 0
app/boxInBox/index.tsx

@@ -0,0 +1,617 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+  ActivityIndicator,
+  Alert,
+  Animated,
+  Dimensions,
+  ImageBackground,
+  ScrollView,
+  StatusBar,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { useAuth } from '@/contexts/AuthContext';
+import { getBoxDetail, poolIn, poolOut, previewOrder, unlockBox } from '@/services/award';
+import { get } from '@/services/http';
+
+import { CheckoutModal } from '../award-detail/components/CheckoutModal';
+import { RuleModal } from '../award-detail/components/RuleModal';
+import { BoxPopup, BoxPopupRef } from './components/BoxPopup';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface PoolData {
+  id: string;
+  poolName: string;
+  name?: string;
+  cover: string;
+  price: number;
+  specialPrice?: number;
+  bigBoxPrizes?: ProductItem[];
+  activityGoods?: any[];
+}
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  price?: number;
+}
+
+interface BoxData {
+  number: string;
+  leftQuantity: number;
+  lastNumber: number;
+  lock?: { locker: string; leftTime: number };
+  usedStat?: Record<string, { spuId: string; quantity: number }>;
+}
+
+// 使用正确的 API 接口
+const getBoxPoolDetail = async (poolId: string) => {
+  const res = await get('/api/luck/treasure-box/pool-detail', { poolId });
+  return res.data;
+};
+
+const getBoxHistory = async (poolId: string) => {
+  const res = await get('/api/luck/treasure-box/box-history', { poolId });
+  return res.data;
+};
+
+const getEmptyRunStatus = async (poolId: string) => {
+  const res = await get('/api/luck/treasure-box/empty-run-status', { poolId });
+  return res.data;
+};
+
+export default function BoxInBoxScreen() {
+  const { poolId } = useLocalSearchParams<{ poolId: string }>();
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { user } = useAuth();
+
+  const [loading, setLoading] = useState(true);
+  const [data, setData] = useState<PoolData | null>(null);
+  const [products, setProducts] = useState<ProductItem[]>([]);
+  const [activityGoods, setActivityGoods] = useState<any[]>([]);
+  const [boxHistory, setBoxHistory] = useState<any[]>([]);
+  const [boxHistoryInfo, setBoxHistoryInfo] = useState<any>(null);
+  const [box, setBox] = useState<BoxData | null>(null);
+  const [boxNum, setBoxNum] = useState<string>('');
+  const [currentIndex, setCurrentIndex] = useState(0);
+  const [leftTime, setLeftTime] = useState(0);
+  const [emptyRuns, setEmptyRuns] = useState(0);
+  const [scrollTop, setScrollTop] = useState(0);
+  const [tabIndex, setTabIndex] = useState(0);
+
+  const checkoutRef = useRef<any>(null);
+  const ruleRef = useRef<any>(null);
+  const boxPopupRef = useRef<BoxPopupRef>(null);
+  const floatAnim = useRef(new Animated.Value(0)).current;
+  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+  useEffect(() => {
+    Animated.loop(
+      Animated.sequence([
+        Animated.timing(floatAnim, { toValue: 10, duration: 1500, useNativeDriver: true }),
+        Animated.timing(floatAnim, { toValue: -10, duration: 1500, useNativeDriver: true }),
+      ])
+    ).start();
+  }, []);
+
+  const loadData = useCallback(async () => {
+    if (!poolId) return;
+    setLoading(true);
+    try {
+      const detail = await getBoxPoolDetail(poolId);
+      if (detail) {
+        setData({ ...detail, name: detail.poolName, price: detail.price || detail.specialPrice || 0 });
+        setProducts(detail.bigBoxPrizes || []);
+        setActivityGoods(detail.activityGoods || []);
+      }
+    } catch (error) {
+      console.error('加载数据失败:', error);
+    }
+    setLoading(false);
+  }, [poolId]);
+
+  const loadBoxHistory = useCallback(async () => {
+    if (!poolId) return;
+    try {
+      const res = await getBoxHistory(poolId);
+      if (res && res.length > 0) {
+        setBoxHistory(res);
+        setBoxHistoryInfo(res[0]);
+      }
+    } catch {}
+  }, [poolId]);
+
+  const loadBox = useCallback(
+    async (num?: string) => {
+      if (!poolId) return;
+      try {
+        const res = await getBoxDetail(poolId, num);
+        if (res) handleBoxResult(res);
+      } catch (error) {
+        console.error('加载盒子失败:', error);
+      }
+    },
+    [poolId]
+  );
+
+  const loadEmptyRuns = useCallback(async () => {
+    if (!poolId) return;
+    try {
+      const res = await getEmptyRunStatus(poolId);
+      if (res) setEmptyRuns(res.emptyRuns || 0);
+    } catch {}
+  }, [poolId]);
+
+  const refreshBox = useCallback(async () => {
+    if (!poolId) return;
+    try {
+      const res = await getBoxHistory(poolId);
+      if (res && res.length > 0) {
+        setBoxHistory(res);
+        setBoxHistoryInfo(res[0]);
+        loadData();
+        loadBox(res[0].boxNumber);
+      }
+      loadEmptyRuns();
+    } catch {}
+  }, [poolId, loadData, loadBox, loadEmptyRuns]);
+
+  // 打开换盒弹窗
+  const openBoxPopup = useCallback(async () => {
+    if (!poolId) return;
+    try {
+      const res = await getBoxHistory(poolId);
+      if (res && res.length > 0) {
+        boxPopupRef.current?.open(res);
+      }
+    } catch {}
+  }, [poolId]);
+
+  // 选择盒子
+  const handleSelectBox = useCallback((item: any) => {
+    setBoxHistoryInfo(item);
+    loadBox(item.boxNumber);
+    loadEmptyRuns();
+  }, [loadBox, loadEmptyRuns]);
+
+  const handleBoxResult = (res: any) => {
+    const map: Record<string, any> = {};
+    if (res.usedStat)
+      res.usedStat.forEach((item: any) => {
+        map[item.spuId] = item;
+      });
+    res.usedStat = map;
+    setBox(res);
+    setBoxNum(res.number);
+    lockTimeStart(res);
+  };
+
+  const lockTimeStart = (boxData: BoxData) => {
+    lockTimeEnd();
+    if (boxData?.lock) {
+      setLeftTime(boxData.lock.leftTime);
+      timerRef.current = setInterval(() => {
+        setLeftTime((prev) => {
+          if (prev <= 1) {
+            lockTimeEnd();
+            loadBox();
+            return 0;
+          }
+          return prev - 1;
+        });
+      }, 1000);
+    }
+  };
+
+  const lockTimeEnd = () => {
+    if (timerRef.current) {
+      clearInterval(timerRef.current);
+      timerRef.current = null;
+    }
+    setLeftTime(0);
+  };
+
+  useEffect(() => {
+    loadData();
+    loadBox();
+    loadBoxHistory();
+    loadEmptyRuns();
+    if (poolId) poolIn(poolId);
+    return () => {
+      if (poolId) poolOut(poolId);
+      lockTimeEnd();
+    };
+  }, [poolId]);
+
+  const handlePay = async (num: number) => {
+    if (!poolId || !data || !box) {
+      Alert.alert('提示', '请先选择盒子');
+      return;
+    }
+    try {
+      const preview = await previewOrder(poolId, num, box.number);
+      if (preview) checkoutRef.current?.show(num, preview, box.number);
+    } catch (error) {
+      console.error('预览订单失败:', error);
+    }
+  };
+
+  const handleSuccess = () => {
+    setTimeout(() => {
+      loadData();
+      loadBox(boxNum);
+      loadEmptyRuns();
+    }, 500);
+  };
+
+  const handleUnlock = async () => {
+    if (!poolId || !boxNum) return;
+    try {
+      await unlockBox(poolId, boxNum);
+      loadBox(boxNum);
+    } catch {}
+  };
+
+  const handlePrev = () => {
+    if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
+  };
+  const handleNext = () => {
+    if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
+  };
+
+  const getLevelName = (level: string) => {
+    const map: Record<string, string> = { A: '超神款', B: '欧皇款', C: '隐藏款', D: '普通款', NESTED_BOX_GUARANTEED: '保底款' };
+    return map[level] || level;
+  };
+
+  const getLevelBg = (level: string) => {
+    const map: Record<string, string> = {
+      A: Images.box.detail.productItemA,
+      B: Images.box.detail.productItemB,
+      C: Images.box.detail.productItemC,
+      D: Images.box.detail.productItemD,
+      NESTED_BOX_GUARANTEED: Images.box.detail.productItemD,
+    };
+    return map[level] || Images.box.detail.productItemD;
+  };
+
+  const leftNum = box?.lock ? (leftTime / box.lock.leftTime) * 100 : 0;
+  const headerBg = scrollTop > 0 ? '#333' : 'transparent';
+
+  if (loading) return <View style={styles.loadingContainer}><ActivityIndicator size="large" color="#fff" /></View>;
+
+  if (!data)
+    return (
+      <View style={styles.loadingContainer}>
+        <Text style={styles.errorText}>奖池不存在</Text>
+        <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
+          <Text style={styles.backBtn2Text}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+
+  const currentProduct = products[currentIndex];
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backText}>{'<'}</Text>
+          </TouchableOpacity>
+          <Text style={styles.headerTitle} numberOfLines={1}>{data.poolName || data.name}</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false} onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)} scrollEventThrottle={16}>
+          {/* 主商品展示区域 */}
+          <ImageBackground source={{ uri: Images.box.detail.mainGoodsSection }} style={styles.mainGoodsSection} resizeMode="cover">
+            <View style={{ height: 72 + insets.top }} />
+
+            <View style={styles.mainSwiper}>
+              {currentProduct && (
+                <>
+                  <Animated.View style={[styles.productImageBox, { transform: [{ translateY: floatAnim }] }]}>
+                    <Image source={{ uri: currentProduct.cover }} style={styles.productImage} contentFit="contain" />
+                  </Animated.View>
+
+                  {currentProduct.price && <Text style={styles.priceText}>¥{currentProduct.price}</Text>}
+
+                  <ImageBackground source={{ uri: Images.box.detail.detailsBut }} style={styles.detailsBut} resizeMode="contain">
+                    <Text style={styles.levelText}>{getLevelName(currentProduct.level)}</Text>
+                  </ImageBackground>
+
+                  <ImageBackground source={{ uri: Images.box.detail.nameBg }} style={styles.goodsNameBg} resizeMode="contain">
+                    <Text style={styles.goodsNameText} numberOfLines={6}>{currentProduct.name}</Text>
+                  </ImageBackground>
+                </>
+              )}
+
+              {currentIndex > 0 && (
+                <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
+                  <Image source={{ uri: Images.box.detail.left }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+              {currentIndex < products.length - 1 && (
+                <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
+                  <Image source={{ uri: Images.box.detail.right }} style={styles.arrowImg} contentFit="contain" />
+                </TouchableOpacity>
+              )}
+            </View>
+
+            <Image source={{ uri: Images.box.detail.positionBgleftBg }} style={styles.positionBgleftBg} contentFit="contain" />
+            <Image source={{ uri: Images.box.detail.positionBgRightBg }} style={styles.positionBgRightBg} contentFit="contain" />
+          </ImageBackground>
+
+          <Image source={{ uri: Images.box.detail.mainGoodsSectionBtext }} style={styles.mainGoodsSectionBtext} contentFit="cover" />
+
+          {/* 侧边按钮 */}
+          <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButText}>规则</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {box?.lock && user && box.lock.locker === (user.userId || user.id) && (
+            <TouchableOpacity style={[styles.positionBut, styles.positionLock]} onPress={handleUnlock}>
+              <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.positionButBg} resizeMode="contain">
+                <Text style={styles.positionButText}>解锁</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          )}
+
+          <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/boxInBox/boxList' as any)}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButTextR}>宝箱</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.positionBut, styles.positionRefresh]} onPress={() => refreshBox()}>
+            <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.positionButBg} resizeMode="contain">
+              <Text style={styles.positionButTextR}>刷新</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          {/* 空车计数 */}
+          <View style={styles.emptyRunsBox}>
+            <Text style={styles.emptyRunsText}>连续空车:{emptyRuns}</Text>
+          </View>
+
+          {/* 锁定倒计时 */}
+          {box?.lock && (
+            <View style={styles.lockTimeBox}>
+              <Text style={styles.lockTimeLabel}>剩余时间:</Text>
+              <View style={styles.lockTimeBarBox}>
+                <View style={styles.lockTimeBar}>
+                  <View style={[styles.processBar, { width: `${leftNum}%` }]} />
+                </View>
+                <Text style={[styles.lockTimeText, { left: `${leftNum}%` }]}>{leftTime}</Text>
+              </View>
+            </View>
+          )}
+
+          {/* 箱子信息区域 */}
+          <View style={styles.firstLastWrapper}>
+            {/* 标题栏 */}
+            <View style={styles.firstLastTitle}>
+              <View style={styles.firstLastInfo}>
+                {boxHistoryInfo && (
+                  <>
+                    <View style={styles.sizeInfo}>
+                      <Text style={styles.sizeLabel}>箱数:</Text>
+                      <Text style={styles.sizeValue}>{boxHistoryInfo.boxNumber}</Text>
+                      <Text style={styles.sizeLabel}>/{boxHistory.length || '-'}箱</Text>
+                    </View>
+                    <View style={styles.sizeInfo}>
+                      <Text style={styles.sizeLabel}>总数:</Text>
+                      <Text style={styles.sizeValue}>{boxHistoryInfo.leftQuantity}</Text>
+                      <Text style={styles.sizeLabel}>/{boxHistoryInfo.quantity}</Text>
+                    </View>
+                  </>
+                )}
+              </View>
+              <TouchableOpacity style={styles.changeBoxBtn} onPress={openBoxPopup}>
+                <Text style={styles.changeBoxText}>换箱</Text>
+              </TouchableOpacity>
+            </View>
+
+            {/* Tab 切换 */}
+            <View style={styles.tabSection}>
+              <TouchableOpacity style={[styles.tabItem, tabIndex === 0 && styles.tabItemActive]} onPress={() => setTabIndex(0)}>
+                <Text style={[styles.tabText, tabIndex === 0 && styles.tabTextActive]}>赏品预览</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={[styles.tabItem, tabIndex === 1 && styles.tabItemActive]} onPress={() => setTabIndex(1)}>
+                <Text style={[styles.tabText, tabIndex === 1 && styles.tabTextActive]}>中奖记录</Text>
+              </TouchableOpacity>
+            </View>
+
+            {/* 活动商品列表 */}
+            {tabIndex === 0 && (
+              <View style={styles.activityGoodsGrid}>
+                {activityGoods.map((item, index) => (
+                  <View key={item.id || index} style={styles.activityGoodsItem}>
+                    <View style={styles.activityImageBox}>
+                      <Image source={{ uri: item.cover }} style={styles.activityImage} contentFit="cover" />
+                      <View style={styles.probabilityBadge}>
+                        <Text style={styles.probabilityText}>概率:{(item.probability * 100).toFixed(2)}%</Text>
+                      </View>
+                      <View style={styles.priceBadge}>
+                        <Text style={styles.priceTextSmall}>参考价:{item.price}</Text>
+                      </View>
+                    </View>
+                    <View style={[styles.levelBadgeSmall, item.level === 'NESTED_BOX_GUARANTEED' ? styles.levelD : styles.levelAll]}>
+                      <Text style={styles.levelBadgeText}>{item.level === 'NESTED_BOX_GUARANTEED' ? 'D赏' : '全局赏'}</Text>
+                    </View>
+                    <Text style={styles.activityName} numberOfLines={1}>{item.name}</Text>
+                  </View>
+                ))}
+              </View>
+            )}
+
+            {/* 中奖记录 - 暂时显示空状态 */}
+            {tabIndex === 1 && (
+              <View style={styles.emptyRecord}>
+                <Text style={styles.emptyRecordText}>暂无中奖记录</Text>
+              </View>
+            )}
+          </View>
+
+          {/* 奖品列表 */}
+          <View style={styles.productGrid}>
+            <Text style={styles.gridTitle}>奖品列表</Text>
+            <View style={styles.gridContent}>
+              {products.map((item, index) => (
+                <View key={item.id || index} style={styles.gridItem}>
+                  <ImageBackground source={{ uri: getLevelBg(item.level) }} style={styles.gridItemBg} resizeMode="stretch">
+                    <View style={styles.gridImageBox}>
+                      <Image source={{ uri: item.cover }} style={styles.gridImage} contentFit="cover" />
+                    </View>
+                    <Text style={styles.gridName} numberOfLines={2}>{item.name}</Text>
+                    <Text style={styles.gridLevel}>{getLevelName(item.level)}</Text>
+                    {item.price && <Text style={styles.gridPrice}>¥{item.price}</Text>}
+                  </ImageBackground>
+                </View>
+              ))}
+            </View>
+          </View>
+
+          <View style={{ height: 150 }} />
+        </ScrollView>
+
+        {/* 底部购买栏 */}
+        <ImageBackground source={{ uri: Images.box.detail.boxDetailBott }} style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]} resizeMode="cover">
+          <View style={styles.bottomBtns}>
+            <TouchableOpacity style={styles.btnItemFull} onPress={() => handlePay(1)} activeOpacity={0.8}>
+              <ImageBackground source={{ uri: Images.common.butBgV }} style={styles.btnBg} resizeMode="contain">
+                <Text style={styles.btnText}>×1</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          </View>
+        </ImageBackground>
+      </ImageBackground>
+
+      <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} boxNumber={boxNum} onSuccess={handleSuccess} />
+      <RuleModal ref={ruleRef} />
+      <BoxPopup ref={boxPopupRef} onSelect={handleSelectBox} />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
+  errorText: { color: '#999', fontSize: 16 },
+  backBtn2: { marginTop: 20, backgroundColor: '#ff6600', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
+  backBtn2Text: { color: '#fff', fontSize: 14 },
+
+  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10, position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100 },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  headerTitle: { color: '#fff', fontSize: 15, fontWeight: 'bold', flex: 1, textAlign: 'center', width: 250 },
+  placeholder: { width: 40 },
+
+  scrollView: { flex: 1 },
+
+  mainGoodsSection: { width: SCREEN_WIDTH, height: 504, position: 'relative' },
+  mainSwiper: { position: 'relative', width: '100%', height: 375, alignItems: 'center', justifyContent: 'center', marginTop: -50 },
+  productImageBox: { width: 200, height: 280, justifyContent: 'center', alignItems: 'center' },
+  productImage: { width: 200, height: 280 },
+  priceText: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginTop: -20 },
+  detailsBut: { width: 120, height: 45, justifyContent: 'center', alignItems: 'center', marginTop: -10 },
+  levelText: { fontSize: 14, color: '#FBC400', fontWeight: 'bold' },
+  goodsNameBg: { position: 'absolute', left: 47, top: 53, width: 43, height: 100, paddingTop: 8, justifyContent: 'flex-start', alignItems: 'center' },
+  goodsNameText: { fontSize: 12, fontWeight: 'bold', color: '#000', width: 20, textAlign: 'center' },
+  prevBtn: { position: 'absolute', left: 35, top: '40%' },
+  nextBtn: { position: 'absolute', right: 35, top: '40%' },
+  arrowImg: { width: 33, height: 38 },
+
+  positionBgleftBg: { position: 'absolute', left: 0, top: 225, width: 32, height: 188 },
+  positionBgRightBg: { position: 'absolute', right: 0, top: 225, width: 32, height: 188 },
+  mainGoodsSectionBtext: { width: SCREEN_WIDTH, height: 74, marginTop: -10 },
+
+  positionBut: { position: 'absolute', zIndex: 10, width: 35, height: 34 },
+  positionButBg: { width: 35, height: 34, justifyContent: 'center', alignItems: 'center' },
+  positionButText: { fontSize: 12, fontWeight: 'bold', color: '#fff', transform: [{ rotate: '14deg' }], textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+  positionButTextR: { fontSize: 12, fontWeight: 'bold', color: '#fff', transform: [{ rotate: '-16deg' }], textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+  positionRule: { top: 256, left: 0 },
+  positionLock: { top: 300, left: 0 },
+  positionStore: { top: 256, right: 0 },
+  positionRefresh: { top: 300, right: 0 },
+
+  emptyRunsBox: { alignItems: 'center', marginTop: -60, marginBottom: 10 },
+  emptyRunsText: { color: '#fff', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 15, paddingVertical: 5, borderRadius: 10 },
+
+  lockTimeBox: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#71ccff', padding: 10, marginHorizontal: 10, borderRadius: 8, marginBottom: 10 },
+  lockTimeLabel: { color: '#000', fontSize: 12 },
+  lockTimeBarBox: { flex: 1, height: 30, position: 'relative', justifyContent: 'center' },
+  lockTimeBar: { height: 8, backgroundColor: 'rgba(255,255,255,0.6)', borderRadius: 4, overflow: 'hidden' },
+  processBar: { height: '100%', backgroundColor: '#209ae5', borderRadius: 4 },
+  lockTimeText: { position: 'absolute', top: -5, fontSize: 10, backgroundColor: '#000', color: '#fff', paddingHorizontal: 4, borderRadius: 2, marginLeft: -13 },
+
+  // 箱子信息区域样式
+  firstLastWrapper: { marginHorizontal: 10, marginBottom: 10, backgroundColor: '#fff', borderRadius: 8, overflow: 'hidden', borderWidth: 2, borderColor: '#000' },
+  firstLastTitle: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#ffc900', paddingHorizontal: 20, paddingVertical: 15, borderBottomWidth: 2, borderBottomColor: '#000' },
+  firstLastInfo: { flexDirection: 'row', alignItems: 'center' },
+  sizeInfo: { flexDirection: 'row', alignItems: 'center', marginRight: 15 },
+  sizeLabel: { fontSize: 11, color: '#fff' },
+  sizeValue: { fontSize: 16, fontWeight: 'bold', color: '#fff' },
+  changeBoxBtn: { backgroundColor: '#ff8c16', paddingHorizontal: 15, paddingVertical: 8, borderRadius: 4, borderWidth: 1, borderColor: '#333' },
+  changeBoxText: { fontSize: 11, color: '#fff' },
+
+  // Tab 切换样式
+  tabSection: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#E4E4E4' },
+  tabItem: { flex: 1, paddingVertical: 12, alignItems: 'center' },
+  tabItemActive: {},
+  tabText: { fontSize: 14, color: '#9E9E9E' },
+  tabTextActive: { color: '#ff8c16', fontWeight: 'bold' },
+
+  // 活动商品列表样式
+  activityGoodsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8 },
+  activityGoodsItem: { width: '33.33%', padding: 4, alignItems: 'center', marginBottom: 10 },
+  activityImageBox: { width: 100, height: 100, borderWidth: 2, borderColor: '#1A1A1A', borderRadius: 4, overflow: 'hidden', position: 'relative' },
+  activityImage: { width: '100%', height: '100%' },
+  probabilityBadge: { position: 'absolute', top: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.5)', paddingVertical: 2 },
+  probabilityText: { fontSize: 7, color: '#fff', textAlign: 'center' },
+  priceBadge: { position: 'absolute', bottom: 5, left: 5, right: 5, backgroundColor: 'rgba(0,0,0,0.5)', paddingVertical: 4, borderRadius: 2 },
+  priceTextSmall: { fontSize: 7, color: '#fff', textAlign: 'center' },
+  levelBadgeSmall: { marginTop: 5, paddingHorizontal: 10, paddingVertical: 3, borderRadius: 2 },
+  levelD: { backgroundColor: '#6340FF', borderWidth: 1, borderColor: '#A2BBFF' },
+  levelAll: { backgroundColor: '#A3E100', borderWidth: 1, borderColor: '#EAFFB1' },
+  levelBadgeText: { fontSize: 12, textAlign: 'center' },
+  activityName: { fontSize: 12, color: '#333', marginTop: 4, textAlign: 'center' },
+
+  // 空记录样式
+  emptyRecord: { padding: 40, alignItems: 'center' },
+  emptyRecordText: { fontSize: 14, color: '#999' },
+
+  productGrid: { margin: 10, backgroundColor: 'rgba(0,0,0,0.3)', borderRadius: 15, padding: 15 },
+  gridTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 15 },
+  gridContent: { flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -5 },
+  gridItem: { width: '33.33%', paddingHorizontal: 5, marginBottom: 10 },
+  gridItemBg: { width: '100%', aspectRatio: 0.75, padding: 8, alignItems: 'center' },
+  gridImageBox: { width: '100%', aspectRatio: 1, borderRadius: 5, overflow: 'hidden', backgroundColor: 'rgba(255,255,255,0.1)' },
+  gridImage: { width: '100%', height: '100%' },
+  gridName: { color: '#fff', fontSize: 10, marginTop: 5, textAlign: 'center', height: 26 },
+  gridLevel: { color: '#FBC400', fontSize: 9, marginTop: 2 },
+  gridPrice: { color: '#ff6600', fontSize: 10, marginTop: 2 },
+
+  bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 69, paddingHorizontal: 5 },
+  bottomBtns: { flexDirection: 'row', height: 64, alignItems: 'center', justifyContent: 'center' },
+  btnItemFull: { width: 200 },
+  btnBg: { width: '100%', height: 54, justifyContent: 'center', alignItems: 'center' },
+  btnText: { fontSize: 18, fontWeight: 'bold', color: '#fff' },
+});

+ 10 - 0
app/orders/_layout.tsx

@@ -0,0 +1,10 @@
+import { Stack } from 'expo-router';
+
+export default function OrdersLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="index" />
+      <Stack.Screen name="[tradeNo]" />
+    </Stack>
+  );
+}

+ 11 - 0
app/weal/_layout.tsx

@@ -0,0 +1,11 @@
+import { Stack } from 'expo-router';
+
+export default function WealLayout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }}>
+      <Stack.Screen name="room" />
+      <Stack.Screen name="catchDoll" />
+      <Stack.Screen name="wish" />
+    </Stack>
+  );
+}

+ 404 - 0
app/weal/catchDoll.tsx

@@ -0,0 +1,404 @@
+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 { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+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;
+}
+
+export default function CatchDollScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [goodsList, setGoodsList] = useState<GoodsItem[]>([]);
+  const [molibi, setMolibi] = useState(0);
+  const [balls] = useState(() =>
+    Array.from({ length: 50 }, (_, i) => ({
+      id: i,
+      bottom: Math.random() * 80,
+      left: Math.random() * 172,
+    }))
+  );
+
+  // 动画
+  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);
+    }
+  }, []);
+
+  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);
+    }
+  }, []);
+
+  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 handlePress = (count: number) => {
+    if (molibi < count) {
+      // TODO: 显示源力币不足弹窗
+      return;
+    }
+    animateBalls();
+    // TODO: 调用抽奖接口
+  };
+
+  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>
+
+        {/* 规则按钮 - 固定位置 */}
+        <TouchableOpacity style={[styles.ruleBtn, { top: insets.top + 200 }]}>
+          <Image source={{ uri: catchDollImages.ruleBtn }} style={styles.ruleBtnImg} contentFit="contain" />
+        </TouchableOpacity>
+
+        {/* 中奖记录按钮 - 固定位置 */}
+        <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>
+        </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" />
+                    </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>
+
+              {/* 源力币信息框 */}
+              <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" />
+                </TouchableOpacity>
+              </View>
+
+              {/* 开口动画区域 */}
+              <View style={styles.openingBox}>
+                <Image source={{ uri: catchDollImages.opening }} style={styles.openingImg} contentFit="contain" />
+              </View>
+
+              {/* 扭蛋把手 */}
+              <TouchableOpacity style={[styles.switchBox, styles.switchBox1]} onPress={() => handlePress(1)}>
+                <Image source={{ uri: catchDollImages.dollBi1 }} style={styles.switchImg} contentFit="contain" />
+              </TouchableOpacity>
+              <TouchableOpacity style={[styles.switchBox, styles.switchBox5]} onPress={() => handlePress(5)}>
+                <Image source={{ uri: catchDollImages.dollBi5 }} style={styles.switchImg} contentFit="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 style={{ height: 100 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    paddingBottom: 10,
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  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: {
+    width: 26,
+    height: 70,
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingVertical: 9,
+  },
+  recordText: {
+    color: '#fff',
+    fontSize: 12,
+    fontWeight: 'bold',
+    textShadowColor: '#6C3200',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+
+  // 扭蛋机主体
+  machineWrapper: {
+    width: SCREEN_WIDTH,
+    alignItems: 'center',
+  },
+  machineImg: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_WIDTH * 1.88,
+    position: 'relative',
+  },
+
+  // 奖品列表
+  goodsListWrapper: {
+    position: 'absolute',
+    top: SCREEN_WIDTH * 0.29,
+    left: 0,
+    right: 0,
+    alignItems: 'center',
+  },
+  goodsScroll: {
+    paddingHorizontal: SCREEN_WIDTH * 0.18,
+  },
+  goodsItem: {
+    width: 46,
+    height: 46,
+    borderRadius: 4,
+    backgroundColor: '#ADAEF6',
+    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',
+  },
+  ball: {
+    position: 'absolute',
+    width: 66,
+    height: 66,
+  },
+  ballImg: { width: '100%', height: '100%' },
+
+  // 源力币信息框
+  molibiBox: {
+    position: 'absolute',
+    top: SCREEN_WIDTH * 1.24,
+    left: 42,
+    width: 120,
+    height: 67,
+    backgroundColor: '#1E1C5B',
+    borderRadius: 8,
+    paddingTop: 5,
+    alignItems: 'center',
+  },
+  molibiLabel: {
+    color: '#7982CB',
+    fontSize: 12,
+  },
+  molibiNum: {
+    color: '#FF8400',
+    fontSize: 18,
+    fontWeight: 'bold',
+  },
+  molibiBtn: {
+    marginTop: 5,
+  },
+  molibiBtnImg: {
+    width: 105,
+    height: 30,
+  },
+
+  // 开口动画区域
+  openingBox: {
+    position: 'absolute',
+    top: SCREEN_WIDTH * 1.21,
+    right: 42,
+    width: 133,
+    height: 82,
+  },
+  openingImg: { width: '100%', height: '100%' },
+
+  // 扭蛋把手
+  switchBox: {
+    position: 'absolute',
+    top: SCREEN_WIDTH * 1.49,
+    width: 65,
+    height: 65,
+  },
+  switchBox1: { left: 42 },
+  switchBox5: { right: 42 },
+  switchImg: { width: '100%', height: '100%' },
+
+  // 底部按钮
+  bottomBtns: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    marginTop: -SCREEN_WIDTH * 0.24,
+    paddingHorizontal: 20,
+  },
+  submitBtn: {
+    marginHorizontal: SCREEN_WIDTH * 0.08,
+  },
+  submitBtnImg: {
+    width: 73,
+    height: 50,
+  },
+});

+ 291 - 0
app/weal/room.tsx

@@ -0,0 +1,291 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    ImageBackground,
+    RefreshControl,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { get } from '@/services/http';
+
+interface RoomItem {
+  id: string;
+  name: string;
+  type: string;
+  goodsQuantity: number;
+  officialFlag: number;
+  user: { avatar: string; username: string };
+  luckRoomGoodsList: { spu: { cover: string } }[];
+  participatingList: any[];
+}
+
+const roomTypes = [
+  { name: '全部', value: '' },
+  { name: '福利营', value: 'COMMON' },
+  { name: '欧气营', value: 'EUROPEAN_GAS' },
+  { name: '口令营', value: 'PASSWORD' },
+  { name: '成就营', value: 'ACHIEVEMENT' },
+  { name: '荣耀榜', value: 'GLORY' },
+];
+
+const getTypeLabel = (type: string) => {
+  const map: Record<string, string> = {
+    COMMON: '福利营',
+    PASSWORD: '口令营',
+    EUROPEAN_GAS: '欧气营',
+    ACHIEVEMENT: '成就营',
+  };
+  return map[type] || type;
+};
+
+export default function RoomScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [loading, setLoading] = useState(true);
+  const [refreshing, setRefreshing] = useState(false);
+  const [list, setList] = useState<RoomItem[]>([]);
+  const [searchVal, setSearchVal] = useState('');
+  const [typeIndex, setTypeIndex] = useState(0);
+  const [scrollTop, setScrollTop] = useState(0);
+
+  const loadData = useCallback(async (isRefresh = false) => {
+    if (isRefresh) setRefreshing(true);
+    else setLoading(true);
+    try {
+      const type = roomTypes[typeIndex].value;
+      const res = await get('/api/luckRoom/list', {
+        pageNum: 1,
+        pageSize: 20,
+        keyword: searchVal,
+        type: type || undefined,
+      });
+      setList(res.data?.records || res.data || []);
+    } catch (error) {
+      console.error('加载房间列表失败:', error);
+    }
+    setLoading(false);
+    setRefreshing(false);
+  }, [typeIndex, searchVal]);
+
+  useEffect(() => {
+    loadData();
+  }, [typeIndex]);
+
+  const handleSearch = () => {
+    loadData();
+  };
+
+  const handleTypeChange = (index: number) => {
+    setTypeIndex(index);
+  };
+
+  const handleRoomPress = (item: RoomItem) => {
+    router.push({ pathname: '/weal/detail', params: { id: item.id } } as any);
+  };
+
+  const headerBg = scrollTop > 0 ? '#333' : 'transparent';
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backText}>←</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>福利</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* 头部背景 */}
+        <ImageBackground source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headBg} resizeMode="cover" />
+
+        <ScrollView
+          style={styles.scrollView}
+          showsVerticalScrollIndicator={false}
+          onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
+          scrollEventThrottle={16}
+          refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} tintColor="#fff" />}
+        >
+          <View style={{ height: 90 + insets.top }} />
+
+          {/* 搜索栏和功能按钮 */}
+          <View style={styles.topSection}>
+            <ImageBackground source={{ uri: Images.welfare.roomInputBg }} style={styles.searchBox} resizeMode="stretch">
+              <Image source={{ uri: Images.home.search2 }} style={styles.searchIcon} contentFit="contain" />
+              <TextInput
+                style={styles.searchInput}
+                placeholder="搜索"
+                placeholderTextColor="#6C6C6C"
+                value={searchVal}
+                onChangeText={setSearchVal}
+                onSubmitEditing={handleSearch}
+                returnKeyType="search"
+              />
+            </ImageBackground>
+            <View style={styles.funcBtns}>
+              <TouchableOpacity style={styles.funcItem}>
+                <Image source={{ uri: Images.welfare.roomIcon0 }} style={styles.funcIcon} contentFit="contain" />
+                <Text style={styles.funcText}>创建</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.funcItem}>
+                <Image source={{ uri: Images.welfare.roomIcon1 }} style={styles.funcIcon} contentFit="contain" />
+                <Text style={styles.funcText}>我的</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.funcItem}>
+                <Image source={{ uri: Images.welfare.roomIcon2 }} style={styles.funcIcon} contentFit="contain" />
+                <Text style={styles.funcText}>玩法</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+
+          {/* 类型切换 */}
+          <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.typeSection}>
+            {roomTypes.map((item, index) => (
+              <TouchableOpacity
+                key={index}
+                style={styles.typeItem}
+                onPress={() => handleTypeChange(index)}
+              >
+                <Text style={[styles.typeText, typeIndex === index && styles.typeTextActive]}>{item.name}</Text>
+              </TouchableOpacity>
+            ))}
+          </ScrollView>
+
+          {/* 房间列表 */}
+          {loading ? (
+            <ActivityIndicator size="large" color="#fff" style={{ marginTop: 50 }} />
+          ) : list.length === 0 ? (
+            <View style={styles.emptyBox}>
+              <Text style={styles.emptyText}>暂无房间</Text>
+            </View>
+          ) : (
+            <View style={styles.roomList}>
+              {list.map((item) => (
+                <TouchableOpacity key={item.id} onPress={() => handleRoomPress(item)} activeOpacity={0.8}>
+                  <ImageBackground source={{ uri: Images.welfare.roomItemBg }} style={styles.roomItem} resizeMode="stretch">
+                    {/* 官方标签 */}
+                    {item.officialFlag === 1 && (
+                      <View style={styles.officialBadge}>
+                        <Image source={{ uri: Images.welfare.official }} style={styles.officialImg} contentFit="contain" />
+                      </View>
+                    )}
+                    {/* 成就房标签 */}
+                    {item.type === 'ACHIEVEMENT' && (
+                      <View style={styles.mustBeBadge}>
+                        <Image source={{ uri: Images.welfare.mustBe }} style={styles.mustBeImg} contentFit="contain" />
+                      </View>
+                    )}
+                    {/* 商品图片 */}
+                    <ImageBackground source={{ uri: Images.welfare.roomItemImgBg }} style={styles.roomCover} resizeMode="contain">
+                      {item.luckRoomGoodsList?.[0]?.spu?.cover && (
+                        <Image source={{ uri: item.luckRoomGoodsList[0].spu.cover }} style={styles.roomCoverImg} contentFit="cover" />
+                      )}
+                    </ImageBackground>
+                    {/* 房间信息 */}
+                    <View style={styles.roomInfo}>
+                      <View style={styles.roomTop}>
+                        <Text style={styles.roomName} numberOfLines={1}>{item.name}</Text>
+                        <Text style={styles.roomType}>{getTypeLabel(item.type)}</Text>
+                      </View>
+                      <View style={styles.roomBottom}>
+                        <View style={styles.userInfo}>
+                          {item.user?.avatar && (
+                            <Image source={{ uri: item.user.avatar }} style={styles.userAvatar} contentFit="cover" />
+                          )}
+                          <Text style={styles.userName} numberOfLines={1}>{item.user?.username || '匿名'}</Text>
+                        </View>
+                        <Text style={styles.goodsNum}>共{item.goodsQuantity}件赠品</Text>
+                        <View style={styles.participantBox}>
+                          <Image source={{ uri: Images.welfare.participationIcon }} style={styles.participantIcon} contentFit="contain" />
+                          <Text style={styles.participantNum}>{item.participatingList?.length || 0}</Text>
+                        </View>
+                      </View>
+                    </View>
+                  </ImageBackground>
+                </TouchableOpacity>
+              ))}
+            </View>
+          )}
+
+          <View style={{ height: 100 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    paddingBottom: 10,
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#fff', fontSize: 20 },
+  title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+  headBg: { position: 'absolute', top: 0, left: 0, right: 0, height: 215 },
+  scrollView: { flex: 1 },
+
+  // 顶部搜索和功能区
+  topSection: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, marginBottom: 15 },
+  searchBox: { flex: 1, height: 37, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15 },
+  searchIcon: { width: 19, height: 19, marginRight: 8 },
+  searchInput: { flex: 1, fontSize: 12, color: '#000', padding: 0 },
+  funcBtns: { flexDirection: 'row', marginLeft: 10 },
+  funcItem: { alignItems: 'center', marginLeft: 12 },
+  funcIcon: { width: 16, height: 16 },
+  funcText: { fontSize: 10, color: '#DFDFDF', marginTop: 2 },
+
+  // 类型切换
+  typeSection: { paddingHorizontal: 10, marginBottom: 40 },
+  typeItem: { marginRight: 15, paddingVertical: 8 },
+  typeText: { fontSize: 12, color: '#DFDFDF' },
+  typeTextActive: { color: '#e79018', fontWeight: 'bold', fontSize: 15, textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+
+  // 房间列表
+  roomList: { paddingHorizontal: 10 },
+  roomItem: { width: '100%', height: 84, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 6, position: 'relative' },
+  officialBadge: { position: 'absolute', right: 0, top: 0, width: 48, height: 22 },
+  officialImg: { width: '100%', height: '100%' },
+  mustBeBadge: { position: 'absolute', left: 0, top: 0, width: 51, height: 51 },
+  mustBeImg: { width: '100%', height: '100%' },
+  roomCover: { width: 58, height: 58, justifyContent: 'center', alignItems: 'center', marginRight: 14 },
+  roomCoverImg: { width: 44, height: 44 },
+  roomInfo: { flex: 1 },
+  roomTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 },
+  roomName: { flex: 1, color: '#fff', fontSize: 14, fontWeight: '400', marginRight: 10, textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+  roomType: { color: '#2E0000', fontSize: 12 },
+  roomBottom: { flexDirection: 'row', alignItems: 'center' },
+  userInfo: { flexDirection: 'row', alignItems: 'center', width: '45%' },
+  userAvatar: { width: 24, height: 24, borderRadius: 2, backgroundColor: '#FFDD00', borderWidth: 1.5, borderColor: '#000', marginRight: 5 },
+  userName: { color: '#2E0000', fontSize: 12, fontWeight: 'bold', maxWidth: 60 },
+  goodsNum: { color: '#2E0000', fontSize: 10, width: '35%' },
+  participantBox: { flexDirection: 'row', alignItems: 'center', width: '20%' },
+  participantIcon: { width: 14, height: 14 },
+  participantNum: { color: '#2E0000', fontSize: 12, marginLeft: 3 },
+
+  emptyBox: { alignItems: 'center', paddingTop: 50 },
+  emptyText: { color: '#999', fontSize: 14 },
+});

+ 275 - 0
app/weal/wish.tsx

@@ -0,0 +1,275 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    Dimensions,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+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 wishImages = {
+  bg: `${CDN_BASE}/common/wishBg.png`,
+  title: `${CDN_BASE}/welfare/wishTitle.png`,
+  rule: `${CDN_BASE}/welfare/wishRule.png`,
+  addBg: `${CDN_BASE}/welfare/addBg.png`,
+  addLiBg: `${CDN_BASE}/welfare/addLiBg.png`,
+  addClose: `${CDN_BASE}/welfare/addClose.png`,
+  addSectionBg: `${CDN_BASE}/welfare/toys/addSectionBg.png`,
+  left: `${CDN_BASE}/box/detail/left.png`,
+  right: `${CDN_BASE}/box/detail/right.png`,
+};
+
+interface WishItem {
+  id: string;
+  name: string;
+  spu: { cover: string };
+  quantity: number;
+  completeQuantity: number;
+}
+
+interface GoodsItem {
+  id: string;
+  spu: { cover: string };
+}
+
+export default function WishScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [tableData, setTableData] = useState<WishItem[]>([]);
+  const [active, setActive] = useState(0);
+  const [goodsList, setGoodsList] = useState<GoodsItem[]>([]);
+  const [progress, setProgress] = useState(0);
+
+  const loadData = useCallback(async () => {
+    try {
+      const res = await get('/api/wish/list');
+      if (res.data && res.data.length > 0) {
+        setTableData(res.data);
+      }
+    } catch (error) {
+      console.error('加载祈愿数据失败:', error);
+    }
+  }, []);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  const handlePrev = () => {
+    if (active > 0) setActive(active - 1);
+  };
+
+  const handleNext = () => {
+    if (active < tableData.length - 1) setActive(active + 1);
+  };
+
+  const currentItem = tableData[active];
+  const remaining = currentItem ? currentItem.quantity - currentItem.completeQuantity : 0;
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: wishImages.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>
+
+        <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+          <View style={{ height: insets.top + 50 }} />
+
+          {/* 标题 */}
+          <View style={styles.titleBox}>
+            <Image source={{ uri: wishImages.title }} style={styles.titleImg} contentFit="contain" />
+          </View>
+
+          {/* 规则按钮 */}
+          <TouchableOpacity style={styles.ruleBtn}>
+            <Image source={{ uri: wishImages.rule }} style={styles.ruleBtnImg} contentFit="contain" />
+          </TouchableOpacity>
+
+          {/* 商品轮播 */}
+          <View style={styles.swiperBox}>
+            {active > 0 && (
+              <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
+                <Image source={{ uri: wishImages.left }} style={styles.arrowImg} contentFit="contain" />
+              </TouchableOpacity>
+            )}
+
+            <View style={styles.cardBox}>
+              {currentItem && (
+                <View style={styles.card}>
+                  <View style={styles.imgBox}>
+                    <Image source={{ uri: currentItem.spu?.cover }} style={styles.spuImage} contentFit="contain" />
+                    <View style={styles.remainingBox}>
+                      <Text style={styles.remainingText}>仅剩:{remaining}</Text>
+                    </View>
+                  </View>
+                  <Text style={styles.cardName} numberOfLines={1}>{currentItem.name}</Text>
+                </View>
+              )}
+            </View>
+
+            {active < tableData.length - 1 && (
+              <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
+                <Image source={{ uri: wishImages.right }} style={styles.arrowImg} contentFit="contain" />
+              </TouchableOpacity>
+            )}
+          </View>
+
+          {/* 进度条 */}
+          <View style={styles.progressSection}>
+            <Text style={styles.progressLabel}>进度:</Text>
+            <View style={styles.progressBar}>
+              <View style={[styles.progressFill, { width: `${progress}%` }]} />
+            </View>
+            <Text style={styles.progressText}>{progress}%</Text>
+          </View>
+
+          {/* 材料添加区域 */}
+          <ImageBackground source={{ uri: wishImages.addSectionBg }} style={styles.addSection} resizeMode="stretch">
+            <Text style={styles.addTitle}>材料添加</Text>
+            <View style={styles.addMain}>
+              <TouchableOpacity style={styles.addBtn}>
+                <ImageBackground source={{ uri: wishImages.addBg }} style={styles.addBtnBg} resizeMode="contain">
+                  <Image source={{ uri: `${CDN_BASE}/welfare/add.png` }} style={styles.addIcon} contentFit="contain" />
+                  <Text style={styles.addBtnText}>添加</Text>
+                </ImageBackground>
+              </TouchableOpacity>
+              <View style={styles.addCenter}>
+                <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+                  {goodsList.map((item, index) => (
+                    <View key={item.id || index} style={styles.addItem}>
+                      <ImageBackground source={{ uri: wishImages.addLiBg }} style={styles.addItemBg} resizeMode="contain">
+                        <Image source={{ uri: item.spu?.cover }} style={styles.addItemImg} contentFit="cover" />
+                      </ImageBackground>
+                      <TouchableOpacity style={styles.closeBtn}>
+                        <Image source={{ uri: wishImages.addClose }} style={styles.closeIcon} contentFit="contain" />
+                      </TouchableOpacity>
+                    </View>
+                  ))}
+                  {/* 空位占位 */}
+                  {Array.from({ length: Math.max(0, 6 - goodsList.length) }).map((_, i) => (
+                    <View key={`empty-${i}`} style={styles.emptySlot} />
+                  ))}
+                </ScrollView>
+              </View>
+            </View>
+          </ImageBackground>
+
+          {/* 底部按钮 */}
+          <TouchableOpacity style={styles.bottomBtn}>
+            <ImageBackground source={{ uri: Images.common.butBgHui }} style={styles.bottomBtnBg} resizeMode="contain">
+              <Text style={styles.bottomBtnText}>还差一点点,去收集</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+
+          <Text style={styles.bottomTip}>*本活动最终解释权归本平台所有</Text>
+
+          <View style={{ height: 100 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    paddingBottom: 10,
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#fff', fontSize: 20 },
+  title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+  scrollView: { flex: 1 },
+
+  titleBox: { alignItems: 'center', marginBottom: 10 },
+  titleImg: { width: 288, height: 86 },
+
+  ruleBtn: { position: 'absolute', left: 0, top: 180, zIndex: 10 },
+  ruleBtnImg: { width: 39, height: 20 },
+
+  swiperBox: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20 },
+  prevBtn: { position: 'absolute', left: 15, zIndex: 10 },
+  nextBtn: { position: 'absolute', right: 15, zIndex: 10 },
+  arrowImg: { width: 51, height: 49 },
+
+  cardBox: { alignItems: 'center' },
+  card: { alignItems: 'center' },
+  imgBox: {
+    width: 215,
+    height: 284,
+    backgroundColor: '#FFF7D8',
+    borderWidth: 4.5,
+    borderColor: '#000',
+    justifyContent: 'center',
+    alignItems: 'center',
+    position: 'relative',
+  },
+  spuImage: { width: 200, height: 275 },
+  remainingBox: {
+    position: 'absolute',
+    bottom: '15%',
+    backgroundColor: '#fff',
+    borderWidth: 3,
+    borderColor: '#000',
+    paddingHorizontal: 15,
+    paddingVertical: 5,
+  },
+  remainingText: { fontSize: 13, fontWeight: 'bold', color: '#000' },
+  cardName: { color: '#D0D0D0', fontSize: 12, marginTop: 13, textAlign: 'center', maxWidth: 200 },
+
+  progressSection: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 5, paddingHorizontal: 20 },
+  progressLabel: { color: '#fff', fontSize: 12, width: 40 },
+  progressBar: { flex: 1, height: 14, backgroundColor: '#FFEABE', borderWidth: 2, borderColor: '#000', marginHorizontal: 5 },
+  progressFill: { height: '100%', backgroundColor: '#FFAD00', borderRightWidth: 2, borderRightColor: '#000' },
+  progressText: { color: '#fff', fontSize: 12, width: 40, textAlign: 'right' },
+
+  addSection: { width: SCREEN_WIDTH - 20, height: 124, marginHorizontal: 10, marginTop: 10, paddingTop: 10 },
+  addTitle: { color: '#fff', fontSize: 14, textAlign: 'center', fontWeight: '400', textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, marginBottom: 7 },
+  addMain: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 25 },
+  addBtn: { marginRight: 10 },
+  addBtnBg: { width: 42, height: 42, justifyContent: 'center', alignItems: 'center', paddingTop: 5 },
+  addIcon: { width: 16, height: 16 },
+  addBtnText: { fontSize: 12, color: '#000', fontWeight: '500' },
+  addCenter: { flex: 1, height: 60, backgroundColor: '#FFFBEA', paddingLeft: 20, paddingTop: 7 },
+  addItem: { position: 'relative', marginRight: 10 },
+  addItemBg: { width: 42, height: 42, justifyContent: 'center', alignItems: 'center' },
+  addItemImg: { width: '80%', height: '80%' },
+  closeBtn: { position: 'absolute', right: 0, top: 0, width: 13, height: 13 },
+  closeIcon: { width: '100%', height: '100%' },
+  emptySlot: { width: 42, height: 42, backgroundColor: '#f0f0f0', borderWidth: 1, borderColor: '#ddd', marginRight: 10 },
+
+  bottomBtn: { alignItems: 'center', marginTop: -10 },
+  bottomBtnBg: { width: 160, height: 60, justifyContent: 'center', alignItems: 'center', paddingTop: 4 },
+  bottomBtnText: { color: '#fff', fontSize: 15, fontWeight: 'bold', textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
+
+  bottomTip: { color: 'rgba(255,255,255,0.67)', fontSize: 10, textAlign: 'center', marginTop: 5 },
+});

+ 54 - 0
constants/images.ts

@@ -64,6 +64,8 @@ export const Images = {
     awardMainImg: `${CDN_BASE}/box/awardMainImg.png`,
     goodsItemBg: `${CDN_BASE}/box/goodsItemBg.png`,
     barrageItem: `${CDN_BASE}/box/barrageItem.png`,
+    jackpotImg: `${CDN_BASE}/box/jackpotImg.png`,
+    firstLastBg: `${CDN_BASE}/box/firstLastBg.png`,
     type1: `${CDN_BASE}/box/type1.png`,
     type1On: `${CDN_BASE}/box/type1On.png`,
     type2: `${CDN_BASE}/box/type2.png`,
@@ -77,6 +79,49 @@ export const Images = {
     sortAmount: `${CDN_BASE}/box/sortAmount.png`,
     sortAmountOnT: `${CDN_BASE}/box/sortAmountOnT.png`,
     sortAmountOnB: `${CDN_BASE}/box/sortAmountOnB.png`,
+    detail: {
+      mainGoodsSection: `${CDN_BASE}/box/detail/mainGoodsSection.png`,
+      mainGoodsSectionBtext: `${CDN_BASE}/box/detail/mainGoodsSectionBtext.png`,
+      detailsBut: `${CDN_BASE}/box/detail/detailsBut.png`,
+      nameBg: `${CDN_BASE}/box/detail/nameBg.png`,
+      left: `${CDN_BASE}/box/detail/left.png`,
+      right: `${CDN_BASE}/box/detail/right.png`,
+      positionBgLeft: `${CDN_BASE}/box/detail/positionBgLeft.png`,
+      positionBgRight: `${CDN_BASE}/box/detail/positionBgRight.png`,
+      positionBgleftBg: `${CDN_BASE}/box/detail/positionBgleftBg.png`,
+      positionBgRightBg: `${CDN_BASE}/box/detail/positionBgRightBg.png`,
+      boxDetailBott: `${CDN_BASE}/box/detail/boxDetailBott.png`,
+      base: `${CDN_BASE}/box/detail/base.png`,
+      refresh: `${CDN_BASE}/box/detail/refresh.png`,
+      lock: `${CDN_BASE}/box/detail/lock.png`,
+      unlock: `${CDN_BASE}/box/detail/unlock.png`,
+      Inclusive: `${CDN_BASE}/box/detail/Inclusive.png`,
+      productTitle: `${CDN_BASE}/box/detail/productTitle.png`,
+      levelBoxBg: `${CDN_BASE}/box/detail/levelBoxBg.png`,
+      firstItemBg: `${CDN_BASE}/box/detail/firstItemBg.png`,
+      firstBoxBg: `${CDN_BASE}/box/detail/firstBoxBg.png`,
+      funBoxBg: `${CDN_BASE}/box/detail/funBoxBg.png`,
+      productItemA: `${CDN_BASE}/box/detail/productItemA.png`,
+      productItemB: `${CDN_BASE}/box/detail/productItemB.png`,
+      productItemC: `${CDN_BASE}/box/detail/productItemC.png`,
+      productItemD: `${CDN_BASE}/box/detail/productItemD.png`,
+      levelA: `${CDN_BASE}/box/detail/levelA.png`,
+      levelB: `${CDN_BASE}/box/detail/levelB.png`,
+      levelC: `${CDN_BASE}/box/detail/levelC.png`,
+      levelD: `${CDN_BASE}/box/detail/levelD.png`,
+      recordBg: `${CDN_BASE}/box/detail/recordBg.png`,
+      recordTitleLeft: `${CDN_BASE}/box/detail/recordTitleLeft.png`,
+      recordTitleRight: `${CDN_BASE}/box/detail/recordTitleRight.png`,
+      recordCenterBg: `${CDN_BASE}/box/detail/recordCenterBg.png`,
+      taggingBg: `${CDN_BASE}/box/detail/taggingBg.png`,
+      freedomTitle: `${CDN_BASE}/box/detail/freedomTitle.png`,
+      freedomClose: `${CDN_BASE}/box/detail/freedomClose.png`,
+      lotteryItem: `${CDN_BASE}/box/detail/lotteryItem.png`,
+      lotteryTitle: `${CDN_BASE}/box/detail/lotteryTitle.png`,
+      real100: `${CDN_BASE}/box/detail/real100.png`,
+      packagingBox1: `${CDN_BASE}/box/detail/packagingBox1.png`,
+      packagingBox2: `${CDN_BASE}/box/detail/packagingBox2.png`,
+    },
   },
   welfare: {
     kaixinWelfareBg: `${CDN_BASE}/welfare/kaixinWelfareBg.png`,
@@ -88,6 +133,15 @@ export const Images = {
     indexItem1: `${CDN_BASE}/welfare/indexItem1.png`,
     indexItem2: `${CDN_BASE}/welfare/indexItem2.png`,
     indexItem3: `${CDN_BASE}/welfare/indexItem3.png`,
+    roomInputBg: `${CDN_BASE}/welfare/roomInputBg.png`,
+    roomIcon0: `${CDN_BASE}/welfare/roomIcon0.png`,
+    roomIcon1: `${CDN_BASE}/welfare/roomIcon1.png`,
+    roomIcon2: `${CDN_BASE}/welfare/roomIcon2.png`,
+    roomItemBg: `${CDN_BASE}/welfare/roomItemBg.png`,
+    roomItemImgBg: `${CDN_BASE}/welfare/roomItemImgBg.png`,
+    participationIcon: `${CDN_BASE}/welfare/participationIcon.png`,
+    official: `${CDN_BASE}/welfare/official.png`,
+    mustBe: `${CDN_BASE}/welfare/mustBe.png`,
   },
   mine: {
     kaixinMineBg: `${CDN_BASE}/mine/kaixinMineBg.png`,

+ 47 - 0
services/award.ts

@@ -155,6 +155,12 @@ export const previewOrder = async (poolId: string, quantity?: number, boxNumber?
   if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.PREVIEW, param);
+  
+  // 如果请求失败,抛出错误
+  if (!res.success) {
+    throw new Error(res.msg || '获取订单信息失败');
+  }
+  
   return res.data;
 };
 
@@ -165,6 +171,12 @@ export const applyOrder = async (poolId: string, quantity: number, paymentType:
   if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.APPLY, param);
+  
+  // 如果请求失败,抛出错误
+  if (!res.success) {
+    throw new Error(res.msg || '支付失败');
+  }
+  
   return res.data;
 };
 
@@ -322,6 +334,36 @@ export const getBoxDetail = async (poolId: string, boxNumber?: string) => {
   return res.data;
 };
 
+// 获取上一个盒子
+export const getPreBox = async (poolId: string, boxNumber: string) => {
+  const res = await get(apis.BOX_PRE, { poolId, boxNumber });
+  return res.data;
+};
+
+// 获取下一个盒子
+export const getNextBox = async (poolId: string, boxNumber: string) => {
+  const res = await get(apis.BOX_NEXT, { poolId, boxNumber });
+  return res.data;
+};
+
+// 锁定盒子
+export const lockBox = async (poolId: string, boxNumber: string) => {
+  const res = await postL(apis.BOX_LOCK, { poolId, boxNumber });
+  return res.success;
+};
+
+// 解锁盒子
+export const unlockBox = async (poolId: string, boxNumber: string) => {
+  const res = await postL(apis.BOX_UN_LOCK, { poolId, boxNumber });
+  return res.success;
+};
+
+// 获取不可用座位号
+export const getUnavailableSeatNumbers = async (poolId: string, boxNumber: string) => {
+  const res = await get(apis.UNAVAILABLE_SEAT_NUMBERS, { poolId, boxNumber });
+  return res.data;
+};
+
 // 获取弹幕列表
 export const getFeedbackList = async () => {
   const res = await get(apis.FEEDBACK_LIST);
@@ -385,6 +427,11 @@ export default {
   getPreKing,
   getBoxList,
   getBoxDetail,
+  getPreBox,
+  getNextBox,
+  lockBox,
+  unlockBox,
+  getUnavailableSeatNumbers,
   getFeedbackList,
   submitFeedback,
   getLuckDetail,

+ 2 - 1
services/user.ts

@@ -26,6 +26,7 @@ const apis = {
 
 export interface UserInfo {
   id: string;
+  userId?: string;
   nickname: string;
   avatar: string;
   phone?: string;
@@ -174,7 +175,7 @@ export const claimDailyReward = async (params?: any) => {
 
 // 获取新用户数量
 export const getNewUserNum = async (params?: any) => {
-  const res = await getL(apis.NEW_USER_NUM, params);
+  const res = await get(apis.NEW_USER_NUM, params);
   return res;
 };