box.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useState } from 'react';
  4. import {
  5. ActivityIndicator,
  6. FlatList,
  7. ImageBackground,
  8. RefreshControl,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TextInput,
  13. TouchableOpacity,
  14. View,
  15. } from 'react-native';
  16. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  17. import { Images } from '@/constants/images';
  18. import { getFeedbackList, getPoolList, PoolItem } from '@/services/award';
  19. const typeList = [
  20. { label: '全部', value: '', type: 0, img: Images.box.type1, imgOn: Images.box.type1On },
  21. { label: '高保赏', value: 'UNLIMITED', type: 2, img: Images.box.type3, imgOn: Images.box.type3On },
  22. { label: '高爆赏', value: 'UNLIMITED', type: 1, img: Images.box.type2, imgOn: Images.box.type2On },
  23. { label: '擂台赏', value: 'YFS_PRO', type: 7, img: Images.box.type5, imgOn: Images.box.type5On },
  24. { label: '一番赏', value: 'YFS_PRO', type: 0, img: Images.box.type4, imgOn: Images.box.type4On },
  25. ];
  26. interface BarrageItem {
  27. id: string;
  28. content: string;
  29. nickname?: string;
  30. }
  31. export default function BoxScreen() {
  32. const router = useRouter();
  33. const insets = useSafeAreaInsets();
  34. const [keyword, setKeyword] = useState('');
  35. const [typeIndex, setTypeIndex] = useState(0);
  36. const [priceSort, setPriceSort] = useState(0);
  37. const [list, setList] = useState<PoolItem[]>([]);
  38. const [loading, setLoading] = useState(false);
  39. const [refreshing, setRefreshing] = useState(false);
  40. const [current, setCurrent] = useState(1);
  41. const [total, setTotal] = useState(0);
  42. const [hasMore, setHasMore] = useState(true);
  43. const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
  44. // 加载弹幕
  45. const loadBarrage = useCallback(async () => {
  46. try {
  47. const res = await getFeedbackList();
  48. if (res.data) {
  49. setBarrageList(res.data);
  50. }
  51. } catch (error) {
  52. console.error('加载弹幕失败:', error);
  53. }
  54. }, []);
  55. const loadData = useCallback(async (isRefresh = false) => {
  56. if (loading) return;
  57. const page = isRefresh ? 1 : current;
  58. if (!isRefresh && !hasMore) return;
  59. setLoading(true);
  60. try {
  61. const selectedType = typeList[typeIndex];
  62. const res = await getPoolList({
  63. current: page,
  64. size: 10,
  65. mode: selectedType.value || undefined,
  66. type: selectedType.type,
  67. keyword: keyword || undefined,
  68. priceSort: priceSort || undefined,
  69. });
  70. if (res.success && res.data) {
  71. const newList = isRefresh ? res.data : [...list, ...res.data];
  72. setList(newList);
  73. setTotal(res.count || 0);
  74. setCurrent(page + 1);
  75. setHasMore(newList.length < (res.count || 0));
  76. }
  77. } catch (error) {
  78. console.error('加载奖池列表失败:', error);
  79. }
  80. setLoading(false);
  81. setRefreshing(false);
  82. }, [current, hasMore, loading, typeIndex, list, keyword, priceSort]);
  83. useEffect(() => {
  84. loadData(true);
  85. loadBarrage();
  86. }, [typeIndex, priceSort]);
  87. // 执行搜索
  88. const handleSearch = () => {
  89. setList([]);
  90. setCurrent(1);
  91. setHasMore(true);
  92. // 需要延迟一下让状态更新
  93. setTimeout(() => {
  94. loadData(true);
  95. }, 100);
  96. };
  97. const handleRefresh = () => {
  98. setRefreshing(true);
  99. loadData(true);
  100. };
  101. const handleLoadMore = () => {
  102. if (!loading && hasMore) {
  103. loadData(false);
  104. }
  105. };
  106. const handleTypeChange = (index: number) => {
  107. setTypeIndex(index);
  108. setList([]);
  109. setCurrent(1);
  110. setHasMore(true);
  111. };
  112. const handlePriceSort = () => {
  113. setPriceSort((prev) => (prev + 1) % 3);
  114. };
  115. const handleItemPress = (item: PoolItem) => {
  116. // 检查商品状态
  117. if (item.status !== undefined && item.status !== 1) return;
  118. console.log('点击商品:', item.id, 'mode:', item.mode, 'type:', item.type);
  119. // 根据类型跳转到不同页面 - 按照小程序逻辑
  120. if (item.type === 7) {
  121. // 擂台赏跳转到 boxInBox 页面
  122. router.push({ pathname: '/boxInBox', params: { poolId: item.id } } as any);
  123. } else if (item.mode === 'UNLIMITED') {
  124. // 高爆赏/高保赏
  125. router.push({ pathname: '/award-detail', params: { poolId: item.id } } as any);
  126. } else if (item.mode === 'YFS_PRO') {
  127. // 一番赏
  128. router.push({ pathname: '/award-detail-yfs', params: { poolId: item.id } } as any);
  129. } else {
  130. // 其他商品
  131. router.push(`/product/${item.id}` as any);
  132. }
  133. };
  134. const renderItem = ({ item }: { item: PoolItem }) => (
  135. <TouchableOpacity
  136. style={styles.itemContainer}
  137. onPress={() => handleItemPress(item)}
  138. activeOpacity={0.8}
  139. >
  140. <ImageBackground
  141. source={{ uri: Images.box.goodsItemBg }}
  142. style={styles.itemBg}
  143. resizeMode="stretch"
  144. >
  145. <Image
  146. source={{ uri: item.cover }}
  147. style={styles.itemImage}
  148. contentFit="cover"
  149. />
  150. <View style={styles.itemInfo}>
  151. <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
  152. <Text style={styles.itemPrice}>
  153. <Text style={styles.priceUnit}>¥</Text>
  154. {item.price}起
  155. </Text>
  156. </View>
  157. </ImageBackground>
  158. </TouchableOpacity>
  159. );
  160. const renderHeader = () => (
  161. <View>
  162. {/* 占位空间 - 给顶部搜索栏留出空间 */}
  163. <View style={{ height: 53 }} />
  164. {/* 顶部主图 - 绝对定位,叠在背景上 */}
  165. <View style={styles.mainImageContainer}>
  166. <Image
  167. source={{ uri: Images.box.awardMainImg }}
  168. style={styles.mainImage}
  169. contentFit="contain"
  170. />
  171. </View>
  172. {/* 占位空间 - 主图高度 */}
  173. <View style={{ height: 300 }} />
  174. {/* 弹幕区域 */}
  175. {barrageList.length > 0 && (
  176. <View style={styles.barrageSection}>
  177. <View style={styles.barrageRow}>
  178. {barrageList.slice(0, 3).map((item, index) => (
  179. <View key={item.id || index} style={styles.barrageItem}>
  180. <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
  181. </View>
  182. ))}
  183. </View>
  184. <View style={[styles.barrageRow, { marginLeft: 25 }]}>
  185. {barrageList.slice(3, 6).map((item, index) => (
  186. <View key={item.id || index} style={styles.barrageItem}>
  187. <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
  188. </View>
  189. ))}
  190. </View>
  191. </View>
  192. )}
  193. </View>
  194. );
  195. // 分类筛选单独渲染
  196. const renderTypeSection = () => (
  197. <View style={styles.typeSection}>
  198. <View style={styles.typeList}>
  199. {typeList.map((item, index) => (
  200. <TouchableOpacity
  201. key={index}
  202. style={styles.typeItem}
  203. onPress={() => handleTypeChange(index)}
  204. activeOpacity={0.7}
  205. >
  206. <Image
  207. source={{ uri: typeIndex === index ? item.imgOn : item.img }}
  208. style={styles.typeImage}
  209. contentFit="contain"
  210. />
  211. </TouchableOpacity>
  212. ))}
  213. </View>
  214. <TouchableOpacity style={styles.sortBtn} onPress={handlePriceSort} activeOpacity={0.7}>
  215. <Image
  216. source={{
  217. uri: priceSort === 0
  218. ? Images.box.sortAmount
  219. : priceSort === 1
  220. ? Images.box.sortAmountOnT
  221. : Images.box.sortAmountOnB,
  222. }}
  223. style={styles.sortIcon}
  224. contentFit="contain"
  225. />
  226. </TouchableOpacity>
  227. </View>
  228. );
  229. const renderFooter = () => {
  230. if (!loading) return null;
  231. return (
  232. <View style={styles.footer}>
  233. <ActivityIndicator size="small" color="#fff" />
  234. <Text style={styles.footerText}>加载中...</Text>
  235. </View>
  236. );
  237. };
  238. const renderEmpty = () => {
  239. if (loading) return null;
  240. return (
  241. <View style={styles.empty}>
  242. <Text style={styles.emptyText}>暂无数据</Text>
  243. </View>
  244. );
  245. };
  246. return (
  247. <View style={styles.container}>
  248. <StatusBar barStyle="light-content" />
  249. <ImageBackground
  250. source={{ uri: Images.common.indexBg }}
  251. style={styles.background}
  252. resizeMode="cover"
  253. >
  254. {/* 顶部搜索栏 */}
  255. <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
  256. <Image
  257. source={{ uri: Images.home.portrait }}
  258. style={styles.logo}
  259. contentFit="contain"
  260. />
  261. <View style={styles.searchBar}>
  262. <Image
  263. source={{ uri: Images.home.search }}
  264. style={styles.searchIcon}
  265. contentFit="contain"
  266. />
  267. <TextInput
  268. style={styles.searchInput}
  269. value={keyword}
  270. onChangeText={setKeyword}
  271. placeholder="搜索"
  272. placeholderTextColor="rgba(255,255,255,0.5)"
  273. returnKeyType="search"
  274. onSubmitEditing={handleSearch}
  275. />
  276. {keyword.length > 0 && (
  277. <TouchableOpacity onPress={handleSearch} style={styles.searchBtn}>
  278. <Text style={styles.searchBtnText}>搜索</Text>
  279. </TouchableOpacity>
  280. )}
  281. </View>
  282. </View>
  283. {/* 列表 */}
  284. <FlatList
  285. data={list}
  286. renderItem={renderItem}
  287. keyExtractor={(item) => item.id}
  288. ListHeaderComponent={() => (
  289. <>
  290. {renderHeader()}
  291. {renderTypeSection()}
  292. </>
  293. )}
  294. ListFooterComponent={renderFooter}
  295. ListEmptyComponent={renderEmpty}
  296. contentContainerStyle={styles.listContent}
  297. showsVerticalScrollIndicator={false}
  298. refreshControl={
  299. <RefreshControl
  300. refreshing={refreshing}
  301. onRefresh={handleRefresh}
  302. tintColor="#fff"
  303. />
  304. }
  305. onEndReached={handleLoadMore}
  306. onEndReachedThreshold={0.3}
  307. />
  308. </ImageBackground>
  309. </View>
  310. );
  311. }
  312. const styles = StyleSheet.create({
  313. container: {
  314. flex: 1,
  315. backgroundColor: '#1a1a2e',
  316. },
  317. background: {
  318. flex: 1,
  319. },
  320. header: {
  321. position: 'relative',
  322. zIndex: 11,
  323. flexDirection: 'row',
  324. alignItems: 'center',
  325. paddingHorizontal: 15,
  326. paddingBottom: 10,
  327. },
  328. logo: {
  329. width: 67,
  330. height: 25,
  331. marginRight: 20,
  332. },
  333. searchBar: {
  334. flex: 1,
  335. flexDirection: 'row',
  336. alignItems: 'center',
  337. backgroundColor: 'rgba(255,255,255,0.38)',
  338. borderRadius: 180,
  339. paddingHorizontal: 15,
  340. height: 28,
  341. },
  342. searchIcon: {
  343. width: 15,
  344. height: 15,
  345. marginRight: 5,
  346. },
  347. searchInput: {
  348. flex: 1,
  349. color: '#fff',
  350. fontSize: 12,
  351. padding: 0,
  352. },
  353. searchBtn: {
  354. paddingHorizontal: 8,
  355. paddingVertical: 2,
  356. },
  357. searchBtnText: {
  358. color: '#fff',
  359. fontSize: 12,
  360. },
  361. mainImageContainer: {
  362. position: 'absolute',
  363. left: 0,
  364. top: 0,
  365. zIndex: 2,
  366. width: '100%',
  367. height: 395,
  368. },
  369. mainImage: {
  370. width: '100%',
  371. height: '100%',
  372. },
  373. typeSection: {
  374. flexDirection: 'row',
  375. alignItems: 'center',
  376. paddingHorizontal: 10,
  377. paddingVertical: 10,
  378. position: 'relative',
  379. zIndex: 10,
  380. backgroundColor: 'transparent',
  381. },
  382. typeList: {
  383. flexDirection: 'row',
  384. justifyContent: 'flex-start',
  385. flex: 1,
  386. },
  387. typeItem: {
  388. width: 60,
  389. height: 30,
  390. marginRight: 5,
  391. },
  392. typeImage: {
  393. width: '100%',
  394. height: '100%',
  395. },
  396. sortBtn: {
  397. width: 40,
  398. height: 30,
  399. alignItems: 'center',
  400. justifyContent: 'center',
  401. },
  402. sortIcon: {
  403. width: 30,
  404. height: 30,
  405. },
  406. listContent: {
  407. paddingHorizontal: 10,
  408. paddingBottom: 100,
  409. },
  410. itemContainer: {
  411. marginBottom: 8,
  412. },
  413. itemBg: {
  414. width: '100%',
  415. height: 210,
  416. padding: 8,
  417. },
  418. itemImage: {
  419. width: '100%',
  420. height: 142,
  421. borderRadius: 0,
  422. },
  423. itemInfo: {
  424. flexDirection: 'row',
  425. justifyContent: 'space-between',
  426. alignItems: 'center',
  427. paddingHorizontal: 15,
  428. paddingTop: 15,
  429. },
  430. itemName: {
  431. flex: 1,
  432. color: '#fff',
  433. fontSize: 14,
  434. },
  435. itemPrice: {
  436. color: '#ff0000',
  437. fontSize: 12,
  438. fontWeight: 'bold',
  439. marginLeft: 10,
  440. },
  441. priceUnit: {
  442. fontSize: 12,
  443. marginRight: 2,
  444. },
  445. footer: {
  446. flexDirection: 'row',
  447. justifyContent: 'center',
  448. alignItems: 'center',
  449. paddingVertical: 15,
  450. },
  451. footerText: {
  452. color: 'rgba(255,255,255,0.6)',
  453. fontSize: 12,
  454. marginLeft: 8,
  455. },
  456. empty: {
  457. alignItems: 'center',
  458. paddingVertical: 50,
  459. },
  460. emptyText: {
  461. color: 'rgba(255,255,255,0.6)',
  462. fontSize: 14,
  463. },
  464. barrageSection: {
  465. marginVertical: 10,
  466. paddingHorizontal: 10,
  467. },
  468. barrageRow: {
  469. flexDirection: 'row',
  470. marginBottom: 5,
  471. },
  472. barrageItem: {
  473. backgroundColor: 'rgba(0,0,0,0.5)',
  474. borderRadius: 15,
  475. paddingHorizontal: 12,
  476. paddingVertical: 6,
  477. marginRight: 8,
  478. maxWidth: 150,
  479. },
  480. barrageText: {
  481. color: '#fff',
  482. fontSize: 12,
  483. },
  484. });