Forráskód Böngészése

一番赏奖池详情页 购买记录页面 规则页面仓库按钮 购买多盒子 自主选盒子页面

zbb 3 hónapja
szülő
commit
05a3a2c198

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

@@ -1,420 +0,0 @@
-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',
-  },
-});

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

@@ -1,547 +0,0 @@
-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,
-  },
-});

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

@@ -1,227 +0,0 @@
-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,
-  },
-});

+ 304 - 632
app/award-detail-yfs/index.tsx

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

+ 295 - 187
app/award-detail/components/RecordModal.tsx

@@ -1,214 +1,322 @@
-import { Image } from 'expo-image';
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
-import {
-    ActivityIndicator,
-    FlatList,
-    Modal,
-    StyleSheet,
-    Text,
-    TouchableOpacity,
-    View,
-} from 'react-native';
-
-import { getBuyRecord } from '@/services/award';
-
-interface RecordItem {
-  id: string;
-  nickname: string;
-  avatar: string;
-  goodsName: string;
-  cover: string;  // API 返回的是 cover 字段
-  level: string;
-  createTime: string;
-}
+import { LEVEL_MAP } from '@/constants/config';
+import { Images } from '@/constants/images';
+import { countRecordsAfterLastLevel, getBuyRecord } from '@/services/award';
+import { Image, ImageBackground } from 'expo-image';
+import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
+import { ActivityIndicator, Dimensions, FlatList, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+const { width, height } = Dimensions.get('window');
+
+const TABS = [
+    { title: '全部', value: '' },
+    { title: '超神款', value: 'A' },
+    { title: '欧皇款', value: 'B' },
+    { title: '隐藏款', value: 'C' },
+    { title: '普通款', value: 'D' }
+];
 
 interface RecordModalProps {
-  poolId: string;
+    poolId: string;
 }
 
 export interface RecordModalRef {
-  show: () => void;
-  close: () => void;
+    show: () => void;
+    close: () => void;
 }
 
-const LEVEL_MAP: Record<string, string> = {
-  A: '超神款',
-  B: '欧皇款',
-  C: '隐藏款',
-  D: '普通款',
-};
-
-export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(
-  ({ poolId }, ref) => {
+export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(({ poolId }, ref) => {
     const [visible, setVisible] = useState(false);
+    const [data, setData] = useState<any[]>([]);
+    const [activeTab, setActiveTab] = useState(TABS[0]);
     const [loading, setLoading] = useState(false);
-    const [records, setRecords] = useState<RecordItem[]>([]);
-    const [levelFilter, setLevelFilter] = useState<number | undefined>();
+    const [refreshing, setRefreshing] = useState(false);
+    const [lastId, setLastId] = useState<string | undefined>(undefined);
+    const [taggingA, setTaggingA] = useState(0);
+    const [taggingB, setTaggingB] = useState(0);
+
+    const [hasMore, setHasMore] = useState(true);
 
     useImperativeHandle(ref, () => ({
-      show: () => {
-        setVisible(true);
-        loadRecords();
-      },
-      close: () => setVisible(false),
+        show: () => {
+            setVisible(true);
+            refresh();
+        },
+        close: () => setVisible(false)
     }));
 
-    const loadRecords = async (level?: number) => {
-      setLoading(true);
-      try {
-        const res = await getBuyRecord(poolId, undefined, level);
-        setRecords(res || []);
-      } catch (error) {
-        console.error('加载记录失败:', error);
-      }
-      setLoading(false);
+    const refresh = () => {
+        setLastId(undefined);
+        setData([]);
+        setHasMore(true);
+        loadData(true);
+        loadStats();
     };
 
-    const handleLevelFilter = (level?: number) => {
-      setLevelFilter(level);
-      loadRecords(level);
+    const loadStats = async () => {
+        try {
+            const resA = await countRecordsAfterLastLevel({ levelEnumList: ['A'], poolId });
+            setTaggingA(resA.data || 0); // Check API structure: {data: number}
+            const resB = await countRecordsAfterLastLevel({ levelEnumList: ['B'], poolId });
+            setTaggingB(resB.data || 0);
+        } catch (e) {
+            console.error('Failed to load stats', e);
+        }
     };
 
-    const renderItem = ({ item }: { item: RecordItem }) => (
-      <View style={styles.recordItem}>
-        <Image
-          source={{ uri: item.avatar }}
-          style={styles.avatar}
-          contentFit="cover"
-        />
-        <View style={styles.recordInfo}>
-          <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
-          <Text style={styles.time}>{item.createTime}</Text>
-        </View>
-        <View style={styles.goodsInfo}>
-          <Image
-            source={{ uri: item.cover }}
-            style={styles.goodsImage}
-            contentFit="cover"
-          />
-          <View style={styles.goodsDetail}>
-            <Text style={styles.goodsName} numberOfLines={1}>{item.goodsName}</Text>
-            <Text style={styles.levelText}>{LEVEL_MAP[item.level] || item.level}</Text>
-          </View>
-        </View>
-      </View>
-    );
+    const loadData = async (isRefresh = false) => {
+        if (loading || (!isRefresh && !hasMore)) return;
+        setLoading(true);
+        try {
+           const currentLastId = isRefresh ? undefined : lastId;
+           const res = await getBuyRecord(poolId, currentLastId, activeTab.value as any);
+           if (res && res.length > 0) {
+               setData(prev => isRefresh ? res : [...prev, ...res]);
+               setLastId(res[res.length - 1].id);
+               // Assuming page size is 10 or 20. If less than that, no more data.
+               // Let's assume default is 10. If strictly < 10, definitely no more. 
+               // If == 10, maybe more. 
+               // Safer: if 0 returned, handled by outside if.
+           } else {
+               setHasMore(false);
+           }
+           if (res && res.length < 10) {
+               setHasMore(false);
+           }
+        } catch (e) {
+            console.error('Failed to load records', e);
+        }
+        setLoading(false);
+        setRefreshing(false);
+    };
 
-    const levels = [
-      { label: '全部', value: undefined },
-      { label: '超神款', value: 1 },
-      { label: '欧皇款', value: 2 },
-      { label: '隐藏款', value: 3 },
-      { label: '普通款', value: 4 },
-    ];
+    const handleTabChange = (tab: any) => {
+        setActiveTab(tab);
+        // Reset and reload
+        setLastId(undefined);
+        setData([]);
+        setHasMore(true);
+        // We can't immediately call loadData due to state update async nature, 
+        // but let's assume useEffect or immediate call with arg works.
+        // Actually best to useEffect on activeTab change?
+        // Or specific effect for tab
+        // Let's do explicit reload in effect or here
+        // setData is sync-ish in RN usually batching, let's use effect.
+    };
 
-    return (
-      <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
-        <View style={styles.overlay}>
-          <View style={styles.container}>
-            <View style={styles.header}>
-              <Text style={styles.title}>购买记录</Text>
-              <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
-                <Text style={styles.closeText}>×</Text>
-              </TouchableOpacity>
-            </View>
+    useEffect(() => {
+        if (visible) {
+             setLastId(undefined);
+             setData([]);
+             setHasMore(true);
+             loadData(true);
+        }
+    }, [activeTab]);
 
-            <View style={styles.filterRow}>
-              {levels.map((item) => (
-                <TouchableOpacity
-                  key={item.label}
-                  style={[styles.filterBtn, levelFilter === item.value && styles.filterBtnActive]}
-                  onPress={() => handleLevelFilter(item.value)}
-                >
-                  <Text style={[styles.filterText, levelFilter === item.value && styles.filterTextActive]}>
-                    {item.label}
-                  </Text>
-                </TouchableOpacity>
-              ))}
+    const renderItem = ({ item }: { item: any }) => (
+        <View style={styles.item}>
+            <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
+            <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
+            
+            {/* Level Icon - Assuming we have images for level text similar to legacy */}
+            {/* Legacy: :src="LEVEL_MAP[item.level].titleText" */}
+            {/* We don't have titleText in our config yet, need to map or use text */}
+            {/* For now use Text with color */}
+            <View style={[styles.levelTag, { borderColor: LEVEL_MAP[item.level]?.color || '#000' }]}>
+                 <Text style={[styles.levelText, { color: LEVEL_MAP[item.level]?.color || '#000' }]}>
+                     {LEVEL_MAP[item.level]?.title}
+                 </Text>
             </View>
 
-            {loading ? (
-              <View style={styles.loadingBox}>
-                <ActivityIndicator size="large" color="#ff6600" />
-              </View>
-            ) : (
-              <FlatList
-                data={records}
-                renderItem={renderItem}
-                keyExtractor={(item, index) => item.id || index.toString()}
-                contentContainerStyle={styles.listContent}
-                ListEmptyComponent={
-                  <View style={styles.emptyBox}>
-                    <Text style={styles.emptyText}>暂无记录</Text>
-                  </View>
-                }
-              />
-            )}
-          </View>
+            <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
+            <Text style={styles.countText}>×{item.lastCount}</Text>
+            <Image source={{ uri: item.cover }} style={styles.itemImage} />
         </View>
-      </Modal>
     );
-  }
-);
+
+    return (
+        <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+             <View style={styles.overlay}>
+                 <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+                 <ImageBackground 
+                    source={{ uri: Images.box.detail.recordBg }} 
+                    style={styles.container}
+                    resizeMode="stretch"
+                 >
+                     {/* Header */}
+                     <View style={styles.header}>
+                         <Text style={styles.title}>购买记录</Text>
+                         <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+                             {/* Close Icon or Text */}
+                             <Text style={{ fontSize: 20 }}>×</Text>
+                         </TouchableOpacity>
+                     </View>
+
+                     {/* Stats Banner */}
+                     <ImageBackground 
+                        source={{ uri: Images.box.detail.taggingBg }} 
+                        style={styles.statsBanner}
+                        resizeMode="stretch"
+                     >
+                         <View style={styles.statItem}>
+                             <Text style={styles.statNum}>{taggingA}</Text>
+                             <Text style={styles.statLabel}>发未出</Text>
+                             <Text style={[styles.statLevel, { color: LEVEL_MAP.A.color }]}>超神款</Text>
+                         </View>
+                         <View style={styles.statItem}>
+                             <Text style={styles.statNum}>{taggingB}</Text>
+                             <Text style={styles.statLabel}>发未出</Text>
+                             <Text style={[styles.statLevel, { color: LEVEL_MAP.B.color }]}>欧皇款</Text>
+                         </View>
+                     </ImageBackground>
+
+                     {/* Tabs */}
+                     <View style={styles.tabs}>
+                         {TABS.map(tab => (
+                             <TouchableOpacity 
+                                key={tab.value} 
+                                style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
+                                onPress={() => handleTabChange(tab)}
+                             >
+                                 <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
+                                     {tab.title}
+                                 </Text>
+                             </TouchableOpacity>
+                         ))}
+                     </View>
+
+                     {/* List */}
+                     <FlatList
+                        data={data}
+                        renderItem={renderItem}
+                        keyExtractor={(item, index) => item.id || String(index)}
+                        style={styles.list}
+                        contentContainerStyle={{ paddingBottom: 20 }}
+                        onEndReached={() => loadData(false)}
+                        onEndReachedThreshold={0.5}
+                        ListEmptyComponent={
+                            !loading ? <Text style={styles.emptyText}>暂无记录</Text> : null
+                        }
+                        ListFooterComponent={loading ? <ActivityIndicator color="#000" /> : null}
+                     />
+
+                 </ImageBackground>
+             </View>
+        </Modal>
+    );
+});
 
 const styles = StyleSheet.create({
-  overlay: {
-    flex: 1,
-    backgroundColor: 'rgba(0,0,0,0.5)',
-    justifyContent: 'flex-end',
-  },
-  container: {
-    backgroundColor: '#fff',
-    borderTopLeftRadius: 20,
-    borderTopRightRadius: 20,
-    height: '70%',
-  },
-  header: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    padding: 15,
-    borderBottomWidth: 1,
-    borderBottomColor: '#eee',
-  },
-  title: { fontSize: 16, fontWeight: '600', color: '#333' },
-  closeBtn: { position: 'absolute', right: 15, top: 10 },
-  closeText: { fontSize: 24, color: '#999' },
-  filterRow: {
-    flexDirection: 'row',
-    paddingHorizontal: 10,
-    paddingVertical: 10,
-    borderBottomWidth: 1,
-    borderBottomColor: '#eee',
-  },
-  filterBtn: {
-    paddingHorizontal: 12,
-    paddingVertical: 6,
-    borderRadius: 15,
-    marginRight: 8,
-    backgroundColor: '#f5f5f5',
-  },
-  filterBtnActive: { backgroundColor: '#ff6600' },
-  filterText: { fontSize: 12, color: '#666' },
-  filterTextActive: { color: '#fff' },
-  listContent: { padding: 15 },
-  recordItem: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 12,
-    borderBottomWidth: 1,
-    borderBottomColor: '#f5f5f5',
-  },
-  avatar: { width: 40, height: 40, borderRadius: 20 },
-  recordInfo: { flex: 1, marginLeft: 10 },
-  nickname: { fontSize: 14, color: '#333' },
-  time: { fontSize: 11, color: '#999', marginTop: 2 },
-  goodsInfo: { flexDirection: 'row', alignItems: 'center' },
-  goodsImage: { width: 50, height: 50, borderRadius: 5 },
-  goodsDetail: { marginLeft: 8, alignItems: 'flex-end' },
-  goodsName: { fontSize: 12, color: '#333', maxWidth: 80 },
-  levelText: { fontSize: 11, color: '#ff6600', marginTop: 2 },
-  loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
-  emptyBox: { alignItems: 'center', paddingVertical: 50 },
-  emptyText: { color: '#999', fontSize: 14 },
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.5)',
+        justifyContent: 'flex-end',
+    },
+    mask: {
+        flex: 1,
+    },
+    container: {
+        height: height * 0.7,
+        paddingTop: 0,
+        backgroundColor: '#fff', 
+        borderTopLeftRadius: 15,
+        borderTopRightRadius: 15,
+        overflow: 'hidden',
+    },
+    header: {
+        height: 50,
+        justifyContent: 'center',
+        alignItems: 'center',
+        marginTop: 10,
+    },
+    title: {
+        fontSize: 18,
+        fontWeight: 'bold',
+    },
+    closeBtn: {
+        position: 'absolute',
+        right: 20,
+        top: 15,
+        width: 30,
+        height: 30,
+        alignItems: 'center',
+        justifyContent: 'center', 
+        backgroundColor: '#eee',
+        borderRadius: 15,
+    },
+    statsBanner: {
+        width: '90%',
+        alignSelf: 'center',
+        height: 50,
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        alignItems: 'center',
+        marginBottom: 10,
+    },
+    statItem: {
+        flexDirection: 'row',
+        alignItems: 'center',
+    },
+    statNum: { 
+        fontSize: 16, 
+        fontWeight: 'bold', 
+        color: '#fff', 
+        textShadowColor: '#000', 
+        textShadowOffset: { width: -1, height: -1 },
+        textShadowRadius: 1,
+    },
+    statLabel: { 
+        fontSize: 12, 
+        color: '#928D81', 
+        fontWeight: '300',
+        marginHorizontal: 8 
+    },
+    statLevel: { fontSize: 12, fontWeight: 'bold' },
+    
+    tabs: {
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        paddingHorizontal: 10,
+        marginBottom: 10,
+        backgroundColor: 'rgba(255,255,255,0.1)',
+        paddingVertical: 5,
+    },
+    tab: {
+        paddingVertical: 5,
+        paddingHorizontal: 10,
+        borderRadius: 5,
+        backgroundColor: '#eee',
+    },
+    activeTab: {
+        backgroundColor: '#FEC433',
+    },
+    tabText: { fontSize: 12, color: '#666' },
+    activeTabText: { color: '#000', fontWeight: 'bold' },
+
+    list: {
+        flex: 1,
+        paddingHorizontal: 15,
+    },
+    item: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        backgroundColor: '#FFF7E3',
+        marginBottom: 10,
+        padding: 5,
+        borderRadius: 8,
+        height: 60,
+    },
+    avatar: { width: 40, height: 40, borderRadius: 20 },
+    nickname: { flex: 1, marginLeft: 10, fontSize: 12, color: '#333' },
+    levelTag: { 
+        borderWidth: 1, 
+        paddingHorizontal: 4, 
+        borderRadius: 4, 
+        marginHorizontal: 5
+    },
+    levelText: { fontSize: 10, fontWeight: 'bold' },
+    itemName: { width: 80, fontSize: 12, color: '#FEC433', marginHorizontal: 5 },
+    countText: { fontSize: 12, color: '#000', fontWeight: 'bold', marginRight: 5 },
+    itemImage: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#fff' },
+    emptyText: { textAlign: 'center', marginTop: 20, color: '#999' }
+
 });

