zxz преди 3 месеца
родител
ревизия
8dc38ecb60
променени са 47 файла, в които са добавени 7135 реда и са изтрити 641 реда
  1. 92 50
      app/(tabs)/box.tsx
  2. 12 12
      app/(tabs)/mine.tsx
  3. 7 0
      app/_layout.tsx
  4. 9 0
      app/agreement/_layout.tsx
  5. 168 0
      app/agreement/index.tsx
  6. 420 0
      app/award-detail-yfs/components/BoxChooseModal.tsx
  7. 547 0
      app/award-detail-yfs/components/NumChooseModal.tsx
  8. 227 0
      app/award-detail-yfs/components/ProductListYfs.tsx
  9. 175 23
      app/award-detail-yfs/index.tsx
  10. 1 0
      app/award-detail/_layout.tsx
  11. 35 5
      app/award-detail/components/CheckoutModal.tsx
  12. 110 0
      app/award-detail/components/ExplainSection.tsx
  13. 511 0
      app/award-detail/components/LotteryResultModal.tsx
  14. 129 78
      app/award-detail/components/ProductList.tsx
  15. 2 2
      app/award-detail/components/RecordModal.tsx
  16. 58 29
      app/award-detail/components/RuleModal.tsx
  17. 30 17
      app/award-detail/index.tsx
  18. 481 0
      app/award-detail/swipe.tsx
  19. 0 1
      app/boxInBox/_layout.tsx
  20. 302 82
      app/boxInBox/boxList.tsx
  21. 411 0
      app/boxInBox/components/DetailsPopup.tsx
  22. 81 21
      app/boxInBox/index.tsx
  23. 9 0
      app/exchange/_layout.tsx
  24. 222 0
      app/exchange/index.tsx
  25. 9 0
      app/feedback/_layout.tsx
  26. 262 0
      app/feedback/index.tsx
  27. 9 0
      app/integral/_layout.tsx
  28. 610 0
      app/integral/index.tsx
  29. 9 0
      app/message/_layout.tsx
  30. 254 0
      app/message/index.tsx
  31. 9 0
      app/profile/_layout.tsx
  32. 364 0
      app/profile/index.tsx
  33. 9 0
      app/setting/_layout.tsx
  34. 272 0
      app/setting/index.tsx
  35. 11 0
      app/store/_layout.tsx
  36. 204 0
      app/store/checkout.tsx
  37. 213 0
      app/store/components/CheckoutModal.tsx
  38. 431 302
      app/store/index.tsx
  39. 215 0
      app/store/packages.tsx
  40. 16 0
      constants/images.ts
  41. 9 6
      contexts/AuthContext.tsx
  42. 71 0
      package-lock.json
  43. 3 0
      package.json
  44. 40 6
      services/award.ts
  45. 9 0
      services/base.ts
  46. 59 7
      services/http.ts
  47. 8 0
      services/user.ts

+ 92 - 50
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';
 
@@ -96,6 +96,17 @@ export default function BoxScreen() {
     loadBarrage();
   }, [typeIndex, priceSort]);
 
+  // 执行搜索
+  const handleSearch = () => {
+    setList([]);
+    setCurrent(1);
+    setHasMore(true);
+    // 需要延迟一下让状态更新
+    setTimeout(() => {
+      loadData(true);
+    }, 100);
+  };
+
   const handleRefresh = () => {
     setRefreshing(true);
     loadData(true);
@@ -203,38 +214,41 @@ export default function BoxScreen() {
           </View>
         </View>
       )}
+    </View>
+  );
 
-      {/* 分类筛选 */}
-      <View style={styles.typeSection}>
-        <View style={styles.typeList}>
-          {typeList.map((item, index) => (
-            <TouchableOpacity
-              key={index}
-              style={styles.typeItem}
-              onPress={() => handleTypeChange(index)}
-            >
-              <Image
-                source={{ uri: typeIndex === index ? item.imgOn : item.img }}
-                style={styles.typeImage}
-                contentFit="contain"
-              />
-            </TouchableOpacity>
-          ))}
-        </View>
-        <TouchableOpacity style={styles.sortBtn} onPress={handlePriceSort}>
-          <Image
-            source={{
-              uri: priceSort === 0
-                ? Images.box.sortAmount
-                : priceSort === 1
-                ? Images.box.sortAmountOnT
-                : Images.box.sortAmountOnB,
-            }}
-            style={styles.sortIcon}
-            contentFit="contain"
-          />
-        </TouchableOpacity>
+  // 分类筛选单独渲染
+  const renderTypeSection = () => (
+    <View style={styles.typeSection}>
+      <View style={styles.typeList}>
+        {typeList.map((item, index) => (
+          <TouchableOpacity
+            key={index}
+            style={styles.typeItem}
+            onPress={() => handleTypeChange(index)}
+            activeOpacity={0.7}
+          >
+            <Image
+              source={{ uri: typeIndex === index ? item.imgOn : item.img }}
+              style={styles.typeImage}
+              contentFit="contain"
+            />
+          </TouchableOpacity>
+        ))}
       </View>
+      <TouchableOpacity style={styles.sortBtn} onPress={handlePriceSort} activeOpacity={0.7}>
+        <Image
+          source={{
+            uri: priceSort === 0
+              ? Images.box.sortAmount
+              : priceSort === 1
+              ? Images.box.sortAmountOnT
+              : Images.box.sortAmountOnB,
+          }}
+          style={styles.sortIcon}
+          contentFit="contain"
+        />
+      </TouchableOpacity>
     </View>
   );
 
