|
@@ -1,19 +1,19 @@
|
|
|
import { useRouter } from 'expo-router';
|
|
import { useRouter } from 'expo-router';
|
|
|
-import React, { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
|
|
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
import {
|
|
import {
|
|
|
- ActivityIndicator,
|
|
|
|
|
- ImageBackground,
|
|
|
|
|
- RefreshControl,
|
|
|
|
|
- ScrollView,
|
|
|
|
|
- StatusBar,
|
|
|
|
|
- StyleSheet,
|
|
|
|
|
- Text,
|
|
|
|
|
- View,
|
|
|
|
|
|
|
+ ActivityIndicator,
|
|
|
|
|
+ FlatList,
|
|
|
|
|
+ ImageBackground,
|
|
|
|
|
+ RefreshControl,
|
|
|
|
|
+ StatusBar,
|
|
|
|
|
+ StyleSheet,
|
|
|
|
|
+ Text,
|
|
|
|
|
+ View,
|
|
|
} from 'react-native';
|
|
} from 'react-native';
|
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
|
|
|
|
|
import { Banner } from '@/components/home/Banner';
|
|
import { Banner } from '@/components/home/Banner';
|
|
|
-import { GoodsList } from '@/components/home/GoodsList';
|
|
|
|
|
|
|
+import { GoodsCard } from '@/components/home/GoodsCard';
|
|
|
import { IPFilter } from '@/components/home/IPFilter';
|
|
import { IPFilter } from '@/components/home/IPFilter';
|
|
|
import { QuickEntry } from '@/components/home/QuickEntry';
|
|
import { QuickEntry } from '@/components/home/QuickEntry';
|
|
|
import { SearchBar } from '@/components/home/SearchBar';
|
|
import { SearchBar } from '@/components/home/SearchBar';
|
|
@@ -29,6 +29,7 @@ export default function HomeScreen() {
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
+ const [listLoading, setListLoading] = useState(false);
|
|
|
|
|
|
|
|
// 数据状态
|
|
// 数据状态
|
|
|
const [goods, setGoods] = useState<GoodsItem[]>([]);
|
|
const [goods, setGoods] = useState<GoodsItem[]>([]);
|
|
@@ -48,6 +49,7 @@ export default function HomeScreen() {
|
|
|
// 加载商品列表
|
|
// 加载商品列表
|
|
|
const loadGoods = useCallback(async (params: GoodsListParams, append = false) => {
|
|
const loadGoods = useCallback(async (params: GoodsListParams, append = false) => {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ if (!append) setListLoading(true);
|
|
|
const data = await getGoodsList(params);
|
|
const data = await getGoodsList(params);
|
|
|
if (append) {
|
|
if (append) {
|
|
|
setGoods((prev) => [...prev, ...data]);
|
|
setGoods((prev) => [...prev, ...data]);
|
|
@@ -56,6 +58,8 @@ export default function HomeScreen() {
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载商品失败:', error);
|
|
console.error('加载商品失败:', error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (!append) setListLoading(false);
|
|
|
}
|
|
}
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
@@ -125,11 +129,17 @@ export default function HomeScreen() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// IP 筛选
|
|
// IP 筛选
|
|
|
- const handleIPSelect = async (item: IPItem, index: number) => {
|
|
|
|
|
|
|
+ const handleIPSelect = (item: IPItem, index: number) => {
|
|
|
|
|
+ // 立即更新 UI,不等待网络请求
|
|
|
setIpIndex(index);
|
|
setIpIndex(index);
|
|
|
- const newParams = { ...searchParams, worksId: item.id, current: 1 };
|
|
|
|
|
- setSearchParams(newParams);
|
|
|
|
|
- await loadGoods(newParams);
|
|
|
|
|
|
|
+ setGoods([]); // 清空列表,给予用户切换反馈 (或者可以保留旧数据直到新数据到来,取决于需求,清空通常感觉更"快"因为有反馈)
|
|
|
|
|
+
|
|
|
|
|
+ // 异步加载数据
|
|
|
|
|
+ requestAnimationFrame(async () => {
|
|
|
|
|
+ const newParams = { ...searchParams, worksId: item.id, current: 1 };
|
|
|
|
|
+ setSearchParams(newParams);
|
|
|
|
|
+ await loadGoods(newParams);
|
|
|
|
|
+ });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 商品点击
|
|
// 商品点击
|
|
@@ -137,6 +147,52 @@ export default function HomeScreen() {
|
|
|
router.push(`/product/${item.id}` as any);
|
|
router.push(`/product/${item.id}` as any);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 列表头部组件
|
|
|
|
|
+ const ListHeader = useMemo(() => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* 搜索栏 */}
|
|
|
|
|
+ <SearchBar onSearch={handleSearch} />
|
|
|
|
|
+
|
|
|
|
|
+ {/* Banner 轮播 */}
|
|
|
|
|
+ {banners.length > 0 && <Banner data={banners} onPress={handleBannerPress} />}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 功能入口 */}
|
|
|
|
|
+ <View style={styles.section}>
|
|
|
|
|
+ {tabs.length > 0 && <QuickEntry data={tabs} onPress={handleQuickEntryPress} />}
|
|
|
|
|
+
|
|
|
|
|
+ {/* IP 分类筛选 */}
|
|
|
|
|
+ {ipList.length > 0 && (
|
|
|
|
|
+ <IPFilter data={ipList} activeIndex={ipIndex} onSelect={handleIPSelect} />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+ }, [banners, tabs, ipList, ipIndex]); // 依赖项
|
|
|
|
|
+
|
|
|
|
|
+ const renderItem = useCallback(({ item }: { item: GoodsItem }) => {
|
|
|
|
|
+ return <GoodsCard data={item} onPress={handleGoodsPress} />;
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const ListEmptyComponent = useMemo(() => {
|
|
|
|
|
+ if (listLoading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View style={styles.loadingListContainer}>
|
|
|
|
|
+ <ActivityIndicator size="small" color="#aaa" />
|
|
|
|
|
+ <Text style={styles.loadingText}>加载商品中...</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!loading && goods.length === 0) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View style={styles.emptyContainer}>
|
|
|
|
|
+ <Text style={styles.emptyText}>暂无商品</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }, [listLoading, loading, goods.length]);
|
|
|
|
|
+
|
|
|
if (loading) {
|
|
if (loading) {
|
|
|
return (
|
|
return (
|
|
|
<View style={styles.loadingContainer}>
|
|
<View style={styles.loadingContainer}>
|
|
@@ -154,33 +210,22 @@ export default function HomeScreen() {
|
|
|
style={styles.background}
|
|
style={styles.background}
|
|
|
resizeMode="cover"
|
|
resizeMode="cover"
|
|
|
>
|
|
>
|
|
|
- <ScrollView
|
|
|
|
|
- style={styles.scrollView}
|
|
|
|
|
- contentContainerStyle={{ paddingTop: insets.top + 10 }}
|
|
|
|
|
|
|
+ <FlatList
|
|
|
|
|
+ data={goods}
|
|
|
|
|
+ renderItem={renderItem}
|
|
|
|
|
+ keyExtractor={(item) => item.id}
|
|
|
|
|
+ ListHeaderComponent={ListHeader}
|
|
|
|
|
+ numColumns={2}
|
|
|
|
|
+ columnWrapperStyle={styles.columnWrapper}
|
|
|
|
|
+ contentContainerStyle={{ paddingTop: insets.top + 10, paddingBottom: 20 }}
|
|
|
showsVerticalScrollIndicator={false}
|
|
showsVerticalScrollIndicator={false}
|
|
|
refreshControl={
|
|
refreshControl={
|
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
|
|
|
}
|
|
}
|
|
|
- >
|
|
|
|
|
- {/* 搜索栏 */}
|
|
|
|
|
- <SearchBar onSearch={handleSearch} />
|
|
|
|
|
-
|
|
|
|
|
- {/* Banner 轮播 */}
|
|
|
|
|
- {banners.length > 0 && <Banner data={banners} onPress={handleBannerPress} />}
|
|
|
|
|
-
|
|
|
|
|
- {/* 功能入口 */}
|
|
|
|
|
- <View style={styles.section}>
|
|
|
|
|
- {tabs.length > 0 && <QuickEntry data={tabs} onPress={handleQuickEntryPress} />}
|
|
|
|
|
-
|
|
|
|
|
- {/* IP 分类筛选 */}
|
|
|
|
|
- {ipList.length > 0 && (
|
|
|
|
|
- <IPFilter data={ipList} activeIndex={ipIndex} onSelect={handleIPSelect} />
|
|
|
|
|
- )}
|
|
|
|
|
- </View>
|
|
|
|
|
-
|
|
|
|
|
- {/* 商品列表 */}
|
|
|
|
|
- <GoodsList data={goods} onItemPress={handleGoodsPress} />
|
|
|
|
|
- </ScrollView>
|
|
|
|
|
|
|
+ ListEmptyComponent={ListEmptyComponent}
|
|
|
|
|
+ onEndReachedThreshold={0.5}
|
|
|
|
|
+ // onEndReached={() => { /* Implement pagination if needed */ }}
|
|
|
|
|
+ />
|
|
|
</ImageBackground>
|
|
</ImageBackground>
|
|
|
</View>
|
|
</View>
|
|
|
);
|
|
);
|
|
@@ -194,9 +239,6 @@ const styles = StyleSheet.create({
|
|
|
background: {
|
|
background: {
|
|
|
flex: 1,
|
|
flex: 1,
|
|
|
},
|
|
},
|
|
|
- scrollView: {
|
|
|
|
|
- flex: 1,
|
|
|
|
|
- },
|
|
|
|
|
section: {
|
|
section: {
|
|
|
paddingHorizontal: 10,
|
|
paddingHorizontal: 10,
|
|
|
},
|
|
},
|
|
@@ -206,9 +248,25 @@ const styles = StyleSheet.create({
|
|
|
justifyContent: 'center',
|
|
justifyContent: 'center',
|
|
|
alignItems: 'center',
|
|
alignItems: 'center',
|
|
|
},
|
|
},
|
|
|
|
|
+ loadingListContainer: {
|
|
|
|
|
+ padding: 20,
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
loadingText: {
|
|
loadingText: {
|
|
|
color: '#fff',
|
|
color: '#fff',
|
|
|
marginTop: 10,
|
|
marginTop: 10,
|
|
|
fontSize: 14,
|
|
fontSize: 14,
|
|
|
},
|
|
},
|
|
|
|
|
+ columnWrapper: {
|
|
|
|
|
+ justifyContent: 'space-between',
|
|
|
|
|
+ paddingHorizontal: 10,
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyContainer: {
|
|
|
|
|
+ padding: 50,
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyText: {
|
|
|
|
|
+ color: '#999',
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|