+ 17 - 11
app/weal/room.tsx

@@ -2,16 +2,16 @@ import { Image } from 'expo-image';
 import { useRouter } from 'expo-router';
 import React, { useCallback, useEffect, useRef, useState } from 'react';
 import {
-  ActivityIndicator,
-  ImageBackground,
-  RefreshControl,
-  ScrollView,
-  StatusBar,
-  StyleSheet,
-  Text,
-  TextInput,
-  TouchableOpacity,
-  View,
+    ActivityIndicator,
+    ImageBackground,
+    RefreshControl,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
@@ -27,7 +27,9 @@ interface RoomItem {
   officialFlag: number;
   user: { avatar: string; username: string };
   luckRoomGoodsList: { spu: { cover: string } }[];
+
   participatingList: any[];
+  mode?: string;
 }
 
 export default function RoomScreen() {
@@ -96,7 +98,11 @@ export default function RoomScreen() {
   };
 
   const handleRoomPress = (item: RoomItem) => {
-    router.push({ pathname: '/weal/detail', params: { id: item.id } } as any);
+    if (item.mode === 'YFS_PRO') {
+        router.push({ pathname: '/award-detail-yfs', params: { id: item.id } } as any);
+    } else {
+        router.push({ pathname: '/weal/detail', params: { id: item.id } } as any);
+    }
   };
 
   const isItemType = (type: string) => {

+ 374 - 0
components/award-detail-yfs/BoxSelectionModal.tsx

@@ -0,0 +1,374 @@
+import { Images } from '@/constants/images';
+import { getBoxList } from '@/services/award';
+import { Image, ImageBackground } from 'expo-image';
+import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    Dimensions,
+    FlatList,
+    Modal,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface BoxSelectionModalProps {
+  poolId: string;
+  onSelect: (box: any) => void;
+}
+
+export interface BoxSelectionModalRef {
+  show: () => void;
+  close: () => void;
+}
+
+const TABS = [
+  { title: '全部', value: '' },
+  { title: '超神款', value: 'A' },
+  { title: '欧皇款', value: 'B' },
+  { title: '隐藏款', value: 'C' },
+  { title: '普通款', value: 'D' },
+];
+
+export const BoxSelectionModal = forwardRef<BoxSelectionModalRef, BoxSelectionModalProps>(
+  ({ poolId, onSelect }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [activeTab, setActiveTab] = useState(TABS[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 loadData = useCallback(async (pageNum = 1, tabValue = '', isRefresh = false) => {
+      if (isRefresh) setRefreshing(true);
+      if (pageNum === 1 && !isRefresh) setLoading(true);
+
+      try {
+        const res = await getBoxList(poolId, tabValue as any, pageNum, 20); // Using 20 as pageSize
+        const newData = res?.records || []; // Assuming standard paginated response
+        
+        if (pageNum === 1) {
+            setList(newData);
+        } else {
+            setList(prev => [...prev, ...newData]);
+        }
+        
+        setHasMore(newData.length >= 20);
+        setPage(pageNum);
+      } catch (error) {
+        console.error('Failed to load boxes:', error);
+      } finally {
+        setLoading(false);
+        setRefreshing(false);
+      }
+    }, [poolId]);
+
+    useImperativeHandle(ref, () => ({
+      show: () => {
+        setVisible(true);
+        setActiveTab(TABS[0]);
+        loadData(1, TABS[0].value);
+      },
+      close: () => {
+        setVisible(false);
+      },
+    }));
+
+    const handleTabChange = (tab: typeof TABS[0]) => {
+      setActiveTab(tab);
+      loadData(1, tab.value);
+    };
+
+    const handleLoadMore = () => {
+        if (!loading && hasMore) {
+            loadData(page + 1, activeTab.value);
+        }
+    };
+
+    const handleRefresh = () => {
+        loadData(1, activeTab.value, true);
+    }
+
+    const renderItem = ({ item, index }: { item: any; index: number }) => {
+        if (item.leftQuantity <= 0) return null;
+
+        return (
+            <TouchableOpacity 
+                style={styles.item} 
+                onPress={() => {
+                    setVisible(false);
+                    onSelect(item);
+                }}
+            >
+                <View style={styles.itemIndex}>
+                    <Text style={styles.indexText}>{index + 1}</Text>
+                </View>
+                
+                <View style={styles.itemContent}>
+                    {/* Left Icon & Count */}
+                    <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} />
+
+                    {/* Breakdown */}
+                    <View style={styles.breakdown}>
+                        <BreakdownItem label="A" current={item.leftQuantityA} total={item.quantityA} icon={Images.box.detail.levelTextA} />
+                        <BreakdownItem label="B" current={item.leftQuantityB} total={item.quantityB} icon={Images.box.detail.levelTextB} />
+                        <BreakdownItem label="C" current={item.leftQuantityC} total={item.quantityC} icon={Images.box.detail.levelTextC} />
+                        <BreakdownItem label="D" current={item.leftQuantityD} total={item.quantityD} icon={Images.box.detail.levelTextD} />
+                    </View>
+                </View>
+            </TouchableOpacity>
+        );
+    };
+
+    return (
+      <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+        <View style={styles.overlay}>
+            <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+            
+            <ImageBackground 
+                source={{ uri: Images.box.detail.recordBg }} 
+                style={styles.container}
+                resizeMode="stretch"
+            >
+                {/* Header */}
+                <View style={styles.header}>
+                    <Image source={{ uri: Images.box.detail.recordTitleLeft }} style={styles.titleDecor} contentFit="contain" />
+                    <Text style={styles.title}>换盒</Text>
+                    <Image source={{ uri: Images.box.detail.recordTitleRight }} style={styles.titleDecor} contentFit="contain" />
+                    
+                    <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+                         <Text style={styles.closeText}>×</Text>
+                    </TouchableOpacity>
+                </View>
+
+                {/* Tabs */}
+                <View style={styles.tabs}>
+                    {TABS.map(tab => (
+                        <TouchableOpacity 
+                            key={tab.value} 
+                            style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
+                            onPress={() => handleTabChange(tab)}
+                        >
+                            <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
+                                {tab.title}
+                            </Text>
+                        </TouchableOpacity>
+                    ))}
+                </View>
+
+                {/* List */}
+                <View style={styles.listContainer}>
+                     <FlatList
+                        data={list}
+                        renderItem={renderItem}
+                        keyExtractor={(item, index) => item.id || String(index)}
+                        contentContainerStyle={styles.listContent}
+                        onEndReached={handleLoadMore}
+                        onEndReachedThreshold={0.5}
+                        refreshing={refreshing}
+                        onRefresh={handleRefresh}
+                        ListEmptyComponent={
+                            !loading ? <Text style={styles.emptyText}>暂无数据</Text> : null
+                        }
+                        ListFooterComponent={loading && !refreshing ? <ActivityIndicator /> : null}
+                    />
+                </View>
+
+            </ImageBackground>
+        </View>
+      </Modal>
+    );
+  }
+);
+
+const BreakdownItem = ({ label, current, total, icon }: { label: string, current: number, total: number, icon: string }) => (
+    <View style={styles.breakdownItem}>
+        <Image source={{ uri: icon }} style={styles.levelIcon} contentFit="contain" />
+        <Text style={styles.breakdownText}>
+            <Text style={styles.currentNum}>{current}</Text>/{total}
+        </Text>
+    </View>
+);
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.5)',
+    justifyContent: 'flex-end',
+  },
+  mask: { flex: 1 },
+  container: {
+    height: 600, // wrapperWidth rpx in Vue
+    paddingTop: 15,
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    overflow: 'hidden',
+    backgroundColor: '#fff', // Fallback
+  },
+  header: {
+      flexDirection: 'row',
+      alignItems: 'center',
+      justifyContent: 'center',
+      marginBottom: 15,
+      position: 'relative',
+      height: 44,
+  },
+  title: {
+      fontSize: 18,
+      fontWeight: 'bold',
+      color: '#fff',
+      marginHorizontal: 10,
+      textShadowColor: '#000',
+      textShadowOffset: { width: 1, height: 1 },
+      textShadowRadius: 1,
+  },
+  titleDecor: {
+      width: 17,
+      height: 19,
+  },
+  closeBtn: {
+      position: 'absolute',
+      right: 15,
+      top: 0,
+      width: 24,
+      height: 24,
+      backgroundColor: '#ebebeb',
+      borderRadius: 12,
+      justifyContent: 'center',
+      alignItems: 'center',
+  },
+  closeText: {
+      fontSize: 18,
+      color: '#a2a2a2',
+      marginTop: -2,
+  },
+  tabs: {
+      flexDirection: 'row',
+      justifyContent: 'space-around',
+      paddingHorizontal: 15,
+      marginBottom: 10,
+  },
+  tab: {
+      paddingVertical: 6,
+      paddingHorizontal: 12,
+      borderRadius: 15,
+      backgroundColor: '#f5f5f5',
+  },
+  activeTab: {
+      backgroundColor: '#ffdb4d',
+  },
+  tabText: {
+      fontSize: 12,
+      color: '#666',
+  },
+  activeTabText: {
+      color: '#333',
+      fontWeight: 'bold',
+  },
+  listContainer: {
+      flex: 1,
+      backgroundColor: '#f3f3f3',
+      marginHorizontal: 15,
+      marginBottom: 20,
+      borderWidth: 2,
+      borderColor: '#000',
+      padding: 10,
+  },
+  listContent: {
+      paddingBottom: 20,
+  },
+  item: {
+      backgroundColor: '#fff',
+      borderWidth: 3,
+      borderColor: '#000',
+      marginBottom: 10,
+      flexDirection: 'row',
+      height: 80,
+      position: 'relative',
+      overflow: 'hidden',
+  },
+  itemIndex: {
+      position: 'absolute',
+      left: 0,
+      top: 0,
+      width: 22,
+      height: 22,
+      backgroundColor: '#fff',
+      borderRightWidth: 2,
+      borderBottomWidth: 2,
+      borderColor: '#000',
+      justifyContent: 'center',
+      alignItems: 'center',
+      zIndex: 1,
+  },
+  indexText: {
+      fontSize: 12,
+      fontWeight: 'bold',
+  },
+  itemContent: {
+      flex: 1,
+      flexDirection: 'row',
+      alignItems: 'center',
+      paddingHorizontal: 10,
+      paddingLeft: 22, // Space for index
+  },
+  leftSection: {
+      width: 60,
+      alignItems: 'center',
+      justifyContent: 'center',
+  },
+  boxIcon: {
+      width: 24,
+      height: 24,
+      marginBottom: 2,
+  },
+  leftText: {
+      fontSize: 10,
+      color: '#333',
+  },
+  divider: {
+      width: 1,
+      height: 40,
+      backgroundColor: '#dcdad3',
+      marginHorizontal: 10,
+      opacity: 0.5,
+  },
+  breakdown: {
+      flex: 1,
+      flexDirection: 'row',
+      flexWrap: 'wrap',
+  },
+  breakdownItem: {
+      width: '50%',
+      flexDirection: 'row',
+      alignItems: 'center',
+      marginBottom: 4,
+  },
+  levelIcon: {
+      width: 45,
+      height: 16,
+      marginRight: 4,
+  },
+  breakdownText: {
+      fontSize: 12,
+      color: '#666',
+  },
+  currentNum: {
+      color: '#000',
+      fontWeight: 'bold',
+  },
+  emptyText: {
+      textAlign: 'center',
+      marginTop: 20,
+      color: '#999',
+  },
+});