@@ -285,8 +299,13 @@ export default function BoxScreen() {
               placeholder="搜索"
               placeholderTextColor="rgba(255,255,255,0.5)"
               returnKeyType="search"
-              onSubmitEditing={() => loadData(true)}
+              onSubmitEditing={handleSearch}
             />
+            {keyword.length > 0 && (
+              <TouchableOpacity onPress={handleSearch} style={styles.searchBtn}>
+                <Text style={styles.searchBtnText}>搜索</Text>
+              </TouchableOpacity>
+            )}
           </View>
         </View>
 
@@ -295,7 +314,12 @@ export default function BoxScreen() {
           data={list}
           renderItem={renderItem}
           keyExtractor={(item) => item.id}
-          ListHeaderComponent={renderHeader}
+          ListHeaderComponent={() => (
+            <>
+              {renderHeader()}
+              {renderTypeSection()}
+            </>
+          )}
           ListFooterComponent={renderFooter}
           ListEmptyComponent={renderEmpty}
           contentContainerStyle={styles.listContent}
@@ -356,6 +380,14 @@ const styles = StyleSheet.create({
     fontSize: 12,
     padding: 0,
   },
+  searchBtn: {
+    paddingHorizontal: 8,
+    paddingVertical: 2,
+  },
+  searchBtnText: {
+    color: '#fff',
+    fontSize: 12,
+  },
   mainImageContainer: {
     position: 'absolute',
     left: 0,
@@ -373,27 +405,33 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     paddingHorizontal: 10,
     paddingVertical: 10,
+    position: 'relative',
+    zIndex: 10,
+    backgroundColor: 'transparent',
   },
   typeList: {
-    flex: 1,
     flexDirection: 'row',
-    justifyContent: 'space-around',
+    justifyContent: 'flex-start',
+    flex: 1,
   },
   typeItem: {
-    width: 75,
+    width: 60,
     height: 30,
+    marginRight: 5,
   },
   typeImage: {
     width: '100%',
     height: '100%',
   },
   sortBtn: {
-    width: '10%',
+    width: 40,
+    height: 30,
     alignItems: 'center',
+    justifyContent: 'center',
   },
   sortIcon: {
-    width: 20,
-    height: 20,
+    width: 30,
+    height: 30,
   },
   listContent: {
     paddingHorizontal: 10,
@@ -410,13 +448,17 @@ const styles = StyleSheet.create({
   itemImage: {
     width: '100%',
     height: 142,
-    borderRadius: 8,
+    borderRadius: 0,
   },
   itemInfo: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
     paddingHorizontal: 15,
     paddingTop: 15,
   },
   itemName: {
+    flex: 1,
     color: '#fff',
     fontSize: 14,
   },
@@ -424,7 +466,7 @@ const styles = StyleSheet.create({
     color: '#ff0000',
     fontSize: 12,
     fontWeight: 'bold',
-    marginTop: 5,
+    marginLeft: 10,
   },
   priceUnit: {
     fontSize: 12,

+ 12 - 12
app/(tabs)/mine.tsx

@@ -3,14 +3,14 @@ import { Image } from 'expo-image';
 import { useFocusEffect, useRouter } from 'expo-router';
 import React, { useCallback, useState } from 'react';
 import {
-  Alert,
-  ImageBackground,
-  ScrollView,
-  StatusBar,
-  StyleSheet,
-  Text,
-  TouchableOpacity,
-  View,
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
@@ -108,7 +108,7 @@ export default function MineScreen() {
         router.push('/orders' as any);
         break;
       case '6_1': // 兑换码
-        Alert.alert('提示', '兑换码功能');
+        router.push('/exchange' as any);
         break;
       case '4_4': // 联系客服
         Alert.alert('联系客服', '客服时间:10:00 ~ 18:00');
@@ -117,10 +117,10 @@ export default function MineScreen() {
         router.push('/address' as any);
         break;
       case '4_9': // 意见反馈
-        Alert.alert('提示', '意见反馈功能');
+        router.push('/feedback' as any);
         break;
       case '4_5': // 设置
-        Alert.alert('提示', '设置功能');
+        router.push('/setting' as any);
         break;
       default:
         break;
@@ -245,7 +245,7 @@ export default function MineScreen() {
                   {userInfo?.nickname || '暂未登录!'}
                 </Text>
                 {userInfo && (
-                  <TouchableOpacity onPress={() => handleMenuPress('/user-info')}>
+                  <TouchableOpacity onPress={() => handleMenuPress('/profile')}>
                     <Image
                       source={{ uri: Images.mine.editIcon }}
                       style={styles.editIcon}

+ 7 - 0
app/_layout.tsx

@@ -31,6 +31,13 @@ export default function RootLayout() {
             <Stack.Screen name="coupon" options={{ headerShown: false }} />
             <Stack.Screen name="store" options={{ headerShown: false }} />
             <Stack.Screen name="magic" options={{ headerShown: false }} />
+            <Stack.Screen name="integral" options={{ headerShown: false }} />
+            <Stack.Screen name="message" options={{ headerShown: false }} />
+            <Stack.Screen name="exchange" options={{ headerShown: false }} />
+            <Stack.Screen name="feedback" options={{ headerShown: false }} />
+            <Stack.Screen name="setting" options={{ headerShown: false }} />
+            <Stack.Screen name="agreement" options={{ headerShown: false }} />
+            <Stack.Screen name="profile" options={{ headerShown: false }} />
             <Stack.Screen name="test" options={{ headerShown: false }} />
             <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
           </Stack>

+ 9 - 0
app/agreement/_layout.tsx

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

+ 168 - 0
app/agreement/index.tsx

@@ -0,0 +1,168 @@
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { getParamConfig } from '@/services/user';
+
+// 简单的HTML标签清理函数
+const stripHtmlTags = (html: string): string => {
+  if (!html) return '';
+  // 移除HTML标签,保留文本内容
+  return html
+    .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
+    .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
+    .replace(/<[^>]+>/g, '\n')
+    .replace(/&nbsp;/g, ' ')
+    .replace(/&lt;/g, '<')
+    .replace(/&gt;/g, '>')
+    .replace(/&amp;/g, '&')
+    .replace(/&quot;/g, '"')
+    .replace(/\n\s*\n/g, '\n\n')
+    .trim();
+};
+
+export default function AgreementScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const params = useLocalSearchParams<{ type: string }>();
+  
+  const [loading, setLoading] = useState(true);
+  const [title, setTitle] = useState('');
+  const [content, setContent] = useState('');
+
+  useEffect(() => {
+    loadContent();
+  }, [params.type]);
+
+  const loadContent = async () => {
+    try {
+      setLoading(true);
+      const type = params.type || 'user.html';
+      console.log('加载协议类型:', type);
+      const res = await getParamConfig(type) as any;
+      console.log('协议内容返回:', res);
+      
+      if (res) {
+        // API返回的数据结构: { title: string, data: string (HTML内容) }
+        const titleText = res.title || (type === 'user.html' ? '用户协议' : '隐私协议');
+        setTitle(titleText);
+        // 清理HTML标签,只保留纯文本
+        const rawContent = res.data || res || '';
+        setContent(stripHtmlTags(typeof rawContent === 'string' ? rawContent : JSON.stringify(rawContent)));
+      }
+    } catch (error) {
+      console.error('获取协议内容失败:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>{title || '协议'}</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      {loading ? (
+        <View style={styles.loadingBox}>
+          <ActivityIndicator size="large" color="#FC7D2E" />
+        </View>
+      ) : content ? (
+        <ScrollView 
+          style={styles.scrollView}
+          contentContainerStyle={styles.scrollContent}
+          showsVerticalScrollIndicator={false}
+        >
+          <Text style={styles.contentText}>{content}</Text>
+        </ScrollView>
+      ) : (
+        <View style={styles.emptyBox}>
+          <Text style={styles.emptyText}>暂无内容</Text>
+        </View>
+      )}
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#333',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+    backgroundColor: '#333',
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  loadingBox: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: '#fff',
+  },
+  scrollView: {
+    flex: 1,
+    backgroundColor: '#fff',
+  },
+  scrollContent: {
+    padding: 15,
+    paddingBottom: 50,
+  },
+  contentText: {
+    fontSize: 14,
+    lineHeight: 22,
+    color: '#333',
+  },
+  emptyBox: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: '#fff',
+  },
+  emptyText: {
+    fontSize: 14,
+    color: '#999',
+  },
+});

+ 420 - 0
app/award-detail-yfs/components/BoxChooseModal.tsx

@@ -0,0 +1,420 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    ImageBackground,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+
+import { Images } from '@/constants/images';
+import { getBoxList } from '@/services/award';
+
+// 等级筛选标签
+const TABS = [
+  { title: '全部', value: '' },
+  { title: '超神款', value: 'A' },
+  { title: '欧皇款', value: 'B' },
+  { title: '隐藏款', value: 'C' },
+  { title: '普通款', value: 'D' },
+];
+
+interface BoxItem {
+  number: string;
+  quantity: number;
+  leftQuantity: number;
+  quantityA: number;
+  quantityB: number;
+  quantityC: number;
+  quantityD: number;
+  leftQuantityA: number;
+  leftQuantityB: number;
+  leftQuantityC: number;
+  leftQuantityD: number;
+}
+
+interface BoxChooseModalProps {
+  poolId: string;
+  onChoose: (boxNumber: string) => void;
+}
+
+export interface BoxChooseModalRef {
+  show: () => void;
+  close: () => void;
+}
+
+export const BoxChooseModal = forwardRef<BoxChooseModalRef, BoxChooseModalProps>(
+  ({ poolId, onChoose }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [loading, setLoading] = useState(false);
+    const [currentTab, setCurrentTab] = useState(TABS[0]);
+    const [boxList, setBoxList] = useState<BoxItem[]>([]);
+    const [pageNum, setPageNum] = useState(1);
+    const [hasMore, setHasMore] = useState(true);
+
+    const loadData = useCallback(async (page: number, level?: string) => {
+      if (page === 1) setLoading(true);
+
+      try {
+        const levelValue = level === '' ? undefined : level === 'A' ? 1 : level === 'B' ? 2 : level === 'C' ? 3 : level === 'D' ? 4 : undefined;
+        const res = await getBoxList(poolId, levelValue, page, 20);
+        if (res && res.records) {
+          const newList = res.records.filter((item: BoxItem) => item.leftQuantity > 0);
+          if (page === 1) {
+            setBoxList(newList);
+          } else {
+            setBoxList(prev => [...prev, ...newList]);
+          }
+          setHasMore(res.records.length >= 20);
+          setPageNum(page);
+        }
+      } catch (error) {
+        console.error('加载盒子列表失败:', error);
+      } finally {
+        setLoading(false);
+      }
+    }, [poolId]);
+
+    useImperativeHandle(ref, () => ({
+      show: () => {
+        setVisible(true);
+        setCurrentTab(TABS[0]);
+        setPageNum(1);
+        loadData(1, '');
+      },
+      close: () => {
+        setVisible(false);
+        setBoxList([]);
+      },
+    }));
+
+    const close = () => {
+      setVisible(false);
+    };
+
+    const clickTab = (tab: typeof TABS[0]) => {
+      setCurrentTab(tab);
+      setPageNum(1);
+      loadData(1, tab.value);
+    };
+
+    const loadMore = () => {
+      if (!loading && hasMore) {
+        loadData(pageNum + 1, currentTab.value);
+      }
+    };
+
+    const choose = (item: BoxItem) => {
+      if (item.leftQuantity <= 0) return;
+      onChoose(item.number);
+      close();
+    };
+
+    const handleScroll = (event: any) => {
+      const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
+      const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 50;
+      if (isCloseToBottom) {
+        loadMore();
+      }
+    };
+
+    return (
+      <Modal visible={visible} transparent animationType="slide" onRequestClose={close}>
+        <View style={styles.overlay}>
+          <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={close} />
+          <ImageBackground source={{ uri: Images.box.detail.recordBg }} style={styles.container} resizeMode="cover">
+            {/* 标题 */}
+            <View style={styles.titleSection}>
+              <Text style={styles.title}>换盒</Text>
+              <TouchableOpacity style={styles.closeBtn} onPress={close}>
+                <Text style={styles.closeText}>×</Text>
+              </TouchableOpacity>
+            </View>
+
+            {/* 标签页 */}
+            <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsScroll}>
+              <View style={styles.tabs}>
+                {TABS.map((tab) => (
+                  <TouchableOpacity
+                    key={tab.value}
+                    style={[styles.tabItem, currentTab.value === tab.value && styles.tabItemActive]}
+                    onPress={() => clickTab(tab)}
+                  >
+                    <Text style={[styles.tabText, currentTab.value === tab.value && styles.tabTextActive]}>
+                      {tab.title}
+                    </Text>
+                  </TouchableOpacity>
+                ))}
+              </View>
+            </ScrollView>
+
+            {/* 盒子列表 */}
+            <ScrollView
+              style={styles.listScroll}
+              showsVerticalScrollIndicator={false}
+              onScroll={handleScroll}
+              scrollEventThrottle={16}
+            >
+              <View style={styles.listContainer}>
+                {loading && pageNum === 1 ? (
+                  <View style={styles.loadingBox}>
+                    <ActivityIndicator color="#FFC900" size="large" />
+                  </View>
+                ) : boxList.length === 0 ? (
+                  <View style={styles.emptyBox}>
+                    <Text style={styles.emptyText}>暂无可用盒子</Text>
+                  </View>
+                ) : (
+                  boxList.map((item, index) => (
+                    <TouchableOpacity
+                      key={item.number}
+                      style={styles.boxItem}
+                      onPress={() => choose(item)}
+                      activeOpacity={0.8}
+                    >
+                      <View style={styles.itemIndex}>
+                        <Text style={styles.itemIndexText}>{index + 1}</Text>
+                      </View>
+                      <View style={styles.itemContent}>
+                        <View style={styles.leftSection}>
+                          <Image
+                            source={{ uri: Images.box.detail.boxIcon }}
+                            style={styles.boxIcon}
+                            contentFit="contain"
+                          />
+                          <Text style={styles.leftText}>剩{item.leftQuantity}发</Text>
+                        </View>
+                        <View style={styles.divider} />
+                        <View style={styles.levelList}>
+                          <View style={styles.levelBox}>
+                            <Image source={{ uri: Images.box.detail.levelTextA }} style={styles.levelIcon} contentFit="contain" />
+                            <View style={styles.numBox}>
+                              <Text style={styles.currentNum}>{item.leftQuantityA}</Text>
+                              <Text style={styles.totalNum}>/{item.quantityA}</Text>
+                            </View>
+                          </View>
+                          <View style={styles.levelBox}>
+                            <Image source={{ uri: Images.box.detail.levelTextB }} style={styles.levelIcon} contentFit="contain" />
+                            <View style={styles.numBox}>
+                              <Text style={styles.currentNum}>{item.leftQuantityB}</Text>
+                              <Text style={styles.totalNum}>/{item.quantityB}</Text>
+                            </View>
+                          </View>
+                          <View style={styles.levelBox}>
+                            <Image source={{ uri: Images.box.detail.levelTextC }} style={styles.levelIcon} contentFit="contain" />
+                            <View style={styles.numBox}>
+                              <Text style={styles.currentNum}>{item.leftQuantityC}</Text>
+                              <Text style={styles.totalNum}>/{item.quantityC}</Text>
+                            </View>
+                          </View>
+                          <View style={styles.levelBox}>
+                            <Image source={{ uri: Images.box.detail.levelTextD }} style={styles.levelIcon} contentFit="contain" />
+                            <View style={styles.numBox}>
+                              <Text style={styles.currentNum}>{item.leftQuantityD}</Text>
+                              <Text style={styles.totalNum}>/{item.quantityD}</Text>
+                            </View>
+                          </View>
+                        </View>
+                      </View>
+                    </TouchableOpacity>
+                  ))
+                )}
+                {loading && pageNum > 1 && (
+                  <View style={styles.loadMoreBox}>
+                    <ActivityIndicator color="#FFC900" size="small" />
+                  </View>
+                )}
+              </View>
+            </ScrollView>
+          </ImageBackground>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  mask: { flex: 1 },
+  container: {
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingTop: 15,
+    paddingBottom: 34,
+    maxHeight: '80%',
+  },
+  titleSection: {
+    alignItems: 'center',
+    paddingVertical: 15,
+    position: 'relative',
+  },
+  title: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 2,
+  },
+  closeBtn: {
+    position: 'absolute',
+    right: 15,
+    top: 10,
+    width: 24,
+    height: 24,
+    backgroundColor: '#ebebeb',
+    borderRadius: 12,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  closeText: { fontSize: 18, color: '#a2a2a2', marginTop: -2 },
+  tabsScroll: {
+    maxHeight: 40,
+    marginHorizontal: 10,
+    marginBottom: 10,
+  },
+  tabs: {
+    flexDirection: 'row',
+  },
+  tabItem: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    backgroundColor: 'rgba(255,255,255,0.2)',
+    borderRadius: 15,
+    marginRight: 8,
+  },
+  tabItemActive: {
+    backgroundColor: '#FFC900',
+  },
+  tabText: {
+    fontSize: 12,
+    color: '#fff',
+  },
+  tabTextActive: {
+    color: '#000',
+    fontWeight: 'bold',
+  },
+  listScroll: {
+    height: 400,
+    marginHorizontal: 10,
+  },
+  listContainer: {
+    backgroundColor: '#f3f3f3',
+    borderWidth: 2,
+    borderColor: '#000',
+    padding: 15,
+    borderRadius: 4,
+  },
+  loadingBox: {
+    height: 200,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  emptyBox: {
+    height: 200,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  emptyText: {
+    fontSize: 14,
+    color: '#999',
+  },
+  boxItem: {
+    backgroundColor: '#fff',
+    borderWidth: 3,
+    borderColor: '#000',
+    borderRadius: 4,
+    marginBottom: 10,
+    position: 'relative',
+    shadowColor: '#FFC900',
+    shadowOffset: { width: 0, height: 3 },
+    shadowOpacity: 1,
+    shadowRadius: 0,
+    elevation: 3,
+  },
+  itemIndex: {
+    position: 'absolute',
+    left: 0,
+    top: 0,
+    width: 22,
+    height: 22,
+    borderWidth: 1.5,
+    borderColor: '#000',
+    backgroundColor: '#fff',
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 1,
+  },
+  itemIndexText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#000',
+  },
+  itemContent: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    padding: 12,
+  },
+  leftSection: {
+    width: 74,
+    alignItems: 'center',
+  },
+  boxIcon: {
+    width: 24,
+    height: 24,
+  },
+  leftText: {
+    fontSize: 12,
+    color: '#000',
+    marginTop: 4,
+  },
+  divider: {
+    width: 1,
+    height: 40,
+    backgroundColor: '#dcdad3',
+    opacity: 0.5,
+    marginRight: 10,
+  },
+  levelList: {
+    flex: 1,
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+  },
+  levelBox: {
+    width: '50%',
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 4,
+  },
+  levelIcon: {
+    width: 45,
+    height: 16,
+    marginRight: 4,
+  },
+  numBox: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  currentNum: {
+    fontSize: 13,
+    fontWeight: '500',
+    color: '#000',
+  },
+  totalNum: {
+    fontSize: 11,
+    color: '#666',
+  },
+  loadMoreBox: {
+    paddingVertical: 15,
+    alignItems: 'center',
+  },
+});

+ 547 - 0
app/award-detail-yfs/components/NumChooseModal.tsx

@@ -0,0 +1,547 @@
+import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    Dimensions,
+    ImageBackground,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+
+import { Images } from '@/constants/images';
+import { getUnavailableSeatNumbers, previewOrder } from '@/services/award';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const ITEM_WIDTH = Math.floor((SCREEN_WIDTH - 28 - 12 * 4) / 5);
+
+// 等级配置
+const LEVEL_MAP: Record<string, { title: string; color: string }> = {
+  A: { title: '超神款', color: '#FF4444' },
+  B: { title: '欧皇款', color: '#FF9600' },
+  C: { title: '隐藏款', color: '#9B59B6' },
+  D: { title: '普通款', color: '#00CCFF' },
+};
+
+interface BoxData {
+  number: string;
+  quantity: number;
+  lastNumber: number;
+}
+
+interface NumChooseModalProps {
+  poolId: string;
+  onPay: (params: { preview: any; seatNumbers: number[]; boxNumber: string }) => void;
+}
+
+export interface NumChooseModalRef {
+  show: (box: BoxData) => void;
+  close: () => void;
+}
+
+export const NumChooseModal = forwardRef<NumChooseModalRef, NumChooseModalProps>(
+  ({ poolId, onPay }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [box, setBox] = useState<BoxData | null>(null);
+    const [tabs, setTabs] = useState<{ title: string; value: number; data: number[] }[]>([]);
+    const [currentTab, setCurrentTab] = useState<{ title: string; value: number; data: number[] } | null>(null);
+    const [checkMap, setCheckMap] = useState<Record<number, number>>({});
+    const [useMap, setUseMap] = useState<Record<number, string>>({});
+    const [lockMap, setLockMap] = useState<Record<number, number>>({});
+    const [loading, setLoading] = useState(false);
+
+    // 已选择的号码列表
+    const chooseData = Object.values(checkMap);
+
+    // 初始化标签页
+    const handleTab = useCallback((boxData: BoxData) => {
+      const totalData: number[] = [];
+      for (let i = 1; i <= boxData.quantity; i++) {
+        totalData.push(i);
+      }
+      const newTabs: { title: string; value: number; data: number[] }[] = [];
+      const count = Math.floor(boxData.quantity / 100) + (boxData.quantity % 100 > 0 ? 1 : 0);
+      for (let i = 0; i < count; i++) {
+        let title = `${100 * i + 1}~${100 * i + 100}`;
+        if (100 * (i + 1) > totalData.length) {
+          title = `${100 * i + 1}~${totalData.length}`;
+        }
+        newTabs.push({
+          title,
+          value: i + 1,
+          data: totalData.slice(100 * i, 100 * (i + 1)),
+        });
+      }
+      setTabs(newTabs);
+      setCurrentTab(newTabs[0] || null);
+    }, []);
+
+    // 获取不可用座位号
+    const getData = useCallback(async (boxData: BoxData, tab?: { data: number[] }) => {
+      try {
+        let startSeatNumber = 1;
+        let endSeatNumber = 100;
+        if (tab && tab.data.length > 0) {
+          startSeatNumber = tab.data[0];
+          endSeatNumber = tab.data[tab.data.length - 1];
+        }
+        const res = await getUnavailableSeatNumbers(poolId, boxData.number, startSeatNumber, endSeatNumber);
+        if (res) {
+          const nums: number[] = [];
+          if (res.usedSeatNumbers) {
+            const map: Record<number, string> = {};
+            res.usedSeatNumbers.forEach((item: { seatNumber: number; level: string }) => {
+              map[item.seatNumber] = item.level;
+              nums.push(item.seatNumber);
+            });
+            setUseMap(map);
+          }
+          if (res.applyedSeatNumbers) {
+            const map: Record<number, number> = {};
+            res.applyedSeatNumbers.forEach((item: number) => {
+              map[item] = item;
+              nums.push(item);
+            });
+            setLockMap(map);
+          }
+          // 移除已被占用的选择
+          if (nums.length > 0) {
+            setCheckMap((prev) => {
+              const newMap = { ...prev };
+              nums.forEach((n) => delete newMap[n]);
+              return newMap;
+            });
+          }
+        }
+      } catch (error) {
+        console.error('获取座位号失败:', error);
+      }
+    }, [poolId]);
+
+    useImperativeHandle(ref, () => ({
+      show: (boxData: BoxData) => {
+        setBox(boxData);
+        setCheckMap({});
+        setUseMap({});
+        setLockMap({});
+        setVisible(true);
+        handleTab(boxData);
+        // 延迟获取数据,等待标签页初始化完成
+        setTimeout(() => {
+          const totalData: number[] = [];
+          for (let i = 1; i <= boxData.quantity; i++) {
+            totalData.push(i);
+          }
+          const firstTabData = totalData.slice(0, 100);
+          getData(boxData, { data: firstTabData });
+        }, 100);
+      },
+      close: () => {
+        setVisible(false);
+        setBox(null);
+        setTabs([]);
+        setCurrentTab(null);
+        setCheckMap({});
+        setUseMap({});
+        setLockMap({});
+      },
+    }));
+
+    const close = () => {
+      setVisible(false);
+    };
+
+    // 切换标签页
+    const clickTab = (tab: { title: string; value: number; data: number[] }) => {
+      setCurrentTab(tab);
+      if (box) getData(box, tab);
+    };
+
+    // 选择/取消选择号码
+    const choose = (item: number) => {
+      if (useMap[item] || lockMap[item]) return;
+      setCheckMap((prev) => {
+        const newMap = { ...prev };
+        if (newMap[item]) {
+          delete newMap[item];
+        } else {
+          if (Object.keys(newMap).length >= 50) {
+            Alert.alert('提示', '最多不超过50发');
+            return prev;
+          }
+          newMap[item] = item;
+        }
+        return newMap;
+      });
+    };
+
+    // 删除已选择的号码
+    const deleteChoose = (item: number) => {
+      setCheckMap((prev) => {
+        const newMap = { ...prev };
+        delete newMap[item];
+        return newMap;
+      });
+    };
+
+    // 预览订单
+    const preview = async () => {
+      if (!box) return;
+      if (chooseData.length <= 0) {
+        Alert.alert('提示', '请选择号码');
+        return;
+      }
+      setLoading(true);
+      try {
+        const res = await previewOrder(poolId, chooseData.length, box.number, chooseData);
+        if (res) {
+          if (res.duplicateSeatNumbers && res.duplicateSeatNumbers.length > 0) {
+            Alert.alert('提示', `${res.duplicateSeatNumbers.join(',')}号被占用`);
+            // 移除被占用的号码
+            setCheckMap((prev) => {
+              const newMap = { ...prev };
+              res.duplicateSeatNumbers.forEach((n: number) => delete newMap[n]);
+              return newMap;
+            });
+            getData(box);
+            return;
+          }
+          onPay({ preview: res, seatNumbers: chooseData, boxNumber: box.number });
+          close();
+        }
+      } catch (error: any) {
+        Alert.alert('提示', error?.message || '获取订单信息失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    return (
+      <Modal visible={visible} transparent animationType="slide" onRequestClose={close}>
+        <View style={styles.overlay}>
+          <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={close} />
+          <ImageBackground source={{ uri: Images.box.detail.recordBg }} style={styles.container} resizeMode="cover">
+            {/* 标题 */}
+            <View style={styles.titleSection}>
+              <Text style={styles.title}>换盒</Text>
+              <TouchableOpacity style={styles.closeBtn} onPress={close}>
+                <Text style={styles.closeText}>×</Text>
+              </TouchableOpacity>
+            </View>
+
+            {/* 标签页 */}
+            {tabs.length > 1 && (
+              <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsScroll}>
+                <View style={styles.tabs}>
+                  {tabs.map((tab) => (
+                    <TouchableOpacity
+                      key={tab.value}
+                      style={[styles.tabItem, currentTab?.value === tab.value && styles.tabItemActive]}
+                      onPress={() => clickTab(tab)}
+                    >
+                      <Text style={[styles.tabText, currentTab?.value === tab.value && styles.tabTextActive]}>
+                        {tab.title}
+                      </Text>
+                    </TouchableOpacity>
+                  ))}
+                </View>
+              </ScrollView>
+            )}
+
+            {/* 号码网格 */}
+            <ScrollView style={styles.gridScroll} showsVerticalScrollIndicator={false}>
+              <View style={styles.grid}>
+                {currentTab?.data.map((item) => {
+                  const isChecked = !!checkMap[item];
+                  const isUsed = !!useMap[item];
+                  const isLocked = !!lockMap[item];
+                  return (
+                    <TouchableOpacity
+                      key={item}
+                      style={[
+                        styles.gridItem,
+                        isChecked && styles.gridItemActive,
+                        isUsed && styles.gridItemUsed,
+                        isLocked && styles.gridItemLocked,
+                      ]}
+                      onPress={() => choose(item)}
+                      disabled={isUsed || isLocked}
+                    >
+                      {isUsed ? (
+                        <View style={styles.usedContent}>
+                          <Text style={styles.usedNum}>{item}号</Text>
+                          <Text style={[styles.levelTitle, { color: LEVEL_MAP[useMap[item]]?.color }]}>
+                            {LEVEL_MAP[useMap[item]]?.title}
+                          </Text>
+                        </View>
+                      ) : (
+                        <Text style={[styles.gridItemText, isLocked && styles.gridItemTextLocked]}>
+                          {item}号
+                        </Text>
+                      )}
+                      {isChecked && (
+                        <View style={styles.checkIcon}>
+                          <Text style={styles.checkIconText}>✓</Text>
+                        </View>
+                      )}
+                      {isLocked && (
+                        <View style={styles.lockIcon}>
+                          <Text style={styles.lockIconText}>🔒</Text>
+                        </View>
+                      )}
+                    </TouchableOpacity>
+                  );
+                })}
+              </View>
+            </ScrollView>
+
+            {/* 已选择的号码 */}
+            <View style={styles.selectedSection}>
+              <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.selectedScroll}>
+                {chooseData.map((item) => (
+                  <View key={item} style={styles.selectedItem}>
+                    <Text style={styles.selectedText}>{item}号</Text>
+                    <TouchableOpacity style={styles.selectedClose} onPress={() => deleteChoose(item)}>
+                      <Text style={styles.selectedCloseText}>×</Text>
+                    </TouchableOpacity>
+                  </View>
+                ))}
+              </ScrollView>
+            </View>
+
+            {/* 确认按钮 */}
+            <View style={styles.btnBox}>
+              <TouchableOpacity
+                style={[styles.submitBtn, loading && styles.submitBtnDisabled]}
+                onPress={preview}
+                disabled={loading}
+              >
+                {loading ? (
+                  <ActivityIndicator color="#fff" size="small" />
+                ) : (
+                  <>
+                    <Text style={styles.submitText}>确定选择</Text>
+                    <Text style={styles.submitSubText}>已选择({chooseData.length})发</Text>
+                  </>
+                )}
+              </TouchableOpacity>
+            </View>
+          </ImageBackground>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  mask: { flex: 1 },
+  container: {
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingTop: 15,
+    paddingBottom: 34,
+    maxHeight: '80%',
+  },
+  titleSection: {
+    alignItems: 'center',
+    paddingVertical: 15,
+    position: 'relative',
+  },
+  title: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 2,
+  },
+  closeBtn: {
+    position: 'absolute',
+    right: 15,
+    top: 10,
+    width: 24,
+    height: 24,
+    backgroundColor: '#ebebeb',
+    borderRadius: 12,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  closeText: { fontSize: 18, color: '#a2a2a2', marginTop: -2 },
+  tabsScroll: {
+    maxHeight: 40,
+    marginHorizontal: 10,
+    marginBottom: 10,
+  },
+  tabs: {
+    flexDirection: 'row',
+  },
+  tabItem: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    backgroundColor: 'rgba(255,255,255,0.2)',
+    borderRadius: 15,
+    marginRight: 8,
+  },
+  tabItemActive: {
+    backgroundColor: '#FFC900',
+  },
+  tabText: {
+    fontSize: 12,
+    color: '#fff',
+  },
+  tabTextActive: {
+    color: '#000',
+    fontWeight: 'bold',
+  },
+  gridScroll: {
+    height: 350,
+    backgroundColor: '#fff',
+    marginHorizontal: 10,
+    borderRadius: 10,
+  },
+  grid: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    padding: 10,
+  },
+  gridItem: {
+    width: ITEM_WIDTH,
+    height: ITEM_WIDTH / 2,
+    backgroundColor: '#FFC900',
+    borderWidth: 3,
+    borderColor: '#000',
+    borderRadius: 4,
+    justifyContent: 'center',
+    alignItems: 'center',
+    margin: 4,
+    position: 'relative',
+  },
+  gridItemActive: {},
+  gridItemUsed: {
+    backgroundColor: '#e8e8e8',
+  },
+  gridItemLocked: {
+    backgroundColor: 'rgba(98, 99, 115, 0.3)',
+    borderWidth: 0,
+  },
+  gridItemText: {
+    fontSize: 12,
+    color: '#000',
+    fontWeight: 'bold',
+  },
+  gridItemTextLocked: {
+    color: 'rgba(255,255,255,0.3)',
+  },
+  usedContent: {
+    alignItems: 'center',
+  },
+  usedNum: {
+    fontSize: 10,
+    color: '#000',
+    opacity: 0.5,
+  },
+  levelTitle: {
+    fontSize: 10,
+    fontWeight: 'bold',
+  },
+  checkIcon: {
+    position: 'absolute',
+    right: 0,
+    bottom: 0,
+    width: 16,
+    height: 14,
+    backgroundColor: '#000',
+    borderTopLeftRadius: 6,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  checkIconText: {
+    fontSize: 10,
+    color: '#FFC900',
+  },
+  lockIcon: {
+    position: 'absolute',
+    right: 0,
+    bottom: 0,
+    width: 16,
+    height: 14,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  lockIconText: {
+    fontSize: 10,
+  },
+  selectedSection: {
+    height: 40,
+    marginHorizontal: 10,
+    marginTop: 10,
+  },
+  selectedScroll: {
+    flex: 1,
+  },
+  selectedItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: '#FFC900',
+    borderWidth: 3,
+    borderColor: '#000',
+    borderRadius: 4,
+    paddingHorizontal: 10,
+    paddingVertical: 4,
+    marginRight: 8,
+    height: 32,
+  },
+  selectedText: {
+    fontSize: 12,
+    color: '#000',
+    fontWeight: 'bold',
+  },
+  selectedClose: {
+    marginLeft: 6,
+    width: 16,
+    height: 16,
+    backgroundColor: 'rgba(255,255,255,0.3)',
+    borderRadius: 8,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  selectedCloseText: {
+    fontSize: 12,
+    color: '#000',
+  },
+  btnBox: {
+    paddingHorizontal: 20,
+    paddingTop: 15,
+  },
+  submitBtn: {
+    backgroundColor: '#ff9600',
+    height: 50,
+    borderRadius: 25,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  submitBtnDisabled: {
+    opacity: 0.6,
+  },
+  submitText: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+  submitSubText: {
+    fontSize: 10,
+    color: '#fff',
+    marginTop: 2,
+  },
+});

+ 227 - 0
app/award-detail-yfs/components/ProductListYfs.tsx

@@ -0,0 +1,227 @@
+import { Image } from 'expo-image';
+import React from 'react';
+import { ScrollView, StyleSheet, Text, View } from 'react-native';
+
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  probability: number;
+  price?: number;
+  quantity?: number;
+  spu?: {
+    id: string;
+    cover: string;
+    name: string;
+  };
+}
+
+interface BoxData {
+  leftQuantity: number;
+  usedStat?: Record<string, { spuId: string; quantity: number }>;
+}
+
+interface ProductListYfsProps {
+  products: ProductItem[];
+  levelList?: any[];
+  poolId: string;
+  price: number;
+  box?: BoxData | null;
+}
+
+// 等级配置
+const LEVEL_CONFIG: Record<string, { title: string; color: string; bgColor: string }> = {
+  A: { title: '超神款', color: '#fff', bgColor: '#FF4444' },
+  B: { title: '欧皇款', color: '#fff', bgColor: '#FF9900' },
+  C: { title: '隐藏款', color: '#fff', bgColor: '#9966FF' },
+  D: { title: '普通款', color: '#fff', bgColor: '#00CCFF' },
+};
+
+// 格式化概率,去除末尾的0
+const formatProbability = (val: string | number) => {
+  const num = parseFloat(String(val));
+  if (isNaN(num)) return '0';
+  // 保留两位小数,去除末尾的0
+  return num.toFixed(2).replace(/\.?0+$/, '');
+};
+
+export const ProductListYfs: React.FC<ProductListYfsProps> = ({ products, levelList, box }) => {
+  // 按等级分组
+  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 getLevelProbability = (level: string) => {
+    const item = levelList?.find((e) => e.level === level);
+    return item ? `${formatProbability(item.probability)}%` : '0%';
+  };
+
+  // 获取商品剩余数量
+  const getLeftNum = (item: ProductItem) => {
+    const spuId = item.spu?.id || item.id;
+    if (!box?.usedStat || !box.usedStat[spuId]?.quantity) {
+      return item.quantity || 1;
+    }
+    return (item.quantity || 1) - box.usedStat[spuId].quantity;
+  };
+
+  // 获取商品概率
+  const getProbability = (item: ProductItem) => {
+    if (!box || box.leftQuantity <= 0) return '0';
+    const leftNum = getLeftNum(item);
+    return ((leftNum / box.leftQuantity) * 100).toFixed(2);
+  };
+
+  const renderLevelSection = (level: string, items: ProductItem[]) => {
+    const config = LEVEL_CONFIG[level] || LEVEL_CONFIG['D'];
+
+    return (
+      <View key={level} style={styles.levelBox}>
+        {/* 等级标题行 */}
+        <View style={styles.levelTitleRow}>
+          <Text style={[styles.levelTitle, { color: config.bgColor }]}>{config.title}</Text>
+          <View style={styles.levelProportion}>
+            <Text style={styles.probabilityLabel}>概率:</Text>
+            <Text style={styles.probabilityValue}>{getLevelProbability(level)}</Text>
+          </View>
+        </View>
+
+        {/* 商品横向滚动列表 */}
+        <ScrollView
+          horizontal
+          showsHorizontalScrollIndicator={false}
+          contentContainerStyle={styles.scrollContent}
+        >
+          {items.map((item, index) => {
+            const cover = item.spu?.cover || item.cover;
+            const leftNum = getLeftNum(item);
+            const prob = formatProbability(getProbability(item));
+            return (
+              <View key={item.id || index} style={styles.productItem}>
+                {/* 商品图片 */}
+                <View style={styles.productImageBox}>
+                  <Image
+                    source={{ uri: cover }}
+                    style={styles.productImage}
+                    contentFit="contain"
+                  />
+                </View>
+                {/* 等级标签 */}
+                <View style={[styles.levelTag, { backgroundColor: config.bgColor }]}>
+                  <Text style={styles.levelTagLabel}>概率</Text>
+                  <Text style={styles.levelTagText}>{prob}%</Text>
+                </View>
+                {/* 剩余数量 */}
+                <Text style={styles.leftNumText}>{leftNum}/{item.quantity || 1}</Text>
+              </View>
+            );
+          })}
+        </ScrollView>
+      </View>
+    );
+  };
+
+  const levelOrder = ['A', 'B', 'C', 'D'];
+
+  return (
+    <View style={styles.container}>
+      {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,
+  },
+  levelBox: {
+    marginBottom: 20,
+  },
+  levelTitleRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingHorizontal: 14,
+    paddingBottom: 10,
+    position: 'relative',
+  },
+  levelTitle: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    textAlign: 'center',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+  levelProportion: {
+    position: 'absolute',
+    right: 0,
+    bottom: 10,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  probabilityLabel: {
+    fontSize: 12,
+    color: '#ffc901',
+  },
+  probabilityValue: {
+    fontSize: 12,
+    color: '#fff',
+  },
+  scrollContent: {
+    paddingHorizontal: 5,
+  },
+  productItem: {
+    width: 90,
+    marginRight: 10,
+    alignItems: 'center',
+  },
+  productImageBox: {
+    width: 90,
+    height: 90,
+    backgroundColor: '#fff',
+    borderRadius: 4,
+    overflow: 'hidden',
+  },
+  productImage: {
+    width: 90,
+    height: 90,
+  },
+  levelTag: {
+    width: 80,
+    height: 26,
+    borderRadius: 2,
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: -8,
+  },
+  levelTagLabel: {
+    fontSize: 10,
+    color: '#fff',
+    marginRight: 2,
+  },
+  levelTagText: {
+    fontSize: 12,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  leftNumText: {
+    fontSize: 10,
+    color: '#999',
+    marginTop: 4,
+  },
+});

+ 175 - 23
app/award-detail-yfs/index.tsx

@@ -30,9 +30,11 @@ import {
 } 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';
+import { BoxChooseModal } from './components/BoxChooseModal';
+import { NumChooseModal } from './components/NumChooseModal';
+import { ProductListYfs } from './components/ProductListYfs';
 
 const { width: SCREEN_WIDTH } = Dimensions.get('window');
 
@@ -60,6 +62,7 @@ interface ProductItem {
 
 interface BoxData {
   number: string;
+  quantity: number;
   leftQuantity: number;
   leftQuantityA: number;
   leftQuantityB: number;
@@ -90,6 +93,8 @@ export default function AwardDetailYfsScreen() {
   const checkoutRef = useRef<any>(null);
   const recordRef = useRef<any>(null);
   const ruleRef = useRef<any>(null);
+  const numChooseRef = useRef<any>(null);
+  const boxChooseRef = useRef<any>(null);
   const floatAnim = useRef(new Animated.Value(0)).current;
   const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
@@ -151,10 +156,10 @@ export default function AwardDetailYfsScreen() {
       ]);
     } 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 },
+        { level: 'A', probability: ((res.leftQuantityA / res.leftQuantity) * 100).toFixed(2) },
+        { level: 'B', probability: ((res.leftQuantityB / res.leftQuantity) * 100).toFixed(2) },
+        { level: 'C', probability: ((res.leftQuantityC / res.leftQuantity) * 100).toFixed(2) },
+        { level: 'D', probability: ((res.leftQuantityD / res.leftQuantity) * 100).toFixed(2) },
       ]);
     }
   };
@@ -211,6 +216,31 @@ export default function AwardDetailYfsScreen() {
     }, 500);
   };
 
+  // 处理多盒购买(号码选择后的回调)
+  const handleNumPay = async (params: { preview: any; seatNumbers: number[]; boxNumber: string }) => {
+    checkoutRef.current?.show(params.seatNumbers.length, params.preview, params.boxNumber, params.seatNumbers);
+  };
+
+  // 打开号码选择弹窗
+  const handleShowNumChoose = () => {
+    if (!box) return;
+    numChooseRef.current?.show({
+      number: box.number,
+      quantity: box.quantity || box.leftQuantity,
+      lastNumber: box.lastNumber,
+    });
+  };
+
+  // 打开换盒弹窗
+  const handleShowBoxChoose = () => {
+    boxChooseRef.current?.show();
+  };
+
+  // 选择盒子后的回调
+  const handleChooseBox = (boxNumber: string) => {
+    loadBox(boxNumber);
+  };
+
   const handlePreBox = async () => {
     if (!poolId || !boxNum || parseInt(boxNum) <= 1) return;
     try {
@@ -434,21 +464,54 @@ export default function AwardDetailYfsScreen() {
             <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 source={{ uri: Images.box.detail.firstBoxBg }} style={styles.firstLastBox}>
+            {/* 当前盒子剩余数量 */}
+            {box && (
+              <View style={styles.boxSizeRow}>
+                <Text style={styles.boxSizeText}>当前盒子剩余:</Text>
+                <Text style={styles.boxSizeText}>
+                  <Text style={styles.boxSizeNum}>{box.leftQuantity}</Text>
+                  /{box.quantity || '-'}发
+                </Text>
+              </View>
+            )}
+
+            {/* 抢先赏/最终赏/全局赏展示 */}
+            {box?.prizeList && box.prizeList.length > 0 && (
+              <View style={styles.prizeListRow}>
+                {box.prizeList.map((item: any, index: number) => (
+                  <ImageBackground key={index} source={{ uri: Images.box.detail.firstItemBg }} style={styles.prizeItem}>
+                    <Image source={{ uri: item.cover }} style={styles.prizeImage} contentFit="contain" />
+                    <View style={[styles.prizeLevelTag, {
+                      backgroundColor: item.level === 'FIRST' ? 'rgba(91, 189, 208, 0.8)' :
+                        item.level === 'LAST' ? 'rgba(246, 44, 113, 0.8)' : 'rgba(44, 246, 74, 0.8)'
+                    }]}>
+                      <Text style={styles.prizeLevelText}>
+                        {item.level === 'FIRST' ? '抢先赏' : item.level === 'LAST' ? '最终赏' : '全局赏'}
+                      </Text>
+                    </View>
+                  </ImageBackground>
+                ))}
+              </View>
+            )}
+
+            {/* 换盒控制区 */}
+            <ImageBackground source={{ uri: Images.box.detail.funBoxBg }} style={styles.funBox}>
+              <TouchableOpacity style={styles.preBoxBtn} onPress={handlePreBox} disabled={!boxNum || parseInt(boxNum) <= 1}>
+                <Text style={[styles.funBoxText, (!boxNum || parseInt(boxNum) <= 1) && styles.disabled]}>上一盒</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.changeBoxBtn} onPress={handleShowBoxChoose}>
+                <Text style={styles.changeBoxText}>换盒({boxNum || '-'}/{box?.lastNumber || '-'})</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={styles.nextBoxBtn} onPress={handleNextBox} disabled={!box || parseInt(boxNum) >= box.lastNumber}>
+                <Text style={[styles.funBoxText, (!box || parseInt(boxNum) >= box.lastNumber) && styles.disabled]}>下一盒</Text>
+              </TouchableOpacity>
+            </ImageBackground>
           </ImageBackground>
 
           {/* 商品列表 */}
-          <ProductList products={products} levelList={probability} poolId={poolId!} price={data.price} />
+          <ProductListYfs products={products} levelList={probability} poolId={poolId!} price={data.price} box={box} />
 
           <View style={{ height: 150 }} />
         </ScrollView>
@@ -468,7 +531,7 @@ export default function AwardDetailYfsScreen() {
                 <Text style={styles.btnPrice}>(¥{data.specialPriceFive || data.price * 5})</Text>
               </ImageBackground>
             </TouchableOpacity>
-            <TouchableOpacity style={styles.btnItem} onPress={() => checkoutRef.current?.showFreedom()} activeOpacity={0.8}>
+            <TouchableOpacity style={styles.btnItem} onPress={handleShowNumChoose} activeOpacity={0.8}>
               <ImageBackground source={{ uri: Images.common.butBgH }} style={styles.btnBg} resizeMode="contain">
                 <Text style={styles.btnText}>购买多盒</Text>
               </ImageBackground>
@@ -480,6 +543,8 @@ export default function AwardDetailYfsScreen() {
       <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} boxNumber={boxNum} onSuccess={handleSuccess} />
       <RecordModal ref={recordRef} poolId={poolId!} />
       <RuleModal ref={ruleRef} />
+      <NumChooseModal ref={numChooseRef} poolId={poolId!} onPay={handleNumPay} />
+      <BoxChooseModal ref={boxChooseRef} poolId={poolId!} onChoose={handleChooseBox} />
     </View>
   );
 }
@@ -549,12 +614,99 @@ const styles = StyleSheet.create({
   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 },
+  // 一番赏盒子信息区域
+  firstLastBox: {
+    marginHorizontal: 10,
+    height: 193,
+    marginBottom: 10,
+    paddingTop: 20,
+  },
+  boxSizeRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingLeft: 10,
+    paddingTop: 12,
+  },
+  boxSizeText: {
+    fontSize: 12,
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 2,
+  },
+  boxSizeNum: {
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  prizeListRow: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingTop: 20,
+    minHeight: 66,
+  },
+  prizeItem: {
+    width: 63,
+    height: 65,
+    marginHorizontal: 5,
+    position: 'relative',
+  },
+  prizeImage: {
+    width: 60,
+    height: 50,
+  },
+  prizeLevelTag: {
+    position: 'absolute',
+    left: 0,
+    bottom: -8,
+    width: '100%',
+    height: 22,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  prizeLevelText: {
+    fontSize: 10,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  funBox: {
+    width: 270,
+    height: 38,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    alignSelf: 'center',
+    marginTop: 15,
+  },
+  preBoxBtn: {
+    width: 90,
+    height: 50,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  nextBoxBtn: {
+    width: 90,
+    height: 50,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  changeBoxBtn: {
+    width: 110,
+    height: 60,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  funBoxText: {
+    fontSize: 12,
+    color: '#fff',
+    fontWeight: '500',
+  },
+  changeBoxText: {
+    fontSize: 12,
+    color: '#000',
+    fontWeight: '500',
+  },
   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' },

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

@@ -4,6 +4,7 @@ export default function AwardDetailLayout() {
   return (
     <Stack screenOptions={{ headerShown: false }}>
       <Stack.Screen name="index" />
+      <Stack.Screen name="swipe" />
     </Stack>
   );
 }

+ 35 - 5
app/award-detail/components/CheckoutModal.tsx

@@ -1,5 +1,6 @@
 import { Image } from 'expo-image';
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { useRouter } from 'expo-router';
+import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
 import {
     ActivityIndicator,
     Alert,
@@ -13,6 +14,7 @@ import {
 } from 'react-native';
 
 import { applyOrder, getApplyResult, previewOrder } from '@/services/award';
+import { LotteryResultModal, LotteryResultModalRef } from './LotteryResultModal';
 
 const { width: SCREEN_WIDTH } = Dimensions.get('window');
 
@@ -50,6 +52,8 @@ const FREEDOM_NUMS = [10, 20, 30, 40, 50];
 
 export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
   ({ data, poolId, onSuccess, boxNumber }, ref) => {
+    const router = useRouter();
+    const lotteryResultRef = useRef<LotteryResultModalRef>(null);
     const [visible, setVisible] = useState(false);
     const [num, setNum] = useState(1);
     const [checked, setChecked] = useState(true);
@@ -158,7 +162,15 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
         if (res?.paySuccess || res?.bizTradeNo || res?.tradeNo) {
           const tradeNo = res.bizTradeNo || res.tradeNo;
           setVisible(false);
-          fetchLotteryResult(tradeNo);
+          
+          // 10发以上使用全屏抽奖结果弹窗
+          if (num >= 10) {
+            lotteryResultRef.current?.show(tradeNo);
+            // onSuccess 在 LotteryResultModal 关闭时调用
+          } else {
+            // 10发以下使用简单弹窗展示结果
+            fetchLotteryResult(tradeNo);
+          }
         } else {
           Alert.alert('提示', res?.message || '支付失败,请重试');
         }
@@ -169,7 +181,7 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       }
     };
 
-    // 获取抽奖结果
+    // 获取抽奖结果(10发以下用弹窗)
     const fetchLotteryResult = async (tradeNo: string) => {
       setResultLoading(true);
       setResultVisible(true);
@@ -184,12 +196,15 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
           if (res?.inventoryList && res.inventoryList.length > 0) {
             setResultList(res.inventoryList);
             setResultLoading(false);
+            // 不在这里调用 onSuccess,等用户关闭弹窗时再调用
           } else if (attempts < maxAttempts) {
             attempts++;
             setTimeout(poll, 1000);
           } else {
             setResultLoading(false);
-            Alert.alert('提示', '获取结果超时,请在仓库中查看');
+            if (typeof window !== 'undefined') {
+              window.alert('获取结果超时,请在仓库中查看');
+            }
           }
         } catch {
           if (attempts < maxAttempts) {
@@ -197,7 +212,9 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
             setTimeout(poll, 1000);
           } else {
             setResultLoading(false);
-            Alert.alert('提示', '获取结果失败,请在仓库中查看');
+            if (typeof window !== 'undefined') {
+              window.alert('获取结果失败,请在仓库中查看');
+            }
           }
         }
       };
@@ -208,6 +225,19 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
 
     return (
       <>
+        {/* 10发以上的全屏抽奖结果弹窗 */}
+        <LotteryResultModal 
+          ref={lotteryResultRef} 
+          onClose={() => {
+            // 抽奖结果弹窗关闭后刷新数据
+            onSuccess({ tradeNo: '', num });
+          }}
+          onGoStore={() => {
+            onSuccess({ tradeNo: '', num });
+            router.replace('/store' as any);
+          }}
+        />
+
         {/* 自由购买数量选择弹窗 */}
         <Modal visible={freedomSelectVisible} transparent animationType="fade" onRequestClose={() => setFreedomSelectVisible(false)}>
           <View style={styles.overlay}>

+ 110 - 0
app/award-detail/components/ExplainSection.tsx

@@ -0,0 +1,110 @@
+import React, { useEffect, useState } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+
+import { getMessages } from '@/services/base';
+
+interface ExplainSectionProps {
+  poolId: string;
+  rules?: string;
+}
+
+export const ExplainSection: React.FC<ExplainSectionProps> = ({ rules }) => {
+  const [shippingRule, setShippingRule] = useState('全国所有地区每单运费15元,满五件享快递包邮服务。默认5个工作日内发货;若遇缺货需补货,预计需要10个工作日。');
+
+  useEffect(() => {
+    loadShippingRule();
+  }, []);
+
+  const loadShippingRule = async () => {
+    try {
+      const res = await getMessages(0, 10);
+      if (res && res.length > 0 && res[0].content) {
+        setShippingRule(res[0].content);
+      }
+    } catch (error) {
+      console.error('获取发货规则失败:', error);
+    }
+  };
+
+  return (
+    <View style={styles.container}>
+      {/* 关于签收货 */}
+      <View style={styles.section}>
+        <Text style={styles.title}>1关于签收货:</Text>
+        <View style={styles.content}>
+          <Text style={styles.text}>
+            1.在签收快件时,请本人亲自在不拆封商品包装的情况下,在快递前当面验货,确认无误后再签收
+          </Text>
+          <Text style={styles.text}>2.商品的退款请参考商品售后条款</Text>
+        </View>
+      </View>
+
+      {/* 关于发货 */}
+      <View style={styles.section}>
+        <Text style={styles.title}>关于发货:</Text>
+        <View style={styles.content}>
+          <Text style={styles.text}>1.{shippingRule}</Text>
+          <Text style={styles.text}>
+            2.商品的可配送区域为中国大陆地区(除特殊偏远地区)
+          </Text>
+          <Text style={styles.text}>
+            3.平台统一采用高规格包装和配送,最大程度保护商品在配送过程中的安全
+          </Text>
+          <Text style={styles.text}>
+            4.为确保包裹配送成功,平台会根据发货地和收件地匹配合适的物流公司,合作物流可能为顺丰、京东、EMS、申通、中通、邮政等
+          </Text>
+        </View>
+      </View>
+
+      {/* 售后 */}
+      <View style={styles.section}>
+        <Text style={styles.title}>售后:</Text>
+        <View style={styles.content}>
+          <Text style={styles.text}>
+            1."一番赏""无限赏"作为盲盒类商品,根据《消费者权益保护法》第25条之规定,因产品售出后,无法按照售卖规则二次销售不适用7天无理由退换的规定,请谨慎、理性购买。
+          </Text>
+          <Text style={styles.text}>
+            2.收到的商品如遇质量问题,可联系客服,凭有效的售后凭证,我司提供包含补偿、换货、退款在内的方式进行售后处理。若商品存在损坏、少件或遗失,在提供有效凭证,证明签收时即存在上述问题。若您发现有任何断件或缺件问题,请勿再打开内包装,并第一时间联系客服。
+          </Text>
+          <Text style={styles.text}>
+            3.对于需要退换货的商品,若因产品质量问题退换货,来回运费均由平台承担
+          </Text>
+        </View>
+      </View>
+
+      {/* 关于配送 */}
+      {rules ? (
+        <View style={styles.section}>
+          <Text style={styles.title}>关于配送:</Text>
+          <View style={styles.content}>
+            <Text style={styles.text}>{rules}</Text>
+          </View>
+        </View>
+      ) : null}
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 14,
+    paddingBottom: 50,
+  },
+  section: {
+    marginTop: 15,
+  },
+  title: {
+    fontSize: 14,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  content: {
+    marginTop: 10,
+  },
+  text: {
+    fontSize: 12,
+    color: '#999',
+    lineHeight: 20,
+    marginBottom: 5,
+  },
+});

+ 511 - 0
app/award-detail/components/LotteryResultModal.tsx

@@ -0,0 +1,511 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
+import {
+    ActivityIndicator,
+    Animated,
+    Dimensions,
+    ImageBackground,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { convertApply, getApplyResult } from '@/services/award';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const CARD_WIDTH = (SCREEN_WIDTH - 60) / 3;
+const CARD_HEIGHT = CARD_WIDTH * 1.5;
+
+const CDN_BASE = 'https://cdn.acetoys.cn';
+const imgUrl = `${CDN_BASE}/kai_xin_ma_te/supermart`;
+const imgUrlSupermart = `${CDN_BASE}/supermart`;
+
+const LEVEL_MAP: Record<string, { title: string; color: string; rgba: string; resultBg: string; borderImg: string }> = {
+  A: { 
+    title: '超神款', 
+    color: '#F62C71', 
+    rgba: 'rgba(246, 44, 113, 1)',
+    resultBg: `${imgUrlSupermart}/supermart/box/resultBgA.png`, 
+    borderImg: `${imgUrlSupermart}/supermart/box/borderImgA.png` 
+  },
+  B: { 
+    title: '欧皇款', 
+    color: '#E9C525', 
+    rgba: 'rgba(233,197,37, 1)',
+    resultBg: `${imgUrlSupermart}/supermart/box/resultBgB.png`, 
+    borderImg: `${imgUrlSupermart}/supermart/box/borderImgB.png` 
+  },
+  C: { 
+    title: '隐藏款', 
+    color: '#A72CF6', 
+    rgba: 'rgba(167, 44, 246, 1)',
+    resultBg: `${imgUrlSupermart}/supermart/box/resultBgC.png`, 
+    borderImg: `${imgUrlSupermart}/supermart/box/borderImgC.png` 
+  },
+  D: { 
+    title: '普通款', 
+    color: '#40c9d7', 
+    rgba: 'rgba(64, 201, 215, 1)',
+    resultBg: `${imgUrlSupermart}/supermart/box/resultBgD.png`, 
+    borderImg: `${imgUrlSupermart}/supermart/box/borderImgD.png` 
+  },
+};
+
+const LotteryImages = {
+  lotteryBg: `${imgUrlSupermart}/supermart/box/sequence/sequence0.jpg`,
+  cardBack: `${imgUrl}/box/back1.png`,
+  halo: `${imgUrlSupermart}/supermart/box/halo.gif`,
+};
+
+interface LotteryItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  magicAmount?: number;
+  spu?: { marketPrice: number };
+}
+
+export interface LotteryResultModalRef {
+  show: (tradeNo: string) => void;
+  close: () => void;
+}
+
+interface LotteryResultModalProps {
+  onClose?: () => void;
+  onGoStore?: () => void;
+}
+
+export const LotteryResultModal = forwardRef<LotteryResultModalRef, LotteryResultModalProps>(
+  ({ onClose, onGoStore }, ref) => {
+    const insets = useSafeAreaInsets();
+    
+    const [visible, setVisible] = useState(false);
+    const [loading, setLoading] = useState(true);
+    const [tableData, setTableData] = useState<LotteryItem[]>([]);
+    const [total, setTotal] = useState(0);
+    const [showResult, setShowResult] = useState(false);
+    const [showDh, setShowDh] = useState(false);
+    const [haloShow, setHaloShow] = useState(false);
+    const [rebateAmount, setRebateAmount] = useState(0);
+    const [animationEnabled, setAnimationEnabled] = useState(true);
+    const [isSkip, setIsSkip] = useState(true);
+    const [tradeNo, setTradeNo] = useState('');
+
+    const flipAnims = useRef<Animated.Value[]>([]);
+    const dataLoadedRef = useRef(false);
+
+    useImperativeHandle(ref, () => ({
+      show: (tNo: string) => {
+        setTradeNo(tNo);
+        setVisible(true);
+        setLoading(true);
+        setTableData([]);
+        setTotal(0);
+        setShowResult(false);
+        setShowDh(false);
+        setHaloShow(false);
+        setRebateAmount(0);
+        setIsSkip(true);
+        dataLoadedRef.current = false;
+        flipAnims.current = [];
+      },
+      close: () => {
+        setVisible(false);
+        dataLoadedRef.current = false;
+      },
+    }));
+
+    const flipCards = (data: LotteryItem[]) => {
+      setIsSkip(false);
+      setHaloShow(true);
+      setTimeout(() => setHaloShow(false), 1000);
+
+      const maxCards = Math.min(data.length, 9);
+      const animations = flipAnims.current.slice(0, maxCards).map((anim, index) => {
+        return Animated.sequence([
+          Animated.delay(index * 200),
+          Animated.timing(anim, { toValue: 1, duration: 200, useNativeDriver: true }),
+        ]);
+      });
+
+      Animated.parallel(animations).start(() => {
+        setTimeout(() => {
+          setShowDh(data.every((item) => item.level !== 'B' && item.level !== 'A'));
+          setShowResult(true);
+        }, 900);
+      });
+    };
+
+    useEffect(() => {
+      if (!visible || !tradeNo || dataLoadedRef.current) return;
+
+      let attempts = 0;
+      const maxAttempts = 13;
+      let timeoutId: ReturnType<typeof setTimeout>;
+      let isMounted = true;
+
+      const fetchData = async () => {
+        if (!isMounted) return;
+        
+        try {
+          const res = await getApplyResult(tradeNo);
+          
+          if (!isMounted) return;
+          
+          if (res?.inventoryList && res.inventoryList.length > 0) {
+            dataLoadedRef.current = true;
+            if (res.rebateAmount) setRebateAmount(res.rebateAmount);
+
+            let array = res.inventoryList;
+            if (res.magicFireworksList && res.magicFireworksList.length > 0) {
+              array = [...res.magicFireworksList, ...res.inventoryList];
+            }
+
+            flipAnims.current = array.map(() => new Animated.Value(0));
+            const sum = array.reduce((acc: number, item: LotteryItem) => acc + (item.magicAmount || 0), 0);
+            setTotal(sum);
+            setTableData(array);
+            setLoading(false);
+            
+            // 直接开始翻牌动画
+            setTimeout(() => flipCards(array), 500);
+          } else if (attempts < maxAttempts) {
+            attempts++;
+            timeoutId = setTimeout(fetchData, 400);
+          } else {
+            setLoading(false);
+            window.alert('获取结果超时,请在仓库中查看');
+          }
+        } catch (error) {
+          if (attempts < maxAttempts) {
+            attempts++;
+            timeoutId = setTimeout(fetchData, 400);
+          } else {
+            setLoading(false);
+          }
+        }
+      };
+
+      fetchData();
+      
+      return () => {
+        isMounted = false;
+        if (timeoutId) clearTimeout(timeoutId);
+      };
+    }, [visible, tradeNo]);
+
+    const handleSkip = () => {
+      setIsSkip(false);
+      flipAnims.current.forEach((anim) => anim.setValue(1));
+      setShowDh(tableData.every((item) => item.level !== 'B' && item.level !== 'A'));
+      setShowResult(true);
+    };
+
+    const handleDhAll = async () => {
+      if (!total) return;
+      try {
+        const ids = tableData.filter((item) => item.magicAmount).map((item) => item.id);
+        const res = await convertApply(ids);
+        if (res) {
+          setTableData((prev) => prev.map((item) => ({ ...item, magicAmount: 0 })));
+          setTotal(0);
+          window.alert('兑换成功');
+        }
+      } catch {
+        window.alert('兑换失败,请重试');
+      }
+    };
+
+    const handleBack = () => {
+      setVisible(false);
+      onClose?.();
+    };
+
+    const handleGoStore = () => {
+      setVisible(false);
+      onGoStore?.();
+    };
+
+    const renderCard = (item: LotteryItem, index: number) => {
+      const levelConfig = LEVEL_MAP[item.level] || LEVEL_MAP.D;
+      const flipAnim = flipAnims.current[index] || new Animated.Value(1);
+      const isFirst9 = index < 9;
+
+      // 前9张卡片有翻转动画
+      if (isFirst9) {
+        const backRotate = flipAnim.interpolate({ 
+          inputRange: [0, 1], 
+          outputRange: ['0deg', '90deg'] 
+        });
+        const frontRotate = flipAnim.interpolate({ 
+          inputRange: [0, 1], 
+          outputRange: ['-90deg', '0deg'] 
+        });
+        const backOpacity = flipAnim.interpolate({ 
+          inputRange: [0, 0.5, 1], 
+          outputRange: [1, 0, 0] 
+        });
+        const frontOpacity = flipAnim.interpolate({ 
+          inputRange: [0, 0.5, 1], 
+          outputRange: [0, 0, 1] 
+        });
+
+        return (
+          <View key={item.id || index} style={styles.cardWrapper}>
+            {/* 背面 - 卡牌背面 */}
+            <Animated.View style={[styles.cardBack, { transform: [{ rotateY: backRotate }], opacity: backOpacity }]}>
+              <Image source={{ uri: LotteryImages.cardBack }} style={styles.cardBackImage} contentFit="cover" />
+            </Animated.View>
+            {/* 正面 - 商品信息 */}
+            <Animated.View style={[styles.cardFront, { transform: [{ rotateY: frontRotate }], opacity: frontOpacity }]}>
+              <ImageBackground source={{ uri: levelConfig.resultBg }} style={styles.cardFrontBg} resizeMode="cover">
+                <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="contain" />
+                <Image source={{ uri: levelConfig.borderImg }} style={styles.borderImage} contentFit="cover" />
+                <View style={styles.cardInfo}>
+                  <View style={styles.infoRow}>
+                    <Text style={styles.levelText}>{levelConfig.title}</Text>
+                    <Text style={styles.priceText}>¥{item.spu?.marketPrice || 0}</Text>
+                  </View>
+                  <View style={styles.exchangeRow}>
+                    <Text style={styles.exchangeText}>价值:{item.magicAmount || 0}果实</Text>
+                  </View>
+                  <Text style={styles.nameText} numberOfLines={1}>{item.name}</Text>
+                </View>
+              </ImageBackground>
+            </Animated.View>
+          </View>
+        );
+      }
+
+      // 9张以后的卡片直接显示正面
+      return (
+        <View key={item.id || index} style={styles.cardWrapper}>
+          <View style={styles.cardFrontStatic}>
+            <ImageBackground source={{ uri: levelConfig.resultBg }} style={styles.cardFrontBg} resizeMode="cover">
+              <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="contain" />
+              <Image source={{ uri: levelConfig.borderImg }} style={styles.borderImage} contentFit="cover" />
+              <View style={styles.cardInfo}>
+                <View style={styles.infoRow}>
+                  <Text style={styles.levelText}>{levelConfig.title}</Text>
+                  <Text style={styles.priceText}>¥{item.spu?.marketPrice || 0}</Text>
+                </View>
+                <View style={styles.exchangeRow}>
+                  <Text style={styles.exchangeText}>价值:{item.magicAmount || 0}果实</Text>
+                </View>
+                <Text style={styles.nameText} numberOfLines={1}>{item.name}</Text>
+              </View>
+            </ImageBackground>
+          </View>
+        </View>
+      );
+    };
+
+    return (
+      <Modal visible={visible} transparent animationType="fade" onRequestClose={handleBack}>
+        <View style={styles.container}>
+          <ImageBackground source={{ uri: LotteryImages.lotteryBg }} style={styles.background} resizeMode="cover">
+            {/* 光晕效果 */}
+            {haloShow && (
+              <View style={styles.haloSection}>
+                <Image source={{ uri: LotteryImages.halo }} style={styles.halo} contentFit="cover" />
+              </View>
+            )}
+            
+            {/* 标题 */}
+            <View style={[styles.titleSection, { marginTop: insets.top + 40 }]}>
+              <Text style={styles.titleText}>恭喜您获得</Text>
+            </View>
+            
+            {/* 主内容区 */}
+            <View style={styles.mainSection}>
+              {loading ? (
+                <View style={styles.loadingBox}>
+                  <ActivityIndicator size="large" color="#fff" />
+                  <Text style={styles.loadingText}>正在开启宝箱...</Text>
+                </View>
+              ) : (
+                <ScrollView style={styles.cardList} showsVerticalScrollIndicator={false}>
+                  <View style={styles.cardGrid}>
+                    {tableData.map((item, index) => renderCard(item, index))}
+                  </View>
+                </ScrollView>
+              )}
+            </View>
+
+            {/* 底部按钮区 */}
+            {showResult && (
+              <View style={[styles.bottomSection, { paddingBottom: insets.bottom + 20 }]}>
+                <View style={styles.bottomBtns}>
+                  {total > 0 && showDh && (
+                    <TouchableOpacity style={styles.dhBtn} onPress={handleDhAll}>
+                      <Text style={styles.dhBtnText}>全部兑换</Text>
+                      <Text style={styles.dhBtnSubText}>共兑换果实 {total}</Text>
+                    </TouchableOpacity>
+                  )}
+                  <TouchableOpacity style={styles.againBtn} onPress={handleBack}>
+                    <Text style={styles.againBtnText}>再来一发</Text>
+                  </TouchableOpacity>
+                </View>
+                <TouchableOpacity style={styles.storeLink} onPress={handleGoStore}>
+                  <Text style={styles.storeLinkText}>前往 <Text style={styles.storeHighlight}>仓库</Text> 查看</Text>
+                </TouchableOpacity>
+                {rebateAmount > 0 && (
+                  <Text style={styles.rebateText}>本次支付返还果实 <Text style={styles.rebateAmount}>{rebateAmount}</Text> 枚</Text>
+                )}
+                <View style={styles.animationSwitch}>
+                  <Text style={styles.switchLabel}>是否开启动画</Text>
+                  <TouchableOpacity 
+                    style={[styles.switchBtn, animationEnabled && styles.switchBtnActive]} 
+                    onPress={() => setAnimationEnabled(!animationEnabled)}
+                  >
+                    <View style={[styles.switchThumb, animationEnabled && styles.switchThumbActive]} />
+                  </TouchableOpacity>
+                </View>
+              </View>
+            )}
+            
+            {/* 跳过动画按钮 */}
+            {isSkip && !loading && (
+              <TouchableOpacity style={styles.skipBtn} onPress={handleSkip}>
+                <Text style={styles.skipText}>跳过动画</Text>
+              </TouchableOpacity>
+            )}
+          </ImageBackground>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  haloSection: { position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, zIndex: 9999 },
+  halo: { width: '100%', height: '100%' },
+  titleSection: { alignItems: 'center', marginBottom: 15 },
+  titleText: { 
+    fontSize: 31, 
+    fontWeight: 'bold', 
+    color: '#fffecc', 
+    textShadowColor: '#a06939', 
+    textShadowOffset: { width: 1, height: 1 }, 
+    textShadowRadius: 2 
+  },
+  mainSection: { flex: 1, paddingTop: 20 },
+  loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
+  loadingText: { marginTop: 15, fontSize: 14, color: '#fff' },
+  cardList: { flex: 1 },
+  cardGrid: { 
+    flexDirection: 'row', 
+    flexWrap: 'wrap', 
+    paddingHorizontal: 15, 
+    justifyContent: 'flex-start' 
+  },
+  cardWrapper: { 
+    width: CARD_WIDTH, 
+    height: CARD_HEIGHT, 
+    marginHorizontal: 5, 
+    marginBottom: 15, 
+    position: 'relative' 
+  },
+  cardBack: { 
+    position: 'absolute', 
+    width: '100%', 
+    height: '100%', 
+    backfaceVisibility: 'hidden' 
+  },
+  cardBackImage: { width: '100%', height: '100%', borderRadius: 10 },
+  cardFront: { 
+    position: 'absolute', 
+    width: '100%', 
+    height: '100%', 
+    backfaceVisibility: 'hidden', 
+    borderRadius: 10, 
+    overflow: 'hidden' 
+  },
+  cardFrontStatic: { 
+    width: '100%', 
+    height: '100%', 
+    borderRadius: 10, 
+    overflow: 'hidden' 
+  },
+  cardFrontBg: { 
+    width: '100%', 
+    height: '100%', 
+    paddingTop: 15, 
+    borderRadius: 10, 
+    overflow: 'hidden' 
+  },
+  productImage: { width: '85%', height: '55%', alignSelf: 'center' },
+  borderImage: { position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' },
+  cardInfo: { position: 'absolute', left: 0, right: 0, bottom: 7, paddingHorizontal: 10 },
+  infoRow: { 
+    flexDirection: 'row', 
+    justifyContent: 'space-between', 
+    alignItems: 'center', 
+    marginBottom: 2 
+  },
+  levelText: { fontSize: 13, fontWeight: 'bold', color: '#fff' },
+  priceText: { fontSize: 12, fontWeight: 'bold', color: '#fff' },
+  exchangeRow: { marginBottom: 2 },
+  exchangeText: { fontSize: 10, color: '#fff', fontWeight: 'bold' },
+  nameText: { fontSize: 12, fontWeight: 'bold', color: '#fff' },
+  bottomSection: { paddingHorizontal: 20, paddingTop: 20 },
+  bottomBtns: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
+  dhBtn: { 
+    backgroundColor: '#fff7e3', 
+    borderRadius: 20, 
+    paddingHorizontal: 18, 
+    paddingVertical: 8, 
+    marginRight: 10, 
+    alignItems: 'center' 
+  },
+  dhBtnText: { fontSize: 14, fontWeight: '500', color: '#000' },
+  dhBtnSubText: { fontSize: 10, color: '#735200' },
+  againBtn: { 
+    backgroundColor: '#fec433', 
+    borderRadius: 20, 
+    paddingHorizontal: 25, 
+    paddingVertical: 12 
+  },
+  againBtnText: { fontSize: 14, fontWeight: '600', color: '#000' },
+  storeLink: { alignItems: 'center', marginTop: 12 },
+  storeLinkText: { fontSize: 12, color: '#fff' },
+  storeHighlight: { color: '#ff9600' },
+  rebateText: { textAlign: 'center', marginTop: 13, fontSize: 13, color: '#fff' },
+  rebateAmount: { color: '#ffeb3b', fontSize: 14, fontWeight: 'bold' },
+  animationSwitch: { 
+    flexDirection: 'row', 
+    justifyContent: 'center', 
+    alignItems: 'center', 
+    marginTop: 8 
+  },
+  switchLabel: { fontSize: 12, color: '#dedede', marginRight: 10 },
+  switchBtn: { 
+    width: 44, 
+    height: 24, 
+    borderRadius: 12, 
+    backgroundColor: '#666', 
+    justifyContent: 'center', 
+    paddingHorizontal: 2 
+  },
+  switchBtnActive: { backgroundColor: '#ff9600' },
+  switchThumb: { width: 20, height: 20, borderRadius: 10, backgroundColor: '#fff' },
+  switchThumbActive: { alignSelf: 'flex-end' },
+  skipBtn: { 
+    position: 'absolute', 
+    bottom: '10%', 
+    alignSelf: 'center', 
+    backgroundColor: 'rgba(0,0,0,0.4)', 
+    paddingHorizontal: 15, 
+    paddingVertical: 7, 
+    borderRadius: 15 
+  },
+  skipText: { fontSize: 14, color: '#fff' },
+});

+ 129 - 78
app/award-detail/components/ProductList.tsx

@@ -1,6 +1,7 @@
 import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
 import React from 'react';
-import { ImageBackground, StyleSheet, Text, View } from 'react-native';
+import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
 
 import { Images } from '@/constants/images';
 
@@ -12,6 +13,11 @@ interface ProductItem {
   probability: number;
   price?: number;
   quantity?: number;
+  spu?: {
+    id: string;
+    cover: string;
+    name: string;
+  };
 }
 
 interface ProductListProps {
@@ -21,20 +27,28 @@ interface ProductListProps {
   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 },
+// 等级配置 - 对应小程序的 LEVEL_MAP
+const LEVEL_CONFIG: Record<string, { title: string; color: string; bgColor: string }> = {
+  A: { title: '超神款', color: '#fff', bgColor: '#FF4444' },
+  B: { title: '欧皇款', color: '#fff', bgColor: '#FF9900' },
+  C: { title: '隐藏款', color: '#fff', bgColor: '#9966FF' },
+  D: { title: '普通款', color: '#fff', bgColor: '#00CCFF' },
 };
 
 const ignoreRatio0 = (val: number) => {
-  const str = String(val);
-  const match = str.match(/^(\d+\.?\d*?)0*$/);
-  return match ? match[1].replace(/\.$/, '') : str;
+  // 将小数转换为百分比,保留两位小数
+  const percent = val * 100;
+  // 如果是整数则不显示小数点
+  if (percent === Math.floor(percent)) {
+    return String(Math.floor(percent));
+  }
+  // 保留两位小数,去除末尾的0
+  return percent.toFixed(2).replace(/\.?0+$/, '');
 };
 
-export const ProductList: React.FC<ProductListProps> = ({ products, levelList }) => {
+export const ProductList: React.FC<ProductListProps> = ({ products, levelList, poolId }) => {
+  const router = useRouter();
+  
   // 按等级分组
   const groupedProducts = products.reduce(
     (acc, item) => {
@@ -47,43 +61,67 @@ export const ProductList: React.FC<ProductListProps> = ({ products, levelList })
   );
 
   // 计算各等级概率
-  const levelProbabilities =
-    levelList?.reduce(
-      (acc, item) => {
-        acc[item.level] = item.probability;
-        return acc;
-      },
-      {} as Record<string, number>
-    ) || {};
+  const getLevelProbability = (level: string) => {
+    const item = levelList?.find((e) => e.level === level);
+    return item ? `${item.probability}%` : '0%';
+  };
+
+  // 点击产品跳转到详情页
+  const handleProductPress = (item: ProductItem) => {
+    // 找到该产品在原始列表中的索引
+    const index = products.findIndex((p) => p.id === item.id);
+    router.push({
+      pathname: '/award-detail/swipe' as any,
+      params: { poolId, index: index >= 0 ? index : 0 },
+    });
+  };
 
   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);
+    const config = LEVEL_CONFIG[level] || LEVEL_CONFIG['D'];
 
     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 key={level} style={styles.levelBox}>
+        {/* 等级标题行 */}
+        <View style={styles.levelTitleRow}>
+          <Text style={[styles.levelTitle, { color: config.bgColor }]}>{config.title}</Text>
+          <View style={styles.levelProportion}>
+            <Text style={styles.probabilityLabel}>概率:</Text>
+            <Text style={styles.probabilityValue}>{getLevelProbability(level)}</Text>
+          </View>
+        </View>
 
-        {/* 商品网格 */}
-        <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">
+        {/* 商品横向滚动列表 */}
+        <ScrollView
+          horizontal
+          showsHorizontalScrollIndicator={false}
+          contentContainerStyle={styles.scrollContent}
+        >
+          {items.map((item, index) => {
+            const cover = item.spu?.cover || item.cover;
+            return (
+              <TouchableOpacity 
+                key={item.id || index} 
+                style={styles.productItem}
+                onPress={() => handleProductPress(item)}
+                activeOpacity={0.8}
+              >
+                {/* 商品图片 */}
                 <View style={styles.productImageBox}>
-                  <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="cover" />
+                  <Image
+                    source={{ uri: cover }}
+                    style={styles.productImage}
+                    contentFit="contain"
+                  />
                 </View>
-                <Text style={styles.productName} numberOfLines={2}>
-                  {item.name}
-                </Text>
-                <Text style={styles.productProbability}>概率: {ignoreRatio0((item.probability || 0) * 100)}%</Text>
-              </ImageBackground>
-            </View>
-          ))}
-        </View>
+                {/* 等级标签 - 显示概率和等级名称 */}
+                <View style={[styles.levelTag, { backgroundColor: config.bgColor }]}>
+                  <Text style={styles.levelTagLabel}>概率</Text>
+                  <Text style={styles.levelTagText}>{ignoreRatio0(item.probability)}%</Text>
+                </View>
+              </TouchableOpacity>
+            );
+          })}
+        </ScrollView>
       </View>
     );
   };
@@ -113,69 +151,82 @@ const styles = StyleSheet.create({
   },
   titleBox: {
     alignItems: 'center',
-    marginBottom: 10,
+    marginBottom: 15,
   },
   titleImg: {
     width: 121,
     height: 29,
   },
-  levelSection: {
-    marginBottom: 15,
+  levelBox: {
+    marginBottom: 20,
   },
-  levelHeader: {
-    height: 40,
+  levelTitleRow: {
     flexDirection: 'row',
     alignItems: 'center',
-    justifyContent: 'space-between',
-    paddingHorizontal: 15,
-    marginBottom: 10,
+    justifyContent: 'center',
+    paddingHorizontal: 14,
+    paddingBottom: 10,
+    position: 'relative',
   },
   levelTitle: {
-    color: '#fff',
-    fontSize: 16,
+    fontSize: 18,
     fontWeight: 'bold',
+    textAlign: 'center',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
   },
-  levelProbability: {
-    color: 'rgba(255,255,255,0.8)',
+  levelProportion: {
+    position: 'absolute',
+    right: 0,
+    bottom: 10,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  probabilityLabel: {
     fontSize: 12,
+    color: '#ffc901',
   },
-  productGrid: {
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    marginHorizontal: -5,
+  probabilityValue: {
+    fontSize: 12,
+    color: '#fff',
   },
-  productItem: {
-    width: '33.33%',
+  scrollContent: {
     paddingHorizontal: 5,
-    marginBottom: 10,
   },
-  productItemBg: {
-    width: '100%',
-    aspectRatio: 0.75,
-    padding: 8,
+  productItem: {
+    width: 90,
+    marginRight: 10,
     alignItems: 'center',
   },
   productImageBox: {
-    width: '100%',
-    aspectRatio: 1,
-    borderRadius: 5,
+    width: 90,
+    height: 90,
+    backgroundColor: '#fff',
+    borderRadius: 4,
     overflow: 'hidden',
-    backgroundColor: 'rgba(255,255,255,0.1)',
   },
   productImage: {
-    width: '100%',
-    height: '100%',
+    width: 90,
+    height: 90,
   },
-  productName: {
-    color: '#fff',
-    fontSize: 10,
-    marginTop: 5,
-    textAlign: 'center',
+  levelTag: {
+    width: 80,
     height: 26,
+    borderRadius: 2,
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginTop: -8,
+  },
+  levelTagLabel: {
+    fontSize: 10,
+    color: '#fff',
+    marginRight: 2,
   },
-  productProbability: {
-    color: '#FBC400',
-    fontSize: 9,
-    marginTop: 2,
+  levelTagText: {
+    fontSize: 12,
+    color: '#fff',
+    fontWeight: 'bold',
   },
 });

+ 2 - 2
app/award-detail/components/RecordModal.tsx

@@ -17,7 +17,7 @@ interface RecordItem {
   nickname: string;
   avatar: string;
   goodsName: string;
-  goodsCover: string;
+  cover: string;  // API 返回的是 cover 字段
   level: string;
   createTime: string;
 }
@@ -82,7 +82,7 @@ export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(
         </View>
         <View style={styles.goodsInfo}>
           <Image
-            source={{ uri: item.goodsCover }}
+            source={{ uri: item.cover }}
             style={styles.goodsImage}
             contentFit="cover"
           />

+ 58 - 29
app/award-detail/components/RuleModal.tsx

@@ -1,13 +1,16 @@
+import { Image } from 'expo-image';
 import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
 import {
+    ImageBackground,
     Modal,
     ScrollView,
     StyleSheet,
     Text,
     TouchableOpacity,
-    View
+    View,
 } from 'react-native';
 
+import { Images } from '@/constants/images';
 import { getParamConfig } from '@/services/user';
 
 export interface RuleModalRef {
@@ -30,9 +33,12 @@ export const RuleModal = forwardRef<RuleModalRef>((_, ref) => {
   const loadRules = async () => {
     try {
       const res = await getParamConfig('show_rule');
-      setRules(res?.data || '暂无规则说明');
+      // 去除HTML标签
+      const text = res?.data?.replace(/<[^>]+>/g, '') || '平台发货零门槛,全国统一运费15元/单,满五件享快递包邮服务,默认5个工作日内完成发货。';
+      setRules(text);
     } catch (error) {
       console.error('加载规则失败:', error);
+      setRules('平台发货零门槛,全国统一运费15元/单,满五件享快递包邮服务,默认5个工作日内完成发货。');
     }
   };
 
@@ -44,22 +50,28 @@ export const RuleModal = forwardRef<RuleModalRef>((_, ref) => {
     <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
       <View style={styles.overlay}>
         <View style={styles.container}>
-          <View style={styles.header}>
+          {/* 关闭按钮 */}
+          <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
+            <Image source={{ uri: Images.common.closeBut }} style={styles.closeIcon} contentFit="contain" />
+          </TouchableOpacity>
+
+          {/* 内容区域 */}
+          <ImageBackground
+            source={{ uri: Images.mine.dialogContentBg }}
+            style={styles.contentBg}
+            resizeMode="stretch"
+          >
             <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>
+            <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+              <Text style={styles.ruleText}>{rules}</Text>
+            </ScrollView>
+          </ImageBackground>
         </View>
       </View>
     </Modal>
   );
 });
 
-
 const styles = StyleSheet.create({
   overlay: {
     flex: 1,
@@ -68,24 +80,41 @@ const styles = StyleSheet.create({
     alignItems: 'center',
   },
   container: {
-    width: '85%',
-    maxHeight: '70%',
-    backgroundColor: '#fff',
-    borderRadius: 15,
-    overflow: 'hidden',
-  },
-  header: {
-    flexDirection: 'row',
+    width: '90%',
     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 },
+  closeBtn: {
+    position: 'absolute',
+    right: 15,
+    top: 20,
+    zIndex: 10,
+    width: 35,
+    height: 35,
+  },
+  closeIcon: {
+    width: 35,
+    height: 35,
+  },
+  contentBg: {
+    width: '100%',
+    height: 400,
+    paddingTop: 75,
+    paddingHorizontal: 30,
+    paddingBottom: 30,
+  },
+  title: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    color: '#333',
+    textAlign: 'center',
+    marginBottom: 15,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  ruleText: {
+    fontSize: 14,
+    color: '#666',
+    lineHeight: 24,
+  },
 });

+ 30 - 17
app/award-detail/index.tsx

@@ -2,28 +2,29 @@ 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,
+    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,
+    getPoolDetail,
+    poolIn,
+    poolOut,
+    previewOrder,
 } from '@/services/award';
 
 import { CheckoutModal } from './components/CheckoutModal';
+import { ExplainSection } from './components/ExplainSection';
 import { ProductList } from './components/ProductList';
 import { RecordModal } from './components/RecordModal';
 import { RuleModal } from './components/RuleModal';
@@ -113,6 +114,13 @@ export default function AwardDetailScreen() {
     setTimeout(() => loadData(), 500);
   };
 
+  const handleProductPress = (index: number) => {
+    router.push({
+      pathname: '/award-detail/swipe' as any,
+      params: { poolId, index },
+    });
+  };
+
   const handlePrev = () => {
     if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
   };
@@ -184,9 +192,11 @@ export default function AwardDetailScreen() {
             <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>
+                  <TouchableOpacity onPress={() => handleProductPress(currentIndex)} activeOpacity={0.9}>
+                    <Animated.View style={[styles.productImageBox, { transform: [{ translateY: floatAnim }] }]}>
+                      <Image source={{ uri: currentProduct.cover }} style={styles.productImage} contentFit="contain" />
+                    </Animated.View>
+                  </TouchableOpacity>
 
                   {/* 等级信息 */}
                   <ImageBackground
@@ -255,6 +265,9 @@ export default function AwardDetailScreen() {
           {/* 商品列表 */}
           <ProductList products={products} levelList={data.luckGoodsLevelProbabilityList} poolId={poolId!} price={data.price} />
 
+          {/* 说明文字 */}
+          <ExplainSection poolId={poolId!} />
+
           <View style={{ height: 150 }} />
         </ScrollView>
 

+ 481 - 0
app/award-detail/swipe.tsx

@@ -0,0 +1,481 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Dimensions,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { getPoolDetail } from '@/services/award';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface ProductItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  probability: number;
+  quantity?: number;
+  spu?: {
+    id: string;
+    cover: string;
+    name: string;
+    marketPrice?: number;
+    parameter?: string;
+    brandName?: string;
+    worksName?: string;
+    pic?: string;
+  };
+}
+
+interface PoolData {
+  id: string;
+  name: string;
+  price: number;
+  luckGoodsList: ProductItem[];
+  recommendedLuckPool?: any[];
+}
+
+export default function AwardDetailSwipeScreen() {
+  const { poolId, index } = useLocalSearchParams<{ poolId: string; index: 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(parseInt(index || '0', 10));
+
+  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();
+  }, [poolId]);
+
+  const handlePrev = () => {
+    if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
+  };
+
+  const handleNext = () => {
+    if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
+  };
+
+  const parseParameter = (paramStr?: string) => {
+    if (!paramStr) return [];
+    try {
+      return JSON.parse(paramStr);
+    } catch {
+      return [];
+    }
+  };
+
+  if (loading) {
+    return (
+      <View style={[styles.loadingContainer, { paddingTop: insets.top }]}>
+        <ActivityIndicator size="large" color="#ff6600" />
+      </View>
+    );
+  }
+
+  if (!data || products.length === 0) {
+    return (
+      <View style={[styles.loadingContainer, { paddingTop: insets.top }]}>
+        <Text style={styles.errorText}>商品不存在</Text>
+        <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
+          <Text style={styles.backBtn2Text}>返回</Text>
+        </TouchableOpacity>
+      </View>
+    );
+  }
+
+  const currentProduct = products[currentIndex];
+  const params = parseParameter(currentProduct?.spu?.parameter);
+  const detailPics = currentProduct?.spu?.pic ? currentProduct.spu.pic.split(',').filter(Boolean) : [];
+  const recommendList = data.recommendedLuckPool || [];
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="dark-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+          <Text style={styles.backText}>{'<'}</Text>
+        </TouchableOpacity>
+        <View style={styles.placeholder} />
+      </View>
+
+      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+        {/* 商品图片区域 */}
+        <View style={styles.imageSection}>
+          <Image
+            source={{ uri: currentProduct?.spu?.cover || currentProduct?.cover }}
+            style={styles.productImage}
+            contentFit="contain"
+          />
+
+          {/* 左右切换按钮 */}
+          {currentIndex > 0 && (
+            <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
+              <Text style={styles.arrowText}>{'<'}</Text>
+            </TouchableOpacity>
+          )}
+          {currentIndex < products.length - 1 && (
+            <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
+              <Text style={styles.arrowText}>{'>'}</Text>
+            </TouchableOpacity>
+          )}
+        </View>
+
+        {/* 价格和名称区域 */}
+        <View style={styles.priceSection}>
+          <View style={styles.priceRow}>
+            <Text style={styles.priceText}>¥{currentProduct?.spu?.marketPrice || data.price}</Text>
+          </View>
+          <Text style={styles.productName}>{currentProduct?.name}</Text>
+        </View>
+
+        {/* 参数区域 */}
+        <View style={styles.paramSection}>
+          <View style={styles.paramHeader}>
+            <Text style={styles.paramTitle}>参数</Text>
+          </View>
+          {params.length > 0 && (
+            <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.paramScroll}>
+              {params.map((param: { label: string; value: string }, idx: number) => (
+                <View key={idx} style={styles.paramItem}>
+                  <Text style={styles.paramLabel}>{param.label}</Text>
+                  <Text style={styles.paramValue}>{param.value}</Text>
+                </View>
+              ))}
+            </ScrollView>
+          )}
+          {currentProduct?.spu?.worksName && (
+            <View style={styles.paramRow}>
+              <Text style={styles.paramRowLabel}>IP</Text>
+              <Text style={styles.paramRowValue}>{currentProduct.spu.worksName}</Text>
+            </View>
+          )}
+          {currentProduct?.spu?.brandName && (
+            <View style={styles.paramRow}>
+              <Text style={styles.paramRowLabel}>品牌</Text>
+              <Text style={styles.paramRowValue}>{currentProduct.spu.brandName}</Text>
+            </View>
+          )}
+        </View>
+
+        {/* 放心购 正品保障 */}
+        <View style={styles.guaranteeSection}>
+          <Text style={styles.guaranteeTitle}>放心购  正品保障</Text>
+          <Text style={styles.guaranteeText}>不支持七天无理由退换货 包邮</Text>
+        </View>
+
+        {/* 商品推荐 */}
+        {recommendList.length > 0 && (
+          <View style={styles.recommendSection}>
+            <Text style={styles.recommendTitle}>商品推荐</Text>
+            <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.recommendScroll}>
+              {recommendList.map((item: any) => (
+                <TouchableOpacity 
+                  key={item.id} 
+                  style={styles.recommendItem} 
+                  activeOpacity={0.8}
+                  onPress={() => router.push({ pathname: '/award-detail', params: { poolId: item.id } } as any)}
+                >
+                  <Image source={{ uri: item.cover }} style={styles.recommendImage} contentFit="contain" />
+                  <Text style={styles.recommendName} numberOfLines={1}>{item.name}</Text>
+                  <Text style={styles.recommendPrice}>¥{item.price}</Text>
+                </TouchableOpacity>
+              ))}
+            </ScrollView>
+          </View>
+        )}
+
+        {/* 商品详情 */}
+        <View style={styles.detailSection}>
+          <Text style={styles.detailTitle}>商品详情</Text>
+          {detailPics.length > 0 ? (
+            detailPics.map((pic, idx) => (
+              <Image key={idx} source={{ uri: pic }} style={styles.detailImage} contentFit="contain" />
+            ))
+          ) : (
+            <View style={styles.detailContent}>
+              <Text style={styles.detailHeading}>商城购买须知!</Text>
+              
+              <Text style={styles.detailSubTitle}>商城现货</Text>
+              <Text style={styles.detailText}>
+                商城所售现货商品均为全新正版商品。手办模玩非艺术品,因厂商品控差异导致的微小瑕疵属于正常情况,官图仅供参考,具体以实物为准。
+              </Text>
+              
+              <Text style={styles.detailSubTitle}>新品预定</Text>
+              <Text style={styles.detailText}>
+                预定商品的总价=定金+尾款,在预定期限内支付定金后,商品到货并补齐尾款后,超级商城才会发货相应商品预定订单确认成功后,定金不可退。{'\n'}
+                商品页面显示的商品制作完成时间及预计补款时间,都是按照官方预估的时间推测,具体到货时间请以实际出货为准。如因厂商、海关等因素造成延期的,不接受以此原因申请定金退款,请耐心等待。
+              </Text>
+              
+              <Text style={styles.detailSubTitle}>预售补款</Text>
+              <Text style={styles.detailText}>
+                商品到货后超级商城会通过您在预定时预留的号码进行短信通知请自行留意。为防止错过补款通知,可添加商城客服,并备注所购商品进入对应社群,社群会同步推送新品咨询及补款通知。
+              </Text>
+            </View>
+          )}
+        </View>
+
+        <View style={{ height: 50 }} />
+      </ScrollView>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#f5f5f5' },
+  loadingContainer: { flex: 1, backgroundColor: '#f5f5f5', 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,
+    backgroundColor: 'transparent',
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 10,
+  },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backText: { color: '#333', fontSize: 24, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+
+  scrollView: { flex: 1 },
+
+  // 图片区域
+  imageSection: {
+    paddingTop: 80,
+    paddingBottom: 20,
+    alignItems: 'center',
+    position: 'relative',
+  },
+  productImage: {
+    width: SCREEN_WIDTH * 0.7,
+    height: 350,
+  },
+  prevBtn: {
+    position: 'absolute',
+    left: 10,
+    top: '50%',
+    width: 36,
+    height: 36,
+    backgroundColor: 'rgba(0,0,0,0.3)',
+    borderRadius: 18,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  nextBtn: {
+    position: 'absolute',
+    right: 10,
+    top: '50%',
+    width: 36,
+    height: 36,
+    backgroundColor: 'rgba(0,0,0,0.3)',
+    borderRadius: 18,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  arrowText: { color: '#fff', fontSize: 18, fontWeight: 'bold' },
+
+  // 价格区域
+  priceSection: {
+    backgroundColor: '#fff',
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+  },
+  priceRow: {
+    flexDirection: 'row',
+    alignItems: 'baseline',
+  },
+  priceText: {
+    fontSize: 24,
+    color: '#ff4444',
+    fontWeight: 'bold',
+  },
+  productName: {
+    fontSize: 16,
+    color: '#333',
+    marginTop: 8,
+    lineHeight: 22,
+  },
+
+  // 参数区域
+  paramSection: {
+    backgroundColor: '#fff',
+    marginTop: 10,
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+  },
+  paramHeader: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingBottom: 10,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  paramTitle: {
+    fontSize: 14,
+    color: '#666',
+  },
+  paramScroll: {
+    marginTop: 10,
+  },
+  paramItem: {
+    paddingHorizontal: 12,
+    borderRightWidth: 1,
+    borderRightColor: '#eee',
+  },
+  paramLabel: { fontSize: 14, color: '#666' },
+  paramValue: { fontSize: 14, color: '#333', marginTop: 4 },
+  paramRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingTop: 12,
+  },
+  paramRowLabel: {
+    fontSize: 14,
+    color: '#666',
+    width: 50,
+  },
+  paramRowValue: {
+    fontSize: 14,
+    color: '#333',
+    flex: 1,
+  },
+
+  // 放心购区域
+  guaranteeSection: {
+    backgroundColor: '#fff',
+    marginTop: 10,
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+  },
+  guaranteeTitle: {
+    fontSize: 14,
+    color: '#333',
+    fontWeight: 'bold',
+  },
+  guaranteeText: {
+    fontSize: 12,
+    color: '#999',
+  },
+
+  // 商品推荐
+  recommendSection: {
+    backgroundColor: '#1a1a1a',
+    marginTop: 10,
+    paddingHorizontal: 16,
+    paddingVertical: 16,
+  },
+  recommendTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    marginBottom: 12,
+  },
+  recommendScroll: {
+    paddingRight: 10,
+  },
+  recommendItem: {
+    width: 90,
+    marginRight: 12,
+  },
+  recommendImage: {
+    width: 90,
+    height: 90,
+    backgroundColor: '#fff',
+    borderRadius: 4,
+  },
+  recommendName: {
+    fontSize: 12,
+    color: '#fff',
+    marginTop: 6,
+  },
+  recommendPrice: {
+    fontSize: 14,
+    color: '#ff4444',
+    fontWeight: 'bold',
+    marginTop: 4,
+  },
+
+  // 商品详情
+  detailSection: {
+    backgroundColor: '#1a1a1a',
+    marginTop: 10,
+    paddingHorizontal: 16,
+    paddingVertical: 16,
+  },
+  detailTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    marginBottom: 16,
+  },
+  detailImage: {
+    width: SCREEN_WIDTH - 32,
+    height: 400,
+    marginBottom: 10,
+  },
+  detailContent: {
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    padding: 16,
+  },
+  detailHeading: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    color: '#333',
+    textAlign: 'center',
+    marginBottom: 20,
+  },
+  detailSubTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#333',
+    marginTop: 16,
+    marginBottom: 8,
+    paddingLeft: 10,
+    borderLeftWidth: 3,
+    borderLeftColor: '#333',
+  },
+  detailText: {
+    fontSize: 14,
+    color: '#666',
+    lineHeight: 22,
+  },
+});

+ 0 - 1
app/boxInBox/_layout.tsx

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

+ 302 - 82
app/boxInBox/boxList.tsx

@@ -1,9 +1,9 @@
 import { Image } from 'expo-image';
 import { useRouter } from 'expo-router';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import {
-    ActivityIndicator,
     Alert,
+    ImageBackground,
     ScrollView,
     StatusBar,
     StyleSheet,
@@ -13,7 +13,8 @@ import {
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
-import { get, post } from '@/services/http';
+import { Images } from '@/constants/images';
+import { getMyBoxes, openBox } from '@/services/award';
 
 interface BoxItem {
   id: string;
@@ -21,127 +22,346 @@ interface BoxItem {
   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 [loading, setLoading] = useState(false);
 
-  const loadData = async () => {
-    setLoading(true);
+  const loadData = useCallback(async () => {
     try {
+      setLoading(true);
       const res = await getMyBoxes();
-      setList(res || []);
+      // 确保返回的是数组
+      setList(Array.isArray(res) ? res : []);
     } catch (error) {
-      console.error('加载宝箱列表失败:', error);
+      console.error('获取宝箱列表失败:', error);
+      setList([]);
+    } finally {
+      setLoading(false);
     }
-    setLoading(false);
-  };
+  }, []);
 
   useEffect(() => {
     loadData();
-  }, []);
+  }, [loadData]);
 
-  const toggleSelect = (id: string) => {
-    setSelectedIds(prev => 
-      prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
-    );
+  const toggleSelect = (item: BoxItem) => {
+    if (!item.id) return;
+    
+    setSelectedIds(prev => {
+      const index = prev.indexOf(item.id);
+      if (index > -1) {
+        return prev.filter(id => id !== item.id);
+      } else {
+        return [...prev, item.id];
+      }
+    });
   };
 
+  const isSelected = (id: string) => selectedIds.includes(id);
+
+  const isAllSelected = list.length > 0 && selectedIds.length === list.length;
+
   const selectAll = () => {
-    if (selectedIds.length === list.length) {
+    if (isAllSelected) {
       setSelectedIds([]);
     } else {
       setSelectedIds(list.map(item => item.id));
     }
   };
 
-  const handleOpen = async () => {
+  const openSelectedBoxes = async () => {
     if (selectedIds.length === 0) {
       Alert.alert('提示', '请先选择宝箱');
       return;
     }
+
     try {
-      const res = await openBoxes(selectedIds.join(','));
+      const boxIds = selectedIds.join(',');
+      const res = await openBox({ boxIds });
+      
       if (res.code === 0) {
-        Alert.alert('成功', '开启成功');
-        setSelectedIds([]);
-        loadData();
+        // TODO: 显示开箱结果弹窗
+        Alert.alert('开箱成功', '恭喜获得奖品!', [
+          { text: '确定', onPress: () => {
+            setSelectedIds([]);
+            loadData();
+          }}
+        ]);
+      } else {
+        Alert.alert('提示', res.msg || '开箱失败');
       }
     } catch (error) {
-      console.error('开启失败:', error);
+      console.error('开箱失败:', error);
+      Alert.alert('提示', '开箱失败,请重试');
     }
   };
 
+  const handleBack = () => {
+    router.back();
+  };
+
   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)}
+      <ImageBackground
+        source={{ uri: Images.mine.kaixinMineBg }}
+        style={styles.background}
+        resizeMode="cover"
+      >
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+            <Text style={styles.backIcon}>‹</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>宝箱</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        {/* 内容区域 */}
+        <View style={styles.wrapper}>
+          <View style={styles.contentBox}>
+            <ScrollView 
+              showsVerticalScrollIndicator={false}
+              contentContainerStyle={styles.scrollContent}
+            >
+              <View style={styles.itemList}>
+                {list.map((item, index) => (
+                  <TouchableOpacity
+                    key={item.id || index}
+                    style={styles.item}
+                    onPress={() => toggleSelect(item)}
+                    activeOpacity={0.8}
+                  >
+                    <View style={[
+                      styles.itemCenter,
+                      isSelected(item.id) && styles.itemSelected
+                    ]}>
+                      <View style={styles.imgBox}>
+                        <Image
+                          source={{ uri: item.boxCover }}
+                          style={styles.boxImage}
+                          contentFit="cover"
+                        />
+                        {isSelected(item.id) && (
+                          <View style={styles.selectMask}>
+                            <Text style={styles.checkIcon}>✓</Text>
+                          </View>
+                        )}
+                      </View>
+                      <View style={styles.textBox}>
+                        <Text style={styles.boxName} numberOfLines={1}>
+                          {item.boxName}
+                        </Text>
+                      </View>
+                    </View>
+                  </TouchableOpacity>
+                ))}
+              </View>
+
+              {list.length === 0 && !loading && (
+                <View style={styles.empty}>
+                  <Text style={styles.emptyText}>暂无宝箱</Text>
+                </View>
+              )}
+            </ScrollView>
+          </View>
+        </View>
+
+        {/* 底部按钮 */}
+        <View style={[styles.bottomBtnBox, { paddingBottom: insets.bottom + 80 }]}>
+          <View style={styles.btnGroup}>
+            <TouchableOpacity
+              style={styles.selectAllBtn}
+              onPress={selectAll}
+            >
+              <ImageBackground
+                source={{ uri: Images.common.loginBtn }}
+                style={styles.btnBg}
+                resizeMode="stretch"
+              >
+                <Text style={styles.btnText}>
+                  {isAllSelected ? '取消全选' : '全选'}
+                </Text>
+              </ImageBackground>
+            </TouchableOpacity>
+            
+            <TouchableOpacity
+              style={[
+                styles.openBtn,
+                selectedIds.length === 0 && styles.openBtnDisabled
+              ]}
+              onPress={openSelectedBoxes}
+              disabled={selectedIds.length === 0}
+            >
+              <ImageBackground
+                source={{ uri: Images.common.loginBtn }}
+                style={styles.btnBg}
+                resizeMode="stretch"
               >
-                <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>
-            ))}
+                <Text style={styles.btnText}>
+                  开启宝箱 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
+                </Text>
+              </ImageBackground>
+            </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>
+      </ImageBackground>
     </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' },
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  background: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+    zIndex: 100,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  title: {
+    fontSize: 15,
+    fontWeight: 'bold',
+    color: '#fff',
+    textAlign: 'center',
+  },
+  placeholder: {
+    width: 40,
+  },
+  wrapper: {
+    flex: 1,
+    padding: 10,
+  },
+  contentBox: {
+    flex: 1,
+    backgroundColor: '#fff',
+    borderRadius: 12,
+    borderWidth: 1,
+    borderColor: '#000',
+    padding: 10,
+  },
+  scrollContent: {
+    paddingBottom: 60,
+  },
+  itemList: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+  },
+  item: {
+    width: '33.33%',
+    height: 110,
+    padding: 5,
+  },
+  itemCenter: {
+    width: '100%',
+    height: '100%',
+    alignItems: 'center',
+  },
+  itemSelected: {
+    transform: [{ scale: 0.95 }],
+    opacity: 0.8,
+  },
+  imgBox: {
+    width: '85%',
+    height: '75%',
+    position: 'relative',
+  },
+  boxImage: {
+    width: '100%',
+    height: '100%',
+    borderRadius: 8,
+  },
+  selectMask: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    backgroundColor: 'rgba(0, 0, 0, 0.4)',
+    borderRadius: 8,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  checkIcon: {
+    fontSize: 30,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  textBox: {
+    width: '100%',
+    alignItems: 'center',
+    marginTop: 4,
+  },
+  boxName: {
+    fontSize: 13,
+    color: '#000',
+  },
+  empty: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingVertical: 100,
+  },
+  emptyText: {
+    fontSize: 14,
+    color: '#999',
+  },
+  bottomBtnBox: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    padding: 10,
+  },
+  btnGroup: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    width: '90%',
+    alignSelf: 'center',
+  },
+  selectAllBtn: {
+    width: '30%',
+    height: 44,
+    marginRight: 10,
+  },
+  openBtn: {
+    flex: 1,
+    height: 44,
+  },
+  openBtnDisabled: {
+    opacity: 0.6,
+  },
+  btnBg: {
+    width: '100%',
+    height: '100%',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  btnText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#000',
+  },
 });

+ 411 - 0
app/boxInBox/components/DetailsPopup.tsx

@@ -0,0 +1,411 @@
+import { Image } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
+import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+
+interface PrizeItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  price?: number;
+  probability?: number;
+}
+
+interface GoodsItem {
+  id: string;
+  name: string;
+  cover: string;
+  level: string;
+  price?: number;
+  probability?: number;
+}
+
+interface DetailsPopupProps {}
+
+export interface DetailsPopupRef {
+  open: (item: GoodsItem, prizes: PrizeItem[]) => void;
+  close: () => void;
+}
+
+export const DetailsPopup = forwardRef<DetailsPopupRef, DetailsPopupProps>((_, ref) => {
+  const [visible, setVisible] = useState(false);
+  const [info, setInfo] = useState<GoodsItem | null>(null);
+  const [prizes, setPrizes] = useState<PrizeItem[]>([]);
+
+  // 按等级分类奖品
+  const { cellAList, cellBList, cellCList, cellDList, cellAprobability, cellBprobability, cellCprobability, cellDprobability } = useMemo(() => {
+    const result = {
+      cellAList: [] as PrizeItem[],
+      cellBList: [] as PrizeItem[],
+      cellCList: [] as PrizeItem[],
+      cellDList: [] as PrizeItem[],
+      cellAprobability: 0,
+      cellBprobability: 0,
+      cellCprobability: 0,
+      cellDprobability: 0,
+    };
+
+    prizes.forEach((item) => {
+      switch (item.level) {
+        case 'A':
+          result.cellAList.push(item);
+          result.cellAprobability += item.probability || 0;
+          break;
+        case 'B':
+          result.cellBList.push(item);
+          result.cellBprobability += item.probability || 0;
+          break;
+        case 'C':
+          result.cellCList.push(item);
+          result.cellCprobability += item.probability || 0;
+          break;
+        case 'D':
+          result.cellDList.push(item);
+          result.cellDprobability += item.probability || 0;
+          break;
+      }
+    });
+
+    return result;
+  }, [prizes]);
+
+  useImperativeHandle(ref, () => ({
+    open: (item: GoodsItem, prizeList: PrizeItem[]) => {
+      setInfo(item);
+      setPrizes(prizeList);
+      setVisible(true);
+    },
+    close: () => setVisible(false),
+  }));
+
+  const isGuaranteed = info?.level === 'NESTED_BOX_GUARANTEED';
+
+  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} numberOfLines={1}>{info?.name}</Text>
+            <View style={[styles.levelBadge, isGuaranteed ? styles.levelD : styles.levelAll]}>
+              <Text style={styles.levelText}>{isGuaranteed ? 'D赏' : '全局赏'}</Text>
+            </View>
+          </View>
+
+          {/* 主图展示区 */}
+          <View style={styles.mainContent}>
+            <View style={styles.productImage}>
+              <Image source={{ uri: info?.cover }} style={styles.image} contentFit="cover" />
+            </View>
+            <View style={styles.priceInfo}>
+              <View style={styles.priceItem}>
+                <Text style={styles.label}>指导价:</Text>
+                <Text style={styles.value}>¥ {info?.price || 0}</Text>
+              </View>
+              <View style={styles.priceItem}>
+                <Text style={styles.label}>概率:</Text>
+                <Text style={styles.value}>{((info?.probability || 0) * 100).toFixed(2)}%</Text>
+              </View>
+              <Text style={styles.tips}>1~3抽完赠机送</Text>
+            </View>
+          </View>
+
+          {/* 奖品列表 - 仅非保底款显示 */}
+          {!isGuaranteed && (
+            <ScrollView style={styles.prizeScroll} showsVerticalScrollIndicator={false}>
+              {/* A赏 */}
+              {cellAList.length > 0 && (
+                <View style={styles.prizeSection}>
+                  <View style={styles.sectionHeader}>
+                    <View style={[styles.levelTag, styles.levelTagA]}>
+                      <Text style={styles.levelTagText}>A赏</Text>
+                    </View>
+                    <Text style={styles.probability}>概率{(cellAprobability * 100).toFixed(2)}%</Text>
+                  </View>
+                  <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.prizeList}>
+                    {cellAList.map((item, index) => (
+                      <View key={item.id || index} style={styles.prizeItem}>
+                        <View style={styles.prizeImageBox}>
+                          <Image source={{ uri: item.cover }} style={styles.prizeImage} contentFit="cover" />
+                          <View style={styles.itemPrice}>
+                            <Text style={styles.itemPriceText}>¥{item.price || 0}</Text>
+                          </View>
+                        </View>
+                        <Text style={styles.prizeName} numberOfLines={1}>{item.name}</Text>
+                      </View>
+                    ))}
+                  </ScrollView>
+                </View>
+              )}
+
+              {/* B赏 */}
+              {cellBList.length > 0 && (
+                <View style={styles.prizeSection}>
+                  <View style={styles.sectionHeader}>
+                    <View style={[styles.levelTag, styles.levelTagB]}>
+                      <Text style={styles.levelTagText}>B赏</Text>
+                    </View>
+                    <Text style={styles.probability}>概率{(cellBprobability * 100).toFixed(2)}%</Text>
+                  </View>
+                  <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.prizeList}>
+                    {cellBList.map((item, index) => (
+                      <View key={item.id || index} style={styles.prizeItem}>
+                        <View style={styles.prizeImageBox}>
+                          <Image source={{ uri: item.cover }} style={styles.prizeImage} contentFit="cover" />
+                          <View style={styles.itemPrice}>
+                            <Text style={styles.itemPriceText}>¥{item.price || 0}</Text>
+                          </View>
+                        </View>
+                        <Text style={styles.prizeName} numberOfLines={1}>{item.name}</Text>
+                      </View>
+                    ))}
+                  </ScrollView>
+                </View>
+              )}
+
+              {/* C赏 */}
+              {cellCList.length > 0 && (
+                <View style={styles.prizeSection}>
+                  <View style={styles.sectionHeader}>
+                    <View style={[styles.levelTag, styles.levelTagB]}>
+                      <Text style={styles.levelTagText}>C赏</Text>
+                    </View>
+                    <Text style={styles.probability}>概率{(cellCprobability * 100).toFixed(2)}%</Text>
+                  </View>
+                  <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.prizeList}>
+                    {cellCList.map((item, index) => (
+                      <View key={item.id || index} style={styles.prizeItem}>
+                        <View style={styles.prizeImageBox}>
+                          <Image source={{ uri: item.cover }} style={styles.prizeImage} contentFit="cover" />
+                          <View style={styles.itemPrice}>
+                            <Text style={styles.itemPriceText}>¥{item.price || 0}</Text>
+                          </View>
+                        </View>
+                        <Text style={styles.prizeName} numberOfLines={1}>{item.name}</Text>
+                      </View>
+                    ))}
+                  </ScrollView>
+                </View>
+              )}
+
+              {/* D赏 */}
+              {cellDList.length > 0 && (
+                <View style={styles.prizeSection}>
+                  <View style={styles.sectionHeader}>
+                    <View style={[styles.levelTag, styles.levelTagB]}>
+                      <Text style={styles.levelTagText}>D赏</Text>
+                    </View>
+                    <Text style={styles.probability}>概率{(cellDprobability * 100).toFixed(2)}%</Text>
+                  </View>
+                  <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.prizeList}>
+                    {cellDList.map((item, index) => (
+                      <View key={item.id || index} style={styles.prizeItem}>
+                        <View style={styles.prizeImageBox}>
+                          <Image source={{ uri: item.cover }} style={styles.prizeImage} contentFit="cover" />
+                          <View style={styles.itemPrice}>
+                            <Text style={styles.itemPriceText}>¥{item.price || 0}</Text>
+                          </View>
+                        </View>
+                        <Text style={styles.prizeName} numberOfLines={1}>{item.name}</Text>
+                      </View>
+                    ))}
+                  </ScrollView>
+                </View>
+              )}
+            </ScrollView>
+          )}
+
+          {/* 关闭按钮 */}
+          <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+            <Text style={styles.closeBtnText}>×</Text>
+          </TouchableOpacity>
+        </View>
+      </View>
+    </Modal>
+  );
+});
+
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'center',
+    alignItems: 'center',
+    padding: 20,
+  },
+  container: {
+    width: '100%',
+    maxWidth: 310,
+    backgroundColor: '#ffb300',
+    borderRadius: 10,
+    overflow: 'hidden',
+    maxHeight: '80%',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    padding: 15,
+  },
+  title: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    color: '#000',
+    flex: 1,
+    marginRight: 10,
+  },
+  levelBadge: {
+    paddingHorizontal: 12,
+    paddingVertical: 4,
+    borderRadius: 2,
+  },
+  levelD: {
+    backgroundColor: '#6340FF',
+    borderWidth: 1,
+    borderColor: '#A2BBFF',
+  },
+  levelAll: {
+    backgroundColor: '#A3E100',
+    borderWidth: 1,
+    borderColor: '#EAFFB1',
+  },
+  levelText: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  mainContent: {
+    backgroundColor: '#fff',
+    padding: 15,
+    flexDirection: 'row',
+  },
+  productImage: {
+    width: 100,
+    height: 100,
+    borderRadius: 6,
+    borderWidth: 2,
+    borderColor: '#E0E0E0',
+    overflow: 'hidden',
+  },
+  image: {
+    width: '100%',
+    height: '100%',
+  },
+  priceInfo: {
+    flex: 1,
+    paddingLeft: 15,
+    justifyContent: 'center',
+  },
+  priceItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 8,
+  },
+  label: {
+    fontSize: 14,
+    color: '#333',
+  },
+  value: {
+    fontSize: 14,
+    color: '#333',
+    fontWeight: 'bold',
+  },
+  tips: {
+    fontSize: 13,
+    color: '#FF9500',
+    marginTop: 4,
+  },
+  prizeScroll: {
+    maxHeight: 325,
+    backgroundColor: '#fff',
+  },
+  prizeSection: {
+    marginTop: 15,
+  },
+  sectionHeader: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 15,
+    marginBottom: 10,
+  },
+  levelTag: {
+    paddingHorizontal: 8,
+    paddingVertical: 3,
+    borderRadius: 3,
+    borderWidth: 1.5,
+    borderColor: '#000',
+    marginRight: 10,
+  },
+  levelTagA: {
+    backgroundColor: '#FFD700',
+  },
+  levelTagB: {
+    backgroundColor: '#FFA500',
+  },
+  levelTagText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#000',
+  },
+  probability: {
+    fontSize: 14,
+    color: '#000',
+  },
+  prizeList: {
+    paddingHorizontal: 15,
+    paddingBottom: 15,
+  },
+  prizeItem: {
+    marginRight: 10,
+    alignItems: 'center',
+  },
+  prizeImageBox: {
+    width: 80,
+    height: 80,
+    borderRadius: 6,
+    borderWidth: 2,
+    borderColor: '#000',
+    overflow: 'hidden',
+    position: 'relative',
+  },
+  prizeImage: {
+    width: '100%',
+    height: '100%',
+  },
+  itemPrice: {
+    position: 'absolute',
+    bottom: 0,
+    left: 0,
+    right: 0,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    paddingVertical: 2,
+  },
+  itemPriceText: {
+    fontSize: 10,
+    color: '#fff',
+    textAlign: 'center',
+  },
+  prizeName: {
+    marginTop: 5,
+    fontSize: 12,
+    color: '#000',
+    textAlign: 'center',
+    width: 80,
+  },
+  closeBtn: {
+    position: 'absolute',
+    right: 10,
+    top: 10,
+    width: 24,
+    height: 24,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  closeBtnText: {
+    fontSize: 20,
+    color: '#333',
+    fontWeight: 'bold',
+  },
+});

+ 81 - 21
app/boxInBox/index.tsx

@@ -2,28 +2,29 @@ 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,
+    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 { getBoxDetail, getWinRecords, 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';
+import { DetailsPopup, DetailsPopupRef } from './components/DetailsPopup';
 
 const { width: SCREEN_WIDTH } = Dimensions.get('window');
 
@@ -89,10 +90,12 @@ export default function BoxInBoxScreen() {
   const [emptyRuns, setEmptyRuns] = useState(0);
   const [scrollTop, setScrollTop] = useState(0);
   const [tabIndex, setTabIndex] = useState(0);
+  const [recordList, setRecordList] = useState<any[]>([]);
 
   const checkoutRef = useRef<any>(null);
   const ruleRef = useRef<any>(null);
   const boxPopupRef = useRef<BoxPopupRef>(null);
+  const detailsPopupRef = useRef<DetailsPopupRef>(null);
   const floatAnim = useRef(new Animated.Value(0)).current;
   const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
@@ -153,6 +156,19 @@ export default function BoxInBoxScreen() {
     } catch {}
   }, [poolId]);
 
+  // 加载中奖记录
+  const loadRecords = useCallback(async (boxNumber?: string) => {
+    if (!poolId) return;
+    try {
+      const num = boxNumber || boxHistoryInfo?.boxNumber;
+      if (!num) return;
+      const res = await getWinRecords(poolId, num);
+      if (res && res.records) {
+        setRecordList(res.records);
+      }
+    } catch {}
+  }, [poolId, boxHistoryInfo]);
+
   const refreshBox = useCallback(async () => {
     if (!poolId) return;
     try {
@@ -162,10 +178,11 @@ export default function BoxInBoxScreen() {
         setBoxHistoryInfo(res[0]);
         loadData();
         loadBox(res[0].boxNumber);
+        loadRecords(res[0].boxNumber);
       }
       loadEmptyRuns();
     } catch {}
-  }, [poolId, loadData, loadBox, loadEmptyRuns]);
+  }, [poolId, loadData, loadBox, loadEmptyRuns, loadRecords]);
 
   // 打开换盒弹窗
   const openBoxPopup = useCallback(async () => {
@@ -183,7 +200,20 @@ export default function BoxInBoxScreen() {
     setBoxHistoryInfo(item);
     loadBox(item.boxNumber);
     loadEmptyRuns();
-  }, [loadBox, loadEmptyRuns]);
+    loadRecords(item.boxNumber);
+  }, [loadBox, loadEmptyRuns, loadRecords]);
+
+  // 打开商品详情弹窗
+  const handleShowDetails = useCallback((item: any) => {
+    // 根据商品的 level 传入对应的奖品列表
+    let prizes = products;
+    if (item.level === 'NESTED_BOX_MEDIUM') {
+      prizes = data?.mediumBoxPrizes || products;
+    } else if (item.level === 'NESTED_BOX_SMALL') {
+      prizes = data?.smallBoxPrizes || products;
+    }
+    detailsPopupRef.current?.open(item, prizes);
+  }, [products, data]);
 
   const handleBoxResult = (res: any) => {
     const map: Record<string, any> = {};
@@ -227,6 +257,7 @@ export default function BoxInBoxScreen() {
     loadBox();
     loadBoxHistory();
     loadEmptyRuns();
+    loadRecords();
     if (poolId) poolIn(poolId);
     return () => {
       if (poolId) poolOut(poolId);
@@ -442,11 +473,11 @@ export default function BoxInBoxScreen() {
             {tabIndex === 0 && (
               <View style={styles.activityGoodsGrid}>
                 {activityGoods.map((item, index) => (
-                  <View key={item.id || index} style={styles.activityGoodsItem}>
+                  <TouchableOpacity key={item.id || index} style={styles.activityGoodsItem} onPress={() => handleShowDetails(item)}>
                     <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>
+                        <Text style={styles.probabilityText}>概率:{((item.probability || 0) * 100).toFixed(2)}%</Text>
                       </View>
                       <View style={styles.priceBadge}>
                         <Text style={styles.priceTextSmall}>参考价:{item.price}</Text>
@@ -456,16 +487,33 @@ export default function BoxInBoxScreen() {
                       <Text style={styles.levelBadgeText}>{item.level === 'NESTED_BOX_GUARANTEED' ? 'D赏' : '全局赏'}</Text>
                     </View>
                     <Text style={styles.activityName} numberOfLines={1}>{item.name}</Text>
-                  </View>
+                  </TouchableOpacity>
                 ))}
               </View>
             )}
 
-            {/* 中奖记录 - 暂时显示空状态 */}
+            {/* 中奖记录 */}
             {tabIndex === 1 && (
-              <View style={styles.emptyRecord}>
-                <Text style={styles.emptyRecordText}>暂无中奖记录</Text>
-              </View>
+              <ScrollView style={styles.recordScroll} showsVerticalScrollIndicator={false}>
+                {recordList.length === 0 ? (
+                  <View style={styles.emptyRecord}>
+                    <Text style={styles.emptyRecordText}>暂无中奖记录</Text>
+                  </View>
+                ) : (
+                  recordList.map((item, index) => (
+                    <View key={index} style={styles.recordItem}>
+                      <View style={styles.recordLeft}>
+                        <Text style={styles.recordNickname}>{item.nickname}</Text>
+                        <Text style={styles.recordTime}>{item.createTime} | 第{item.seatNumber}发</Text>
+                        <Text style={styles.recordPrize}>获得:<Text style={styles.recordPrizeName}>{item.prizeName}</Text></Text>
+                      </View>
+                      <View style={[styles.recordLevelBadge, item.level === 'D' ? styles.levelD : styles.levelAll]}>
+                        <Text style={styles.recordLevelText}>{item.level === 'D' ? 'D赏' : '全局赏'}</Text>
+                      </View>
+                    </View>
+                  ))
+                )}
+              </ScrollView>
             )}
           </View>
 
@@ -506,6 +554,7 @@ export default function BoxInBoxScreen() {
       <CheckoutModal ref={checkoutRef} data={data} poolId={poolId!} boxNumber={boxNum} onSuccess={handleSuccess} />
       <RuleModal ref={ruleRef} />
       <BoxPopup ref={boxPopupRef} onSelect={handleSelectBox} />
+      <DetailsPopup ref={detailsPopupRef} />
     </View>
   );
 }
@@ -598,6 +647,17 @@ const styles = StyleSheet.create({
   emptyRecord: { padding: 40, alignItems: 'center' },
   emptyRecordText: { fontSize: 14, color: '#999' },
 
+  // 中奖记录样式
+  recordScroll: { maxHeight: 220 },
+  recordItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 13, paddingHorizontal: 15, borderBottomWidth: 1, borderBottomColor: '#D8D8D8' },
+  recordLeft: { flex: 1 },
+  recordNickname: { fontSize: 12, fontWeight: 'bold', color: '#333' },
+  recordTime: { fontSize: 12, color: '#666', marginVertical: 3 },
+  recordPrize: { fontSize: 12, color: '#666' },
+  recordPrizeName: { fontWeight: '500', color: '#FF5100' },
+  recordLevelBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 2 },
+  recordLevelText: { fontSize: 12, fontWeight: 'bold', color: '#fff' },
+
   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 },

+ 9 - 0
app/exchange/_layout.tsx

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

+ 222 - 0
app/exchange/index.tsx

@@ -0,0 +1,222 @@
+import { useRouter } from 'expo-router';
+import React, { useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { harryExchange } from '@/services/award';
+
+export default function ExchangeScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [code, setCode] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const handleSubmit = async () => {
+    if (!code.trim()) {
+      Alert.alert('提示', '请输入兑换码');
+      return;
+    }
+
+    try {
+      setLoading(true);
+      const res = await harryExchange({ code: code.trim() });
+      
+      if (res?.success) {
+        let rewardType = '';
+        const data = res.data;
+        
+        switch (data?.rewardType) {
+          case 'USER_CREDIT':
+            rewardType = '积分';
+            break;
+          case 'CASH':
+            rewardType = '现金';
+            break;
+          case 'MAGIC':
+            rewardType = '果实';
+            break;
+          case 'MAGIC_POWER_COIN':
+            rewardType = '源力币';
+            break;
+          case 'COUPON':
+            rewardType = '优惠券';
+            break;
+          case 'SPU_ID':
+            rewardType = '商品';
+            break;
+          default:
+            rewardType = '奖励';
+        }
+
+        const text = data?.rewardType === 'COUPON' || data?.rewardType === 'SPU_ID'
+          ? `成功获得${rewardType}`
+          : `成功获得${rewardType}${data?.rewardAmount || ''}`;
+
+        Alert.alert('提示', text, [
+          { text: '确定', onPress: () => router.back() }
+        ]);
+        setCode('');
+      } else {
+        Alert.alert('提示', res?.msg || '兑换失败');
+      }
+    } catch (error) {
+      console.error('兑换失败:', error);
+      Alert.alert('提示', '兑换失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <ImageBackground
+      source={{ uri: Images.mine.kaixinMineBg }}
+      style={styles.container}
+      resizeMode="cover"
+    >
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>兑换码</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <View style={styles.content}>
+        <View style={styles.card}>
+          <Text style={styles.describe}>请在下方输入您的兑换码</Text>
+          
+          <View style={styles.inputWrapper}>
+            <TextInput
+              style={styles.input}
+              value={code}
+              onChangeText={setCode}
+              placeholder="请输入兑换码"
+              placeholderTextColor="#999"
+              multiline
+            />
+          </View>
+
+          <View style={styles.btnsWrapper}>
+            <TouchableOpacity 
+              style={[styles.btn, styles.cancelBtn]} 
+              onPress={handleBack}
+            >
+              <Text style={styles.cancelBtnText}>取消</Text>
+            </TouchableOpacity>
+            <TouchableOpacity 
+              style={[styles.btn, styles.confirmBtn]} 
+              onPress={handleSubmit}
+              disabled={loading}
+            >
+              <Text style={styles.confirmBtnText}>
+                {loading ? '兑换中...' : '确定'}
+              </Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+      </View>
+    </ImageBackground>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  content: {
+    flex: 1,
+    paddingHorizontal: 15,
+    paddingTop: 20,
+  },
+  card: {
+    backgroundColor: '#fff',
+    borderRadius: 15,
+    padding: 20,
+  },
+  describe: {
+    fontSize: 14,
+    color: '#333',
+    marginBottom: 15,
+    textAlign: 'center',
+  },
+  inputWrapper: {
+    backgroundColor: '#f5f5f5',
+    borderRadius: 8,
+    padding: 10,
+    marginBottom: 20,
+  },
+  input: {
+    height: 80,
+    fontSize: 16,
+    color: '#333',
+    textAlignVertical: 'top',
+  },
+  btnsWrapper: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+  },
+  btn: {
+    width: '45%',
+    height: 44,
+    borderRadius: 22,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  cancelBtn: {
+    backgroundColor: '#e0e0e0',
+  },
+  cancelBtnText: {
+    fontSize: 16,
+    color: '#666',
+  },
+  confirmBtn: {
+    backgroundColor: '#FC7D2E',
+  },
+  confirmBtnText: {
+    fontSize: 16,
+    color: '#fff',
+  },
+});

+ 9 - 0
app/feedback/_layout.tsx

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

+ 262 - 0
app/feedback/index.tsx

@@ -0,0 +1,262 @@
+import { useRouter } from 'expo-router';
+import React, { useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { submitFeedback } from '@/services/base';
+
+const FEEDBACK_TYPES = ['BUG', '建议', '投诉', '其他'];
+
+export default function FeedbackScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [feedbackType, setFeedbackType] = useState('BUG');
+  const [feedbackContent, setFeedbackContent] = useState('');
+  const [loading, setLoading] = useState(false);
+  const maxContentLength = 3000;
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const handleSubmit = async () => {
+    if (!feedbackContent.trim()) {
+      Alert.alert('提示', '反馈内容是必填的!');
+      return;
+    }
+
+    try {
+      setLoading(true);
+      await submitFeedback({
+        type: feedbackType,
+        text: feedbackContent.trim(),
+      });
+      
+      Alert.alert('提示', '提交成功!', [
+        { text: '确定', onPress: () => router.back() }
+      ]);
+    } catch (error) {
+      console.error('提交失败:', error);
+      Alert.alert('提示', '提交失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <ImageBackground
+      source={{ uri: Images.mine.kaixinMineBg }}
+      style={styles.container}
+      resizeMode="cover"
+    >
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>意见反馈</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+        <View style={styles.content}>
+          <View style={styles.card}>
+            {/* 类型选择 */}
+            <View style={styles.typeSelector}>
+              {FEEDBACK_TYPES.map((type) => (
+                <TouchableOpacity
+                  key={type}
+                  style={[
+                    styles.typeItem,
+                    feedbackType === type && styles.typeItemActive
+                  ]}
+                  onPress={() => setFeedbackType(type)}
+                >
+                  <Text style={[
+                    styles.typeText,
+                    feedbackType === type && styles.typeTextActive
+                  ]}>
+                    {type}
+                  </Text>
+                </TouchableOpacity>
+              ))}
+            </View>
+
+            {/* 投诉提示 */}
+            <View style={styles.complaintTip}>
+              <Text style={styles.complaintTipText}>
+                此投诉为本小程序自有投诉渠道,非微信官方投诉渠道
+              </Text>
+            </View>
+
+            {/* 输入框 */}
+            <View style={styles.inputWrapper}>
+              <TextInput
+                style={styles.input}
+                value={feedbackContent}
+                onChangeText={setFeedbackContent}
+                placeholder="请输入您的反馈内容,我们将及时为您处理。"
+                placeholderTextColor="#999"
+                multiline
+                maxLength={maxContentLength}
+              />
+              <View style={styles.inputCount}>
+                <Text style={[
+                  styles.countNum,
+                  feedbackContent.length > 0 && styles.countNumActive
+                ]}>
+                  {feedbackContent.length}
+                </Text>
+                <Text style={styles.countText}>/{maxContentLength}</Text>
+              </View>
+            </View>
+
+            {/* 提交按钮 */}
+            <TouchableOpacity
+              style={styles.submitBtn}
+              onPress={handleSubmit}
+              disabled={loading}
+            >
+              <Text style={styles.submitBtnText}>
+                {loading ? '提交中...' : '提交'}
+              </Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+      </ScrollView>
+    </ImageBackground>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  content: {
+    paddingHorizontal: 15,
+    paddingTop: 10,
+    paddingBottom: 30,
+  },
+  card: {
+    backgroundColor: '#fff',
+    borderRadius: 15,
+    padding: 15,
+  },
+  typeSelector: {
+    flexDirection: 'row',
+    marginBottom: 10,
+  },
+  typeItem: {
+    paddingHorizontal: 15,
+    paddingVertical: 8,
+    marginRight: 10,
+    borderRadius: 20,
+    backgroundColor: '#f8f8f8',
+  },
+  typeItemActive: {
+    backgroundColor: '#FC7D2E',
+  },
+  typeText: {
+    fontSize: 14,
+    color: '#666',
+  },
+  typeTextActive: {
+    color: '#fff',
+  },
+  complaintTip: {
+    backgroundColor: '#fff7e6',
+    borderWidth: 1,
+    borderColor: '#ffd591',
+    borderRadius: 4,
+    padding: 10,
+    marginBottom: 15,
+  },
+  complaintTipText: {
+    fontSize: 13,
+    color: '#fa8c16',
+  },
+  inputWrapper: {
+    position: 'relative',
+    marginBottom: 20,
+  },
+  input: {
+    height: 150,
+    backgroundColor: '#f8f8f8',
+    borderRadius: 8,
+    padding: 12,
+    fontSize: 14,
+    color: '#333',
+    textAlignVertical: 'top',
+  },
+  inputCount: {
+    position: 'absolute',
+    bottom: 10,
+    right: 10,
+    flexDirection: 'row',
+  },
+  countNum: {
+    fontSize: 12,
+    color: '#999',
+  },
+  countNumActive: {
+    color: '#1FA4FF',
+  },
+  countText: {
+    fontSize: 12,
+    color: '#999',
+  },
+  submitBtn: {
+    backgroundColor: '#FC7D2E',
+    height: 44,
+    borderRadius: 22,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  submitBtnText: {
+    fontSize: 16,
+    color: '#fff',
+    fontWeight: '500',
+  },
+});

+ 9 - 0
app/integral/_layout.tsx

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

+ 610 - 0
app/integral/index.tsx

@@ -0,0 +1,610 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getCreditRecord, getWalletInfo, signIn } from '@/services/user';
+
+interface DayInfo {
+  text: string;
+  date: string;
+  isFutureDate: boolean;
+  istoday: boolean;
+  isSignIn: boolean;
+}
+
+interface RecordItem {
+  id: string;
+  credit: number;
+  createTime: string;
+}
+
+export default function IntegralScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  
+  const [integral, setIntegral] = useState(0);
+  const [daysIntegral, setDaysIntegral] = useState(0);
+  const [istodaySignIn, setIstodaySignIn] = useState(false);
+  const [record, setRecord] = useState<RecordItem[]>([]);
+  const [dataInfo, setDataInfo] = useState<DayInfo[]>([]);
+  const [todayIntegral, setTodayIntegral] = useState(1);
+  const [tomorrowIntegral, setTomorrowIntegral] = useState(1);
+  const [isTooltip, setIsTooltip] = useState(false);
+
+  // 格式化数字为两位
+  const padWithZeros = (number: number, length: number) => {
+    return String(number).padStart(length, '0');
+  };
+
+  // 设置日期数据
+  const setDate = useCallback(() => {
+    const now = new Date();
+    const dataInfoArr: DayInfo[] = [];
+
+    // 前4天
+    for (let i = -4; i <= 0; i++) {
+      const date = new Date(now);
+      date.setDate(date.getDate() + i);
+      const m = date.getMonth() + 1;
+      const d = date.getDate();
+      const y = date.getFullYear();
+      
+      dataInfoArr.push({
+        text: `${m}.${d}`,
+        date: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
+        isFutureDate: false,
+        istoday: i === 0,
+        isSignIn: false,
+      });
+    }
+
+    // 明天
+    const tomorrow = new Date(now);
+    tomorrow.setDate(tomorrow.getDate() + 1);
+    const tm = tomorrow.getMonth() + 1;
+    const td = tomorrow.getDate();
+    const ty = tomorrow.getFullYear();
+    
+    dataInfoArr.push({
+      text: `${tm}.${td}`,
+      date: `${ty}-${padWithZeros(tm, 2)}-${padWithZeros(td, 2)}`,
+      isFutureDate: true,
+      istoday: false,
+      isSignIn: false,
+    });
+
+    setDataInfo(dataInfoArr);
+    return dataInfoArr;
+  }, []);
+
+  // 获取积分信息
+  const getInfo = useCallback(async () => {
+    try {
+      const info = await getWalletInfo('USER_CREDIT');
+      setIntegral(info?.balance || 0);
+    } catch (error) {
+      console.error('获取积分失败:', error);
+    }
+  }, []);
+
+  // 获取签到记录
+  const getData = useCallback(async (dateInfo: DayInfo[]) => {
+    try {
+      const fourDaysAgo = new Date();
+      fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
+      const m = fourDaysAgo.getMonth() + 1;
+      const d = fourDaysAgo.getDate();
+      const y = fourDaysAgo.getFullYear();
+      
+      const param = {
+        createTime: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
+      };
+
+      const res = await getCreditRecord(param);
+      const recordData = res?.data || [];
+      setRecord(recordData);
+
+      // 今天日期
+      const now = new Date();
+      const todayStr = `${now.getFullYear()}-${padWithZeros(now.getMonth() + 1, 2)}-${padWithZeros(now.getDate(), 2)}`;
+
+      // 更新签到状态
+      const updatedDataInfo = dateInfo.map(item => {
+        const newItem = { ...item };
+        
+        // 检查是否已签到
+        for (const rec of recordData) {
+          const createTime = rec.createTime?.slice(0, 10);
+          if (createTime === item.date) {
+            newItem.isSignIn = true;
+            if (item.date === todayStr) {
+              setIstodaySignIn(true);
+            }
+          }
+        }
+        
+        return newItem;
+      });
+
+      setDataInfo(updatedDataInfo);
+
+      // 计算签到积分
+      let total = 0;
+      for (const rec of recordData) {
+        if (rec.credit > 0) {
+          total += rec.credit;
+        }
+      }
+      setDaysIntegral(total);
+
+      // 计算今天和明天的积分
+      if (recordData.length > 0) {
+        const lastCredit = recordData[0]?.credit || 1;
+        const yesterday = new Date();
+        yesterday.setDate(yesterday.getDate() - 1);
+        const yesterdayStr = `${yesterday.getFullYear()}-${padWithZeros(yesterday.getMonth() + 1, 2)}-${padWithZeros(yesterday.getDate(), 2)}`;
+        const lastRecordDate = recordData[0]?.createTime?.slice(0, 10);
+
+        if (yesterdayStr === lastRecordDate) {
+          setTodayIntegral(lastCredit + 1);
+          setTomorrowIntegral(lastCredit + 2);
+        } else {
+          setTodayIntegral(lastCredit);
+          setTomorrowIntegral(Math.min(lastCredit + 1, 7));
+        }
+      }
+    } catch (error) {
+      console.error('获取签到记录失败:', error);
+    }
+  }, []);
+
+  // 初始化
+  useEffect(() => {
+    const dateInfo = setDate();
+    getInfo();
+    getData(dateInfo);
+  }, [setDate, getInfo, getData]);
+
+  // 签到
+  const handleSignIn = async () => {
+    if (istodaySignIn) {
+      Alert.alert('提示', '今天已签到');
+      return;
+    }
+
+    try {
+      const res = await signIn();
+      if (res?.code === 0) {
+        Alert.alert('提示', '签到成功');
+        setIstodaySignIn(true);
+        const dateInfo = setDate();
+        getInfo();
+        getData(dateInfo);
+      } else {
+        Alert.alert('提示', res?.msg || '签到失败');
+      }
+    } catch (error) {
+      console.error('签到失败:', error);
+      Alert.alert('提示', '签到失败');
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="dark-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.title}>我的积分</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+        {/* 头部积分展示 */}
+        <ImageBackground
+          source={{ uri: Images.integral?.head || Images.common.commonBg }}
+          style={styles.headerBg}
+          resizeMode="cover"
+        >
+          <Text style={styles.integralNum}>{integral}</Text>
+          <TouchableOpacity 
+            style={styles.allBox}
+            onPress={() => setIsTooltip(!isTooltip)}
+          >
+            <Text style={styles.allText}>所有积分</Text>
+            <Image
+              source={{ uri: Images.integral?.greetings }}
+              style={styles.infoIcon}
+              contentFit="contain"
+            />
+          </TouchableOpacity>
+          <View style={styles.todayBox}>
+            <Text style={styles.todayText}>
+              今日已获得<Text style={styles.todayNum}>{istodaySignIn ? todayIntegral : 0}</Text>积分
+            </Text>
+          </View>
+        </ImageBackground>
+
+        <View style={styles.content}>
+          {/* 签到面板 */}
+          <View style={styles.panel}>
+            <View style={styles.panelTitle}>
+              <View style={styles.panelTitleLeft}>
+                <Image
+                  source={{ uri: Images.integral?.goldCoins }}
+                  style={styles.goldIcon}
+                  contentFit="contain"
+                />
+                <Text style={styles.panelTitleText}>签到领积分</Text>
+              </View>
+              <Text style={[styles.panelTitleRight, istodaySignIn && styles.signedText]}>
+                {istodaySignIn ? '今日已签到' : '今日未签到'}
+              </Text>
+            </View>
+
+            <Text style={styles.explain}>
+              已签到{record.filter(r => r.credit > 0).length}天,共获得<Text style={styles.highlightText}>{daysIntegral}</Text>积分
+            </Text>
+
+            {/* 签到进度 */}
+            <View style={styles.progressBox}>
+              <View style={styles.lineBox}>
+                {[1, 2, 3, 4, 5].map((_, index) => (
+                  <View key={index} style={styles.lineSection}>
+                    <View style={[styles.line, styles.lineOn]} />
+                  </View>
+                ))}
+              </View>
+
+              <View style={styles.daysBox}>
+                {dataInfo.map((item, index) => (
+                  <View key={index} style={styles.dayItem}>
+                    <TouchableOpacity
+                      style={styles.dayCircleBox}
+                      onPress={item.istoday && !istodaySignIn ? handleSignIn : undefined}
+                      activeOpacity={item.istoday && !istodaySignIn ? 0.7 : 1}
+                    >
+                      {item.istoday && !istodaySignIn ? (
+                        <ImageBackground
+                          source={{ uri: Images.integral?.basisBg }}
+                          style={styles.dayCircleBg}
+                          resizeMode="contain"
+                        >
+                          <View style={[styles.dayCircle, styles.todayCircle]}>
+                            <Text style={styles.todayCircleText}>+{todayIntegral}</Text>
+                          </View>
+                        </ImageBackground>
+                      ) : item.isFutureDate ? (
+                        <View style={[styles.dayCircle, styles.futureCircle]}>
+                          <Text style={styles.futureText}>+{tomorrowIntegral}</Text>
+                        </View>
+                      ) : item.isSignIn ? (
+                        <View style={[styles.dayCircle, styles.signedCircle]}>
+                          <Text style={styles.checkIcon}>✓</Text>
+                        </View>
+                      ) : (
+                        <View style={[styles.dayCircle, styles.missedCircle]}>
+                          <Text style={styles.missedIcon}>✗</Text>
+                        </View>
+                      )}
+                    </TouchableOpacity>
+                    <Text style={[styles.dayText, item.istoday && styles.todayDayText]}>
+                      {item.istoday ? '今天' : item.text}
+                    </Text>
+                  </View>
+                ))}
+              </View>
+            </View>
+          </View>
+
+          {/* 积分明细 */}
+          <View style={styles.listSection}>
+            <Text style={styles.listTitle}>积分明细</Text>
+            {record.map((item, index) => (
+              <View key={item.id || index} style={styles.listItem}>
+                <View style={styles.listItemLeft}>
+                  <Text style={styles.listItemTitle}>
+                    {item.credit > 0 ? '积分签到获得' : '大转盘消费'}
+                  </Text>
+                  <Text style={styles.listItemTime}>{item.createTime}</Text>
+                </View>
+                <Text style={[
+                  styles.listItemCredit,
+                  item.credit > 0 ? styles.creditPositive : styles.creditNegative
+                ]}>
+                  {item.credit > 0 ? '+' : ''}{item.credit}
+                </Text>
+              </View>
+            ))}
+            {record.length === 0 && (
+              <View style={styles.emptyBox}>
+                <Text style={styles.emptyText}>暂无积分记录</Text>
+              </View>
+            )}
+          </View>
+        </View>
+      </ScrollView>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#F8FAFB',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+    backgroundColor: 'transparent',
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 100,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#000',
+    fontWeight: 'bold',
+  },
+  title: {
+    fontSize: 15,
+    fontWeight: 'bold',
+    color: '#000',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+  },
+  headerBg: {
+    width: '100%',
+    height: 283,
+    paddingTop: 100,
+    alignItems: 'center',
+  },
+  integralNum: {
+    fontSize: 48,
+    fontWeight: '400',
+    color: '#5B460F',
+    textAlign: 'center',
+  },
+  allBox: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginTop: 7,
+    marginBottom: 12,
+  },
+  allText: {
+    fontSize: 12,
+    color: '#C8B177',
+  },
+  infoIcon: {
+    width: 16,
+    height: 16,
+    marginLeft: 4,
+  },
+  todayBox: {
+    backgroundColor: 'rgba(0,0,0,0.08)',
+    borderRadius: 217,
+    paddingHorizontal: 15,
+    paddingVertical: 5,
+  },
+  todayText: {
+    fontSize: 12,
+    color: '#8A794F',
+  },
+  todayNum: {
+    fontWeight: '800',
+    color: '#5B460F',
+  },
+  content: {
+    paddingHorizontal: 10,
+    marginTop: -50,
+  },
+  panel: {
+    backgroundColor: '#fff',
+    borderRadius: 15,
+    padding: 12,
+  },
+  panelTitle: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingHorizontal: 10,
+  },
+  panelTitleLeft: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  goldIcon: {
+    width: 24,
+    height: 24,
+    marginRight: 6,
+  },
+  panelTitleText: {
+    fontSize: 16,
+    fontWeight: '700',
+    color: '#3D3D3D',
+  },
+  panelTitleRight: {
+    fontSize: 12,
+    color: '#FC7D2E',
+  },
+  signedText: {
+    color: '#999',
+  },
+  explain: {
+    fontSize: 12,
+    color: '#999',
+    paddingHorizontal: 10,
+    marginTop: 7,
+    marginBottom: 11,
+  },
+  highlightText: {
+    color: '#FC7D2E',
+  },
+  progressBox: {
+    paddingTop: 20,
+  },
+  lineBox: {
+    flexDirection: 'row',
+    paddingHorizontal: 10,
+  },
+  lineSection: {
+    flex: 1,
+  },
+  line: {
+    height: 1,
+    backgroundColor: '#9E9E9E',
+  },
+  lineOn: {
+    backgroundColor: '#FC7D2E',
+  },
+  daysBox: {
+    flexDirection: 'row',
+    marginTop: -25,
+  },
+  dayItem: {
+    flex: 1,
+    alignItems: 'center',
+  },
+  dayCircleBox: {
+    width: 46,
+    height: 46,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginBottom: 3,
+  },
+  dayCircleBg: {
+    width: 46,
+    height: 46,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  dayCircle: {
+    width: 32,
+    height: 32,
+    borderRadius: 16,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  todayCircle: {
+    backgroundColor: '#FC7D2E',
+  },
+  todayCircleText: {
+    color: '#fff',
+    fontSize: 12,
+    fontWeight: 'bold',
+  },
+  futureCircle: {
+    backgroundColor: '#F4F6F8',
+    borderWidth: 1,
+    borderColor: 'rgba(226,226,226,0.5)',
+  },
+  futureText: {
+    color: '#505050',
+    fontSize: 12,
+  },
+  signedCircle: {
+    backgroundColor: '#FFE7C4',
+  },
+  checkIcon: {
+    color: '#FC7D2E',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+  missedCircle: {
+    backgroundColor: '#F4F6F8',
+  },
+  missedIcon: {
+    color: '#999',
+    fontSize: 16,
+  },
+  dayText: {
+    fontSize: 12,
+    color: '#9E9E9E',
+  },
+  todayDayText: {
+    color: '#FC7D2E',
+  },
+  listSection: {
+    backgroundColor: '#fff',
+    borderRadius: 15,
+    padding: 12,
+    marginTop: 10,
+    marginBottom: 30,
+  },
+  listTitle: {
+    fontSize: 16,
+    fontWeight: '700',
+    color: '#3D3D3D',
+    marginBottom: 10,
+  },
+  listItem: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 14,
+    borderBottomWidth: 1,
+    borderBottomColor: 'rgba(0,0,0,0.05)',
+  },
+  listItemLeft: {},
+  listItemTitle: {
+    fontSize: 14,
+    color: '#333',
+    marginBottom: 3,
+  },
+  listItemTime: {
+    fontSize: 12,
+    color: '#999',
+  },
+  listItemCredit: {
+    fontSize: 18,
+    fontWeight: '700',
+  },
+  creditPositive: {
+    color: '#588CFF',
+  },
+  creditNegative: {
+    color: '#FC7D2E',
+  },
+  emptyBox: {
+    paddingVertical: 50,
+    alignItems: 'center',
+  },
+  emptyText: {
+    fontSize: 14,
+    color: '#999',
+  },
+});

+ 9 - 0
app/message/_layout.tsx

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

+ 254 - 0
app/message/index.tsx

@@ -0,0 +1,254 @@
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    FlatList,
+    RefreshControl,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { getMessageList } from '@/services/base';
+
+interface MessageItem {
+  id: string;
+  title: string;
+  content: string;
+  createTime: string;
+  type?: string;
+}
+
+export default function MessageScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  
+  const [messages, setMessages] = useState<MessageItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [refreshing, setRefreshing] = useState(false);
+  const [pageNum, setPageNum] = useState(1);
+  const [hasMore, setHasMore] = useState(true);
+  const pageSize = 10;
+
+  const loadData = useCallback(async (page: number, isRefresh = false) => {
+    if (loading && !isRefresh) return;
+    
+    try {
+      if (isRefresh) {
+        setRefreshing(true);
+      } else {
+        setLoading(true);
+      }
+
+      const res = await getMessageList(page, pageSize);
+      const records = res?.records || [];
+      
+      if (isRefresh) {
+        setMessages(records);
+      } else {
+        setMessages(prev => [...prev, ...records]);
+      }
+      
+      setHasMore(records.length >= pageSize);
+      setPageNum(page);
+    } catch (error) {
+      console.error('获取消息失败:', error);
+    } finally {
+      setLoading(false);
+      setRefreshing(false);
+    }
+  }, [loading]);
+
+  useEffect(() => {
+    loadData(1, true);
+  }, []);
+
+  const handleRefresh = () => {
+    loadData(1, true);
+  };
+
+  const handleLoadMore = () => {
+    if (hasMore && !loading) {
+      loadData(pageNum + 1);
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const renderItem = ({ item }: { item: MessageItem }) => (
+    <View style={styles.cell}>
+      <View style={styles.cellHeader}>
+        <View style={styles.icon}>
+          <Text style={styles.iconText}>🔔</Text>
+        </View>
+        <Text style={styles.title} numberOfLines={1}>{item.title}</Text>
+        <Text style={styles.time}>{item.createTime}</Text>
+      </View>
+      <Text style={styles.content}>{item.content}</Text>
+    </View>
+  );
+
+  const renderEmpty = () => (
+    <View style={styles.emptyBox}>
+      <Text style={styles.emptyText}>暂无消息</Text>
+    </View>
+  );
+
+  const renderFooter = () => {
+    if (!hasMore && messages.length > 0) {
+      return (
+        <View style={styles.footer}>
+          <Text style={styles.footerText}>没有更多了</Text>
+        </View>
+      );
+    }
+    if (loading && messages.length > 0) {
+      return (
+        <View style={styles.footer}>
+          <ActivityIndicator size="small" color="#999" />
+        </View>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>消息</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <FlatList
+        data={messages}
+        renderItem={renderItem}
+        keyExtractor={(item, index) => item.id || String(index)}
+        contentContainerStyle={[
+          styles.listContent,
+          messages.length === 0 && styles.emptyList
+        ]}
+        refreshControl={
+          <RefreshControl
+            refreshing={refreshing}
+            onRefresh={handleRefresh}
+            colors={['#FC7D2E']}
+            tintColor="#FC7D2E"
+          />
+        }
+        onEndReached={handleLoadMore}
+        onEndReachedThreshold={0.2}
+        ListEmptyComponent={!loading ? renderEmpty : null}
+        ListFooterComponent={renderFooter}
+      />
+    </View>
+  );
+}
+
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: '#1a1a2e',
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+    backgroundColor: 'transparent',
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  listContent: {
+    paddingHorizontal: 14,
+    paddingBottom: 30,
+  },
+  emptyList: {
+    flex: 1,
+  },
+  cell: {
+    backgroundColor: '#fff',
+    borderRadius: 10,
+    padding: 15,
+    marginTop: 10,
+  },
+  cellHeader: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  icon: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    backgroundColor: '#333',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  iconText: {
+    fontSize: 10,
+  },
+  title: {
+    flex: 1,
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#333',
+    marginLeft: 10,
+  },
+  time: {
+    fontSize: 12,
+    color: '#999',
+  },
+  content: {
+    fontSize: 14,
+    color: '#666',
+    marginTop: 8,
+    lineHeight: 20,
+  },
+  emptyBox: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    paddingTop: 200,
+  },
+  emptyText: {
+    fontSize: 14,
+    color: '#999',
+  },
+  footer: {
+    paddingVertical: 20,
+    alignItems: 'center',
+  },
+  footerText: {
+    fontSize: 12,
+    color: '#999',
+  },
+});

+ 9 - 0
app/profile/_layout.tsx

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

+ 364 - 0
app/profile/index.tsx

@@ -0,0 +1,364 @@
+import { Image } from 'expo-image';
+import * as ImagePicker from 'expo-image-picker';
+import { useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { useAuth } from '@/contexts/AuthContext';
+import { getUserInfo, updateNickname, updateUserInfo } from '@/services/user';
+
+interface FormData {
+  nickname: string;
+  avatar: string;
+  sex: number; // 1-男 2-女 3-保密
+}
+
+export default function ProfileScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { refreshUser } = useAuth();
+  
+  const [formData, setFormData] = useState<FormData>({
+    nickname: '',
+    avatar: '',
+    sex: 3,
+  });
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    loadUserInfo();
+  }, []);
+
+  const loadUserInfo = async () => {
+    try {
+      const res = await getUserInfo();
+      if (res) {
+        setFormData({
+          nickname: res.nickname || '',
+          avatar: res.avatar || '',
+          sex: (res as any).sex || 3,
+        });
+      }
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const handleChooseAvatar = async () => {
+    try {
+      const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+      if (!permissionResult.granted) {
+        Alert.alert('提示', '需要相册权限才能选择头像');
+        return;
+      }
+
+      const result = await ImagePicker.launchImageLibraryAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.Images,
+        allowsEditing: true,
+        aspect: [1, 1],
+        quality: 0.8,
+      });
+
+      if (!result.canceled && result.assets[0]) {
+        const imageUri = result.assets[0].uri;
+        setFormData(prev => ({ ...prev, avatar: imageUri }));
+        Alert.alert('提示', '头像选择成功,保存时将更新');
+      }
+    } catch (error) {
+      console.error('选择头像失败:', error);
+      Alert.alert('提示', '选择头像失败');
+    }
+  };
+
+  const handleSexChange = (sex: number) => {
+    setFormData(prev => ({ ...prev, sex }));
+  };
+
+  const handleSave = async () => {
+    if (!formData.nickname?.trim()) {
+      Alert.alert('提示', '请输入昵称');
+      return;
+    }
+
+    try {
+      setLoading(true);
+      
+      // 更新昵称
+      const nicknameRes = await updateNickname(formData.nickname);
+      
+      // 更新其他信息(性别等)
+      const infoRes = await updateUserInfo({ sex: formData.sex } as any);
+      
+      if (nicknameRes || infoRes) {
+        Alert.alert('提示', '保存成功', [
+          {
+            text: '确定',
+            onPress: () => {
+              refreshUser?.();
+              router.back();
+            }
+          }
+        ]);
+      } else {
+        Alert.alert('提示', '保存失败');
+      }
+    } catch (error) {
+      console.error('保存失败:', error);
+      Alert.alert('提示', '保存失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <ImageBackground
+      source={{ uri: Images.common.commonBg }}
+      style={styles.container}
+      resizeMode="cover"
+    >
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>个人资料</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+        {/* 头像 */}
+        <TouchableOpacity style={styles.avatarSection} onPress={handleChooseAvatar}>
+          <View style={styles.avatarWrapper}>
+            <Image
+              source={{ uri: formData.avatar || Images.common.defaultAvatar }}
+              style={styles.avatar}
+              contentFit="cover"
+            />
+          </View>
+          <Text style={styles.avatarTip}>点击更换头像</Text>
+        </TouchableOpacity>
+
+        {/* 表单 */}
+        <View style={styles.formSection}>
+          {/* 昵称 */}
+          <View style={styles.formItem}>
+            <Text style={styles.formLabel}>昵称</Text>
+            <TextInput
+              style={styles.formInput}
+              value={formData.nickname}
+              onChangeText={(text) => setFormData(prev => ({ ...prev, nickname: text }))}
+              placeholder="请输入昵称"
+              placeholderTextColor="#999"
+              maxLength={20}
+            />
+          </View>
+
+          {/* 性别 */}
+          <View style={styles.formItem}>
+            <Text style={styles.formLabel}>性别</Text>
+            <View style={styles.sexOptions}>
+              <TouchableOpacity
+                style={[styles.sexOption, formData.sex === 1 && styles.sexOptionActive]}
+                onPress={() => handleSexChange(1)}
+              >
+                <View style={[styles.radioOuter, formData.sex === 1 && styles.radioOuterActive]}>
+                  {formData.sex === 1 && <View style={styles.radioInner} />}
+                </View>
+                <Text style={[styles.sexText, formData.sex === 1 && styles.sexTextActive]}>男</Text>
+              </TouchableOpacity>
+              <TouchableOpacity
+                style={[styles.sexOption, formData.sex === 2 && styles.sexOptionActive]}
+                onPress={() => handleSexChange(2)}
+              >
+                <View style={[styles.radioOuter, formData.sex === 2 && styles.radioOuterActive]}>
+                  {formData.sex === 2 && <View style={styles.radioInner} />}
+                </View>
+                <Text style={[styles.sexText, formData.sex === 2 && styles.sexTextActive]}>女</Text>
+              </TouchableOpacity>
+              <TouchableOpacity
+                style={[styles.sexOption, formData.sex === 3 && styles.sexOptionActive]}
+                onPress={() => handleSexChange(3)}
+              >
+                <View style={[styles.radioOuter, formData.sex === 3 && styles.radioOuterActive]}>
+                  {formData.sex === 3 && <View style={styles.radioInner} />}
+                </View>
+                <Text style={[styles.sexText, formData.sex === 3 && styles.sexTextActive]}>保密</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+        </View>
+
+        {/* 保存按钮 */}
+        <TouchableOpacity
+          style={[styles.saveBtn, loading && styles.saveBtnDisabled]}
+          onPress={handleSave}
+          disabled={loading}
+        >
+          <ImageBackground
+            source={{ uri: Images.common.loginBtn }}
+            style={styles.saveBtnBg}
+            resizeMode="contain"
+          >
+            <Text style={styles.saveBtnText}>{loading ? '保存中...' : '确定'}</Text>
+          </ImageBackground>
+        </TouchableOpacity>
+      </ScrollView>
+    </ImageBackground>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+    paddingHorizontal: 20,
+  },
+  avatarSection: {
+    alignItems: 'center',
+    paddingVertical: 30,
+  },
+  avatarWrapper: {
+    width: 80,
+    height: 80,
+    borderRadius: 40,
+    borderWidth: 3,
+    borderColor: '#FFE996',
+    overflow: 'hidden',
+  },
+  avatar: {
+    width: '100%',
+    height: '100%',
+  },
+  avatarTip: {
+    marginTop: 10,
+    fontSize: 12,
+    color: 'rgba(255,255,255,0.7)',
+  },
+  formSection: {
+    backgroundColor: 'rgba(255,255,255,0.1)',
+    borderRadius: 10,
+    padding: 15,
+  },
+  formItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 15,
+    borderBottomWidth: 1,
+    borderBottomColor: 'rgba(255,255,255,0.2)',
+  },
+  formLabel: {
+    width: 60,
+    fontSize: 14,
+    color: '#fff',
+  },
+  formInput: {
+    flex: 1,
+    fontSize: 14,
+    color: '#fff',
+    padding: 0,
+  },
+  sexOptions: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  sexOption: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 20,
+  },
+  sexOptionActive: {},
+  radioOuter: {
+    width: 18,
+    height: 18,
+    borderRadius: 9,
+    borderWidth: 2,
+    borderColor: 'rgba(255,255,255,0.5)',
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginRight: 6,
+  },
+  radioOuterActive: {
+    borderColor: '#FC7D2E',
+  },
+  radioInner: {
+    width: 10,
+    height: 10,
+    borderRadius: 5,
+    backgroundColor: '#FC7D2E',
+  },
+  sexText: {
+    fontSize: 14,
+    color: 'rgba(255,255,255,0.7)',
+  },
+  sexTextActive: {
+    color: '#fff',
+  },
+  saveBtn: {
+    marginTop: 50,
+    alignItems: 'center',
+  },
+  saveBtnDisabled: {
+    opacity: 0.6,
+  },
+  saveBtnBg: {
+    width: 280,
+    height: 60,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  saveBtnText: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 2,
+  },
+});

+ 9 - 0
app/setting/_layout.tsx

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

+ 272 - 0
app/setting/index.tsx

@@ -0,0 +1,272 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    Alert,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Switch,
+    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 { logoff } from '@/services/user';
+
+export default function SettingScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { logout } = useAuth();
+  
+  const [animalChecked, setAnimalChecked] = useState(true);
+  const [vibratorChecked, setVibratorChecked] = useState(true);
+
+  useEffect(() => {
+    loadSettings();
+  }, []);
+
+  const loadSettings = async () => {
+    try {
+      const animal = await AsyncStorage.getItem('closeAnimal');
+      const vibrator = await AsyncStorage.getItem('closeVibrator');
+      setAnimalChecked(animal !== 'true');
+      setVibratorChecked(vibrator !== 'true');
+    } catch (error) {
+      console.error('加载设置失败:', error);
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const handleAnimalChange = async (value: boolean) => {
+    setAnimalChecked(value);
+    await AsyncStorage.setItem('closeAnimal', (!value).toString());
+  };
+
+  const handleVibratorChange = async (value: boolean) => {
+    setVibratorChecked(value);
+    await AsyncStorage.setItem('closeVibrator', (!value).toString());
+  };
+
+  const handleLogout = () => {
+    Alert.alert('提示', '确定要退出登录吗?', [
+      { text: '取消', style: 'cancel' },
+      {
+        text: '确定',
+        onPress: async () => {
+          await logout();
+          router.back();
+        }
+      }
+    ]);
+  };
+
+  const handleLogoff = () => {
+    Alert.alert('提示', '确定要注销当前账号吗?', [
+      { text: '取消', style: 'cancel' },
+      {
+        text: '确定',
+        style: 'destructive',
+        onPress: async () => {
+          try {
+            const res = await logoff();
+            if (res) {
+              await logout();
+              router.back();
+            }
+          } catch (error) {
+            console.error('注销失败:', error);
+            Alert.alert('提示', '注销失败');
+          }
+        }
+      }
+    ]);
+  };
+
+  const handleShowAgreement = (type: string) => {
+    const agreementType = type === 'user' ? 'user.html' : 'privacy.html';
+    console.log('跳转到协议页面:', agreementType);
+    router.push(`/agreement?type=${agreementType}` as any);
+  };
+
+  return (
+    <ImageBackground
+      source={{ uri: Images.mine.kaixinMineBg }}
+      style={styles.container}
+      resizeMode="cover"
+    >
+      <StatusBar barStyle="light-content" />
+      
+      {/* 顶部导航 */}
+      <View style={[styles.header, { paddingTop: insets.top }]}>
+        <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
+          <Text style={styles.backIcon}>‹</Text>
+        </TouchableOpacity>
+        <Text style={styles.headerTitle}>设置</Text>
+        <View style={styles.placeholder} />
+      </View>
+
+      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
+        {/* 通知设置 */}
+        <Text style={styles.sectionTitle}>通知</Text>
+        <View style={styles.card}>
+          <View style={styles.menuItem}>
+            <Text style={styles.menuText}>开箱动画</Text>
+            <Switch
+              value={animalChecked}
+              onValueChange={handleAnimalChange}
+              trackColor={{ false: '#e0e0e0', true: '#FC7D2E' }}
+              thumbColor="#fff"
+            />
+          </View>
+          <View style={styles.divider} />
+          <View style={styles.menuItem}>
+            <Text style={styles.menuText}>开箱震动</Text>
+            <Switch
+              value={vibratorChecked}
+              onValueChange={handleVibratorChange}
+              trackColor={{ false: '#e0e0e0', true: '#FC7D2E' }}
+              thumbColor="#fff"
+            />
+          </View>
+        </View>
+
+        {/* 关于 */}
+        <Text style={styles.sectionTitle}>关于</Text>
+        <View style={styles.card}>
+          <TouchableOpacity 
+            style={styles.menuItem}
+            onPress={() => handleShowAgreement('user')}
+          >
+            <Text style={styles.menuText}>用户协议</Text>
+            <Text style={styles.arrow}>›</Text>
+          </TouchableOpacity>
+          <View style={styles.divider} />
+          <TouchableOpacity 
+            style={styles.menuItem}
+            onPress={() => handleShowAgreement('privacy')}
+          >
+            <Text style={styles.menuText}>隐私协议</Text>
+            <Text style={styles.arrow}>›</Text>
+          </TouchableOpacity>
+        </View>
+
+        {/* 登录相关按钮 - 始终显示 */}
+        <View style={styles.btnSection}>
+          <TouchableOpacity style={styles.logoutBtn} onPress={handleLogout}>
+            <Text style={styles.logoutBtnText}>退出登录</Text>
+          </TouchableOpacity>
+          <TouchableOpacity style={styles.logoffBtn} onPress={handleLogoff}>
+            <Text style={styles.logoffBtnText}>注销账号</Text>
+          </TouchableOpacity>
+        </View>
+
+        <View style={{ height: 50 }} />
+      </ScrollView>
+    </ImageBackground>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 10,
+    height: 80,
+  },
+  backBtn: {
+    width: 40,
+    height: 40,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  backIcon: {
+    fontSize: 32,
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  headerTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#fff',
+  },
+  placeholder: {
+    width: 40,
+  },
+  scrollView: {
+    flex: 1,
+    paddingHorizontal: 15,
+  },
+  sectionTitle: {
+    fontSize: 14,
+    color: '#fff',
+    marginTop: 20,
+    marginBottom: 10,
+    marginLeft: 5,
+  },
+  card: {
+    backgroundColor: '#fff',
+    borderRadius: 10,
+    overflow: 'hidden',
+  },
+  menuItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 15,
+    paddingVertical: 15,
+  },
+  menuText: {
+    fontSize: 14,
+    color: '#666',
+  },
+  arrow: {
+    fontSize: 20,
+    color: '#ccc',
+  },
+  divider: {
+    height: 1,
+    backgroundColor: '#f0f0f0',
+    marginLeft: 15,
+  },
+  btnSection: {
+    marginTop: 30,
+    paddingHorizontal: 20,
+  },
+  logoutBtn: {
+    backgroundColor: '#FC7D2E',
+    height: 44,
+    borderRadius: 22,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginBottom: 15,
+  },
+  logoutBtnText: {
+    fontSize: 16,
+    color: '#fff',
+  },
+  logoffBtn: {
+    backgroundColor: 'transparent',
+    height: 44,
+    borderRadius: 22,
+    borderWidth: 1,
+    borderColor: '#999',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  logoffBtnText: {
+    fontSize: 16,
+    color: '#999',
+  },
+});

+ 11 - 0
app/store/_layout.tsx

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

+ 204 - 0
app/store/checkout.tsx

@@ -0,0 +1,204 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    Platform,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { Address, getDefaultAddress } from '@/services/address';
+import { takeApply, takePreview } from '@/services/award';
+
+interface GroupedGoods {
+  total: number;
+  data: { id: string; cover: string; spuId: string; level: string };
+}
+
+export default function CheckoutScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { ids } = useLocalSearchParams<{ ids: string }>();
+
+  const [loading, setLoading] = useState(true);
+  const [submitting, setSubmitting] = useState(false);
+  const [address, setAddress] = useState<Address | null>(null);
+  const [expressAmount, setExpressAmount] = useState(0);
+  const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
+  const [inventoryIds, setInventoryIds] = useState<string[]>([]);
+
+  const showAlert = (msg: string) => {
+    if (Platform.OS === 'web') window.alert(msg);
+    else Alert.alert('提示', msg);
+  };
+
+  const loadData = useCallback(async () => {
+    if (!ids) return;
+    setLoading(true);
+    try {
+      const idList = ids.split(',');
+      setInventoryIds(idList);
+      
+      // 获取默认地址
+      const addr = await getDefaultAddress();
+      setAddress(addr);
+      
+      // 获取提货预览
+      const res = await takePreview(idList, addr?.id || '');
+      if (res) {
+        setExpressAmount(res.expressAmount || 0);
+        // 合并相同商品
+        const goodsMap: Record<string, GroupedGoods> = {};
+        (res.itemList || []).forEach((item: any) => {
+          const key = `${item.spuId}_${item.level}`;
+          if (goodsMap[key]) {
+            goodsMap[key].total += 1;
+          } else {
+            goodsMap[key] = { total: 1, data: item };
+          }
+        });
+        setGoodsList(Object.values(goodsMap));
+      }
+    } catch (e) {
+      console.error('加载提货信息失败:', e);
+    }
+    setLoading(false);
+  }, [ids]);
+
+  useEffect(() => {
+    loadData();
+  }, [loadData]);
+
+  const goToAddress = () => {
+    router.push('/address?type=1' as any);
+  };
+
+  const handleSubmit = async () => {
+    if (!address) {
+      showAlert('请选择收货地址');
+      return;
+    }
+    if (submitting) return;
+    setSubmitting(true);
+    try {
+      const res = await takeApply(inventoryIds, address.id, 'ALIPAY_H5');
+      if (res) {
+        showAlert('提货成功');
+        router.back();
+      }
+    } catch (e) {
+      console.error('提货失败:', e);
+    }
+    setSubmitting(false);
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
+        <Image source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headerBg} contentFit="cover" />
+        
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backIcon}>‹</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>提货</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={[styles.content, { paddingTop: insets.top + 50 }]} showsVerticalScrollIndicator={false}>
+          {/* 商品列表 */}
+          <View style={styles.goodsSection}>
+            <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+              {goodsList.map((goods, idx) => (
+                <View key={idx} style={styles.goodsItem}>
+                  <Image source={{ uri: goods.data.cover }} style={styles.goodsImg} contentFit="contain" />
+                  <View style={styles.goodsCount}>
+                    <Text style={styles.goodsCountText}>x{goods.total}</Text>
+                  </View>
+                </View>
+              ))}
+            </ScrollView>
+          </View>
+
+          {/* 运费 */}
+          <View style={styles.feeRow}>
+            <Text style={styles.feeLabel}>运费</Text>
+            <Text style={styles.feeValue}>¥{expressAmount}</Text>
+          </View>
+
+          {/* 收货地址 */}
+          <TouchableOpacity style={styles.addressSection} onPress={goToAddress}>
+            {!address ? (
+              <Text style={styles.noAddress}>请填写收货地址</Text>
+            ) : (
+              <View style={styles.addressInfo}>
+                <Text style={styles.addressName}>{address.contactName} {address.contactNo}</Text>
+                <Text style={styles.addressDetail}>{address.province}{address.city}{address.district}{address.address}</Text>
+              </View>
+            )}
+            <Text style={styles.arrowIcon}>›</Text>
+          </TouchableOpacity>
+        </ScrollView>
+
+        {/* 底部按钮 */}
+        <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+          <TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={submitting}>
+            <ImageBackground source={{ uri: Images.common.loginBtn }} style={styles.submitBtnBg} resizeMode="contain">
+              <Text style={styles.submitBtnText}>{submitting ? '提交中...' : '确定发货'}</Text>
+            </ImageBackground>
+          </TouchableOpacity>
+        </View>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
+  headerBg: { position: 'absolute', top: 0, left: 0, width: '100%', height: 160 },
+  header: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10 },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backIcon: { fontSize: 32, color: '#fff', fontWeight: 'bold' },
+  title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+  content: { flex: 1, paddingHorizontal: 15 },
+  goodsSection: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
+  goodsItem: { width: 79, height: 103, backgroundColor: '#fff', borderRadius: 6, marginRight: 10, alignItems: 'center', justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: '#eaeaea' },
+  goodsImg: { width: 73, height: 85 },
+  goodsCount: { position: 'absolute', top: 0, right: 0, backgroundColor: '#ff6b00', borderRadius: 2, paddingHorizontal: 4, paddingVertical: 2 },
+  goodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
+  feeRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
+  feeLabel: { color: '#fff', fontSize: 14 },
+  feeValue: { color: '#ff6b00', fontSize: 16, fontWeight: 'bold' },
+  addressSection: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
+  noAddress: { flex: 1, color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  addressInfo: { flex: 1 },
+  addressName: { color: '#fff', fontSize: 14, fontWeight: 'bold' },
+  addressDetail: { color: '#aaa', fontSize: 12, marginTop: 4 },
+  arrowIcon: { color: '#fff', fontSize: 20, marginLeft: 10 },
+  bottomBar: { paddingHorizontal: 15, paddingTop: 10 },
+  submitBtn: { alignItems: 'center' },
+  submitBtnBg: { width: 260, height: 60, justifyContent: 'center', alignItems: 'center' },
+  submitBtnText: { color: '#000', fontSize: 16, fontWeight: 'bold' },
+});

+ 213 - 0
app/store/components/CheckoutModal.tsx

@@ -0,0 +1,213 @@
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    ImageBackground,
+    Modal,
+    Platform,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { Address, getDefaultAddress } from '@/services/address';
+import { takeApply, takePreview } from '@/services/award';
+
+interface GroupedGoods {
+  total: number;
+  data: { id: string; cover: string; spuId: string; level: string; name?: string };
+}
+
+interface CheckoutModalProps {
+  visible: boolean;
+  selectedItems: Array<{ id: string; spu: { id: string; name: string; cover: string }; level: string }>;
+  onClose: () => void;
+  onSuccess: () => void;
+}
+
+export default function CheckoutModal({ visible, selectedItems, onClose, onSuccess }: CheckoutModalProps) {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [address, setAddress] = useState<Address | null>(null);
+  const [expressAmount, setExpressAmount] = useState(0);
+  const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
+
+  const showAlert = (msg: string) => {
+    if (Platform.OS === 'web') window.alert(msg);
+    else Alert.alert('提示', msg);
+  };
+
+  useEffect(() => {
+    if (visible && selectedItems.length > 0) {
+      loadData();
+    }
+  }, [visible, selectedItems]);
+
+  const loadData = async () => {
+    setLoading(true);
+    try {
+      // 获取默认地址
+      const addr = await getDefaultAddress();
+      setAddress(addr);
+
+      // 获取提货预览
+      const ids = selectedItems.map(item => item.id);
+      const res = await takePreview(ids, addr?.id || '');
+      if (res) {
+        setExpressAmount(res.expressAmount || 0);
+      }
+
+      // 合并相同商品
+      const goodsMap: Record<string, GroupedGoods> = {};
+      selectedItems.forEach((item) => {
+        const key = `${item.spu.id}_${item.level}`;
+        if (goodsMap[key]) {
+          goodsMap[key].total += 1;
+        } else {
+          goodsMap[key] = {
+            total: 1,
+            data: {
+              id: item.id,
+              cover: item.spu.cover,
+              spuId: item.spu.id,
+              level: item.level,
+              name: item.spu.name,
+            },
+          };
+        }
+      });
+      setGoodsList(Object.values(goodsMap));
+    } catch (e) {
+      console.error('加载提货信息失败:', e);
+    }
+    setLoading(false);
+  };
+
+  const goToAddress = () => {
+    onClose();
+    router.push('/address?type=1' as any);
+  };
+
+  const handleSubmit = async () => {
+    if (!address) {
+      showAlert('请选择收货地址');
+      return;
+    }
+    if (submitting) return;
+    setSubmitting(true);
+    try {
+      const ids = selectedItems.map(item => item.id);
+      const res = await takeApply(ids, address.id, 'WALLET');
+      if (res) {
+        showAlert('提货成功');
+        onSuccess();
+      }
+    } catch (e) {
+      console.error('提货失败:', e);
+    }
+    setSubmitting(false);
+  };
+
+  return (
+    <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
+      <View style={styles.overlay}>
+        <TouchableOpacity style={styles.overlayBg} onPress={onClose} activeOpacity={1} />
+        <View style={[styles.container, { paddingBottom: insets.bottom + 20 }]}>
+          {/* 标题 */}
+          <View style={styles.header}>
+            <Text style={styles.title}>提货</Text>
+            <TouchableOpacity style={styles.closeBtn} onPress={onClose}>
+              <Text style={styles.closeBtnText}>×</Text>
+            </TouchableOpacity>
+          </View>
+
+          {loading ? (
+            <View style={styles.loadingBox}>
+              <ActivityIndicator size="large" color="#FC7D2E" />
+            </View>
+          ) : (
+            <>
+              {/* 商品列表 */}
+              <View style={styles.goodsSection}>
+                <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+                  {goodsList.map((goods, idx) => (
+                    <View key={idx} style={styles.goodsItem}>
+                      <Image source={{ uri: goods.data.cover }} style={styles.goodsImg} contentFit="contain" />
+                      <View style={styles.goodsCount}>
+                        <Text style={styles.goodsCountText}>x{goods.total}</Text>
+                      </View>
+                    </View>
+                  ))}
+                </ScrollView>
+              </View>
+
+              {/* 运费 */}
+              {expressAmount > 0 && (
+                <View style={styles.feeRow}>
+                  <Text style={styles.feeLabel}>运费</Text>
+                  <Text style={styles.feeValue}>¥{expressAmount}</Text>
+                </View>
+              )}
+
+              {/* 收货地址 */}
+              <TouchableOpacity style={styles.addressSection} onPress={goToAddress}>
+                {!address ? (
+                  <Text style={styles.noAddress}>请填写收货地址</Text>
+                ) : (
+                  <View style={styles.addressInfo}>
+                    <Text style={styles.addressName}>{address.contactName} {address.contactNo}</Text>
+                    <Text style={styles.addressDetail}>{address.province}{address.city}{address.district}{address.address}</Text>
+                  </View>
+                )}
+                <Text style={styles.arrowIcon}>›</Text>
+              </TouchableOpacity>
+
+              {/* 提交按钮 */}
+              <TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={submitting}>
+                <ImageBackground source={{ uri: Images.common.loginBtn }} style={styles.submitBtnBg} resizeMode="contain">
+                  <Text style={styles.submitBtnText}>{submitting ? '提交中...' : '确定发货'}</Text>
+                </ImageBackground>
+              </TouchableOpacity>
+            </>
+          )}
+        </View>
+      </View>
+    </Modal>
+  );
+}
+
+const styles = StyleSheet.create({
+  overlay: { flex: 1, justifyContent: 'flex-end' },
+  overlayBg: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)' },
+  container: { backgroundColor: '#fff', borderTopLeftRadius: 15, borderTopRightRadius: 15, paddingHorizontal: 14 },
+  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 20, position: 'relative' },
+  title: { fontSize: 16, fontWeight: 'bold', color: '#000' },
+  closeBtn: { position: 'absolute', right: 0, top: 15, width: 24, height: 24, backgroundColor: '#ebebeb', borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
+  closeBtnText: { fontSize: 18, color: '#a2a2a2', lineHeight: 20 },
+  loadingBox: { height: 200, justifyContent: 'center', alignItems: 'center' },
+  goodsSection: { paddingVertical: 10 },
+  goodsItem: { width: 79, height: 103, backgroundColor: '#fff', borderRadius: 6, marginRight: 8, alignItems: 'center', justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: '#eaeaea' },
+  goodsImg: { width: 73, height: 85 },
+  goodsCount: { position: 'absolute', top: 0, right: 0, backgroundColor: '#ff6b00', borderRadius: 2, paddingHorizontal: 4, paddingVertical: 2 },
+  goodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
+  feeRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
+  feeLabel: { fontSize: 14, color: '#333' },
+  feeValue: { fontSize: 14, color: '#ff6b00', fontWeight: 'bold' },
+  addressSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderTopWidth: 1, borderTopColor: '#f0f0f0' },
+  noAddress: { flex: 1, fontSize: 16, fontWeight: 'bold', color: '#333' },
+  addressInfo: { flex: 1 },
+  addressName: { fontSize: 14, color: '#333', fontWeight: 'bold' },
+  addressDetail: { fontSize: 12, color: '#666', marginTop: 4 },
+  arrowIcon: { fontSize: 20, color: '#999', marginLeft: 10 },
+  submitBtn: { alignItems: 'center', marginTop: 15 },
+  submitBtnBg: { width: 260, height: 60, justifyContent: 'center', alignItems: 'center' },
+  submitBtnText: { color: '#000', fontSize: 16, fontWeight: 'bold' },
+});

