box.tsx 13 KB

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