index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { useRouter } from 'expo-router';
  2. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. FlatList,
  6. ImageBackground,
  7. RefreshControl,
  8. StatusBar,
  9. StyleSheet,
  10. Text,
  11. View,
  12. } from 'react-native';
  13. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  14. import { Banner } from '@/components/home/Banner';
  15. import { GoodsCard } from '@/components/home/GoodsCard';
  16. import { IPFilter } from '@/components/home/IPFilter';
  17. import { QuickEntry } from '@/components/home/QuickEntry';
  18. import { SearchBar } from '@/components/home/SearchBar';
  19. import { Images } from '@/constants/images';
  20. // API 服务
  21. import { getIPList, IPItem } from '@/services/award';
  22. import { BannerItem, getPageConfig, TabItem } from '@/services/base';
  23. import { getGoodsList, GoodsItem, GoodsListParams } from '@/services/mall';
  24. export default function HomeScreen() {
  25. const insets = useSafeAreaInsets();
  26. const router = useRouter();
  27. const [refreshing, setRefreshing] = useState(false);
  28. const [loading, setLoading] = useState(true);
  29. const [listLoading, setListLoading] = useState(false);
  30. // 数据状态
  31. const [goods, setGoods] = useState<GoodsItem[]>([]);
  32. const [banners, setBanners] = useState<BannerItem[]>([]);
  33. const [tabs, setTabs] = useState<TabItem[]>([]);
  34. const [ipList, setIpList] = useState<IPItem[]>([]);
  35. const [ipIndex, setIpIndex] = useState(0);
  36. // 搜索参数
  37. const [searchParams, setSearchParams] = useState<GoodsListParams>({
  38. current: 1,
  39. size: 15,
  40. keyword: '',
  41. worksId: '',
  42. });
  43. // 加载商品列表
  44. const loadGoods = useCallback(async (params: GoodsListParams, append = false) => {
  45. try {
  46. if (!append) setListLoading(true);
  47. const data = await getGoodsList(params);
  48. if (append) {
  49. setGoods((prev) => [...prev, ...data]);
  50. } else {
  51. setGoods(data);
  52. }
  53. } catch (error) {
  54. console.error('加载商品失败:', error);
  55. } finally {
  56. if (!append) setListLoading(false);
  57. }
  58. }, []);
  59. // 加载 IP 列表
  60. const loadIPList = useCallback(async () => {
  61. try {
  62. const data = await getIPList();
  63. const allIP: IPItem = { id: '', name: '所有IP' };
  64. setIpList([allIP, ...data.filter((item) => item !== null)]);
  65. } catch (error) {
  66. console.error('加载IP列表失败:', error);
  67. }
  68. }, []);
  69. // 加载页面配置
  70. const loadPageConfig = useCallback(async () => {
  71. try {
  72. const bannerConfig = await getPageConfig('index_banner');
  73. if (bannerConfig?.components?.[0]?.elements) {
  74. setBanners(bannerConfig.components[0].elements);
  75. }
  76. const iconConfig = await getPageConfig('index_icon');
  77. if (iconConfig?.components?.[0]?.elements) {
  78. setTabs(iconConfig.components[0].elements);
  79. }
  80. } catch (error) {
  81. console.error('加载页面配置失败:', error);
  82. }
  83. }, []);
  84. // 初始化数据
  85. const initData = useCallback(async () => {
  86. setLoading(true);
  87. await Promise.all([loadGoods(searchParams), loadIPList(), loadPageConfig()]);
  88. setLoading(false);
  89. }, []);
  90. useEffect(() => {
  91. initData();
  92. }, []);
  93. // 下拉刷新
  94. const onRefresh = async () => {
  95. setRefreshing(true);
  96. const newParams = { ...searchParams, current: 1 };
  97. setSearchParams(newParams);
  98. await Promise.all([loadGoods(newParams), loadIPList(), loadPageConfig()]);
  99. setRefreshing(false);
  100. };
  101. // 搜索
  102. const handleSearch = async (keyword: string) => {
  103. const newParams = { ...searchParams, keyword, current: 1 };
  104. setSearchParams(newParams);
  105. await loadGoods(newParams);
  106. };
  107. // Banner 点击
  108. const handleBannerPress = (item: BannerItem) => {
  109. console.log('Banner pressed:', item);
  110. };
  111. // 功能入口点击
  112. const handleQuickEntryPress = (item: TabItem) => {
  113. console.log('Quick entry pressed:', item);
  114. };
  115. // IP 筛选
  116. const handleIPSelect = (item: IPItem, index: number) => {
  117. // 立即更新 UI,不等待网络请求
  118. setIpIndex(index);
  119. setGoods([]); // 清空列表,给予用户切换反馈 (或者可以保留旧数据直到新数据到来,取决于需求,清空通常感觉更"快"因为有反馈)
  120. // 异步加载数据
  121. requestAnimationFrame(async () => {
  122. const newParams = { ...searchParams, worksId: item.id, current: 1 };
  123. setSearchParams(newParams);
  124. await loadGoods(newParams);
  125. });
  126. };
  127. // 商品点击
  128. const handleGoodsPress = (item: GoodsItem) => {
  129. router.push(`/product/${item.id}` as any);
  130. };
  131. // 列表头部组件
  132. const ListHeader = useMemo(() => {
  133. return (
  134. <>
  135. {/* 搜索栏 */}
  136. <SearchBar onSearch={handleSearch} />
  137. {/* Banner 轮播 */}
  138. {banners.length > 0 && <Banner data={banners} onPress={handleBannerPress} />}
  139. {/* 功能入口 */}
  140. <View style={styles.section}>
  141. {tabs.length > 0 && <QuickEntry data={tabs} onPress={handleQuickEntryPress} />}
  142. {/* IP 分类筛选 */}
  143. {ipList.length > 0 && (
  144. <IPFilter data={ipList} activeIndex={ipIndex} onSelect={handleIPSelect} />
  145. )}
  146. </View>
  147. </>
  148. );
  149. }, [banners, tabs, ipList, ipIndex]); // 依赖项
  150. const renderItem = useCallback(({ item }: { item: GoodsItem }) => {
  151. return <GoodsCard data={item} onPress={handleGoodsPress} />;
  152. }, []);
  153. const ListEmptyComponent = useMemo(() => {
  154. if (listLoading) {
  155. return (
  156. <View style={styles.loadingListContainer}>
  157. <ActivityIndicator size="small" color="#aaa" />
  158. <Text style={styles.loadingText}>加载商品中...</Text>
  159. </View>
  160. );
  161. }
  162. if (!loading && goods.length === 0) {
  163. return (
  164. <View style={styles.emptyContainer}>
  165. <Text style={styles.emptyText}>暂无商品</Text>
  166. </View>
  167. )
  168. }
  169. return null;
  170. }, [listLoading, loading, goods.length]);
  171. if (loading) {
  172. return (
  173. <View style={styles.loadingContainer}>
  174. <ActivityIndicator size="large" color="#fff" />
  175. <Text style={styles.loadingText}>加载中...</Text>
  176. </View>
  177. );
  178. }
  179. return (
  180. <View style={styles.container}>
  181. <StatusBar barStyle="light-content" />
  182. <ImageBackground
  183. source={{ uri: Images.common.indexBg }}
  184. style={styles.background}
  185. resizeMode="cover"
  186. >
  187. <FlatList
  188. data={goods}
  189. renderItem={renderItem}
  190. keyExtractor={(item) => item.id}
  191. ListHeaderComponent={ListHeader}
  192. numColumns={2}
  193. columnWrapperStyle={styles.columnWrapper}
  194. contentContainerStyle={{ paddingTop: insets.top + 10, paddingBottom: 20 }}
  195. showsVerticalScrollIndicator={false}
  196. refreshControl={
  197. <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
  198. }
  199. ListEmptyComponent={ListEmptyComponent}
  200. onEndReachedThreshold={0.5}
  201. // onEndReached={() => { /* Implement pagination if needed */ }}
  202. />
  203. </ImageBackground>
  204. </View>
  205. );
  206. }
  207. const styles = StyleSheet.create({
  208. container: {
  209. flex: 1,
  210. backgroundColor: '#000',
  211. },
  212. background: {
  213. flex: 1,
  214. },
  215. section: {
  216. paddingHorizontal: 10,
  217. },
  218. loadingContainer: {
  219. flex: 1,
  220. backgroundColor: '#1a1a2e',
  221. justifyContent: 'center',
  222. alignItems: 'center',
  223. },
  224. loadingListContainer: {
  225. padding: 20,
  226. alignItems: 'center',
  227. },
  228. loadingText: {
  229. color: '#fff',
  230. marginTop: 10,
  231. fontSize: 14,
  232. },
  233. columnWrapper: {
  234. justifyContent: 'space-between',
  235. paddingHorizontal: 10,
  236. },
  237. emptyContainer: {
  238. padding: 50,
  239. alignItems: 'center',
  240. },
  241. emptyText: {
  242. color: '#999',
  243. fontSize: 14,
  244. },
  245. });