+ 431 - 302
app/store/index.tsx

@@ -1,342 +1,471 @@
-import { Images } from '@/constants/images';
-import ServiceAward from '@/services/award';
-import { Ionicons } from '@expo/vector-icons';
-import { Stack, useRouter } from 'expo-router';
-import React, { useState } from 'react';
+import { Image } from 'expo-image';
+import { useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
 import {
     ActivityIndicator,
-    Dimensions,
+    Alert,
     FlatList,
-    Image,
     ImageBackground,
+    Platform,
+    RefreshControl,
+    ScrollView,
     StatusBar,
     StyleSheet,
     Text,
     TouchableOpacity,
-    View,
+    View
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
-const { width: SCREEN_WIDTH } = Dimensions.get('window');
+import { Images } from '@/constants/images';
+import {
+    getStore,
+    getTakeList,
+    moveOutSafeStore,
+    moveToSafeStore,
+} from '@/services/award';
+import CheckoutModal from './components/CheckoutModal';
 
-const LEVEL_MAP: any = {
-    D: { title: '普通', color: '#666666' },
-    C: { title: '隐藏', color: '#9745e6' },
-    B: { title: '欧皇', color: '#ff0000' },
-    A: { title: '超神', color: '#ffae00' },
+const LEVEL_MAP: Record<string, { title: string; color: string }> = {
+  A: { title: '超神', color: '#ffae00' },
+  B: { title: '欧皇', color: '#ff0000' },
+  C: { title: '隐藏', color: '#9745e6' },
+  D: { title: '普通', color: '#666666' },
 };
 
-export default function StoreScreen() {
-    const router = useRouter();
-    const insets = useSafeAreaInsets();
-    const [list, setList] = useState<any[]>([]);
-    const [loading, setLoading] = useState(false);
-    const [tabIndex, setTabIndex] = useState(0);
-    const [page, setPage] = useState(1);
-    const [hasMore, setHasMore] = useState(true);
+const LEVEL_TABS = [
+  { title: '全部', value: '' },
+  { title: '普通', value: 'D' },
+  { title: '隐藏', value: 'C' },
+  { title: '欧皇', value: 'B' },
+  { title: '超神', value: 'A' },
+];
+
+const FROM_TYPE_MAP: Record<string, string> = {
+  LUCK: '奖池', MALL: '商城', LUCK_ROOM: '福利房', LUCK_WHEEL: '魔天轮',
+  DOLL_MACHINE: '扭蛋', ACTIVITY: '活动', SUBSTITUTE: '置换', TRANSFER: '转赠',
+};
 
-    const tabs = ['未使用', '保险柜', '已提货'];
+interface StoreItem {
+  id: string;
+  level: string;
+  safeFlag: number;
+  magicAmount?: number;
+  fromRelationType: string;
+  spu: { id: string; name: string; cover: string };
+}
 
-    React.useEffect(() => {
-        setPage(1);
-        setList([]);
-        setHasMore(true);
-        loadData(1);
-    }, [tabIndex]);
+interface PickupItem {
+  tradeNo: string;
+  createTime: string;
+  status: number;
+  contactName: string;
+  contactNo: string;
+  province: string;
+  city: string;
+  district: string;
+  address: string;
+  expressAmount: number;
+  paymentTime?: string;
+  paymentTimeoutTime?: string;
+  cancelRemark?: string;
+  itemList: Array<{ id: string; spuId: string; level: string; cover: string }>;
+}
 
-    const loadData = async (pageNum: number) => {
-        if (!hasMore && pageNum > 1) return;
+const STATUS_MAP: Record<number, { text: string; color: string }> = {
+  0: { text: '待支付运费', color: '#ff6b00' },
+  1: { text: '已进仓库进行配货', color: '#ff6b00' },
+  2: { text: '待收货', color: '#ff6b00' },
+  10: { text: '已取消', color: '#ff6b00' },
+  11: { text: '超时取消', color: '#ff6b00' },
+  12: { text: '系统取消', color: '#ff6b00' },
+  99: { text: '已完成', color: '#52c41a' },
+};
 
-        try {
-            if (pageNum === 1) setLoading(true);
+export default function StoreScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const [mainTabIndex, setMainTabIndex] = useState(0);
+  const [levelTabIndex, setLevelTabIndex] = useState(0);
+  const [list, setList] = useState<any[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [refreshing, setRefreshing] = useState(false);
+  const [page, setPage] = useState(1);
+  const [hasMore, setHasMore] = useState(true);
+  const [checkMap, setCheckMap] = useState<Record<string, StoreItem>>({});
+  const [checkoutVisible, setCheckoutVisible] = useState(false);
 
-            let res;
-            if (tabIndex === 0) {
-                // Not in safe, unused (status=0)
-                res = await ServiceAward.getStore(pageNum, 20, 0); // Use smaller size for pagination demo
-            } else if (tabIndex === 1) {
-                // In safe, unused (status=0)
-                res = await ServiceAward.getStore(pageNum, 20, 1);
+  const mainTabs = ['未使用', '保险柜', '已提货'];
+
+  const loadData = useCallback(async (pageNum: number, isRefresh = false) => {
+    if (loading && !isRefresh) return;
+    if (!hasMore && pageNum > 1 && !isRefresh) return;
+    try {
+      if (pageNum === 1) setLoading(true);
+      let res: any;
+      if (mainTabIndex === 0) {
+        res = await getStore(pageNum, 20, 0, LEVEL_TABS[levelTabIndex].value);
+      } else if (mainTabIndex === 1) {
+        res = await getStore(pageNum, 20, 1);
+      } else {
+        res = await getTakeList(pageNum, 20);
+      }
+      let records = Array.isArray(res) ? res : (res?.records || res || []);
+      // 处理已提货数据,合并相同商品
+      if (mainTabIndex === 2 && records.length > 0) {
+        records = records.map((item: PickupItem) => {
+          const goodsMap: Record<string, { total: number; data: any }> = {};
+          (item.itemList || []).forEach((goods: any) => {
+            const key = `${goods.spuId}_${goods.level}`;
+            if (goodsMap[key]) {
+              goodsMap[key].total += 1;
             } else {
-                // Picked up
-                res = await ServiceAward.getTakeList(pageNum, 20);
+              goodsMap[key] = { total: 1, data: goods };
             }
+          });
+          return { ...item, groupedList: Object.values(goodsMap) };
+        });
+      }
+      if (records.length < 20) setHasMore(false);
+      if (pageNum === 1 || isRefresh) setList(records);
+      else setList(prev => [...prev, ...records]);
+    } catch (e) {
+      console.error('加载仓库数据失败:', e);
+    } finally {
+      setLoading(false);
+      setRefreshing(false);
+    }
+  }, [mainTabIndex, levelTabIndex, loading, hasMore]);
 
-            const records = Array.isArray(res) ? res : (res?.records || []);
+  useEffect(() => {
+    setPage(1); setList([]); setHasMore(true); setCheckMap({});
+    loadData(1, true);
+  }, [mainTabIndex]);
 
-            if (records.length < 20) {
-                setHasMore(false);
-            }
+  useEffect(() => {
+    if (mainTabIndex === 0) {
+      setPage(1); setList([]); setHasMore(true); setCheckMap({});
+      loadData(1, true);
+    }
+  }, [levelTabIndex]);
 
-            if (pageNum === 1) {
-                setList(records);
-            } else {
-                setList(prev => [...prev, ...records]);
-            }
-        } catch (e) {
-            console.error(e);
-        } finally {
-            setLoading(false);
-        }
-    };
+  const handleRefresh = () => { setRefreshing(true); setPage(1); setHasMore(true); loadData(1, true); };
+  const handleLoadMore = () => { if (!loading && hasMore) { const np = page + 1; setPage(np); loadData(np); } };
 
-    const handleLoadMore = () => {
-        if (!loading && hasMore) {
-            const nextPage = page + 1;
-            setPage(nextPage);
-            loadData(nextPage);
-        }
-    };
+  const handleChoose = (item: StoreItem) => {
+    if (item.safeFlag === 1 && mainTabIndex === 0) return;
+    setCheckMap(prev => {
+      const newMap = { ...prev };
+      if (newMap[item.id]) delete newMap[item.id];
+      else newMap[item.id] = item;
+      return newMap;
+    });
+  };
 
-    const handleLock = async (item: any) => {
-        // Implement lock/unlock logic if needed
-    };
+  const handleLock = async (item: StoreItem, index: number) => {
+    const res = await moveToSafeStore([item.id]);
+    if (res) {
+      const newList = [...list]; newList[index] = { ...item, safeFlag: 1 }; setList(newList);
+      setCheckMap(prev => { const m = { ...prev }; delete m[item.id]; return m; });
+      showAlert('已锁定到保险柜');
+    }
+  };
 
-    const renderItem = ({ item }: { item: any }) => (
-        <ImageBackground
-            source={{ uri: Images.mine.storeItemBg }}
-            style={styles.cell}
-            resizeMode="stretch"
-        >
-            <View style={styles.cellHeader}>
-                <View style={styles.headerLeft}>
-                    <View style={[styles.checkBox]} />
-                    <Text style={[styles.levelTitle, { color: LEVEL_MAP[item.level]?.color || '#333' }]}>
-                        {LEVEL_MAP[item.level]?.title || '未知'}
-                    </Text>
-                </View>
-                <TouchableOpacity style={styles.lockBox} onPress={() => handleLock(item)}>
-                    <Text style={styles.lockText}>{item.safeFlag !== 1 ? '锁定' : '解锁'}</Text>
-                    <Image
-                        source={{ uri: item.safeFlag !== 1 ? Images.mine.lock : Images.mine.unlock }}
-                        style={styles.lockIcon}
-                    />
-                </TouchableOpacity>
-            </View>
+  const handleUnlock = async (item: StoreItem, index: number) => {
+    const res = await moveOutSafeStore([item.id]);
+    if (res) {
+      if (mainTabIndex === 1) setList(list.filter((_, i) => i !== index));
+      else { const newList = [...list]; newList[index] = { ...item, safeFlag: 0 }; setList(newList); }
+      showAlert('已从保险柜移出');
+    }
+  };
 
-            <View style={styles.cellBody}>
-                <ImageBackground
-                    source={{ uri: Images.mine.storeGoodsImgBg }}
-                    style={styles.goodsImgBg}
-                >
-                    <Image source={{ uri: item.spu?.cover }} style={styles.goodsImg} resizeMode="contain" />
-                </ImageBackground>
-                <View style={styles.goodsInfo}>
-                    <Text style={styles.goodsName} numberOfLines={2}>{item.spu?.name}</Text>
-                    <Text style={styles.goodsSource}>
-                        从{item.fromRelationType === 'LUCK' ? '奖池' : '其他'}获得
-                    </Text>
-                </View>
-            </View>
-        </ImageBackground>
-    );
+  const handleMoveOutAll = async () => {
+    const selected = Object.values(checkMap);
+    if (selected.length === 0) { showAlert('请至少选择一个商品!'); return; }
+    const res = await moveOutSafeStore(selected.map(i => i.id));
+    if (res) { setCheckMap({}); handleRefresh(); showAlert('已从保险柜移出'); }
+  };
+
+  const handleTakeGoods = () => {
+    const selected = Object.values(checkMap);
+    if (selected.length === 0) { showAlert('请至少选择一个商品!'); return; }
+    setCheckoutVisible(true);
+  };
+
+  const handleCheckoutSuccess = () => {
+    setCheckoutVisible(false);
+    setCheckMap({});
+    handleRefresh();
+  };
 
+  const showAlert = (msg: string) => {
+    if (Platform.OS === 'web') window.alert(msg);
+    else Alert.alert('提示', msg);
+  };
+
+  const renderStoreItem = ({ item, index }: { item: StoreItem; index: number }) => {
+    const levelInfo = LEVEL_MAP[item.level] || LEVEL_MAP.D;
+    const isChecked = !!checkMap[item.id];
+    const canSelect = mainTabIndex === 1 || item.safeFlag !== 1;
     return (
-        <View style={styles.container}>
-            <Stack.Screen options={{ headerShown: false }} />
-            <StatusBar barStyle="light-content" />
-            <View style={[styles.header, { paddingTop: insets.top }]}>
-                <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
-                    <Ionicons name="chevron-back" size={24} color="#fff" />
-                </TouchableOpacity>
-                <Text style={styles.title}>仓库</Text>
-            </View>
+      <ImageBackground source={{ uri: Images.mine.storeItemBg }} style={styles.cell} resizeMode="stretch">
+        <TouchableOpacity style={styles.cellHeader} onPress={() => canSelect && handleChoose(item)}>
+          <View style={styles.headerLeft}>
+            {canSelect && (
+              <View style={[styles.checkBox, isChecked && styles.checkBoxChecked]}>
+                {isChecked && <Text style={styles.checkIcon}>✓</Text>}
+              </View>
+            )}
+            <Text style={[styles.levelTitle, { color: levelInfo.color }]}>{levelInfo.title}</Text>
+          </View>
+          <TouchableOpacity style={styles.lockBox} onPress={() => item.safeFlag !== 1 ? handleLock(item, index) : handleUnlock(item, index)}>
+            <Text style={styles.lockText}>{item.safeFlag !== 1 ? '锁定' : '解锁'}</Text>
+            <Image source={{ uri: item.safeFlag !== 1 ? Images.mine.lock : Images.mine.unlock }} style={styles.lockIcon} />
+          </TouchableOpacity>
+        </TouchableOpacity>
+        <View style={styles.cellBody}>
+          <ImageBackground source={{ uri: Images.mine.storeGoodsImgBg }} style={styles.goodsImgBg}>
+            <Image source={{ uri: item.spu?.cover }} style={styles.goodsImg} contentFit="contain" />
+          </ImageBackground>
+          <View style={styles.goodsInfo}>
+            <Text style={styles.goodsName} numberOfLines={2}>{item.spu?.name}</Text>
+            <Text style={styles.goodsSource}>从{FROM_TYPE_MAP[item.fromRelationType] || '其他'}获得</Text>
+          </View>
+        </View>
+      </ImageBackground>
+    );
+  };
 
-            <ImageBackground
-                source={{ uri: Images.mine.kaixinMineBg }}
-                style={styles.background}
-                resizeMode="cover"
-            >
-                <Image
-                    source={{ uri: Images.mine.kaixinMineHeadBg }}
-                    style={styles.headerBg}
-                    resizeMode="cover"
-                />
+  const copyToClipboard = (text: string) => {
+    showAlert(`订单号已复制: ${text}`);
+  };
 
-                <View style={[styles.content, { paddingTop: insets.top + 50 }]}>
-                    {/* Tabs */}
-                    <View style={styles.tabs}>
-                        {tabs.map((tab, index) => (
-                            <TouchableOpacity
-                                key={index}
-                                style={[styles.tabItem, tabIndex === index && styles.tabItemActive]}
-                                onPress={() => setTabIndex(index)}
-                            >
-                                <Text style={[styles.tabText, tabIndex === index && styles.tabTextActive]}>{tab}</Text>
-                                {tabIndex === index && <View style={styles.tabLine} />}
-                            </TouchableOpacity>
-                        ))}
-                    </View>
+  const showExpress = (item: PickupItem) => {
+    router.push({ pathname: '/store/packages' as any, params: { tradeNo: item.tradeNo } });
+  };
 
-                    <FlatList
-                        data={list}
-                        renderItem={renderItem}
-                        keyExtractor={(item, index) => index.toString()}
-                        contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 100 }}
-                        onEndReached={handleLoadMore}
-                        onEndReachedThreshold={0.1}
-                        ListFooterComponent={
-                            loading && list.length > 0 ? (
-                                <ActivityIndicator color="#fff" style={{ marginVertical: 10 }} />
-                            ) : null
-                        }
-                        ListEmptyComponent={
-                            !loading ? (
-                                <View style={styles.emptyBox}>
-                                    <Text style={styles.emptyText}>暂无物品</Text>
-                                </View>
-                            ) : null
-                        }
-                    />
+  const renderPickupItem = ({ item }: { item: PickupItem & { groupedList?: Array<{ total: number; data: any }> } }) => {
+    const statusInfo = STATUS_MAP[item.status] || { text: '未知', color: '#999' };
+    return (
+      <ImageBackground source={{ uri: Images.mine.storeItemBg }} style={styles.pickupCell} resizeMode="stretch">
+        {/* 顶部信息 */}
+        <View style={styles.pickupTop}>
+          <Text style={styles.pickupTime}>下单时间:{item.createTime}</Text>
+          <Text style={[styles.pickupStatus, { color: statusInfo.color }]}>{statusInfo.text}</Text>
+        </View>
+        {item.status === 0 && item.paymentTimeoutTime && (
+          <Text style={styles.pickupTimeout}>{item.paymentTimeoutTime} 将自动取消该订单,如有优惠券,将自动退回</Text>
+        )}
+        
+        {/* 收货地址 */}
+        <View style={styles.pickupAddress}>
+          <Text style={styles.locationIcon}>📍</Text>
+          <View style={styles.addressInfo}>
+            <Text style={styles.addressName}>{item.contactName},{item.contactNo}</Text>
+            <Text style={styles.addressDetail}>{item.province}{item.city}{item.district}{item.address}</Text>
+          </View>
+        </View>
+        
+        {/* 商品列表 */}
+        <View style={styles.pickupGoodsBox}>
+          <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.pickupGoodsList}>
+            {(item.groupedList || []).map((goods, idx) => (
+              <View key={idx} style={styles.pickupGoodsItem}>
+                <Image source={{ uri: goods.data.cover }} style={styles.pickupGoodsImg} contentFit="contain" />
+                <View style={styles.pickupGoodsCount}>
+                  <Text style={styles.pickupGoodsCountText}>x{goods.total}</Text>
                 </View>
-            </ImageBackground>
+              </View>
+            ))}
+          </ScrollView>
+        </View>
+        
+        {/* 订单号 */}
+        <View style={styles.pickupOrderRow}>
+          <Text style={styles.pickupOrderLabel}>订单号:</Text>
+          <Text style={styles.pickupOrderNo} numberOfLines={1}>{item.tradeNo}</Text>
+          <TouchableOpacity style={styles.copyBtn} onPress={() => copyToClipboard(item.tradeNo)}>
+            <Text style={styles.copyBtnText}>复制</Text>
+          </TouchableOpacity>
+        </View>
+        
+        {item.paymentTime && (
+          <View style={styles.pickupInfoRow}>
+            <Text style={styles.pickupInfoLabel}>付款时间:</Text>
+            <Text style={styles.pickupInfoValue}>{item.paymentTime}</Text>
+          </View>
+        )}
+        
+        {item.status === 12 && item.cancelRemark && (
+          <View style={styles.pickupInfoRow}>
+            <Text style={styles.pickupInfoLabel}>备注</Text>
+            <Text style={[styles.pickupInfoValue, { color: '#ff6b00' }]}>{item.cancelRemark}</Text>
+          </View>
+        )}
+        
+        {/* 底部操作 */}
+        <View style={styles.pickupBottom}>
+          <Text style={styles.pickupExpressAmount}>配送费:<Text style={styles.priceText}>¥{item.expressAmount}</Text></Text>
+          {[1, 2, 99].includes(item.status) && (
+            <TouchableOpacity style={styles.expressBtn} onPress={() => showExpress(item)}>
+              <Text style={styles.expressBtnText}>物流信息</Text>
+            </TouchableOpacity>
+          )}
         </View>
+      </ImageBackground>
     );
+  };
+
+  const selectedCount = Object.keys(checkMap).length;
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
+        <Image source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headerBg} contentFit="cover" />
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backIcon}>‹</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>仓库</Text>
+          <View style={styles.placeholder} />
+        </View>
+        <View style={[styles.content, { paddingTop: insets.top + 50 }]}>
+          <View style={styles.mainTabs}>
+            {mainTabs.map((tab, index) => (
+              <TouchableOpacity key={index} style={styles.mainTabItem} onPress={() => setMainTabIndex(index)}>
+                <Text style={[styles.mainTabText, mainTabIndex === index && styles.mainTabTextActive]}>{tab}</Text>
+                {mainTabIndex === index && <View style={styles.mainTabLine} />}
+              </TouchableOpacity>
+            ))}
+          </View>
+          {mainTabIndex === 0 && (
+            <View style={styles.levelTabs}>
+              {LEVEL_TABS.map((tab, index) => (
+                <TouchableOpacity key={index} style={[styles.levelTabItem, levelTabIndex === index && styles.levelTabItemActive]} onPress={() => setLevelTabIndex(index)}>
+                  <Text style={[styles.levelTabText, levelTabIndex === index && styles.levelTabTextActive]}>{tab.title}</Text>
+                </TouchableOpacity>
+              ))}
+            </View>
+          )}
+          <FlatList
+            data={list as any[]}
+            renderItem={mainTabIndex === 2 ? renderPickupItem as any : renderStoreItem as any}
+            keyExtractor={(item: any, index) => item.id || item.tradeNo || index.toString()}
+            contentContainerStyle={styles.listContent}
+            refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor="#fff" />}
+            onEndReached={handleLoadMore}
+            onEndReachedThreshold={0.3}
+            ListHeaderComponent={mainTabIndex === 2 ? (
+              <View style={styles.pickupTip}>
+                <Text style={styles.pickupTipIcon}>⚠️</Text>
+                <Text style={styles.pickupTipText}>您的包裹一般在5个工作日内发货,如遇特殊情况可能会有延迟,敬请谅解~</Text>
+              </View>
+            ) : null}
+            ListFooterComponent={loading && list.length > 0 ? <ActivityIndicator color="#fff" style={{ marginVertical: 10 }} /> : null}
+            ListEmptyComponent={!loading ? <View style={styles.emptyBox}><Text style={styles.emptyText}>暂无物品</Text></View> : null}
+          />
+        </View>
+        {mainTabIndex !== 2 && list.length > 0 && (
+          <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
+            <Text style={styles.bottomInfoText}>已选 <Text style={styles.bottomInfoCount}>{selectedCount}</Text> 件商品</Text>
+            <TouchableOpacity style={styles.bottomBtn} onPress={mainTabIndex === 0 ? handleTakeGoods : handleMoveOutAll}>
+              <ImageBackground source={{ uri: Images.common.loginBtn }} style={styles.bottomBtnBg} resizeMode="contain">
+                <Text style={styles.bottomBtnText}>{mainTabIndex === 0 ? '立即提货' : '移出保险柜'}</Text>
+              </ImageBackground>
+            </TouchableOpacity>
+          </View>
+        )}
+        
+        {/* 提货弹窗 */}
+        <CheckoutModal
+          visible={checkoutVisible}
+          selectedItems={Object.values(checkMap)}
+          onClose={() => setCheckoutVisible(false)}
+          onSuccess={handleCheckoutSuccess}
+        />
+      </ImageBackground>
+    </View>
+  );
 }
 
 const styles = StyleSheet.create({
-    container: {
-        flex: 1,
-        backgroundColor: '#1a1a2e',
-    },
-    header: {
-        position: 'absolute',
-        top: 0,
-        left: 0,
-        right: 0,
-        zIndex: 100,
-        alignItems: 'center',
-        paddingBottom: 10,
-    },
-    headerBg: {
-        position: 'absolute',
-        top: 0,
-        left: 0,
-        width: '100%',
-        height: 260,
-    },
-    backBtn: {
-        position: 'absolute',
-        left: 10,
-        bottom: 10,
-        zIndex: 101,
-    },
-    title: {
-        color: '#fff',
-        fontSize: 16,
-        fontWeight: 'bold',
-    },
-    background: {
-        flex: 1,
-        width: '100%',
-        height: '100%',
-    },
-    content: {
-        flex: 1,
-    },
-    tabs: {
-        flexDirection: 'row',
-        justifyContent: 'space-around',
-        marginBottom: 10,
-    },
-    tabItem: {
-        paddingVertical: 10,
-        paddingHorizontal: 10,
-        alignItems: 'center',
-    },
-    tabItemActive: {},
-    tabText: {
-        color: '#aaa',
-        fontSize: 14,
-    },
-    tabTextActive: {
-        color: '#fff',
-        fontWeight: 'bold',
-        fontSize: 16,
-    },
-    tabLine: {
-        width: 20,
-        height: 3,
-        backgroundColor: '#fff',
-        marginTop: 5,
-        borderRadius: 2,
-    },
-    cell: {
-        width: '100%',
-        height: 154,
-        marginBottom: 10,
-        padding: 15,
-    },
-    cellHeader: {
-        flexDirection: 'row',
-        justifyContent: 'space-between',
-        alignItems: 'center',
-        borderBottomWidth: 1,
-        borderBottomColor: 'rgba(0,0,0,0.1)',
-        paddingBottom: 10,
-        marginBottom: 10,
-    },
-    headerLeft: {
-        flexDirection: 'row',
-        alignItems: 'center',
-    },
-    checkBox: {
-        width: 16,
-        height: 16,
-        borderWidth: 1,
-        borderColor: '#999',
-        marginRight: 10,
-        backgroundColor: '#fff',
-    },
-    levelTitle: {
-        fontSize: 16,
-        fontWeight: 'bold',
-    },
-    lockBox: {
-        flexDirection: 'row',
-        alignItems: 'center',
-    },
-    lockText: {
-        fontSize: 12,
-        color: '#666',
-    },
-    lockIcon: {
-        width: 16,
-        height: 16,
-        marginLeft: 5,
-    },
-    cellBody: {
-        flexDirection: 'row',
-    },
-    goodsImgBg: {
-        width: 65,
-        height: 65,
-        justifyContent: 'center',
-        alignItems: 'center',
-        marginRight: 10,
-    },
-    goodsImg: {
-        width: 60,
-        height: 60,
-    },
-    goodsInfo: {
-        flex: 1,
-        justifyContent: 'space-between',
-    },
-    goodsName: {
-        fontSize: 14,
-        color: '#333',
-        fontWeight: 'bold',
-    },
-    goodsSource: {
-        fontSize: 12,
-        color: '#999',
-    },
-    emptyBox: {
-        marginTop: 100,
-        alignItems: 'center',
-    },
-    emptyText: {
-        color: '#999',
-    },
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  headerBg: { position: 'absolute', top: 0, left: 0, width: '100%', height: 160 },
+  header: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10 },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backIcon: { fontSize: 32, color: '#fff', fontWeight: 'bold' },
+  title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+  content: { flex: 1 },
+  mainTabs: { flexDirection: 'row', justifyContent: 'space-around', paddingVertical: 10 },
+  mainTabItem: { alignItems: 'center', paddingHorizontal: 15 },
+  mainTabText: { color: '#aaa', fontSize: 14 },
+  mainTabTextActive: { color: '#fff', fontWeight: 'bold', fontSize: 16 },
+  mainTabLine: { width: 20, height: 3, backgroundColor: '#fff', marginTop: 5, borderRadius: 2 },
+  levelTabs: { flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 8 },
+  levelTabItem: { paddingHorizontal: 12, paddingVertical: 6, marginRight: 8, borderRadius: 15, backgroundColor: 'rgba(255,255,255,0.1)' },
+  levelTabItemActive: { backgroundColor: '#FC7D2E' },
+  levelTabText: { color: '#aaa', fontSize: 12 },
+  levelTabTextActive: { color: '#fff' },
+  listContent: { paddingHorizontal: 10, paddingBottom: 150 },
+  cell: { width: '100%', minHeight: 154, marginBottom: 10, padding: 15 },
+  cellHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: 'rgba(0,0,0,0.1)', paddingBottom: 10, marginBottom: 10 },
+  headerLeft: { flexDirection: 'row', alignItems: 'center' },
+  checkBox: { width: 18, height: 18, borderWidth: 2, borderColor: '#000', backgroundColor: '#fff', marginRight: 10, justifyContent: 'center', alignItems: 'center' },
+  checkBoxChecked: { backgroundColor: '#000' },
+  checkIcon: { color: '#fff', fontSize: 12 },
+  levelTitle: { fontSize: 16, fontWeight: 'bold' },
+  lockBox: { flexDirection: 'row', alignItems: 'center' },
+  lockText: { fontSize: 12, color: '#666' },
+  lockIcon: { width: 16, height: 16, marginLeft: 5 },
+  cellBody: { flexDirection: 'row' },
+  goodsImgBg: { width: 65, height: 65, justifyContent: 'center', alignItems: 'center', marginRight: 10 },
+  goodsImg: { width: 55, height: 55 },
+  goodsInfo: { flex: 1, justifyContent: 'space-between' },
+  goodsName: { fontSize: 14, color: '#333', fontWeight: 'bold' },
+  goodsSource: { fontSize: 12, color: '#999' },
+  bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.8)', paddingHorizontal: 15, paddingTop: 10, alignItems: 'center' },
+  bottomInfoText: { color: '#fff', fontSize: 14, marginBottom: 10 },
+  bottomInfoCount: { fontWeight: 'bold', fontSize: 18 },
+  bottomBtn: { width: 260 },
+  bottomBtnBg: { width: 260, height: 60, justifyContent: 'center', alignItems: 'center' },
+  bottomBtnText: { color: '#000', fontSize: 16, fontWeight: 'bold' },
+  emptyBox: { marginTop: 100, alignItems: 'center' },
+  emptyText: { color: '#999', fontSize: 14 },
+  // 已提货样式
+  pickupTip: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1FA4FF', padding: 8, marginBottom: 10, borderRadius: 4 },
+  pickupTipIcon: { fontSize: 14, marginRight: 6 },
+  pickupTipText: { flex: 1, color: '#fff', fontSize: 12 },
+  pickupCell: { width: '100%', marginBottom: 10, padding: 15 },
+  pickupTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: 'rgba(0,0,0,0.05)', paddingBottom: 10 },
+  pickupTime: { fontSize: 12, color: '#666' },
+  pickupStatus: { fontSize: 12, fontWeight: 'bold' },
+  pickupTimeout: { fontSize: 11, color: '#ff6b00', marginTop: 5 },
+  pickupAddress: { flexDirection: 'row', alignItems: 'flex-start', paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: 'rgba(0,0,0,0.05)' },
+  locationIcon: { fontSize: 16, marginRight: 10 },
+  addressInfo: { flex: 1 },
+  addressName: { fontSize: 14, color: '#333', fontWeight: 'bold' },
+  addressDetail: { fontSize: 12, color: '#666', marginTop: 4 },
+  pickupGoodsBox: { backgroundColor: '#f8f8f8', borderRadius: 6, padding: 10, marginVertical: 10 },
+  pickupGoodsList: { flexDirection: 'row' },
+  pickupGoodsItem: { width: 79, height: 103, backgroundColor: '#fff', borderRadius: 6, marginRight: 8, alignItems: 'center', justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: '#eaeaea' },
+  pickupGoodsImg: { width: 73, height: 85 },
+  pickupGoodsCount: { position: 'absolute', top: 0, right: 0, backgroundColor: '#ff6b00', borderRadius: 2, paddingHorizontal: 4, paddingVertical: 2 },
+  pickupGoodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
+  pickupOrderRow: { flexDirection: 'row', alignItems: 'center', borderTopWidth: 1, borderTopColor: 'rgba(0,0,0,0.05)', paddingTop: 14 },
+  pickupOrderLabel: { fontSize: 12, color: '#666' },
+  pickupOrderNo: { flex: 1, fontSize: 12, color: '#333', textAlign: 'right' },
+  copyBtn: { backgroundColor: '#1FA4FF', borderRadius: 4, paddingHorizontal: 6, paddingVertical: 4, marginLeft: 5 },
+  copyBtnText: { color: '#fff', fontSize: 12 },
+  pickupInfoRow: { flexDirection: 'row', alignItems: 'center', paddingTop: 10 },
+  pickupInfoLabel: { fontSize: 12, color: '#666' },
+  pickupInfoValue: { fontSize: 12, color: '#333' },
+  pickupBottom: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', paddingTop: 10 },
+  pickupExpressAmount: { fontSize: 12, color: '#333' },
+  priceText: { color: '#ff6b00' },
+  expressBtn: { backgroundColor: '#1FA4FF', borderRadius: 12, paddingHorizontal: 10, paddingVertical: 6, marginLeft: 15 },
+  expressBtnText: { color: '#fff', fontSize: 12 },
 });

