box.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import { Barrage } from "@/components/Barrage";
  2. import { Colors } from "@/constants/Colors";
  3. import { Images } from "@/constants/images";
  4. import { getFeedbackList, getPoolList, PoolItem } from "@/services/award";
  5. import { Image } from "expo-image";
  6. import { useRouter } from "expo-router";
  7. import React, { useCallback, useEffect, useMemo, useState } from "react";
  8. import {
  9. ActivityIndicator,
  10. Dimensions,
  11. FlatList,
  12. RefreshControl,
  13. StatusBar,
  14. StyleSheet,
  15. Text,
  16. TextInput,
  17. TouchableOpacity,
  18. View,
  19. } from "react-native";
  20. import { useSafeAreaInsets } from "react-native-safe-area-context";
  21. const { width: SCREEN_WIDTH } = Dimensions.get("window");
  22. const typeList = [
  23. { label: "全部", value: "", type: 0 },
  24. { label: "高保赏", value: "UNLIMITED", type: 2 },
  25. { label: "高爆赏", value: "UNLIMITED", type: 1 },
  26. { label: "擂台赏", value: "YFS_PRO", type: 7 },
  27. { label: "一番赏", value: "YFS_PRO", type: 0 },
  28. ];
  29. interface BarrageItem {
  30. id: string;
  31. content: string;
  32. nickname?: string;
  33. avatar: string;
  34. poolName?: string;
  35. type?: string;
  36. text?: string;
  37. poolId?: string;
  38. }
  39. // Static Header Component
  40. const StaticHeader = React.memo(
  41. ({ barrageList }: { barrageList: BarrageItem[] }) => (
  42. <View>
  43. <View style={{ height: 60 }} />
  44. {/* Featured Banner Area */}
  45. <View style={styles.bannerContainer}>
  46. {/* Replace image with a styled cyber container for now */}
  47. <View style={styles.cyberBanner}>
  48. <Text style={styles.bannerTitle}>MATRIX</Text>
  49. <Text style={styles.bannerSubtitle}>SUPPLY CRATE</Text>
  50. {/* Visual tech lines */}
  51. <View style={styles.techLine1} />
  52. <View style={styles.techLine2} />
  53. </View>
  54. </View>
  55. {/* Barrage Area */}
  56. {barrageList && barrageList.length > 0 && (
  57. <View style={styles.barrageSection}>
  58. <Barrage
  59. data={barrageList.slice(0, Math.ceil(barrageList.length / 2))}
  60. />
  61. <View style={{ height: 8 }} />
  62. <Barrage
  63. data={barrageList.slice(Math.ceil(barrageList.length / 2))}
  64. speed={35}
  65. />
  66. </View>
  67. )}
  68. </View>
  69. ),
  70. );
  71. // Type Selector Component
  72. const TypeSelector = React.memo(
  73. ({
  74. typeIndex,
  75. priceSort,
  76. onTypeChange,
  77. onSortChange,
  78. }: {
  79. typeIndex: number;
  80. priceSort: number;
  81. onTypeChange: (index: number) => void;
  82. onSortChange: () => void;
  83. }) => (
  84. <View style={styles.typeSection}>
  85. <View style={styles.typeListContainer}>
  86. <FlatList
  87. horizontal
  88. showsHorizontalScrollIndicator={false}
  89. data={typeList}
  90. keyExtractor={(_, index) => index.toString()}
  91. renderItem={({ item, index }) => {
  92. const isActive = typeIndex === index;
  93. return (
  94. <TouchableOpacity
  95. style={[styles.typeItem, isActive && styles.typeItemActive]}
  96. onPress={() => onTypeChange(index)}
  97. activeOpacity={0.7}
  98. >
  99. <Text
  100. style={[styles.typeText, isActive && styles.typeTextActive]}
  101. >
  102. {item.label}
  103. </Text>
  104. </TouchableOpacity>
  105. );
  106. }}
  107. contentContainerStyle={styles.typeListContent}
  108. />
  109. </View>
  110. <TouchableOpacity
  111. style={styles.sortBtn}
  112. onPress={onSortChange}
  113. activeOpacity={0.7}
  114. >
  115. <Text style={styles.sortBtnText}>
  116. 价格 {priceSort === 0 ? "-" : priceSort === 1 ? "↑" : "↓"}
  117. </Text>
  118. </TouchableOpacity>
  119. </View>
  120. ),
  121. );
  122. export default function BoxScreen() {
  123. const router = useRouter();
  124. const insets = useSafeAreaInsets();
  125. const [keyword, setKeyword] = useState("");
  126. const [typeIndex, setTypeIndex] = useState(0);
  127. const [priceSort, setPriceSort] = useState(0);
  128. const [list, setList] = useState<PoolItem[]>([]);
  129. const [loading, setLoading] = useState(false);
  130. const [refreshing, setRefreshing] = useState(false);
  131. const [current, setCurrent] = useState(1);
  132. const [hasMore, setHasMore] = useState(true);
  133. const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
  134. // 加载弹幕
  135. const loadBarrage = useCallback(async () => {
  136. try {
  137. const res = await getFeedbackList();
  138. if (res.data) {
  139. setBarrageList(res.data);
  140. }
  141. } catch (error) {
  142. console.error("加载弹幕失败:", error);
  143. }
  144. }, []);
  145. const loadData = useCallback(
  146. async (isRefresh = false) => {
  147. if (loading) return;
  148. const page = isRefresh ? 1 : current;
  149. if (!isRefresh && !hasMore) return;
  150. setLoading(true);
  151. try {
  152. const selectedType = typeList[typeIndex];
  153. const res = await getPoolList({
  154. current: page,
  155. size: 10,
  156. mode: selectedType.value || undefined,
  157. type: selectedType.type,
  158. keyword: keyword || undefined,
  159. priceSort: priceSort || undefined,
  160. });
  161. if (res.success && res.data) {
  162. const newList = isRefresh ? res.data : [...list, ...res.data];
  163. setList(newList);
  164. setCurrent(page + 1);
  165. setHasMore(newList.length < (res.count || 0));
  166. }
  167. } catch (error) {
  168. console.error("加载奖池列表失败:", error);
  169. }
  170. setLoading(false);
  171. setRefreshing(false);
  172. },
  173. [current, hasMore, loading, typeIndex, list, keyword, priceSort],
  174. );
  175. useEffect(() => {
  176. loadData(true);
  177. loadBarrage();
  178. }, [typeIndex, priceSort]);
  179. // 执行搜索
  180. const handleSearch = () => {
  181. setList([]);
  182. setCurrent(1);
  183. setHasMore(true);
  184. setTimeout(() => {
  185. loadData(true);
  186. }, 100);
  187. };
  188. const handleRefresh = () => {
  189. setRefreshing(true);
  190. loadData(true);
  191. };
  192. const handleLoadMore = () => {
  193. if (!loading && hasMore) {
  194. loadData(false);
  195. }
  196. };
  197. const handleTypeChange = useCallback((index: number) => {
  198. setTypeIndex(index);
  199. setList([]);
  200. setCurrent(1);
  201. setHasMore(true);
  202. }, []);
  203. const handlePriceSort = useCallback(() => {
  204. setPriceSort((prev) => (prev + 1) % 3);
  205. }, []);
  206. const handleItemPress = useCallback(
  207. (item: PoolItem) => {
  208. if (item.status !== undefined && item.status !== 1) return;
  209. if (item.type === 7) {
  210. router.push({
  211. pathname: "/boxInBox",
  212. params: { poolId: item.id },
  213. } as any);
  214. } else if (item.mode === "UNLIMITED") {
  215. router.push({
  216. pathname: "/award-detail",
  217. params: { poolId: item.id },
  218. } as any);
  219. } else if (item.mode === "YFS_PRO") {
  220. router.push({
  221. pathname: "/award-detail-yfs",
  222. params: { poolId: item.id },
  223. } as any);
  224. } else {
  225. router.push(`/product/${item.id}` as any);
  226. }
  227. },
  228. [router],
  229. );
  230. const renderItem = useCallback(
  231. ({ item }: { item: PoolItem }) => (
  232. <TouchableOpacity
  233. style={styles.itemContainer}
  234. onPress={() => handleItemPress(item)}
  235. activeOpacity={0.8}
  236. >
  237. <View style={styles.itemCard}>
  238. <View style={styles.itemImageContainer}>
  239. <Image
  240. source={{ uri: item.cover }}
  241. style={styles.itemImage}
  242. contentFit="cover"
  243. />
  244. {/* Cyber Border Overlay */}
  245. <View style={styles.itemCyberOverlay} />
  246. </View>
  247. <View style={styles.itemInfo}>
  248. <Text style={styles.itemName} numberOfLines={1}>
  249. {item.name}
  250. </Text>
  251. <Text style={styles.itemPrice}>
  252. <Text style={styles.priceUnit}>¥</Text>
  253. {item.price} <Text style={styles.priceStart}>起</Text>
  254. </Text>
  255. </View>
  256. </View>
  257. </TouchableOpacity>
  258. ),
  259. [handleItemPress],
  260. );
  261. const ListHeader = useMemo(
  262. () => (
  263. <View>
  264. <StaticHeader barrageList={barrageList} />
  265. <TypeSelector
  266. typeIndex={typeIndex}
  267. priceSort={priceSort}
  268. onTypeChange={handleTypeChange}
  269. onSortChange={handlePriceSort}
  270. />
  271. </View>
  272. ),
  273. [barrageList, typeIndex, priceSort, handleTypeChange, handlePriceSort],
  274. );
  275. const renderFooter = useCallback(() => {
  276. if (!loading) return null;
  277. return (
  278. <View style={styles.footer}>
  279. <ActivityIndicator size="small" color={Colors.neonBlue} />
  280. <Text style={styles.footerText}>数据传输中...</Text>
  281. </View>
  282. );
  283. }, [loading]);
  284. const renderEmpty = useCallback(() => {
  285. if (loading) return null;
  286. return (
  287. <View style={styles.empty}>
  288. <Text style={styles.emptyText}>暂无数据</Text>
  289. </View>
  290. );
  291. }, [loading]);
  292. return (
  293. <View style={styles.container}>
  294. <StatusBar barStyle="light-content" />
  295. <View style={styles.background}>
  296. {/* Header */}
  297. <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
  298. <View style={styles.logoArea}>
  299. <Text style={styles.logoText}>
  300. CYBER<Text style={styles.logoHighlight}>BOX</Text>
  301. </Text>
  302. </View>
  303. <View style={styles.searchBar}>
  304. <Image
  305. source={{ uri: Images.home.search }}
  306. style={styles.searchIcon}
  307. contentFit="contain"
  308. tintColor={Colors.neonBlue}
  309. />
  310. <TextInput
  311. style={styles.searchInput}
  312. value={keyword}
  313. onChangeText={setKeyword}
  314. placeholder="搜索奖池"
  315. placeholderTextColor={Colors.textTertiary}
  316. returnKeyType="search"
  317. onSubmitEditing={handleSearch}
  318. />
  319. </View>
  320. </View>
  321. {/* List */}
  322. <FlatList
  323. data={list}
  324. renderItem={renderItem}
  325. keyExtractor={(item) => item.id}
  326. ListHeaderComponent={ListHeader}
  327. ListFooterComponent={renderFooter}
  328. ListEmptyComponent={renderEmpty}
  329. contentContainerStyle={styles.listContent}
  330. showsVerticalScrollIndicator={false}
  331. refreshControl={
  332. <RefreshControl
  333. refreshing={refreshing}
  334. onRefresh={handleRefresh}
  335. tintColor={Colors.neonBlue}
  336. />
  337. }
  338. onEndReached={handleLoadMore}
  339. onEndReachedThreshold={0.3}
  340. />
  341. </View>
  342. </View>
  343. );
  344. }
  345. const styles = StyleSheet.create({
  346. container: {
  347. flex: 1,
  348. backgroundColor: Colors.darkBg,
  349. },
  350. background: {
  351. flex: 1,
  352. backgroundColor: Colors.darkBg,
  353. },
  354. header: {
  355. position: "relative",
  356. zIndex: 11,
  357. flexDirection: "row",
  358. alignItems: "center",
  359. paddingHorizontal: 15,
  360. paddingBottom: 10,
  361. backgroundColor: Colors.darkBg,
  362. },
  363. logoArea: {
  364. marginRight: 15,
  365. },
  366. logoText: {
  367. color: "#fff",
  368. fontWeight: "bold",
  369. fontSize: 18,
  370. fontStyle: "italic",
  371. },
  372. logoHighlight: {
  373. color: Colors.neonPink,
  374. },
  375. searchBar: {
  376. flex: 1,
  377. flexDirection: "row",
  378. alignItems: "center",
  379. backgroundColor: "rgba(255,255,255,0.05)",
  380. borderRadius: 20,
  381. paddingHorizontal: 15,
  382. height: 32,
  383. borderWidth: 1,
  384. borderColor: "rgba(0, 243, 255, 0.3)",
  385. },
  386. searchIcon: {
  387. width: 14,
  388. height: 14,
  389. marginRight: 8,
  390. },
  391. searchInput: {
  392. flex: 1,
  393. color: "#fff",
  394. fontSize: 12,
  395. padding: 0,
  396. },
  397. // Banner
  398. bannerContainer: {
  399. height: 180,
  400. marginHorizontal: 10,
  401. marginTop: 10,
  402. marginBottom: 20,
  403. borderRadius: 12,
  404. overflow: "hidden",
  405. },
  406. cyberBanner: {
  407. flex: 1,
  408. backgroundColor: Colors.darkCard,
  409. justifyContent: "center",
  410. alignItems: "center",
  411. borderWidth: 1,
  412. borderColor: Colors.neonPink,
  413. position: "relative",
  414. },
  415. bannerTitle: {
  416. fontSize: 32,
  417. fontWeight: "bold",
  418. color: "#fff",
  419. textShadowColor: Colors.neonPink,
  420. textShadowRadius: 10,
  421. fontStyle: "italic",
  422. },
  423. bannerSubtitle: {
  424. fontSize: 14,
  425. color: Colors.neonBlue,
  426. letterSpacing: 4,
  427. marginTop: 5,
  428. },
  429. techLine1: {
  430. position: "absolute",
  431. top: 10,
  432. left: 10,
  433. width: 20,
  434. height: 2,
  435. backgroundColor: Colors.neonBlue,
  436. },
  437. techLine2: {
  438. position: "absolute",
  439. bottom: 10,
  440. right: 10,
  441. width: 20,
  442. height: 2,
  443. backgroundColor: Colors.neonBlue,
  444. },
  445. // Type Selector
  446. typeSection: {
  447. flexDirection: "row",
  448. alignItems: "center",
  449. paddingHorizontal: 10,
  450. paddingVertical: 10,
  451. zIndex: 10,
  452. },
  453. typeListContainer: {
  454. flex: 1,
  455. marginRight: 5,
  456. },
  457. typeListContent: {
  458. paddingRight: 10,
  459. },
  460. typeItem: {
  461. paddingHorizontal: 12,
  462. paddingVertical: 6,
  463. borderRadius: 15,
  464. backgroundColor: "rgba(255,255,255,0.05)",
  465. marginRight: 8,
  466. borderWidth: 1,
  467. borderColor: "transparent",
  468. },
  469. typeItemActive: {
  470. backgroundColor: "rgba(0, 243, 255, 0.15)",
  471. borderColor: Colors.neonBlue,
  472. },
  473. typeText: {
  474. color: Colors.textSecondary,
  475. fontSize: 12,
  476. fontWeight: "600",
  477. },
  478. typeTextActive: {
  479. color: "#fff",
  480. textShadowColor: Colors.neonBlue,
  481. textShadowRadius: 5,
  482. },
  483. sortBtn: {
  484. paddingHorizontal: 8,
  485. paddingVertical: 6,
  486. alignItems: "center",
  487. justifyContent: "center",
  488. backgroundColor: "rgba(255,255,255,0.05)",
  489. borderRadius: 15,
  490. },
  491. sortBtnText: {
  492. color: Colors.textTertiary,
  493. fontSize: 12,
  494. },
  495. // List
  496. listContent: {
  497. paddingHorizontal: 10,
  498. paddingBottom: 100,
  499. },
  500. itemContainer: {
  501. marginBottom: 12,
  502. },
  503. itemCard: {
  504. width: "100%",
  505. backgroundColor: Colors.darkCard,
  506. borderRadius: 8,
  507. overflow: "hidden",
  508. borderWidth: 1,
  509. borderColor: "rgba(0, 243, 255, 0.2)",
  510. },
  511. itemImageContainer: {
  512. height: 160,
  513. width: "100%",
  514. position: "relative",
  515. },
  516. itemImage: {
  517. width: "100%",
  518. height: "100%",
  519. },
  520. itemCyberOverlay: {
  521. ...StyleSheet.absoluteFillObject,
  522. backgroundColor: "rgba(0,0,0,0.1)", // Slight dark overlay
  523. borderBottomWidth: 1,
  524. borderBottomColor: Colors.neonBlue,
  525. },
  526. itemInfo: {
  527. flexDirection: "row",
  528. justifyContent: "space-between",
  529. alignItems: "center",
  530. padding: 12,
  531. },
  532. itemName: {
  533. flex: 1,
  534. color: Colors.textPrimary,
  535. fontSize: 14,
  536. fontWeight: "bold",
  537. },
  538. itemPrice: {
  539. color: Colors.neonBlue,
  540. fontSize: 16,
  541. fontWeight: "bold",
  542. marginLeft: 10,
  543. textShadowColor: Colors.neonBlue,
  544. textShadowRadius: 5,
  545. },
  546. priceUnit: {
  547. fontSize: 12,
  548. marginRight: 2,
  549. },
  550. priceStart: {
  551. fontSize: 10,
  552. color: Colors.textTertiary,
  553. fontWeight: "normal",
  554. },
  555. // Footer
  556. footer: {
  557. paddingVertical: 15,
  558. alignItems: "center",
  559. flexDirection: "row",
  560. justifyContent: "center",
  561. },
  562. footerText: {
  563. color: Colors.textTertiary,
  564. fontSize: 12,
  565. marginLeft: 8,
  566. },
  567. empty: {
  568. alignItems: "center",
  569. paddingVertical: 50,
  570. },
  571. emptyText: {
  572. color: Colors.textTertiary,
  573. fontSize: 14,
  574. },
  575. // Barrage
  576. barrageSection: {
  577. marginVertical: 10,
  578. paddingHorizontal: 5, // Edge to edge
  579. },
  580. });