+ 213 - 0
components/award-detail-yfs/FirstLast.tsx

@@ -0,0 +1,213 @@
+import { Images } from '@/constants/images';
+import { Image, ImageBackground } from 'expo-image';
+import React from 'react';
+import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+interface FirstLastProps {
+  data: any;
+  box: any;
+  onPrevBox?: () => void;
+  onNextBox?: () => void;
+  onChangeBox?: () => void;
+  onProductClick?: (product: any) => void;
+}
+
+export default function FirstLast({ data, box, onPrevBox, onNextBox, onChangeBox, onProductClick }: FirstLastProps) {
+  // Filter for special prizes: FIRST, LAST, LASTALL
+  const prizeList = React.useMemo(() => {
+    if (!box || !box.prizeList) return [];
+    return box.prizeList.filter((item: any) => 
+        ['FIRST', 'LAST', 'LASTALL'].includes(item.level)
+    );
+  }, [box]);
+
+  if (!box) return null;
+
+  return (
+    <View style={styles.container}>
+      <ImageBackground 
+        source={{ uri: Images.box.detail.firstBoxBg }} 
+        style={styles.bg}
+        resizeMode="stretch"
+      >
+        {/* Title / Remain Info */}
+        <View style={styles.header}>
+            <View style={styles.remainInfo}>
+                <Text style={styles.remainLabel}>当前盒子剩余:</Text>
+                <Text style={styles.remainValue}>
+                     <Text style={styles.remainNum}>{box.leftQuantity}</Text>
+                     /{box.quantity}发
+                </Text>
+            </View>
+            <Image 
+                source={{ uri: Images.box.detail.playMethod }} 
+                style={styles.ruleBtn} 
+                contentFit="contain"
+            />
+        </View>
+
+        {/* Prize List (First/Last) */}
+        <View style={styles.prizeContainer}>
+            {prizeList.map((item: any, index: number) => (
+                <TouchableOpacity 
+                    key={index} 
+                    style={styles.prizeItem} 
+                    onPress={() => onProductClick && onProductClick(item.spu)}
+                >
+                    <ImageBackground 
+                        source={{ uri: Images.box.detail.firstItemBg }} 
+                        style={styles.prizeItemBg}
+                        resizeMode="contain"
+                    >
+                        <Image source={{ uri: item.cover }} style={styles.prizeImg} contentFit="contain" />
+                        
+                        <View style={styles.prizeLabelContainer}>
+                             {item.level === 'FIRST' && (
+                                 <Text style={[styles.prizeLabel, { backgroundColor: 'rgba(91, 189, 208, .8)' }]}>抢先赏</Text>
+                             )}
+                             {item.level === 'LAST' && (
+                                 <Text style={[styles.prizeLabel, { backgroundColor: 'rgba(246, 44, 113, .8)' }]}>最终赏</Text>
+                             )}
+                             {item.level === 'LASTALL' && (
+                                 <Text style={[styles.prizeLabel, { backgroundColor: 'rgba(44, 246, 74, .8)' }]}>全局赏</Text>
+                             )}
+                        </View>
+                        
+                        {/* User Avatar if someone won it? Legacy logic: v-if="item.nickname" */}
+                        {item.nickname && (
+                            <View style={styles.winnerBox}>
+                                <Image source={{ uri: item.avatar }} style={styles.winnerAvatar} />
+                                <Text style={styles.winnerName} numberOfLines={1}>{item.nickname}</Text>
+                            </View>
+                        )}
+                    </ImageBackground>
+                </TouchableOpacity>
+            ))}
+        </View>
+
+        {/* Controls */}
+        <ImageBackground 
+             source={{ uri: Images.box.detail.funBoxBg }} 
+             style={styles.controlsBg}
+             resizeMode="stretch"
+        >
+             <TouchableOpacity style={styles.controlBtn} onPress={onPrevBox}>
+                 <Text style={styles.controlText}>上一盒</Text>
+             </TouchableOpacity>
+
+             <TouchableOpacity style={styles.changeBtn} onPress={onChangeBox}>
+                 <Text style={[styles.controlText, { color: '#000' }]}>
+                     换盒(<Text style={{ fontWeight: 'bold' }}>{box.number}</Text>/{box.lastNumber})
+                 </Text>
+             </TouchableOpacity>
+
+             <TouchableOpacity style={styles.controlBtn} onPress={onNextBox}>
+                 <Text style={styles.controlText}>下一盒</Text>
+             </TouchableOpacity>
+        </ImageBackground>
+
+      </ImageBackground>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 10,
+    marginBottom: 10,
+  },
+  bg: {
+    width: '100%',
+    height: 193, // 386rpx / 2
+    alignItems: 'center',
+  },
+  header: {
+    width: '100%',
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingHorizontal: 20,
+    paddingTop: 20,
+  },
+  remainInfo: {
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  remainLabel: { color: '#fff', fontSize: 12 },
+  remainValue: { color: '#fff', fontSize: 12 },
+  remainNum: { fontSize: 16, fontWeight: 'bold' },
+  ruleBtn: { width: 41, height: 17 },
+  
+  prizeContainer: {
+      flexDirection: 'row',
+      justifyContent: 'space-around',
+      width: '80%',
+      marginTop: 10,
+      height: 70, // Adjust based on item size
+  },
+  prizeItem: {
+      width: 63, // 126rpx / 2
+      height: 65,
+  },
+  prizeItemBg: {
+      flex: 1,
+      alignItems: 'center',
+      justifyContent: 'center',
+  },
+  prizeImg: {
+      width: 60,
+      height: 60,
+  },
+  prizeLabelContainer: {
+      position: 'absolute',
+      bottom: -8,
+      width: '100%',
+      alignItems: 'center',
+  },
+  prizeLabel: {
+      fontSize: 10,
+      color: '#fff',
+      paddingHorizontal: 4,
+      paddingVertical: 1,
+      borderRadius: 4,
+      overflow: 'hidden',
+  },
+  winnerBox: {
+      position: 'absolute',
+      top: 0,
+      left: 0,
+      right: 0,
+      bottom: 0,
+      justifyContent: 'center',
+      alignItems: 'center',
+      backgroundColor: 'rgba(0,0,0,0.6)',
+      borderRadius: 30, // Circleish
+  },
+  winnerAvatar: { width: 24, height: 24, borderRadius: 12, borderWidth: 1, borderColor: '#FFE996' },
+  winnerName: { color: '#FEC433', fontSize: 8, marginTop: 2, maxWidth: 50 },
+
+  controlsBg: {
+      width: 270, // 540rpx / 2
+      height: 38, // 76rpx / 2
+      flexDirection: 'row',
+      alignItems: 'center',
+      justifyContent: 'center',
+      marginTop: 25,
+  },
+  controlBtn: {
+      width: 90,
+      height: '100%',
+      justifyContent: 'center',
+      alignItems: 'center',
+  },
+  changeBtn: {
+      width: 110,
+      height: '100%',
+      justifyContent: 'center',
+      alignItems: 'center',
+  },
+  controlText: {
+      color: '#fff',
+      fontSize: 14,
+      fontWeight: '500',
+  }
+});