+ 215 - 0
app/store/packages.tsx

@@ -0,0 +1,215 @@
+import { Image } from 'expo-image';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { Images } from '@/constants/images';
+import { getAwardExpress, getAwardPackages } from '@/services/award';
+
+interface PackageItem {
+  id: string;
+  expressNo: string;
+  expressCompany: string;
+  itemList: Array<{ cover: string; name: string }>;
+}
+
+interface ExpressDetail {
+  expressNo: string;
+  expressCompany: string;
+  traces: Array<{ time: string; content: string }>;
+}
+
+export default function PackagesScreen() {
+  const router = useRouter();
+  const insets = useSafeAreaInsets();
+  const { tradeNo } = useLocalSearchParams<{ tradeNo: string }>();
+
+  const [loading, setLoading] = useState(true);
+  const [packages, setPackages] = useState<PackageItem[]>([]);
+  const [selectedPackage, setSelectedPackage] = useState<PackageItem | null>(null);
+  const [expressDetail, setExpressDetail] = useState<ExpressDetail | null>(null);
+  const [loadingExpress, setLoadingExpress] = useState(false);
+
+  const loadPackages = useCallback(async () => {
+    if (!tradeNo) return;
+    setLoading(true);
+    try {
+      const res = await getAwardPackages(tradeNo);
+      if (res && Array.isArray(res)) {
+        setPackages(res);
+        if (res.length > 0) {
+          setSelectedPackage(res[0]);
+          loadExpressDetail(res[0].id);
+        }
+      }
+    } catch (e) {
+      console.error('加载包裹信息失败:', e);
+    }
+    setLoading(false);
+  }, [tradeNo]);
+
+  const loadExpressDetail = async (packageId: string) => {
+    if (!tradeNo) return;
+    setLoadingExpress(true);
+    try {
+      const res = await getAwardExpress(tradeNo, packageId);
+      setExpressDetail(res);
+    } catch (e) {
+      console.error('加载物流详情失败:', e);
+    }
+    setLoadingExpress(false);
+  };
+
+  useEffect(() => {
+    loadPackages();
+  }, [loadPackages]);
+
+  const selectPackage = (pkg: PackageItem) => {
+    setSelectedPackage(pkg);
+    loadExpressDetail(pkg.id);
+  };
+
+  if (loading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    );
+  }
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
+        <Image source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headerBg} contentFit="cover" />
+        
+        {/* 顶部导航 */}
+        <View style={[styles.header, { paddingTop: insets.top }]}>
+          <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+            <Text style={styles.backIcon}>‹</Text>
+          </TouchableOpacity>
+          <Text style={styles.title}>物流信息</Text>
+          <View style={styles.placeholder} />
+        </View>
+
+        <ScrollView style={[styles.content, { paddingTop: insets.top + 50 }]} showsVerticalScrollIndicator={false}>
+          {packages.length === 0 ? (
+            <View style={styles.emptyBox}>
+              <Text style={styles.emptyText}>暂无物流信息</Text>
+            </View>
+          ) : (
+            <>
+              {/* 包裹选择 */}
+              {packages.length > 1 && (
+                <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.packageTabs}>
+                  {packages.map((pkg, idx) => (
+                    <TouchableOpacity
+                      key={pkg.id}
+                      style={[styles.packageTab, selectedPackage?.id === pkg.id && styles.packageTabActive]}
+                      onPress={() => selectPackage(pkg)}
+                    >
+                      <Text style={[styles.packageTabText, selectedPackage?.id === pkg.id && styles.packageTabTextActive]}>
+                        包裹{idx + 1}
+                      </Text>
+                    </TouchableOpacity>
+                  ))}
+                </ScrollView>
+              )}
+
+              {/* 快递信息 */}
+              {selectedPackage && (
+                <View style={styles.expressInfo}>
+                  <Text style={styles.expressCompany}>{selectedPackage.expressCompany || '快递公司'}</Text>
+                  <Text style={styles.expressNo}>快递单号:{selectedPackage.expressNo || '暂无'}</Text>
+                </View>
+              )}
+
+              {/* 商品列表 */}
+              {selectedPackage && selectedPackage.itemList && (
+                <View style={styles.goodsSection}>
+                  <ScrollView horizontal showsHorizontalScrollIndicator={false}>
+                    {selectedPackage.itemList.map((item, idx) => (
+                      <View key={idx} style={styles.goodsItem}>
+                        <Image source={{ uri: item.cover }} style={styles.goodsImg} contentFit="contain" />
+                      </View>
+                    ))}
+                  </ScrollView>
+                </View>
+              )}
+
+              {/* 物流轨迹 */}
+              {loadingExpress ? (
+                <ActivityIndicator color="#fff" style={{ marginTop: 20 }} />
+              ) : expressDetail && expressDetail.traces && expressDetail.traces.length > 0 ? (
+                <View style={styles.tracesSection}>
+                  <Text style={styles.tracesTitle}>物流轨迹</Text>
+                  {expressDetail.traces.map((trace, idx) => (
+                    <View key={idx} style={styles.traceItem}>
+                      <View style={styles.traceDot} />
+                      {idx < expressDetail.traces.length - 1 && <View style={styles.traceLine} />}
+                      <View style={styles.traceContent}>
+                        <Text style={styles.traceTime}>{trace.time}</Text>
+                        <Text style={styles.traceText}>{trace.content}</Text>
+                      </View>
+                    </View>
+                  ))}
+                </View>
+              ) : (
+                <View style={styles.noTraces}>
+                  <Text style={styles.noTracesText}>暂无物流轨迹</Text>
+                </View>
+              )}
+            </>
+          )}
+          <View style={{ height: 50 }} />
+        </ScrollView>
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#1a1a2e' },
+  background: { flex: 1 },
+  loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
+  headerBg: { position: 'absolute', top: 0, left: 0, width: '100%', height: 160 },
+  header: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10 },
+  backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
+  backIcon: { fontSize: 32, color: '#fff', fontWeight: 'bold' },
+  title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
+  placeholder: { width: 40 },
+  content: { flex: 1, paddingHorizontal: 15 },
+  emptyBox: { marginTop: 100, alignItems: 'center' },
+  emptyText: { color: '#999', fontSize: 14 },
+  packageTabs: { flexDirection: 'row', marginBottom: 15 },
+  packageTab: { paddingHorizontal: 20, paddingVertical: 10, backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 20, marginRight: 10 },
+  packageTabActive: { backgroundColor: '#FC7D2E' },
+  packageTabText: { color: '#aaa', fontSize: 14 },
+  packageTabTextActive: { color: '#fff' },
+  expressInfo: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
+  expressCompany: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 5 },
+  expressNo: { color: '#aaa', fontSize: 14 },
+  goodsSection: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
+  goodsItem: { width: 60, height: 60, backgroundColor: '#fff', borderRadius: 6, marginRight: 10, alignItems: 'center', justifyContent: 'center' },
+  goodsImg: { width: 50, height: 50 },
+  tracesSection: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15 },
+  tracesTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 15 },
+  traceItem: { flexDirection: 'row', position: 'relative', paddingLeft: 20, marginBottom: 15 },
+  traceDot: { position: 'absolute', left: 0, top: 6, width: 10, height: 10, borderRadius: 5, backgroundColor: '#FC7D2E' },
+  traceLine: { position: 'absolute', left: 4, top: 16, width: 2, height: '100%', backgroundColor: 'rgba(255,255,255,0.2)' },
+  traceContent: { flex: 1 },
+  traceTime: { color: '#aaa', fontSize: 12, marginBottom: 4 },
+  traceText: { color: '#fff', fontSize: 14 },
+  noTraces: { marginTop: 20, alignItems: 'center' },
+  noTracesText: { color: '#999', fontSize: 14 },
+});

