index.tsx 8.5 KB

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