+ 469 - 0
components/award-detail-yfs/NumSelectionModal.tsx

@@ -0,0 +1,469 @@
+import { LEVEL_MAP } from '@/constants/config';
+import { Images } from '@/constants/images';
+import { getUnavailableSeatNumbers, previewOrder } from '@/services/award';
+import { Image, ImageBackground } from 'expo-image';
+import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
+import {
+    ActivityIndicator,
+    Alert,
+    Dimensions,
+    Modal,
+    ScrollView,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface NumSelectionModalProps {
+  poolId: string;
+  onPay: (params: { previewRes: any, chooseNum: number[], boxNum: string }) => void;
+}
+
+export interface NumSelectionModalRef {
+  show: (box: any) => void;
+  close: () => void;
+}
+
+
+
+export const NumSelectionModal = forwardRef<NumSelectionModalRef, NumSelectionModalProps>(
+  ({ poolId, onPay }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [box, setBox] = useState<any>(null);
+    const [tabs, setTabs] = useState<any[]>([]);
+    const [activeTab, setActiveTab] = useState<any>(null);
+    
+    // Status Maps
+    const [useMap, setUseMap] = useState<Record<number, string>>({}); // Sold/Opened (seatNum -> level)
+    const [lockMap, setLockMap] = useState<Record<number, number>>({}); // Locked/Pending (seatNum -> seatNum)
+    const [checkMap, setCheckMap] = useState<Record<number, number>>({}); // Selected by user (seatNum -> seatNum)
+
+    const [loading, setLoading] = useState(false);
+
+    useImperativeHandle(ref, () => ({
+      show: (boxItem: any) => {
+        setBox(boxItem);
+        setVisible(true);
+        setCheckMap({});
+        // Initialize Tabs based on box quantity
+        // Assuming max 100 per tab like legacy
+        const total = boxItem.quantity || 100; // Default or from box
+        const newTabs = [];
+        const itemsPerTab = 100;
+        const count = Math.ceil(total / itemsPerTab);
+        
+        for (let i = 0; i < count; i++) {
+            const start = i * itemsPerTab + 1;
+            const end = Math.min((i + 1) * itemsPerTab, total);
+            const data = Array.from({ length: end - start + 1 }, (_, k) => k + start);
+            newTabs.push({
+                title: `${start}~${end}`,
+                value: i,
+                data
+            });
+        }
+        setTabs(newTabs);
+        if (newTabs.length > 0) {
+            setActiveTab(newTabs[0]);
+            loadData(newTabs[0], boxItem.number);
+        }
+      },
+      close: () => {
+        setVisible(false);
+      },
+    }));
+
+    const loadData = async (tab: any, boxNum: string) => {
+        if (!tab || !boxNum) return;
+        
+        try {
+            const start = tab.data[0];
+            const end = tab.data[tab.data.length - 1];
+            const res = await getUnavailableSeatNumbers(poolId, boxNum, start, end);
+            
+            if (res) {
+                // Determine used (sold) and locked seats
+                const newUseMap: Record<number, string> = { ...useMap }; // Merge? Or reset per tab? Legacy merges logic via object assignment but here standard React state update
+                // Legacy clears map if not careful? No, it sets properties.
+                // Let's reset for the current range to avoid stale data visually if we revisit, but merging is safer for 'global' knowledge. 
+                // Efficiency-wise, let's keep it simple.
+                
+                if (res.usedSeatNumbers) {
+                    res.usedSeatNumbers.forEach((item: any) => {
+                        newUseMap[item.seatNumber] = item.level;
+                    });
+                }
+                 
+                const newLockMap: Record<number, number> = { ...lockMap };
+                if (res.applyedSeatNumbers) {
+                    res.applyedSeatNumbers.forEach((num: number) => {
+                        newLockMap[num] = num;
+                    });
+                }
+                
+                setUseMap(newUseMap);
+                setLockMap(newLockMap);
+
+                // Remove selected if they became unavailable
+                const newCheckMap = { ...checkMap };
+                let changed = false;
+                Object.keys(newCheckMap).forEach(key => {
+                    const num = Number(key);
+                    if (newUseMap[num] || newLockMap[num]) {
+                        delete newCheckMap[num];
+                        changed = true;
+                    }
+                });
+                if (changed) setCheckMap(newCheckMap);
+            }
+        } catch (error) {
+            console.error('Failed to load seat info', error);
+        }
+    };
+
+    useEffect(() => {
+        if (activeTab && box) {
+            loadData(activeTab, box.number);
+        }
+    }, [activeTab]);
+
+    const handleTabChange = (tab: any) => {
+        setActiveTab(tab);
+    };
+
+    const toggleSelect = (num: number) => {
+        if (useMap[num] || lockMap[num]) return;
+
+        setCheckMap(prev => {
+            const next = { ...prev };
+            if (next[num]) {
+                delete next[num];
+            } else {
+                if (Object.keys(next).length >= 50) {
+                    Alert.alert('提示', '最多不超过50发');
+                    return prev;
+                }
+                next[num] = num;
+            }
+            return next;
+        });
+    };
+
+    const handleConfirm = async () => {
+        const selectedNums = Object.values(checkMap).map(Number);
+        if (selectedNums.length === 0) {
+            Alert.alert('提示', '请选择号码');
+            return;
+        }
+
+        setLoading(true);
+        try {
+            const res = await previewOrder(poolId, selectedNums.length, box.number, selectedNums);
+            if (res) {
+                 if (res.duplicateSeatNumbers && res.duplicateSeatNumbers.length > 0) {
+                      Alert.alert('提示', `${res.duplicateSeatNumbers.join(',')}号被占用`);
+                      // Remove duplicates
+                      const newCheckMap = { ...checkMap };
+                      res.duplicateSeatNumbers.forEach((n: number) => delete newCheckMap[n]);
+                      setCheckMap(newCheckMap);
+                      // Refresh data
+                      loadData(activeTab, box.number);
+                      return;
+                 }
+                 
+                 // Proceed to pay callback
+                 onPay({ previewRes: res, chooseNum: selectedNums, boxNum: box.number });
+                 setVisible(false);
+            }
+        } catch (error: any) {
+            Alert.alert('错误', error?.message || '请求失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const renderItem = (num: number) => {
+        const isUsed = !!useMap[num];
+        const isLocked = !!lockMap[num];
+        const isSelected = !!checkMap[num];
+        
+        let content = <Text style={[styles.itemText, (isUsed || isLocked) && styles.disabledText]}>{num}号</Text>;
+        let style = [styles.item];
+
+        if (isUsed) {
+            style.push(styles.levelItem as any);
+            const level = useMap[num];
+            content = (
+                <View style={styles.usedContent}>
+                    <Text style={styles.usedNum}>{num}号</Text>
+                    <Text style={[styles.levelText, { color: LEVEL_MAP[level]?.color }]}>
+                        {LEVEL_MAP[level]?.title || level}
+                    </Text>
+                </View>
+            );
+        } else if (isLocked) {
+             style.push(styles.lockedItem as any);
+             content = (
+                <>
+                   <Text style={styles.disabledText}>{num}号</Text>
+                   <View style={styles.lockIcon}>
+                       {/* Icon placeholder or text */}
+                       <Text style={{fontSize: 10, color: '#fff'}}>🔒</Text>
+                   </View>
+                </>
+             );
+        } else if (isSelected) {
+            style.push(styles.selectedItem as any);
+            content = (
+                <>
+                    <Text style={styles.itemText}>{num}号</Text>
+                    <View style={styles.checkIcon}>
+                         <Text style={{fontSize: 8, color: '#F1423D'}}>✓</Text>
+                    </View>
+                </>
+            );
+        }
+
+        return (
+            <TouchableOpacity 
+                key={num} 
+                style={style} 
+                onPress={() => toggleSelect(num)}
+                disabled={isUsed || isLocked}
+            >
+                {content}
+            </TouchableOpacity>
+        );
+    };
+
+    const selectedList = Object.values(checkMap);
+
+    return (
+      <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+         <View style={styles.overlay}>
+             <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+             
+             <ImageBackground 
+                source={{ uri: Images.box.detail.recordBg }} 
+                style={styles.container}
+                resizeMode="stretch"
+            >
+                {/* Header */}
+                <View style={styles.header}>
+                    <Image source={{ uri: Images.box.detail.recordTitleLeft }} style={styles.titleDecor} contentFit="contain" />
+                    <Text style={styles.title}>换盒</Text>
+                    <Image source={{ uri: Images.box.detail.recordTitleRight }} style={styles.titleDecor} contentFit="contain" />
+                    
+                    <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+                         <Text style={styles.closeText}>×</Text>
+                    </TouchableOpacity>
+                </View>
+
+                {/* Tabs for Ranges */}
+                {tabs.length > 1 && (
+                     <View style={styles.tabs}>
+                        {tabs.map(tab => (
+                            <TouchableOpacity 
+                                key={tab.title} 
+                                style={[styles.tab, activeTab?.value === tab.value && styles.activeTab]}
+                                onPress={() => handleTabChange(tab)}
+                            >
+                                <Text style={[styles.tabText, activeTab?.value === tab.value && styles.activeTabText]}>
+                                    {tab.title}
+                                </Text>
+                            </TouchableOpacity>
+                        ))}
+                    </View>
+                )}
+
+                {/* Grid */}
+                <ScrollView contentContainerStyle={styles.grid}>
+                    {activeTab?.data.map((num: number) => renderItem(num))}
+                </ScrollView>
+
+                {/* Bottom Selection Bar */}
+                <View style={styles.bottomSection}>
+                    <ScrollView horizontal style={styles.selectedScroll} contentContainerStyle={styles.selectedContent}>
+                        {selectedList.map((num) => (
+                            <View key={num} style={styles.selectedChip}>
+                                <Text style={styles.selectedChipText}>{num}号</Text>
+                                <TouchableOpacity onPress={() => toggleSelect(Number(num))} style={styles.chipClose}>
+                                    <Text style={styles.chipCloseText}>×</Text>
+                                </TouchableOpacity>
+                            </View>
+                        ))}
+                    </ScrollView>
+
+                    <TouchableOpacity 
+                        style={styles.confirmBtn} 
+                        onPress={handleConfirm}
+                        disabled={loading}
+                    >
+                         <ImageBackground source={{ uri: Images.common.loginBg }} style={styles.confirmBtnBg} resizeMode="stretch">
+                             {loading ? <ActivityIndicator color="#fff" /> : (
+                                 <View style={{ alignItems: 'center' }}>
+                                     <Text style={styles.confirmText}>确定选择</Text>
+                                     <Text style={styles.subConfirmText}>已选择({selectedList.length})发</Text>
+                                 </View>
+                             )}
+                         </ImageBackground>
+                    </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: {
+    height: 600,
+    paddingTop: 15,
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    overflow: 'hidden',
+    backgroundColor: '#fff',
+  },
+  header: {
+      flexDirection: 'row',
+      alignItems: 'center',
+      justifyContent: 'center',
+      marginBottom: 15,
+      position: 'relative',
+      height: 44,
+  },
+  title: {
+      fontSize: 18,
+      fontWeight: 'bold',
+      color: '#fff',
+      marginHorizontal: 10,
+      textShadowColor: '#000',
+      textShadowOffset: { width: 1, height: 1 },
+      textShadowRadius: 1,
+  },
+  titleDecor: {
+      width: 17,
+      height: 19,
+  },
+  closeBtn: {
+      position: 'absolute',
+      right: 15,
+      top: 0,
+      width: 24,
+      height: 24,
+      backgroundColor: '#ebebeb',
+      borderRadius: 12,
+      justifyContent: 'center',
+      alignItems: 'center',
+  },
+  closeText: { fontSize: 18, color: '#a2a2a2', marginTop: -2 },
+  tabs: {
+      flexDirection: 'row',
+      flexWrap: 'wrap',
+      paddingHorizontal: 10,
+      marginBottom: 10,
+  },
+  tab: {
+      paddingVertical: 5,
+      paddingHorizontal: 10,
+      backgroundColor: '#f5f5f5',
+      borderRadius: 4,
+      marginRight: 8,
+      marginBottom: 5,
+  },
+  activeTab: { backgroundColor: '#ffdb4d' },
+  tabText: { fontSize: 12, color: '#666' },
+  activeTabText: { color: '#000', fontWeight: 'bold' },
+  grid: {
+      flexDirection: 'row',
+      flexWrap: 'wrap',
+      paddingHorizontal: 10,
+      paddingBottom: 20,
+  },
+  item: {
+      width: (SCREEN_WIDTH - 20 - 45) / 5, // Approx 5 items per row
+      height: ((SCREEN_WIDTH - 20 - 45) / 5) * 0.5,
+      margin: 4,
+      backgroundColor: '#FFC900',
+      borderWidth: 3,
+      borderColor: '#000',
+      justifyContent: 'center',
+      alignItems: 'center',
+      borderRadius: 0,
+  },
+  itemText: { fontSize: 12, fontWeight: 'bold', color: '#000' },
+  disabledText: { color: 'rgba(255,255,255,0.3)' },
+  levelItem: { backgroundColor: '#e8e8e8', borderColor: '#e8e8e8' },
+  lockedItem: { backgroundColor: 'rgba(98, 99, 115, 0.3)', borderColor: 'transparent', borderWidth: 0 },
+  selectedItem: { backgroundColor: '#FFC900', borderColor: '#000' }, // Same as default but with icon
+  usedContent: { alignItems: 'center' },
+  usedNum: { fontSize: 10, opacity: 0.5 },
+  levelText: { fontSize: 10, fontWeight: 'bold' },
+  lockIcon: { position: 'absolute', bottom: 0, right: 0, backgroundColor: '#000', padding: 2, borderTopLeftRadius: 5 },
+  checkIcon: {
+      position: 'absolute',
+      bottom: 0,
+      right: 0,
+      backgroundColor: '#fff',
+      paddingHorizontal: 4,
+      borderTopLeftRadius: 5,
+  },
+  bottomSection: {
+      paddingBottom: 30,
+      paddingTop: 10,
+      borderTopWidth: 1,
+      borderColor: '#eee',
+      backgroundColor: '#fff',
+  },
+  selectedScroll: { maxHeight: 50, marginBottom: 10 },
+  selectedContent: { paddingHorizontal: 10 },
+  selectedChip: {
+      flexDirection: 'row',
+      alignItems: 'center',
+      backgroundColor: '#FFC900',
+      borderWidth: 2,
+      borderColor: '#000',
+      paddingHorizontal: 8,
+      paddingVertical: 4,
+      borderRadius: 4,
+      marginRight: 8,
+      height: 30,
+  },
+  selectedChipText: { fontSize: 12, fontWeight: 'bold', marginRight: 5 },
+  chipClose: { backgroundColor: 'rgba(255,255,255,0.5)', borderRadius: 10, width: 16, height: 16, alignItems: 'center', justifyContent: 'center' },
+  chipCloseText: { fontSize: 12, lineHeight: 14 },
+  confirmBtn: {
+      alignSelf: 'center',
+      width: 200,
+      height: 50,
+  },
+  confirmBtnBg: {
+      flex: 1,
+      justifyContent: 'center',
+      alignItems: 'center',
+  },
+  confirmText: {
+      color: '#fff',
+      fontSize: 16,
+      fontWeight: 'bold',
+      textShadowColor: '#000',
+      textShadowOffset: { width: 1, height: 1 },
+      textShadowRadius: 1,
+  },
+  subConfirmText: {
+      color: '#fff',
+      fontSize: 10,
+      marginTop: -2,
+  },
+});

+ 217 - 0
components/award-detail-yfs/ProductListYfs.tsx

@@ -0,0 +1,217 @@
+import { LEVEL_MAP } from '@/constants/config';
+import { Images } from '@/constants/images';
+import { Image, ImageBackground } from 'expo-image';
+import React, { useMemo } from 'react';
+import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+const { width } = Dimensions.get('window');
+
+interface ProductListYfsProps {
+  products: any[];
+  poolId: string;
+  box: any;
+  onProductClick?: (product: any) => void;
+}
+
+export default function ProductListYfs({ products = [], poolId, box, onProductClick }: ProductListYfsProps) {
+  
+  const levels = useMemo(() => {
+    console.log('ProductListYfs products:', products ? products.length : 'null');
+    const grouped: Record<string, any[]> = { A: [], B: [], C: [], D: [] };
+    products.forEach(p => {
+        console.log('Processing item level:', p.level);
+        if (grouped[p.level]) {
+            grouped[p.level].push(p);
+        } else {
+            console.log('Unknown level:', p.level);
+        }
+    });
+
+    const result = [
+        { level: 'A', list: grouped.A, ...LEVEL_MAP.A },
+        { level: 'B', list: grouped.B, ...LEVEL_MAP.B },
+        { level: 'C', list: grouped.C, ...LEVEL_MAP.C },
+        { level: 'D', list: grouped.D, ...LEVEL_MAP.D },
+    ].filter(g => g.list && g.list.length > 0);
+
+    console.log('ProductListYfs levels:', result.length);
+    return result;
+  }, [products]);
+
+  const getLeftNum = (item: any) => {
+      if (!box || !box.usedStat || !box.usedStat[item.spu.id]) {
+          return item.quantity;
+      }
+      return item.quantity - (box.usedStat[item.spu.id].quantity || 0);
+  };
+
+  const getProbability = (item: any) => {
+      if (!box || !box.leftQuantity) return '0%';
+      const left = getLeftNum(item);
+      const prob = (left / box.leftQuantity * 100).toFixed(4);
+      return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
+  };
+
+  const getLevelProbability = (level: string) => {
+       if (!box || !box.leftQuantity) return '0%';
+       let sumLeft = 0;
+       products.filter(p => p.level === level).forEach(p => {
+           sumLeft += getLeftNum(p);
+       });
+       const prob = (sumLeft / box.leftQuantity * 100).toFixed(4);
+        return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
+  };
+
+  if (!products || products.length === 0) return null;
+
+  return (
+    <View style={styles.container}>
+      {levels.map((levelItem) => (
+        <View key={levelItem.level} style={styles.levelGroup}>
+             {/* Header with Title and Probability */}
+             <View style={styles.levelHeader}>
+                 <Text style={[styles.levelTitle, { color: levelItem.color }]}>{levelItem.title}</Text>
+                 <Text style={styles.levelProb}>{getLevelProbability(levelItem.level)}</Text>
+                 
+                 {/* The legacy code used borders and backgrounds for titles, but for now text color + shadow mimics the look well enough or we can add ImageBackground if needed. 
+                     Legacy: :style="{ color: LEVEL_MAP[levelItem.level].color }" and centered. 
+                 */}
+             </View>
+             
+             {/* List Container - Legacy uses levelBoxBg for EACH item container? 
+                 Legacy: 
+                 <view class="levelListScroll"> ... <scroll-view ...>
+                 <view ... class="item" :style="{ backgroundImage: 'url(' + ossurl.box.detail.levelBoxBg + ')' }">
+             */}
+             <View style={styles.listWrapper}>
+                <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
+                    {levelItem.list.map((item, index) => (
+                        <TouchableOpacity 
+                            key={index} 
+                            style={styles.itemContainer}
+                            onPress={() => onProductClick && onProductClick(item)}
+                        >
+                            {/* Item Background (levelBoxBg) */}
+                            <ImageBackground 
+                                source={{ uri: Images.box.detail.levelBoxBg }} 
+                                style={styles.itemBg}
+                                resizeMode="stretch"
+                            >
+                                <Image source={{ uri: item.spu.cover }} style={styles.itemImage} contentFit="contain" />
+                                
+                                {/* Info Box (productItemA/B/C/D) */}
+                                <ImageBackground 
+                                    source={{ uri: levelItem.productItem }} 
+                                    style={styles.textBox}
+                                    resizeMode="stretch"
+                                >
+                                    <View style={styles.probTag}>
+                                        <Text style={[styles.probLabel, { color: levelItem.color }]}>概率:</Text>
+                                        <Text style={styles.probValue}>{getProbability(item)}</Text>
+                                    </View>
+                                    <View style={[styles.countTag, { backgroundColor: levelItem.color }]}>
+                                        <Text style={styles.countText}>{getLeftNum(item)}/{item.quantity}</Text>
+                                    </View>
+                                </ImageBackground>
+                            </ImageBackground>
+                        </TouchableOpacity>
+                    ))}
+                </ScrollView>
+             </View>
+        </View>
+      ))}
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+  },
+  levelGroup: {
+    marginBottom: 5,
+  },
+  levelHeader: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginBottom: 5,
+    position: 'relative',
+    height: 40,
+  },
+  levelTitle: {
+    fontSize: 18,
+    fontWeight: 'bold',
+    textShadowColor: '#000',
+    textShadowOffset: { width: 1, height: 1 },
+    textShadowRadius: 1,
+  },
+  levelProb: {
+    position: 'absolute',
+    right: 15,
+    bottom: 5,
+    color: '#fff',
+    fontSize: 12,
+    fontFamily: 'System', // Prevent potential font missing issues
+  },
+  listWrapper: {
+      paddingBottom: 10,
+  },
+  scrollContent: {
+      paddingRight: 10,
+  },
+  itemContainer: {
+    width: 88, // 175rpx / 2
+    height: 110, // 220rpx / 2
+    marginRight: 8,
+  },
+  itemBg: {
+      width: '100%',
+      height: '100%',
+      position: 'relative',
+  },
+  itemImage: {
+      width: 88,
+      height: 90,
+      position: 'absolute',
+      top: 0,
+      left: 0,
+      zIndex: 1,
+  },
+  textBox: {
+      width: '100%',
+      height: 53, // 106rpx / 2
+      position: 'absolute',
+      bottom: 0, // Adjusted from top: -30rpx legacy logic which was weird, simplified for RN
+      zIndex: 2,
+      justifyContent: 'flex-start',
+      paddingTop: 18, // Push content down below the curve
+      alignItems: 'center',
+  },
+  probTag: {
+      flexDirection: 'row',
+      alignItems: 'center',
+  },
+  probLabel: {
+      fontSize: 8,
+      marginRight: 2,
+  },
+  probValue: {
+      fontSize: 8,
+      fontWeight: 'bold',
+      color: '#333'
+  },
+  countTag: {
+      position: 'absolute',
+      top: 0,
+      right: 0,
+      paddingHorizontal: 4,
+      borderRadius: 2,
+      zIndex: 3,
+  },
+  countText: {
+      color: '#fff',
+      fontSize: 9, 
+  }
+});

+ 194 - 0
components/award-detail-yfs/ProductSwiper.tsx

@@ -0,0 +1,194 @@
+import { Image } from 'expo-image';
+import React, { useState } from 'react';
+import {
+    Dimensions,
+    FlatList,
+    ImageBackground,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+
+import { Images } from '@/constants/images';
+
+const { width } = Dimensions.get('window');
+
+interface Product {
+  cover: string;
+  name: string;
+  level: string;
+  quantity?: number;
+}
+
+interface ProductSwiperProps {
+  products: Product[];
+  onShowSwipe?: (index: number) => void;
+}
+
+export default function ProductSwiper({ products, onShowSwipe }: ProductSwiperProps) {
+    const [current, setCurrent] = useState(0);
+
+    const getProbability = (item: Product) => {
+        return "10%"; // Placeholder
+    };
+
+    if (!products || products.length === 0) return null;
+
+    const renderItem = ({ item, index }: { item: Product; index: number }) => (
+        <TouchableOpacity 
+            activeOpacity={0.9} 
+            onPress={() => onShowSwipe?.(index)}
+            style={styles.itemContainer}
+        >
+                <View style={styles.imageBox}>
+                <Image source={{ uri: item.cover }} style={styles.image} contentFit="contain" />
+            </View>
+            
+            {/* Details Button/Tag */}
+            <ImageBackground 
+                source={{ uri: Images.box.detail.detailsBut }} 
+                style={styles.detailsTag}
+                resizeMode="contain"
+            >
+                <View style={styles.detailsContent}>
+                    <Text style={styles.levelText}>
+                        {item.level === 'A' ? '超神款' : 
+                            item.level === 'B' ? '欧皇款' : 
+                            item.level === 'C' ? '隐藏款' : '普通款'}
+                    </Text>
+                    <Text style={styles.probText}>{getProbability(item)}</Text>
+                </View>
+            </ImageBackground>
+
+            {/* Name Tag */}
+            <ImageBackground 
+                source={{ uri: Images.box.detail.nameBg }} 
+                style={styles.nameTag}
+                resizeMode="stretch"
+            >
+                <Text style={styles.nameText} numberOfLines={1}>{item.name}</Text>
+            </ImageBackground>
+        </TouchableOpacity>
+    );
+
+    return (
+        <View style={styles.container}>
+            <FlatList
+                data={products}
+                renderItem={renderItem}
+                horizontal
+                pagingEnabled
+                showsHorizontalScrollIndicator={false}
+                keyExtractor={(_, index) => index.toString()}
+                onMomentumScrollEnd={(e) => {
+                    const contentOffset = e.nativeEvent.contentOffset;
+                    const viewSize = e.nativeEvent.layoutMeasurement;
+                    const pageNum = Math.floor(contentOffset.x / viewSize.width);
+                    setCurrent(pageNum);
+                }}
+            />
+             {/* Left/Right Arrows if needed */}
+             {current > 0 && (
+                 <View style={[styles.arrow, styles.leftArrow]}>
+                     <Image source={{ uri: Images.box.detail.left }} style={styles.arrowImg} contentFit="contain" />
+                 </View>
+             )}
+             {current < products.length - 1 && (
+                 <View style={[styles.arrow, styles.rightArrow]}>
+                      <Image source={{ uri: Images.box.detail.right }} style={styles.arrowImg} contentFit="contain" />
+                 </View>
+             )}
+        </View>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        width: '100%',
+        height: 380, // Adjusted height
+        alignItems: 'center',
+        marginTop: 20,
+    },
+    itemContainer: {
+        width: width, // Full width for paging
+        height: '100%',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    imageBox: {
+        width: '60%',
+        height: '60%',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    image: {
+        width: '100%',
+        height: '100%',
+    },
+    detailsTag: {
+        position: 'absolute',
+        right: 40,
+        bottom: 80,
+        width: 100, 
+        height: 60,
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    detailsContent: {
+        alignItems: 'center',
+        justifyContent: 'center',
+        marginTop: 5,
+    },
+    levelText: {
+        color: '#fff',
+        fontSize: 12,
+        fontWeight: 'bold',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    },
+    probText: {
+        color: '#fff',
+        fontSize: 10,
+        fontWeight: 'bold',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    },
+    nameTag: {
+        position: 'absolute',
+        left: 40,
+        top: 40,
+        width: 40,
+        height: 120,
+        justifyContent: 'center',
+        alignItems: 'center',
+        paddingVertical: 10,
+    },
+    nameText: {
+        color: '#000',
+        fontSize: 12,
+        fontWeight: 'bold',
+        width: 12,
+        textAlign: 'center',
+    },
+    arrow: {
+        position: 'absolute',
+        top: '50%',
+        marginTop: -20,
+        width: 40,
+        height: 40,
+        zIndex: 10,
+    },
+    leftArrow: {
+        left: 10,
+    },
+    rightArrow: {
+        right: 10,
+    },
+    arrowImg: {
+        width: '100%',
+        height: '100%',
+    }
+});

+ 109 - 0
components/award-detail-yfs/PurchaseBar.tsx

@@ -0,0 +1,109 @@
+import React from 'react';
+import {
+    ImageBackground,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+
+import { Images } from '@/constants/images';
+
+// Helper to determine background image based on type
+const getBtnBg = (type: 'one' | 'five' | 'many') => {
+    switch (type) {
+        case 'one': return Images.common.butBgV;
+        case 'five': return Images.common.butBgL;
+        case 'many': return Images.common.butBgH;
+        default: return Images.common.butBgV;
+    }
+};
+
+interface PurchaseBarProps {
+    price: number;
+    specialPrice?: number;
+    specialPriceFive?: number;
+    onBuy: (count: number) => void;
+    onBuyMany: () => void;
+}
+
+export default function PurchaseBar({ price, specialPrice, specialPriceFive, onBuy, onBuyMany }: PurchaseBarProps) {
+    return (
+        <ImageBackground 
+            source={{ uri: Images.box.detail.boxDetailBott }} 
+            style={styles.container}
+            resizeMode="stretch"
+        >
+            <View style={styles.btnRow}>
+                {/* Buy One */}
+                <TouchableOpacity onPress={() => onBuy(1)} style={styles.btnWrapper}>
+                    <ImageBackground source={{ uri: getBtnBg('one') }} style={styles.btnBg} resizeMode="stretch">
+                        <Text style={styles.btnTitle}>购买一盒</Text>
+                        <Text style={styles.btnPrice}>
+                            (¥{specialPrice ?? price})
+                        </Text>
+                    </ImageBackground>
+                </TouchableOpacity>
+
+                {/* Buy Five */}
+                <TouchableOpacity onPress={() => onBuy(5)} style={styles.btnWrapper}>
+                    <ImageBackground source={{ uri: getBtnBg('five') }} style={styles.btnBg} resizeMode="stretch">
+                        <Text style={styles.btnTitle}>购买五盒</Text>
+                        <Text style={styles.btnPrice}>
+                             (¥{specialPriceFive ?? price * 5})
+                        </Text>
+                    </ImageBackground>
+                </TouchableOpacity>
+
+                {/* Buy Many */}
+                <TouchableOpacity onPress={onBuyMany} style={styles.btnWrapper}>
+                    <ImageBackground source={{ uri: getBtnBg('many') }} style={styles.btnBg} resizeMode="stretch">
+                        <Text style={styles.btnTitle}>购买多盒</Text>
+                    </ImageBackground>
+                </TouchableOpacity>
+            </View>
+        </ImageBackground>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        width: '100%',
+        height: 100, // Adjust based on background aspect ratio
+        justifyContent: 'center',
+        paddingBottom: 20, // Safe area padding simulation
+    },
+    btnRow: {
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        alignItems: 'center',
+        paddingHorizontal: 10,
+    },
+    btnWrapper: {
+        flex: 1,
+        height: 50,
+        marginHorizontal: 5,
+    },
+    btnBg: {
+        width: '100%',
+        height: '100%',
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    btnTitle: {
+        color: '#fff',
+        fontSize: 14,
+        fontWeight: 'bold',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    },
+    btnPrice: {
+        color: '#fff',
+        fontSize: 10,
+        fontWeight: 'bold',
+        textShadowColor: '#000',
+        textShadowOffset: { width: 1, height: 1 },
+        textShadowRadius: 1,
+    }
+});

+ 296 - 0
components/award-detail-yfs/RecordModal.tsx

@@ -0,0 +1,296 @@
+import { LEVEL_MAP } from '@/constants/config';
+import { Images } from '@/constants/images';
+import { countRecordsAfterLastLevel, getBuyRecord } from '@/services/award';
+import { Image, ImageBackground } from 'expo-image';
+import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
+import { ActivityIndicator, Dimensions, FlatList, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
+const { width, height } = Dimensions.get('window');
+
+const TABS = [
+    { title: '全部', value: '' },
+    { title: '超神款', value: 'A' },
+    { title: '欧皇款', value: 'B' },
+    { title: '隐藏款', value: 'C' },
+    { title: '普通款', value: 'D' }
+];
+
+interface RecordModalProps {
+    poolId: string;
+}
+
+export interface RecordModalRef {
+    show: () => void;
+    close: () => void;
+}
+
+export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(({ poolId }, ref) => {
+    const [visible, setVisible] = useState(false);
+    const [data, setData] = useState<any[]>([]);
+    const [activeTab, setActiveTab] = useState(TABS[0]);
+    const [loading, setLoading] = useState(false);
+    const [refreshing, setRefreshing] = useState(false);
+    const [lastId, setLastId] = useState<string | undefined>(undefined);
+    const [taggingA, setTaggingA] = useState(0);
+    const [taggingB, setTaggingB] = useState(0);
+
+    useImperativeHandle(ref, () => ({
+        show: () => {
+            setVisible(true);
+            refresh();
+        },
+        close: () => setVisible(false)
+    }));
+
+    const refresh = () => {
+        setLastId(undefined);
+        setData([]);
+        loadData(true);
+        loadStats();
+    };
+
+    const loadStats = async () => {
+        try {
+            const resA = await countRecordsAfterLastLevel({ levelEnumList: ['A'], poolId });
+            setTaggingA(resA.data || 0); // Check API structure: {data: number}
+            const resB = await countRecordsAfterLastLevel({ levelEnumList: ['B'], poolId });
+            setTaggingB(resB.data || 0);
+        } catch (e) {
+            console.error('Failed to load stats', e);
+        }
+    };
+
+    const loadData = async (isRefresh = false) => {
+        if (loading) return;
+        setLoading(true);
+        try {
+           const currentLastId = isRefresh ? undefined : lastId;
+           const res = await getBuyRecord(poolId, currentLastId, activeTab.value as any);
+           if (res && res.length > 0) {
+               setData(prev => isRefresh ? res : [...prev, ...res]);
+               setLastId(res[res.length - 1].id);
+           }
+        } catch (e) {
+            console.error('Failed to load records', e);
+        }
+        setLoading(false);
+        setRefreshing(false);
+    };
+
+    const handleTabChange = (tab: any) => {
+        setActiveTab(tab);
+        // Reset and reload
+        setLastId(undefined);
+        setData([]);
+        // We can't immediately call loadData due to state update async nature, 
+        // but let's assume useEffect or immediate call with arg works.
+        // Actually best to useEffect on activeTab change?
+        // Or specific effect for tab
+        // Let's do explicit reload in effect or here
+        // setData is sync-ish in RN usually batching, let's use effect.
+    };
+
+    useEffect(() => {
+        if (visible) {
+             setLastId(undefined);
+             setData([]);
+             loadData(true);
+        }
+    }, [activeTab]);
+
+    const renderItem = ({ item }: { item: any }) => (
+        <View style={styles.item}>
+            <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
+            <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
+            
+            {/* Level Icon - Assuming we have images for level text similar to legacy */}
+            {/* Legacy: :src="LEVEL_MAP[item.level].titleText" */}
+            {/* We don't have titleText in our config yet, need to map or use text */}
+            {/* For now use Text with color */}
+            <View style={[styles.levelTag, { borderColor: LEVEL_MAP[item.level]?.color || '#000' }]}>
+                 <Text style={[styles.levelText, { color: LEVEL_MAP[item.level]?.color || '#000' }]}>
+                     {LEVEL_MAP[item.level]?.title}
+                 </Text>
+            </View>
+
+            <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
+            <Text style={styles.countText}>×{item.lastCount}</Text>
+            <Image source={{ uri: item.cover }} style={styles.itemImage} />
+        </View>
+    );
+
+    return (
+        <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
+             <View style={styles.overlay}>
+                 <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
+                 <ImageBackground 
+                    source={{ uri: Images.box.detail.recordBg }} 
+                    style={styles.container}
+                    resizeMode="stretch"
+                 >
+                     {/* Header */}
+                     <View style={styles.header}>
+                         <Text style={styles.title}>购买记录</Text>
+                         <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+                             {/* Close Icon or Text */}
+                             <Text style={{ fontSize: 20 }}>×</Text>
+                         </TouchableOpacity>
+                     </View>
+
+                     {/* Stats Banner */}
+                     <ImageBackground 
+                        source={{ uri: Images.box.detail.taggingBg }} 
+                        style={styles.statsBanner}
+                        resizeMode="stretch"
+                     >
+                         <View style={styles.statItem}>
+                             <Text style={styles.statNum}>{taggingA}</Text>
+                             <Text style={styles.statLabel}>发未出</Text>
+                             <Text style={[styles.statLevel, { color: LEVEL_MAP.A.color }]}>超神款</Text>
+                         </View>
+                         <View style={styles.statItem}>
+                             <Text style={styles.statNum}>{taggingB}</Text>
+                             <Text style={styles.statLabel}>发未出</Text>
+                             <Text style={[styles.statLevel, { color: LEVEL_MAP.B.color }]}>欧皇款</Text>
+                         </View>
+                     </ImageBackground>
+
+                     {/* Tabs */}
+                     <View style={styles.tabs}>
+                         {TABS.map(tab => (
+                             <TouchableOpacity 
+                                key={tab.value} 
+                                style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
+                                onPress={() => handleTabChange(tab)}
+                             >
+                                 <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
+                                     {tab.title}
+                                 </Text>
+                             </TouchableOpacity>
+                         ))}
+                     </View>
+
+                     {/* List */}
+                     <FlatList
+                        data={data}
+                        renderItem={renderItem}
+                        keyExtractor={(item, index) => item.id || String(index)}
+                        style={styles.list}
+                        contentContainerStyle={{ paddingBottom: 20 }}
+                        onEndReached={() => loadData(false)}
+                        onEndReachedThreshold={0.5}
+                        ListEmptyComponent={
+                            !loading ? <Text style={styles.emptyText}>暂无记录</Text> : null
+                        }
+                        ListFooterComponent={loading ? <ActivityIndicator color="#000" /> : null}
+                     />
+
+                 </ImageBackground>
+             </View>
+        </Modal>
+    );
+});
+
+const styles = StyleSheet.create({
+    overlay: {
+        flex: 1,
+        backgroundColor: 'rgba(0,0,0,0.5)',
+        justifyContent: 'flex-end',
+    },
+    mask: {
+        flex: 1,
+    },
+    container: {
+        height: height * 0.7,
+        paddingTop: 0,
+        backgroundColor: '#fff', 
+        borderTopLeftRadius: 15,
+        borderTopRightRadius: 15,
+        overflow: 'hidden',
+    },
+    header: {
+        height: 50,
+        justifyContent: 'center',
+        alignItems: 'center',
+        marginTop: 10,
+    },
+    title: {
+        fontSize: 18,
+        fontWeight: 'bold',
+    },
+    closeBtn: {
+        position: 'absolute',
+        right: 20,
+        top: 15,
+        width: 30,
+        height: 30,
+        alignItems: 'center',
+        justifyContent: 'center', 
+        backgroundColor: '#eee',
+        borderRadius: 15,
+    },
+    statsBanner: {
+        width: '90%',
+        alignSelf: 'center',
+        height: 50,
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        alignItems: 'center',
+        marginBottom: 10,
+    },
+    statItem: {
+        flexDirection: 'row',
+        alignItems: 'center',
+    },
+    statNum: { fontSize: 16, fontWeight: 'bold', color: '#fff', textShadowColor:'#000', textShadowRadius:1 },
+    statLabel: { fontSize: 12, color: '#ddd', marginHorizontal: 5 },
+    statLevel: { fontSize: 12, fontWeight: 'bold' },
+    
+    tabs: {
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        paddingHorizontal: 10,
+        marginBottom: 10,
+        backgroundColor: 'rgba(255,255,255,0.1)',
+        paddingVertical: 5,
+    },
+    tab: {
+        paddingVertical: 5,
+        paddingHorizontal: 10,
+        borderRadius: 5,
+        backgroundColor: '#eee',
+    },
+    activeTab: {
+        backgroundColor: '#FEC433',
+    },
+    tabText: { fontSize: 12, color: '#666' },
+    activeTabText: { color: '#000', fontWeight: 'bold' },
+
+    list: {
+        flex: 1,
+        paddingHorizontal: 15,
+    },
+    item: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        backgroundColor: '#FFF7E3',
+        marginBottom: 10,
+        padding: 5,
+        borderRadius: 8,
+        height: 60,
+    },
+    avatar: { width: 40, height: 40, borderRadius: 20 },
+    nickname: { flex: 1, marginLeft: 10, fontSize: 12, color: '#333' },
+    levelTag: { 
+        borderWidth: 1, 
+        paddingHorizontal: 4, 
+        borderRadius: 4, 
+        marginHorizontal: 5
+    },
+    levelText: { fontSize: 10, fontWeight: 'bold' },
+    itemName: { width: 80, fontSize: 12, color: '#FEC433', marginHorizontal: 5 },
+    countText: { fontSize: 12, color: '#000', fontWeight: 'bold', marginRight: 5 },
+    itemImage: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#fff' },
+    emptyText: { textAlign: 'center', marginTop: 20, color: '#999' }
+
+});

+ 131 - 0
components/award-detail-yfs/RuleModal.tsx

@@ -0,0 +1,131 @@
+import { Images } from '@/constants/images';
+import { getParamConfig } from '@/services/user';
+import { Image, ImageBackground } from 'expo-image';
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { ActivityIndicator, Dimensions, Modal, StyleSheet, TouchableOpacity, View } from 'react-native';
+import { WebView } from 'react-native-webview';
+
+const { width } = Dimensions.get('window');
+
+export interface RuleModalRef {
+  show: (ruleKey?: string) => void;
+  close: () => void;
+}
+
+export const RuleModal = forwardRef<RuleModalRef, {}>((props, ref) => {
+  const [visible, setVisible] = useState(false);
+  const [content, setContent] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  useImperativeHandle(ref, () => ({
+    show: (ruleKey = 'baoxiang_rule') => {
+      setVisible(true);
+      loadRule(ruleKey);
+    },
+    close: () => setVisible(false),
+  }));
+
+  const loadRule = async (key: string) => {
+    setLoading(true);
+    try {
+      const res = await getParamConfig(key);
+      if (res && res.data) {
+        // Simple HTML wrap
+        const html = `
+          <html>
+            <head>
+              <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+              <style>
+                body { font-size: 14px; color: #333; padding: 10px; word-wrap: break-word; }
+                img { max-width: 100%; height: auto; }
+                p { margin: 5px 0; }
+              </style>
+            </head>
+            <body>${res.data}</body>
+          </html>
+        `;
+        setContent(html);
+      }
+    } catch (e) {
+      console.error(e);
+      setContent('<p>加载失败</p>');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
+       <View style={styles.overlay}>
+          <View style={styles.container}>
+             <View style={styles.header}>
+                 <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
+                      <Image source={{ uri: Images.common.closeBut }} style={styles.closeIcon} />
+                 </TouchableOpacity>
+             </View>
+             
+             <ImageBackground 
+                source={{ uri: Images.mine.dialogContentBg }} 
+                style={styles.bg}
+                resizeMode="stretch"
+             >
+                 <View style={styles.content}>
+                     {loading ? (
+                         <ActivityIndicator color="#000" style={{ marginTop: 50 }} />
+                     ) : (
+                         <WebView 
+                            originWhitelist={['*']}
+                            source={{ html: content }}
+                            style={styles.webview}
+                            showsVerticalScrollIndicator={false}
+                         />
+                     )}
+                 </View>
+             </ImageBackground>
+          </View>
+       </View>
+    </Modal>
+  );
+});
+
+const styles = StyleSheet.create({
+  overlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0,0,0,0.6)',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  container: {
+    width: width * 0.9,
+    alignItems: 'center',
+  },
+  header: {
+    width: '100%',
+    alignItems: 'flex-end',
+    marginBottom: -20, // Overlap
+    zIndex: 10,
+    paddingRight: 20,
+  },
+  closeBtn: {
+      padding: 10,
+  },
+  closeIcon: {
+      width: 30,
+      height: 30,
+  },
+  bg: {
+      width: '100%',
+      height: 400, // Adjust height
+      paddingTop: 60,
+      paddingHorizontal: 30,
+      paddingBottom: 30,
+  },
+  content: {
+      flex: 1,
+      overflow: 'hidden',
+  },
+  webview: {
+      flex: 1,
+      backgroundColor: 'transparent',
+  }
+});

+ 28 - 0
constants/config.ts

@@ -0,0 +1,28 @@
+import { Images } from './images';
+
+export const LEVEL_MAP: Record<string, { title: string; color: string; productItem: string; bg: string }> = {
+  A: {
+    title: '超神款',
+    color: '#E82C31',
+    productItem: Images.box.detail.productItemA,
+    bg: Images.box.detail.levelA,
+  },
+  B: {
+    title: '欧皇款',
+    color: '#FF9500',
+    productItem: Images.box.detail.productItemB,
+    bg: Images.box.detail.levelB,
+  },
+  C: {
+    title: '隐藏款',
+    color: '#8E44AD',
+    productItem: Images.box.detail.productItemC,
+    bg: Images.box.detail.levelC,
+  },
+  D: {
+    title: '普通款',
+    color: '#34495E', // Using a dark grey/blue as fallback if not strictly defined in legacy view, legacy had #666
+    productItem: Images.box.detail.productItemD,
+    bg: Images.box.detail.levelD,
+  },
+};

+ 1 - 0
constants/images.ts

@@ -143,6 +143,7 @@ export const Images = {
       levelTextB: `${CDN_BASE}/box/detail/levelTextB.png`,
       levelTextC: `${CDN_BASE}/box/detail/levelTextC.png`,
       levelTextD: `${CDN_BASE}/box/detail/levelTextD.png`,
+      playMethod: `${CDN_BASE}/box/detail/playMethod.png`,
     },
   },
   welfare: {

+ 12 - 0
services/award.ts

@@ -52,8 +52,19 @@ const apis = {
   OPEN_BOX: '/api/luck/treasure-box/open',
   HARRY_EXCHANGE: '/api/redeemCode/exchange',
   WIN_RECORDS: '/api/luck/treasure-box/win-records',
+  COUNT_RECORDS: '/api/luckPool/countRecordsAfterLastLevel',
 };
 
+// ... (existing code)
+
+// 获取最后一次出奖后的记录数
+export const countRecordsAfterLastLevel = async (params: { levelEnumList: string[], poolId: string }) => {
+  const res = await post<number>(apis.COUNT_RECORDS, params);
+  return res;
+};
+
+
+
 export interface IPItem {
   id: string;
   name: string;
@@ -470,4 +481,5 @@ export default {
   getSumInventory,
   harryExchange,
   getWinRecords,
+  countRecordsAfterLastLevel,
 };