+ 16 - 0
constants/images.ts

@@ -121,6 +121,11 @@ export const Images = {
       real100: `${CDN_BASE}/box/detail/real100.png`,
       packagingBox1: `${CDN_BASE}/box/detail/packagingBox1.png`,
       packagingBox2: `${CDN_BASE}/box/detail/packagingBox2.png`,
+      boxIcon: `${CDN_BASE}/box/detail/boxIcon.png`,
+      levelTextA: `${CDN_BASE}/box/detail/levelTextA.png`,
+      levelTextB: `${CDN_BASE}/box/detail/levelTextB.png`,
+      levelTextC: `${CDN_BASE}/box/detail/levelTextC.png`,
+      levelTextD: `${CDN_BASE}/box/detail/levelTextD.png`,
     },
   },
   welfare: {
@@ -211,4 +216,15 @@ export const Images = {
   order: {
     itemBg: `${CDN_BASE}/common/itemBg.png`,
   },
+  // 积分相关
+  integral: {
+    head: `${CDN_SUPERMART}/mine/integral/head.png`,
+    greetings: `${CDN_SUPERMART}/mine/integral/greetings.png`,
+    goldCoins: `${CDN_SUPERMART}/mine/integral/goldCoins.png`,
+    yes: `${CDN_SUPERMART}/mine/integral/yes.png`,
+    no: `${CDN_SUPERMART}/mine/integral/no.png`,
+    basisBg: `${CDN_SUPERMART}/mine/integral/basisBg.png`,
+    tooltip: `${CDN_SUPERMART}/mine/integral/tooltip.png`,
+    turntable: `${CDN_SUPERMART}/mine/integral/turntable.png`,
+  },
 };

+ 9 - 6
contexts/AuthContext.tsx

@@ -1,6 +1,6 @@
 import React, { createContext, useContext, useEffect, useState } from 'react';
 
-import { getToken } from '@/services/http';
+import { clearToken, getToken } from '@/services/http';
 import { getUserInfo, UserInfo } from '@/services/user';
 
 interface AuthContextType {
@@ -8,7 +8,7 @@ interface AuthContextType {
   user: UserInfo | null;
   loading: boolean;
   refreshUser: () => Promise<void>;
-  logout: () => void;
+  logout: () => Promise<void>;
 }
 
 const AuthContext = createContext<AuthContextType>({
@@ -16,16 +16,18 @@ const AuthContext = createContext<AuthContextType>({
   user: null,
   loading: true,
   refreshUser: async () => {},
-  logout: () => {},
+  logout: async () => {},
 });
 
 export function AuthProvider({ children }: { children: React.ReactNode }) {
   const [user, setUser] = useState<UserInfo | null>(null);
   const [loading, setLoading] = useState(true);
+  const [hasToken, setHasToken] = useState(false);
 
   const refreshUser = async () => {
     try {
       const token = getToken();
+      setHasToken(!!token);
       if (token) {
         const info = await getUserInfo();
         setUser(info);
@@ -38,9 +40,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
     }
   };
 
-  const logout = () => {
+  const logout = async () => {
     setUser(null);
-    // 清除token在http服务中处理
+    setHasToken(false);
+    await clearToken();
   };
 
   useEffect(() => {
@@ -54,7 +57,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
   return (
     <AuthContext.Provider
       value={{
-        isLoggedIn: !!user,
+        isLoggedIn: hasToken,
         user,
         loading,
         refreshUser,

+ 71 - 0
package-lock.json

@@ -9,6 +9,7 @@
       "version": "1.0.0",
       "dependencies": {
         "@expo/vector-icons": "^15.0.3",
+        "@react-native-async-storage/async-storage": "^2.2.0",
         "@react-navigation/bottom-tabs": "^7.4.0",
         "@react-navigation/elements": "^2.6.3",
         "@react-navigation/native": "^7.1.8",
@@ -18,6 +19,7 @@
         "expo-font": "~14.0.10",
         "expo-haptics": "~15.0.8",
         "expo-image": "~3.0.11",
+        "expo-image-picker": "~17.0.10",
         "expo-linking": "~8.0.11",
         "expo-router": "~6.0.21",
         "expo-splash-screen": "~31.0.13",
@@ -36,6 +38,7 @@
         "react-native-screens": "~4.16.0",
         "react-native-svg": "^13.4.0",
         "react-native-web": "~0.21.0",
+        "react-native-webview": "^13.16.0",
         "react-native-worklets": "0.5.1"
       },
       "devDependencies": {
@@ -3681,6 +3684,18 @@
         "react-native": "*"
       }
     },
+    "node_modules/@react-native-async-storage/async-storage": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
+      "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
+      "license": "MIT",
+      "dependencies": {
+        "merge-options": "^3.0.4"
+      },
+      "peerDependencies": {
+        "react-native": "^0.0.0-0 || >=0.65 <1.0"
+      }
+    },
     "node_modules/@react-native/assets-registry": {
       "version": "0.81.5",
       "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -7574,6 +7589,27 @@
         }
       }
     },
+    "node_modules/expo-image-loader": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
+      "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
+    "node_modules/expo-image-picker": {
+      "version": "17.0.10",
+      "resolved": "https://registry.npmmirror.com/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
+      "integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
+      "license": "MIT",
+      "dependencies": {
+        "expo-image-loader": "~6.0.0"
+      },
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
     "node_modules/expo-keep-awake": {
       "version": "15.0.8",
       "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
@@ -9273,6 +9309,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-plain-obj": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-regex": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -10325,6 +10370,18 @@
       "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
       "license": "MIT"
     },
+    "node_modules/merge-options": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/merge-options/-/merge-options-3.0.4.tgz",
+      "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-obj": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/merge-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -12154,6 +12211,20 @@
       "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
       "license": "MIT"
     },
+    "node_modules/react-native-webview": {
+      "version": "13.16.0",
+      "resolved": "https://registry.npmmirror.com/react-native-webview/-/react-native-webview-13.16.0.tgz",
+      "integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==",
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^4.0.0",
+        "invariant": "2.2.4"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-worklets": {
       "version": "0.5.1",
       "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",

+ 3 - 0
package.json

@@ -12,6 +12,7 @@
   },
   "dependencies": {
     "@expo/vector-icons": "^15.0.3",
+    "@react-native-async-storage/async-storage": "^2.2.0",
     "@react-navigation/bottom-tabs": "^7.4.0",
     "@react-navigation/elements": "^2.6.3",
     "@react-navigation/native": "^7.1.8",
@@ -21,6 +22,7 @@
     "expo-font": "~14.0.10",
     "expo-haptics": "~15.0.8",
     "expo-image": "~3.0.11",
+    "expo-image-picker": "~17.0.10",
     "expo-linking": "~8.0.11",
     "expo-router": "~6.0.21",
     "expo-splash-screen": "~31.0.13",
@@ -39,6 +41,7 @@
     "react-native-screens": "~4.16.0",
     "react-native-svg": "^13.4.0",
     "react-native-web": "~0.21.0",
+    "react-native-webview": "^13.16.0",
     "react-native-worklets": "0.5.1"
   },
   "devDependencies": {

+ 40 - 6
services/award.ts

@@ -48,6 +48,10 @@ const apis = {
   BOX_UN_LOCK: '/api/luckBox/unlock',
   FEEDBACK_LIST: '/api/app/feedback/list',
   FEEDBACK_SUBMIT: '/api/app/feedback/submit',
+  MY_BOXES: '/api/luck/treasure-box/my-boxes',
+  OPEN_BOX: '/api/luck/treasure-box/open',
+  HARRY_EXCHANGE: '/api/redeemCode/exchange',
+  WIN_RECORDS: '/api/luck/treasure-box/win-records',
 };
 
 export interface IPItem {
@@ -156,9 +160,9 @@ export const previewOrder = async (poolId: string, quantity?: number, boxNumber?
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.PREVIEW, param);
 
-  // 如果请求失败,抛出错误
+  // http.ts 已经统一处理了错误提示,这里只返回数据
   if (!res.success) {
-    throw new Error(res.msg || '获取订单信息失败');
+    return null;
   }
 
   return res.data;
@@ -172,9 +176,9 @@ export const applyOrder = async (poolId: string, quantity: number, paymentType:
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.APPLY, param);
 
-  // 如果请求失败,抛出错误
+  // http.ts 已经统一处理了错误提示,这里只返回数据
   if (!res.success) {
-    throw new Error(res.msg || '支付失败');
+    return null;
   }
 
   return res.data;
@@ -359,8 +363,11 @@ export const unlockBox = async (poolId: string, boxNumber: string) => {
 };
 
 // 获取不可用座位号
-export const getUnavailableSeatNumbers = async (poolId: string, boxNumber: string) => {
-  const res = await get(apis.UNAVAILABLE_SEAT_NUMBERS, { poolId, boxNumber });
+export const getUnavailableSeatNumbers = async (poolId: string, boxNumber: string, startSeatNumber?: number, endSeatNumber?: number) => {
+  const params: any = { poolId, boxNumber };
+  if (startSeatNumber) params.startSeatNumber = startSeatNumber;
+  if (endSeatNumber) params.endSeatNumber = endSeatNumber;
+  const res = await post(apis.UNAVAILABLE_SEAT_NUMBERS, params);
   return res.data;
 };
 
@@ -376,6 +383,19 @@ export const submitFeedback = async (content: string) => {
   return res;
 };
 
+// 获取我的宝箱列表
+export const getMyBoxes = async () => {
+  const res = await get(apis.MY_BOXES);
+  return res.data;
+};
+
+// 开启宝箱
+export const openBox = async (params: { boxIds: string }) => {
+  // 按照小程序的方式,boxIds 作为 query 参数
+  const res = await post(`${apis.OPEN_BOX}?boxIds=${params.boxIds}`, {});
+  return res;
+};
+
 // 获取仓库商品详情
 export const getLuckDetail = async (id: string) => {
   const res = await get('/api/luckInventory/detail', { id });
@@ -388,6 +408,18 @@ export const getSumInventory = async () => {
   return res.data;
 };
 
+// 兑换码兑换
+export const harryExchange = async (params: { code: string }) => {
+  const res = await postL(apis.HARRY_EXCHANGE, params);
+  return res;
+};
+
+// 获取中奖记录
+export const getWinRecords = async (poolId: string, boxNumber: string) => {
+  const res = await get(apis.WIN_RECORDS, { poolId, boxNumber });
+  return res.data;
+};
+
 export default {
   getIndex,
   getIPList,
@@ -436,4 +468,6 @@ export default {
   submitFeedback,
   getLuckDetail,
   getSumInventory,
+  harryExchange,
+  getWinRecords,
 };

+ 9 - 0
services/base.ts

@@ -41,6 +41,14 @@ export const getMessages = async (current: number, size: number, type?: string)
   return res.success ? res.data : null;
 };
 
+// 获取消息列表(分页)
+export const getMessageList = async (current: number, size: number, type?: string) => {
+  const params: any = { current, size };
+  if (type) params.type = type;
+  const res = await get(apis.MESSAGE, params);
+  return res.success ? res.data : { records: [], total: 0 };
+};
+
 // 获取参数配置
 export const getParamConfig = async (code: string) => {
   const res = await get(apis.PARAM_CONFIG, { code });
@@ -62,6 +70,7 @@ export const track = async () => {
 export default {
   getPageConfig,
   getMessages,
+  getMessageList,
   getParamConfig,
   submitFeedback,
   track,

+ 59 - 7
services/http.ts

@@ -1,9 +1,11 @@
 // HTTP 请求封装
 import { router } from 'expo-router';
+import { Alert, Platform } from 'react-native';
 
 import { COMMON_HEADER, SERVICE_URL } from './config';
 
 export const SUCCESS_CODE = 0;
+export const SUCCESS_CODE_200 = 200;
 export const AUTH_INVALID = 401;
 export const AUTH_INVALID_2 = 403;
 
@@ -12,6 +14,7 @@ interface RequestOptions {
   showMsg?: boolean;
   method?: 'GET' | 'POST';
   token?: string;
+  silent?: boolean; // 静默模式,不显示错误提示
 }
 
 interface ApiResponse<T = any> {
@@ -31,6 +34,10 @@ export const setToken = (token: string | null) => {
 
 export const getToken = () => authToken;
 
+export const clearToken = async () => {
+  authToken = null;
+};
+
 // 处理认证失败
 const handleAuthError = () => {
   setToken(null);
@@ -38,13 +45,37 @@ const handleAuthError = () => {
   router.push('/login');
 };
 
+// 检查是否认证失败
+const isAuthError = (code: number | string) => {
+  const numCode = Number(code);
+  return numCode === AUTH_INVALID || numCode === AUTH_INVALID_2;
+};
+
+// 检查是否成功的 code
+const isSuccessCode = (code: number | string) => {
+  const numCode = Number(code);
+  return numCode === SUCCESS_CODE || numCode === SUCCESS_CODE_200;
+};
+
+// 显示错误提示(兼容 Web 和 Native)
+const showErrorMessage = (msg: string) => {
+  const message = msg || '请求失败,请稍后重试';
+  if (Platform.OS === 'web') {
+    // Web 环境使用 window.alert
+    window.alert(message);
+  } else {
+    // Native 环境使用 Alert.alert
+    Alert.alert('提示', message);
+  }
+};
+
 // 基础请求方法
 export const request = async <T = any>(
   url: string,
   data: any = {},
   options: RequestOptions = {}
 ): Promise<ApiResponse<T>> => {
-  const { method = 'POST' } = options;
+  const { method = 'POST', silent = false } = options;
 
   const headers: Record<string, string> = {
     'Content-Type': 'application/json',
@@ -76,8 +107,8 @@ export const request = async <T = any>(
       const response = await fetch(requestUrl, config);
       const result = await response.json();
       
-      // 处理 401/403 认证失败
-      if (result.code === AUTH_INVALID || result.code === AUTH_INVALID_2) {
+      // 处理 401/403 认证失败(不显示错误提示,直接跳转登录)
+      if (isAuthError(result.code)) {
         handleAuthError();
         return {
           code: result.code,
@@ -86,18 +117,28 @@ export const request = async <T = any>(
           success: false,
         };
       }
+
+      const success = isSuccessCode(result.code);
+      
+      // 非成功状态且非静默模式,显示错误提示
+      if (!success && !silent && result.msg) {
+        showErrorMessage(result.msg);
+      }
       
       return {
         ...result,
-        success: result.code == SUCCESS_CODE, // 使用 == 兼容字符串 "0" 和数字 0
+        success,
       };
     } else {
       config.body = JSON.stringify(data);
       const response = await fetch(fullUrl, config);
       const result = await response.json();
       
-      // 处理 401/403 认证失败
-      if (result.code === AUTH_INVALID || result.code === AUTH_INVALID_2) {
+      // 打印接口返回内容
+      console.log('[HTTP Response]', fullUrl, result);
+      
+      // 处理 401/403 认证失败(不显示错误提示,直接跳转登录)
+      if (isAuthError(result.code)) {
         handleAuthError();
         return {
           code: result.code,
@@ -106,14 +147,25 @@ export const request = async <T = any>(
           success: false,
         };
       }
+
+      const success = isSuccessCode(result.code);
+      
+      // 非成功状态且非静默模式,显示错误提示
+      if (!success && !silent && result.msg) {
+        console.log('[HTTP Error]', fullUrl, 'msg:', result.msg, 'silent:', silent);
+        showErrorMessage(result.msg);
+      }
       
       return {
         ...result,
-        success: result.code == SUCCESS_CODE, // 使用 == 兼容字符串 "0" 和数字 0
+        success,
       };
     }
   } catch (error) {
     console.error('Request error:', error);
+    if (!silent) {
+      showErrorMessage('网络异常,请检查网络连接');
+    }
     return {
       code: -1,
       msg: '网络异常',

+ 8 - 0
services/user.ts

@@ -11,6 +11,7 @@ const apis = {
   SEND_CODE: '/api/verifycode/send',
   PARAM_CONFIG: '/param/paramConfig',
   WALLET_AMOUNT: '/api/wallet/ranking/walletAmount',
+  WALLET_INFO: '/api/wallet/info',
   LOGOFF: '/api/removeAccount/submit',
   GET_HIDE: '/api/luckOrder/hide',
   SIGN_IN: '/credit/signIn',
@@ -113,6 +114,12 @@ export const getWalletAmount = async () => {
   return res;
 };
 
+// 获取钱包信息(积分等)
+export const getWalletInfo = async (type: string) => {
+  const res = await get(apis.WALLET_INFO, { type });
+  return res.data;
+};
+
 // 注销账号
 export const logoff = async (): Promise<boolean> => {
   const res = await postL(apis.LOGOFF);
@@ -189,6 +196,7 @@ export default {
   sendVerifyCode,
   getParamConfig,
   getWalletAmount,
+  getWalletInfo,
   logoff,
   getHide,
   signIn,