ソースを参照

个人首页-优惠券-仓库-果实等页面

zbb 3 ヶ月 前
コミット
2090e9f9f4
9 ファイル変更1014 行追加19 行削除
  1. 4 1
      .gitignore
  2. 73 12
      app/(tabs)/mine.tsx
  3. 3 0
      app/_layout.tsx
  4. 252 0
      app/coupon/index.tsx
  5. 315 0
      app/magic/index.tsx
  6. 342 0
      app/store/index.tsx
  7. 9 0
      constants/images.ts
  8. 6 6
      services/award.ts
  9. 10 0
      services/wallet.ts

+ 4 - 1
.gitignore

@@ -57,4 +57,7 @@ yarn-error.log*
 # The following patterns were generated by expo-cli
 
 expo-env.d.ts
-# @end expo-cli
+# @end expo-cli
+
+package.json
+package-lock.json

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

@@ -3,14 +3,14 @@ import { Image } from 'expo-image';
 import { useFocusEffect, useRouter } from 'expo-router';
 import React, { useCallback, useState } from 'react';
 import {
-    Alert,
-    ImageBackground,
-    ScrollView,
-    StatusBar,
-    StyleSheet,
-    Text,
-    TouchableOpacity,
-    View,
+  Alert,
+  ImageBackground,
+  ScrollView,
+  StatusBar,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
@@ -129,9 +129,70 @@ export default function MineScreen() {
 
   const showNumber = (key: keyof IndexData) => {
     if (!indexData) return '-';
-    const val = indexData[key];
-    if (typeof val === 'undefined') return '-';
-    return String(val);
+    // Loose check for undefined to match legacy
+    if (typeof indexData[key] === 'undefined') return '-';
+    return bigNumberTransform(indexData[key]!);
+  };
+
+  const bigNumberTransform = (value: number) => {
+    const newValue = ['', '', ''];
+    let fr = 1000;
+    let num = 3;
+    let text1 = '';
+    let fm = 1;
+
+    // Determine magnitude
+    let tempValue = value;
+    while (tempValue / fr >= 1) {
+      fr *= 10;
+      num += 1;
+    }
+
+    if (num <= 4) {
+      // 千 (Thousand)
+      newValue[0] = parseInt(String(value / 1000)) + '';
+      newValue[1] = '千';
+    } else if (num <= 8) {
+      // 万 (Ten Thousand)
+      text1 = (num - 4) / 3 > 1 ? '千万' : '万';
+      fm = text1 === '万' ? 10000 : 10000000;
+      if (value % fm === 0) {
+        newValue[0] = parseInt(String(value / fm)) + '';
+      } else {
+        newValue[0] = String(Math.floor((value / fm) * 10) / 10);
+      }
+      newValue[1] = text1;
+    } else if (num <= 16) {
+      // 亿 (Hundred Million)
+      text1 = (num - 8) / 3 > 1 ? '千亿' : '亿';
+      text1 = (num - 8) / 4 > 1 ? '万亿' : text1;
+      text1 = (num - 8) / 7 > 1 ? '千万亿' : text1;
+
+      fm = 1;
+      if (text1 === '亿') {
+        fm = 100000000;
+      } else if (text1 === '千亿') {
+        fm = 100000000000;
+      } else if (text1 === '万亿') {
+        fm = 1000000000000;
+      } else if (text1 === '千万亿') {
+        fm = 1000000000000000;
+      }
+
+      if (value % fm === 0) {
+        newValue[0] = parseInt(String(value / fm)) + '';
+      } else {
+        newValue[0] = String(Math.floor((value / fm) * 10) / 10);
+      }
+      newValue[1] = text1;
+    }
+
+    if (value < 1000) {
+      newValue[0] = String(value);
+      newValue[1] = '';
+    }
+
+    return newValue.join('');
   };
 
   return (
@@ -234,7 +295,7 @@ export default function MineScreen() {
               <Text style={styles.dataNum}>{showNumber('magicBalance')}</Text>
               <Text style={styles.dataLabel}>果实</Text>
             </TouchableOpacity>
-            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/box-list')}>
+            <TouchableOpacity style={styles.dataItem} onPress={() => handleMenuPress('/boxInBox/boxList')}>
               <Text style={styles.dataNum}>{showNumber('treasureBoxCount')}</Text>
               <Text style={styles.dataLabel}>宝箱</Text>
             </TouchableOpacity>

+ 3 - 0
app/_layout.tsx

@@ -28,6 +28,9 @@ export default function RootLayout() {
             <Stack.Screen name="award-detail-yfs" options={{ headerShown: false }} />
             <Stack.Screen name="boxInBox" options={{ headerShown: false }} />
             <Stack.Screen name="weal" options={{ headerShown: false }} />
+            <Stack.Screen name="coupon" options={{ headerShown: false }} />
+            <Stack.Screen name="store" options={{ headerShown: false }} />
+            <Stack.Screen name="magic" options={{ headerShown: false }} />
             <Stack.Screen name="test" options={{ headerShown: false }} />
             <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
           </Stack>

+ 252 - 0
app/coupon/index.tsx

@@ -0,0 +1,252 @@
+
+import { Images } from '@/constants/images';
+import ServiceWallet from '@/services/wallet';
+import { Ionicons } from '@expo/vector-icons';
+import { Stack, useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    Dimensions,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+interface CouponItem {
+    id: string;
+    name: string;
+    amount: number;
+    fullAmount: number; // For "Man X Keyong"
+    status: number; // 1: Valid
+    endTime: string;
+    scene?: string; // LUCK, MALL, TRADE
+}
+
+export default function CouponScreen() {
+    const router = useRouter();
+    const insets = useSafeAreaInsets();
+    const [list, setList] = useState<CouponItem[]>([]);
+    const [loading, setLoading] = useState(false);
+
+    useEffect(() => {
+        loadData();
+    }, []);
+
+    const loadData = async () => {
+        try {
+            setLoading(true);
+            const res = await ServiceWallet.coupons();
+            // Ensure res is array or fetch from res.records if paginated
+            const data = Array.isArray(res) ? res : (res?.records || []);
+            setList(data);
+        } catch (e) {
+            console.error(e);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const handleUse = (item: CouponItem) => {
+        if (item.scene === 'LUCK') {
+            // Go to Box/Award
+            router.push('/box' as any); // Adapt route as needed
+        } else if (item.scene === 'MALL') {
+            // Go to Mall/VIP usually
+            router.push('/(tabs)' as any);
+        } else {
+            // Default
+            router.push('/(tabs)' as any);
+        }
+    };
+
+    const formatTime = (time: string) => {
+        return time ? time.slice(0, 10) : '';
+    };
+
+    return (
+        <View style={styles.container}>
+            <Stack.Screen options={{ headerShown: false }} />
+            <StatusBar barStyle="light-content" />
+            <View style={[styles.header, { paddingTop: insets.top }]}>
+                <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+                    <Ionicons name="chevron-back" size={24} color="#fff" />
+                </TouchableOpacity>
+                <Text style={styles.title}>优惠券</Text>
+            </View>
+
+            <ImageBackground
+                source={{ uri: Images.common.commonBg }}
+                style={styles.background}
+                resizeMode="cover"
+            >
+                <ScrollView
+                    style={styles.scrollView}
+                    contentContainerStyle={{ paddingTop: insets.top + 50, paddingBottom: 50, paddingHorizontal: 10 }}
+                >
+                    {list.length > 0 ? (
+                        list.map((item, index) => (
+                            <ImageBackground
+                                key={index}
+                                source={{ uri: Images.mine.couponBg }}
+                                style={[styles.couponItem, item.status !== 1 && styles.grayscale]}
+                                resizeMode="stretch"
+                            >
+                                <View style={styles.left}>
+                                    <View style={styles.amountBox}>
+                                        <Text style={styles.symbol}>¥</Text>
+                                        <Text style={styles.amount}>{item.amount}</Text>
+                                    </View>
+                                    <Text style={styles.fullAmount}>满{item.fullAmount}可用</Text>
+                                </View>
+
+                                <View style={styles.divider} />
+
+                                <View style={styles.right}>
+                                    <View style={styles.info}>
+                                        <Text style={styles.name}>{item.name}</Text>
+                                        <Text style={styles.time}>{formatTime(item.endTime)}过期</Text>
+                                    </View>
+                                    <TouchableOpacity onPress={() => handleUse(item)}>
+                                        <ImageBackground
+                                            source={{ uri: Images.mine.couponItemButBg }}
+                                            style={styles.useBtn}
+                                            resizeMode="contain"
+                                        >
+                                            <Text style={styles.useText}>立即使用</Text>
+                                        </ImageBackground>
+                                    </TouchableOpacity>
+                                </View>
+                            </ImageBackground>
+                        ))
+                    ) : (
+                        <View style={styles.emptyBox}>
+                            <Text style={styles.emptyText}>暂无优惠券</Text>
+                        </View>
+                    )}
+                </ScrollView>
+            </ImageBackground>
+        </View>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        flex: 1,
+        backgroundColor: '#1a1a2e',
+    },
+    header: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        right: 0,
+        zIndex: 100,
+        alignItems: 'center',
+        paddingBottom: 10,
+    },
+    backBtn: {
+        position: 'absolute',
+        left: 10,
+        bottom: 10,
+        zIndex: 101,
+    },
+    title: {
+        color: '#fff',
+        fontSize: 16,
+        fontWeight: 'bold',
+    },
+    background: {
+        flex: 1,
+        width: '100%',
+        height: '100%',
+    },
+    scrollView: {
+        flex: 1,
+    },
+    couponItem: {
+        width: '100%',
+        height: 92, // 184rpx / 2 = 92
+        flexDirection: 'row',
+        alignItems: 'center',
+        marginBottom: 10,
+        paddingHorizontal: 15, // Adjust padding based on image visual
+    },
+    grayscale: {
+        opacity: 0.6,
+        // React Native doesn't support grayscale prop directly on ImageBackground without filter or tintColor tricks
+        // but opactity is a good enough approximation for invalid state
+    },
+    left: {
+        width: 80,
+        alignItems: 'center',
+        justifyContent: 'center',
+    },
+    amountBox: {
+        flexDirection: 'row',
+        alignItems: 'baseline',
+    },
+    symbol: {
+        fontSize: 12,
+        color: '#404040',
+    },
+    amount: {
+        fontSize: 30,
+        fontWeight: 'bold',
+        color: '#404040',
+    },
+    fullAmount: {
+        fontSize: 12,
+        color: '#8F8F8F',
+        marginTop: 2,
+    },
+    divider: {
+        width: 1,
+        height: 24,
+        backgroundColor: '#C3B3DF',
+        marginHorizontal: 16,
+        opacity: 0, // Hidden in layout if image has it, just spacing
+    },
+    right: {
+        flex: 1,
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        alignItems: 'center',
+        paddingLeft: 10,
+    },
+    info: {
+        justifyContent: 'center',
+    },
+    name: {
+        fontSize: 15,
+        fontWeight: 'bold',
+        color: '#404040',
+        marginBottom: 5,
+    },
+    time: {
+        fontSize: 12,
+        color: '#8F8F8F',
+    },
+    useBtn: {
+        width: 87, // 174rpx / 2
+        height: 37, // 74rpx / 2
+        justifyContent: 'center',
+        alignItems: 'center',
+    },
+    useText: {
+        color: '#fff',
+        fontSize: 12,
+    },
+    emptyBox: {
+        marginTop: 100,
+        alignItems: 'center',
+    },
+    emptyText: {
+        color: '#999',
+        fontSize: 14,
+    },
+});

+ 315 - 0
app/magic/index.tsx

@@ -0,0 +1,315 @@
+import { Images } from '@/constants/images';
+import ServiceWallet from '@/services/wallet';
+import { Ionicons } from '@expo/vector-icons';
+import { Stack, useRouter } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+    ActivityIndicator,
+    Dimensions,
+    ImageBackground,
+    ScrollView,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+const TABS = [
+    { title: '全部类型', value: '' },
+    { title: '收入', value: 'IN' },
+    { title: '支出', value: 'OUT' },
+];
+
+export default function MagicScreen() {
+    const router = useRouter();
+    const insets = useSafeAreaInsets();
+    const [data, setData] = useState<any>(null); // Balance info
+    const [list, setList] = useState<any[]>([]);
+    const [loading, setLoading] = useState(false);
+    const [tabIndex, setTabIndex] = useState(0);
+
+    useEffect(() => {
+        loadInfo();
+        loadList();
+    }, [tabIndex]);
+
+    const loadInfo = async () => {
+        try {
+            const res = await ServiceWallet.info('MAGIC');
+            setData(res);
+        } catch (e) {
+            console.error(e);
+        }
+    };
+
+    const loadList = async () => {
+        try {
+            setLoading(true);
+            // bill(current, size, walletType, type)
+            const res = await ServiceWallet.bill(1, 100, 'MAGIC', TABS[tabIndex].value);
+            const records = Array.isArray(res) ? res : (res?.records || []);
+            setList(records);
+        } catch (e) {
+            console.error(e);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const handleTabChange = () => {
+        const nextIndex = (tabIndex + 1) % TABS.length;
+        setTabIndex(nextIndex);
+    };
+
+    return (
+        <View style={styles.container}>
+            <Stack.Screen options={{ headerShown: false }} />
+            <StatusBar barStyle="light-content" />
+            <View style={[styles.header, { paddingTop: insets.top }]}>
+                <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+                    <Ionicons name="chevron-back" size={24} color="#fff" />
+                </TouchableOpacity>
+                <Text style={styles.title}>果实</Text>
+            </View>
+
+            <ImageBackground
+                source={{ uri: Images.mine.kaixinMineBg }}
+                style={styles.background}
+                resizeMode="cover"
+            >
+                <ImageBackground
+                    source={{ uri: Images.mine.kaixinMineHeadBg }}
+                    style={styles.headerBg}
+                    resizeMode="cover"
+                />
+
+                <ScrollView
+                    style={styles.scrollView}
+                    contentContainerStyle={{ paddingTop: insets.top + 50, paddingBottom: 50 }}
+                >
+                    {/* Stats Box - visually mimicking the legacy StoneBox */}
+                    <View style={styles.stoneBox}>
+                        <View style={styles.infoRow}>
+                            <View style={styles.infoCol}>
+                                <Text style={styles.num}>{data?.balance || '0'}</Text>
+                                <Text style={styles.label}>可用</Text>
+                            </View>
+                            <View style={styles.infoCol}>
+                                <Text style={styles.num}>{data?.frozen || '0'}</Text>
+                                <Text style={styles.label}>冻结</Text>
+                            </View>
+                        </View>
+                    </View>
+
+                    <View style={styles.listSection}>
+                        <ImageBackground
+                            source={{ uri: Images.mine.stoneImage }}
+                            style={styles.listTitleBg}
+                            resizeMode="stretch"
+                        >
+                            <Text style={styles.listTitleText}>果实明细</Text>
+                            <TouchableOpacity style={styles.pickerBox} onPress={handleTabChange}>
+                                <ImageBackground
+                                    source={{ uri: Images.mine.magicTypeBg }}
+                                    style={styles.pickerBg}
+                                    resizeMode="stretch"
+                                >
+                                    <Text style={styles.pickerText}>{TABS[tabIndex].title}</Text>
+                                    <Ionicons name="caret-down" size={12} color="#000" style={{ marginLeft: 4 }} />
+                                </ImageBackground>
+                            </TouchableOpacity>
+                        </ImageBackground>
+
+                        <View style={styles.listContainer}>
+                            {loading && list.length === 0 ? (
+                                <ActivityIndicator color="#fff" style={{ marginTop: 20 }} />
+                            ) : list.length > 0 ? (
+                                list.map((item, index) => (
+                                    <View key={index} style={styles.cell}>
+                                        <View style={styles.cellRow}>
+                                            <Text style={styles.cellDesc}>{item.itemDesc}</Text>
+                                            <Text style={[styles.cellAmount, { color: item.type === 'IN' ? '#ff0000' : '#000' }]}>
+                                                {item.type === 'IN' ? '+' : '-'}{item.money}
+                                            </Text>
+                                        </View>
+                                        <Text style={styles.cellTime}>{item.createTime}</Text>
+                                    </View>
+                                ))
+                            ) : (
+                                <View style={styles.emptyBox}>
+                                    <Text style={styles.emptyText}>暂无记录</Text>
+                                </View>
+                            )}
+                        </View>
+                    </View>
+                </ScrollView>
+            </ImageBackground>
+        </View>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        flex: 1,
+        backgroundColor: '#1a1a2e',
+    },
+    header: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        right: 0,
+        zIndex: 100,
+        alignItems: 'center',
+        paddingBottom: 10,
+    },
+    backBtn: {
+        position: 'absolute',
+        left: 10,
+        bottom: 10,
+        zIndex: 101,
+    },
+    title: {
+        color: '#fff',
+        fontSize: 16,
+        fontWeight: 'bold',
+    },
+    background: {
+        flex: 1,
+        width: '100%',
+        height: '100%',
+    },
+    headerBg: {
+        position: 'absolute',
+        width: '100%',
+        height: 240, // 480rpx / 2 = 240
+        top: 0,
+        left: 0,
+    },
+    scrollView: {
+        flex: 1,
+    },
+    stoneBox: {
+        width: 310, // 620rpx / 2
+        height: 100, // 200rpx / 2
+        marginHorizontal: 'auto',
+        alignSelf: 'center',
+        justifyContent: 'center',
+        // No background image as per legacy analysis, relies on transparency over headerBg?
+        // Wait, legacy publicHeaderBg height is 480rpx (240px). StoneBox is inside wrapper padding 200rpx top.
+        // So it sits on top of headerBg.
+    },
+    infoRow: {
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        alignItems: 'center',
+    },
+    infoCol: {
+        alignItems: 'center',
+    },
+    num: {
+        fontSize: 22,
+        fontWeight: 'bold',
+        color: '#fff',
+    },
+    label: {
+        fontSize: 14,
+        color: '#fff',
+        marginTop: 4,
+    },
+    listSection: {
+        paddingHorizontal: 16,
+        marginTop: 10,
+    },
+    listTitleBg: {
+        width: '100%',
+        height: 63, // 126rpx / 2
+        justifyContent: 'center',
+        alignItems: 'center',
+        flexDirection: 'row',
+        position: 'relative',
+        zIndex: 10,
+    },
+    listTitleText: {
+        fontSize: 16,
+        fontWeight: 'bold',
+        color: '#333',
+    },
+    pickerBox: {
+        position: 'absolute',
+        right: 13,
+        top: 20, // Adjust vertically
+    },
+    pickerBg: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        paddingHorizontal: 10,
+        paddingVertical: 5,
+        minWidth: 60,
+        justifyContent: 'center',
+    },
+    pickerText: {
+        fontSize: 12,
+        color: 'rgba(0,0,0,0.85)',
+    },
+    listContainer: {
+        backgroundColor: '#fff', // Legacy wrapper background is transparent, list items have no bg?
+        // Legacy list items have border-bottom.
+        // Wait, legacy wrapper has background-size: 100% 100%, likely kaixinMineBg.
+        // Cells are transparent?
+        // Legacy: .cell border-bottom: 2rpx solid #A2A2A2.
+        // Font color #000.
+        // So list should probably be on a white-ish background or transparent?
+        // Looking at screenshot or legacy, if text is black, background must be light.
+        // But kaixinMineBg is usually dark/blue.
+        // Ah, kaixinMineBg (new images) might be different.
+        // Let's assume transparent cell on whatever background.
+        // But text color is #000 in legacy css!
+        // Maybe headerBg covers the whole page? No.
+        // I'll add a white background to list container to be safe, or check detailed legacy logic.
+        // Legacy wrapper min-height 100vh.
+        // I'll stick to transparency but maybe change text color if needed.
+        // Legacy text: color: #000.
+        marginTop: -10, // Overlap slightly or just separate
+        paddingTop: 10,
+        paddingHorizontal: 10,
+        paddingBottom: 20,
+        borderRadius: 8,
+        backgroundColor: '#fff', // Safest bet for "black text" readability
+        minHeight: 300,
+    },
+    cell: {
+        borderBottomWidth: 1,
+        borderBottomColor: '#eee',
+        paddingVertical: 12,
+    },
+    cellRow: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        marginBottom: 6,
+    },
+    cellDesc: {
+        fontSize: 14,
+        fontWeight: 'bold',
+        color: '#000',
+        flex: 1,
+    },
+    cellAmount: {
+        fontSize: 14,
+        fontWeight: 'bold',
+    },
+    cellTime: {
+        fontSize: 12,
+        color: '#666',
+    },
+    emptyBox: {
+        alignItems: 'center',
+        marginTop: 40,
+    },
+    emptyText: {
+        color: '#999',
+    },
+});

+ 342 - 0
app/store/index.tsx

@@ -0,0 +1,342 @@
+import { Images } from '@/constants/images';
+import ServiceAward from '@/services/award';
+import { Ionicons } from '@expo/vector-icons';
+import { Stack, useRouter } from 'expo-router';
+import React, { useState } from 'react';
+import {
+    ActivityIndicator,
+    Dimensions,
+    FlatList,
+    Image,
+    ImageBackground,
+    StatusBar,
+    StyleSheet,
+    Text,
+    TouchableOpacity,
+    View,
+} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+
+const LEVEL_MAP: any = {
+    D: { title: '普通', color: '#666666' },
+    C: { title: '隐藏', color: '#9745e6' },
+    B: { title: '欧皇', color: '#ff0000' },
+    A: { title: '超神', color: '#ffae00' },
+};
+
+export default function StoreScreen() {
+    const router = useRouter();
+    const insets = useSafeAreaInsets();
+    const [list, setList] = useState<any[]>([]);
+    const [loading, setLoading] = useState(false);
+    const [tabIndex, setTabIndex] = useState(0);
+    const [page, setPage] = useState(1);
+    const [hasMore, setHasMore] = useState(true);
+
+    const tabs = ['未使用', '保险柜', '已提货'];
+
+    React.useEffect(() => {
+        setPage(1);
+        setList([]);
+        setHasMore(true);
+        loadData(1);
+    }, [tabIndex]);
+
+    const loadData = async (pageNum: number) => {
+        if (!hasMore && pageNum > 1) return;
+
+        try {
+            if (pageNum === 1) setLoading(true);
+
+            let res;
+            if (tabIndex === 0) {
+                // Not in safe, unused (status=0)
+                res = await ServiceAward.getStore(pageNum, 20, 0); // Use smaller size for pagination demo
+            } else if (tabIndex === 1) {
+                // In safe, unused (status=0)
+                res = await ServiceAward.getStore(pageNum, 20, 1);
+            } else {
+                // Picked up
+                res = await ServiceAward.getTakeList(pageNum, 20);
+            }
+
+            const records = Array.isArray(res) ? res : (res?.records || []);
+
+            if (records.length < 20) {
+                setHasMore(false);
+            }
+
+            if (pageNum === 1) {
+                setList(records);
+            } else {
+                setList(prev => [...prev, ...records]);
+            }
+        } catch (e) {
+            console.error(e);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const handleLoadMore = () => {
+        if (!loading && hasMore) {
+            const nextPage = page + 1;
+            setPage(nextPage);
+            loadData(nextPage);
+        }
+    };
+
+    const handleLock = async (item: any) => {
+        // Implement lock/unlock logic if needed
+    };
+
+    const renderItem = ({ item }: { item: any }) => (
+        <ImageBackground
+            source={{ uri: Images.mine.storeItemBg }}
+            style={styles.cell}
+            resizeMode="stretch"
+        >
+            <View style={styles.cellHeader}>
+                <View style={styles.headerLeft}>
+                    <View style={[styles.checkBox]} />
+                    <Text style={[styles.levelTitle, { color: LEVEL_MAP[item.level]?.color || '#333' }]}>
+                        {LEVEL_MAP[item.level]?.title || '未知'}
+                    </Text>
+                </View>
+                <TouchableOpacity style={styles.lockBox} onPress={() => handleLock(item)}>
+                    <Text style={styles.lockText}>{item.safeFlag !== 1 ? '锁定' : '解锁'}</Text>
+                    <Image
+                        source={{ uri: item.safeFlag !== 1 ? Images.mine.lock : Images.mine.unlock }}
+                        style={styles.lockIcon}
+                    />
+                </TouchableOpacity>
+            </View>
+
+            <View style={styles.cellBody}>
+                <ImageBackground
+                    source={{ uri: Images.mine.storeGoodsImgBg }}
+                    style={styles.goodsImgBg}
+                >
+                    <Image source={{ uri: item.spu?.cover }} style={styles.goodsImg} resizeMode="contain" />
+                </ImageBackground>
+                <View style={styles.goodsInfo}>
+                    <Text style={styles.goodsName} numberOfLines={2}>{item.spu?.name}</Text>
+                    <Text style={styles.goodsSource}>
+                        从{item.fromRelationType === 'LUCK' ? '奖池' : '其他'}获得
+                    </Text>
+                </View>
+            </View>
+        </ImageBackground>
+    );
+
+    return (
+        <View style={styles.container}>
+            <Stack.Screen options={{ headerShown: false }} />
+            <StatusBar barStyle="light-content" />
+            <View style={[styles.header, { paddingTop: insets.top }]}>
+                <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
+                    <Ionicons name="chevron-back" size={24} color="#fff" />
+                </TouchableOpacity>
+                <Text style={styles.title}>仓库</Text>
+            </View>
+
+            <ImageBackground
+                source={{ uri: Images.mine.kaixinMineBg }}
+                style={styles.background}
+                resizeMode="cover"
+            >
+                <Image
+                    source={{ uri: Images.mine.kaixinMineHeadBg }}
+                    style={styles.headerBg}
+                    resizeMode="cover"
+                />
+
+                <View style={[styles.content, { paddingTop: insets.top + 50 }]}>
+                    {/* Tabs */}
+                    <View style={styles.tabs}>
+                        {tabs.map((tab, index) => (
+                            <TouchableOpacity
+                                key={index}
+                                style={[styles.tabItem, tabIndex === index && styles.tabItemActive]}
+                                onPress={() => setTabIndex(index)}
+                            >
+                                <Text style={[styles.tabText, tabIndex === index && styles.tabTextActive]}>{tab}</Text>
+                                {tabIndex === index && <View style={styles.tabLine} />}
+                            </TouchableOpacity>
+                        ))}
+                    </View>
+
+                    <FlatList
+                        data={list}
+                        renderItem={renderItem}
+                        keyExtractor={(item, index) => index.toString()}
+                        contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 100 }}
+                        onEndReached={handleLoadMore}
+                        onEndReachedThreshold={0.1}
+                        ListFooterComponent={
+                            loading && list.length > 0 ? (
+                                <ActivityIndicator color="#fff" style={{ marginVertical: 10 }} />
+                            ) : null
+                        }
+                        ListEmptyComponent={
+                            !loading ? (
+                                <View style={styles.emptyBox}>
+                                    <Text style={styles.emptyText}>暂无物品</Text>
+                                </View>
+                            ) : null
+                        }
+                    />
+                </View>
+            </ImageBackground>
+        </View>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        flex: 1,
+        backgroundColor: '#1a1a2e',
+    },
+    header: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        right: 0,
+        zIndex: 100,
+        alignItems: 'center',
+        paddingBottom: 10,
+    },
+    headerBg: {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        width: '100%',
+        height: 260,
+    },
+    backBtn: {
+        position: 'absolute',
+        left: 10,
+        bottom: 10,
+        zIndex: 101,
+    },
+    title: {
+        color: '#fff',
+        fontSize: 16,
+        fontWeight: 'bold',
+    },
+    background: {
+        flex: 1,
+        width: '100%',
+        height: '100%',
+    },
+    content: {
+        flex: 1,
+    },
+    tabs: {
+        flexDirection: 'row',
+        justifyContent: 'space-around',
+        marginBottom: 10,
+    },
+    tabItem: {
+        paddingVertical: 10,
+        paddingHorizontal: 10,
+        alignItems: 'center',
+    },
+    tabItemActive: {},
+    tabText: {
+        color: '#aaa',
+        fontSize: 14,
+    },
+    tabTextActive: {
+        color: '#fff',
+        fontWeight: 'bold',
+        fontSize: 16,
+    },
+    tabLine: {
+        width: 20,
+        height: 3,
+        backgroundColor: '#fff',
+        marginTop: 5,
+        borderRadius: 2,
+    },
+    cell: {
+        width: '100%',
+        height: 154,
+        marginBottom: 10,
+        padding: 15,
+    },
+    cellHeader: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        alignItems: 'center',
+        borderBottomWidth: 1,
+        borderBottomColor: 'rgba(0,0,0,0.1)',
+        paddingBottom: 10,
+        marginBottom: 10,
+    },
+    headerLeft: {
+        flexDirection: 'row',
+        alignItems: 'center',
+    },
+    checkBox: {
+        width: 16,
+        height: 16,
+        borderWidth: 1,
+        borderColor: '#999',
+        marginRight: 10,
+        backgroundColor: '#fff',
+    },
+    levelTitle: {
+        fontSize: 16,
+        fontWeight: 'bold',
+    },
+    lockBox: {
+        flexDirection: 'row',
+        alignItems: 'center',
+    },
+    lockText: {
+        fontSize: 12,
+        color: '#666',
+    },
+    lockIcon: {
+        width: 16,
+        height: 16,
+        marginLeft: 5,
+    },
+    cellBody: {
+        flexDirection: 'row',
+    },
+    goodsImgBg: {
+        width: 65,
+        height: 65,
+        justifyContent: 'center',
+        alignItems: 'center',
+        marginRight: 10,
+    },
+    goodsImg: {
+        width: 60,
+        height: 60,
+    },
+    goodsInfo: {
+        flex: 1,
+        justifyContent: 'space-between',
+    },
+    goodsName: {
+        fontSize: 14,
+        color: '#333',
+        fontWeight: 'bold',
+    },
+    goodsSource: {
+        fontSize: 12,
+        color: '#999',
+    },
+    emptyBox: {
+        marginTop: 100,
+        alignItems: 'center',
+    },
+    emptyText: {
+        color: '#999',
+    },
+});

+ 9 - 0
constants/images.ts

@@ -193,6 +193,15 @@ export const Images = {
     address: `${CDN_BASE}/mine/address.png`,
     opinion: `${CDN_BASE}/mine/opinion.png`,
     setting: `${CDN_BASE}/mine/setting.png`,
+    couponBg: `${CDN_BASE}/mine/couponBg.png`,
+    couponItemButBg: `${CDN_BASE}/mine/couponItemButBg.png`,
+    storeItemBg: `${CDN_BASE}/mine/storeItemBg.png`,
+    storeGoodsImgBg: `${CDN_BASE}/mine/storeGoodsImgBg.png`,
+    lock: `${CDN_BASE}/mine/lock.png`,
+    unlock: `${CDN_BASE}/mine/unlock.png`,
+    stoneImage: `${CDN_BASE}/mine/stoneImage.png`,
+    magicTypeBg: `${CDN_BASE}/mine/magicTypeBg.png`,
+    stoneBg: `${CDN_BASE}/mine/stoneBg.png`,
   },
   // 地址相关
   address: {

+ 6 - 6
services/award.ts

@@ -155,12 +155,12 @@ export const previewOrder = async (poolId: string, quantity?: number, boxNumber?
   if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.PREVIEW, param);
-  
+
   // 如果请求失败,抛出错误
   if (!res.success) {
     throw new Error(res.msg || '获取订单信息失败');
   }
-  
+
   return res.data;
 };
 
@@ -171,12 +171,12 @@ export const applyOrder = async (poolId: string, quantity: number, paymentType:
   if (seatNumbers && seatNumbers.length > 0) param.seatNumbers = seatNumbers;
   if (packFlag) param.packFlag = packFlag;
   const res = await postL(apis.APPLY, param);
-  
+
   // 如果请求失败,抛出错误
   if (!res.success) {
     throw new Error(res.msg || '支付失败');
   }
-  
+
   return res.data;
 };
 
@@ -199,8 +199,8 @@ export const getAwardOrders = async (current: number, size: number, tab?: string
 };
 
 // 获取仓库列表
-export const getStore = async (current: number, size: number, safeFlag?: number, tab?: string) => {
-  const res = await post(apis.STORE, { current, size, status: 0, tab, safeFlag });
+export const getStore = async (current: number, size: number, safeFlag?: number, tab?: string, status: number = 0) => {
+  const res = await post(apis.STORE, { current, size, status, tab, safeFlag });
   return res.data;
 };
 

+ 10 - 0
services/wallet.ts

@@ -2,6 +2,8 @@ import { get, post } from './http';
 
 const apis = {
     PRE_ONE_KEY: '/api/substituteOrder/preOneKeySubmit',
+    COUPON: '/api/coupon/pageMyValidCoupon',
+    BILL: '/api/wallet/bill',
 };
 
 export const preOneKeySubmit = async () => {
@@ -17,4 +19,12 @@ export const info = async (type: string, loading = false) => {
 export default {
     preOneKeySubmit,
     info,
+    coupons: async (size = 30) => {
+        const res = await post(apis.COUPON, { size }, { loading: true });
+        return res.data;
+    },
+    bill: async (current: number, size: number, walletType: string, type: string) => {
+        const res = await post(apis.BILL, { current, size, walletType, type });
+        return res.data;
+    },
 };