index.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import { useRouter } from "expo-router";
  2. import React, { useCallback, useEffect, useMemo, useState } from "react";
  3. import {
  4. ActivityIndicator,
  5. FlatList,
  6. RefreshControl,
  7. StatusBar,
  8. StyleSheet,
  9. Text,
  10. View
  11. } from "react-native";
  12. import { useSafeAreaInsets } from "react-native-safe-area-context";
  13. import { Banner } from "@/components/home/Banner";
  14. import { GoodsCard } from "@/components/home/GoodsCard";
  15. import { IPFilter } from "@/components/home/IPFilter";
  16. import { QuickEntry } from "@/components/home/QuickEntry";
  17. import { SearchBar } from "@/components/home/SearchBar";
  18. import { Colors } from "@/constants/Colors"; // Import Colors
  19. // API 服务
  20. import { getIPList, IPItem } from "@/services/award";
  21. import { BannerItem, getPageConfig, TabItem } from "@/services/base";
  22. import { getGoodsList, GoodsItem, GoodsListParams } from "@/services/mall";
  23. export default function HomeScreen() {
  24. const insets = useSafeAreaInsets();
  25. const router = useRouter();
  26. const [refreshing, setRefreshing] = useState(false);
  27. const [loading, setLoading] = useState(true);
  28. const [listLoading, setListLoading] = useState(false);
  29. // 数据状态
  30. const [goods, setGoods] = useState<GoodsItem[]>([]);
  31. const [banners, setBanners] = useState<BannerItem[]>([]);
  32. const [tabs, setTabs] = useState<TabItem[]>([]);
  33. const [ipList, setIpList] = useState<IPItem[]>([]);
  34. const [ipIndex, setIpIndex] = useState(0);
  35. // 搜索参数
  36. const [searchParams, setSearchParams] = useState<GoodsListParams>({
  37. current: 1,
  38. size: 15,
  39. keyword: "",
  40. worksId: "",
  41. });
  42. // 加载商品列表
  43. const loadGoods = useCallback(
  44. 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. [],
  60. );
  61. // 加载 IP 列表
  62. const loadIPList = useCallback(async () => {
  63. try {
  64. const data = await getIPList();
  65. const allIP: IPItem = { id: "", name: "全域" }; // '所有IP' -> '全域'
  66. setIpList([allIP, ...data.filter((item) => item !== null)]);
  67. } catch (error) {
  68. console.error("加载IP列表失败:", error);
  69. }
  70. }, []);
  71. // 加载页面配置
  72. const loadPageConfig = useCallback(async () => {
  73. try {
  74. const bannerConfig = await getPageConfig("index_banner");
  75. if (bannerConfig?.components?.[0]?.elements) {
  76. setBanners(bannerConfig.components[0].elements);
  77. }
  78. const iconConfig = await getPageConfig("index_icon");
  79. if (iconConfig?.components?.[0]?.elements) {
  80. setTabs(iconConfig.components[0].elements);
  81. }
  82. } catch (error) {
  83. console.error("加载页面配置失败:", error);
  84. }
  85. }, []);
  86. // 初始化数据
  87. const initData = useCallback(async () => {
  88. setLoading(true);
  89. await Promise.all([
  90. loadGoods(searchParams),
  91. loadIPList(),
  92. loadPageConfig(),
  93. ]);
  94. setLoading(false);
  95. }, []);
  96. useEffect(() => {
  97. initData();
  98. }, []);
  99. // 下拉刷新
  100. const onRefresh = async () => {
  101. setRefreshing(true);
  102. const newParams = { ...searchParams, current: 1 };
  103. setSearchParams(newParams);
  104. await Promise.all([loadGoods(newParams), loadIPList(), loadPageConfig()]);
  105. setRefreshing(false);
  106. };
  107. // 搜索
  108. const handleSearch = async (keyword: string) => {
  109. const newParams = { ...searchParams, keyword, current: 1 };
  110. setSearchParams(newParams);
  111. await loadGoods(newParams);
  112. };
  113. // Banner 点击
  114. const handleBannerPress = (item: BannerItem) => {
  115. console.log("Banner pressed:", item);
  116. };
  117. // 功能入口点击
  118. const handleQuickEntryPress = (item: TabItem) => {
  119. console.log("Quick entry pressed:", item);
  120. };
  121. // IP 筛选
  122. const handleIPSelect = (item: IPItem, index: number) => {
  123. // 立即更新 UI,不等待网络请求
  124. setIpIndex(index);
  125. setGoods([]);
  126. // 异步加载数据
  127. requestAnimationFrame(async () => {
  128. const newParams = { ...searchParams, worksId: item.id, current: 1 };
  129. setSearchParams(newParams);
  130. await loadGoods(newParams);
  131. });
  132. };
  133. // 商品点击
  134. const handleGoodsPress = (item: GoodsItem) => {
  135. // 虚拟 ID 检查,防止爬虫(Obfuscation)
  136. if (item.id === "99999999") return;
  137. router.push(`/product/${item.id}` as any);
  138. };
  139. // 列表头部组件
  140. const ListHeader = useMemo(() => {
  141. return (
  142. <>
  143. {/* 搜索栏 */}
  144. <SearchBar onSearch={handleSearch} />
  145. {/* Banner 轮播 */}
  146. {banners.length > 0 && (
  147. <Banner data={banners} onPress={handleBannerPress} />
  148. )}
  149. {/* 功能入口 */}
  150. <View style={styles.section}>
  151. {tabs.length > 0 && (
  152. <QuickEntry data={tabs} onPress={handleQuickEntryPress} />
  153. )}
  154. {/* IP 分类筛选 */}
  155. {ipList.length > 0 && (
  156. <IPFilter
  157. data={ipList}
  158. activeIndex={ipIndex}
  159. onSelect={handleIPSelect}
  160. />
  161. )}
  162. </View>
  163. </>
  164. );
  165. }, [banners, tabs, ipList, ipIndex]); // 依赖项
  166. const renderItem = useCallback(({ item }: { item: GoodsItem }) => {
  167. return <GoodsCard data={item} onPress={handleGoodsPress} />;
  168. }, []);
  169. const ListEmptyComponent = useMemo(() => {
  170. if (listLoading) {
  171. return (
  172. <View style={styles.loadingListContainer}>
  173. <ActivityIndicator size="small" color={Colors.neonBlue} />
  174. <Text style={styles.loadingText}>数据加载中...</Text>
  175. </View>
  176. );
  177. }
  178. if (!loading && goods.length === 0) {
  179. return (
  180. <View style={styles.emptyContainer}>
  181. <Text style={styles.emptyText}>暂无装备</Text>
  182. </View>
  183. );
  184. }
  185. return null;
  186. }, [listLoading, loading, goods.length]);
  187. if (loading) {
  188. return (
  189. <View style={styles.loadingContainer}>
  190. <ActivityIndicator size="large" color={Colors.neonBlue} />
  191. <Text style={styles.loadingText}>系统接入中...</Text>
  192. </View>
  193. );
  194. }
  195. return (
  196. <View style={styles.container}>
  197. <StatusBar barStyle="light-content" />
  198. {/* Remove ImageBackground or use a dark one */}
  199. <View style={styles.background}>
  200. <FlatList
  201. data={goods}
  202. renderItem={renderItem}
  203. keyExtractor={(item) => item.id}
  204. ListHeaderComponent={ListHeader}
  205. numColumns={2}
  206. columnWrapperStyle={styles.columnWrapper}
  207. contentContainerStyle={{
  208. paddingTop: insets.top + 10,
  209. paddingBottom: 20,
  210. }}
  211. showsVerticalScrollIndicator={false}
  212. refreshControl={
  213. <RefreshControl
  214. refreshing={refreshing}
  215. onRefresh={onRefresh}
  216. tintColor={Colors.neonBlue}
  217. />
  218. }
  219. ListEmptyComponent={ListEmptyComponent}
  220. onEndReachedThreshold={0.5}
  221. />
  222. </View>
  223. </View>
  224. );
  225. }
  226. const styles = StyleSheet.create({
  227. container: {
  228. flex: 1,
  229. backgroundColor: Colors.darkBg,
  230. },
  231. background: {
  232. flex: 1,
  233. backgroundColor: Colors.darkBg, // Use solid dark background
  234. },
  235. section: {
  236. paddingHorizontal: 10,
  237. },
  238. loadingContainer: {
  239. flex: 1,
  240. backgroundColor: Colors.darkBg,
  241. justifyContent: "center",
  242. alignItems: "center",
  243. },
  244. loadingListContainer: {
  245. padding: 20,
  246. alignItems: "center",
  247. },
  248. loadingText: {
  249. color: Colors.textSecondary,
  250. marginTop: 10,
  251. fontSize: 14,
  252. fontFamily: "System", // Keep default for now
  253. },
  254. columnWrapper: {
  255. justifyContent: "space-between",
  256. paddingHorizontal: 10,
  257. },
  258. emptyContainer: {
  259. padding: 50,
  260. alignItems: "center",
  261. },
  262. emptyText: {
  263. color: Colors.textTertiary,
  264. fontSize: 14,
  265. },
  266. });