box.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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. const handleRefresh = () => {
  88. setRefreshing(true);
  89. loadData(true);
  90. };
  91. const handleLoadMore = () => {
  92. if (!loading && hasMore) {
  93. loadData(false);
  94. }
  95. };
  96. const handleTypeChange = (index: number) => {
  97. setTypeIndex(index);
  98. setList([]);
  99. setCurrent(1);
  100. setHasMore(true);
  101. };
  102. const handlePriceSort = () => {
  103. setPriceSort((prev) => (prev + 1) % 3);
  104. };
  105. const handleItemPress = (item: PoolItem) => {
  106. // 检查商品状态
  107. if (item.status !== undefined && item.status !== 1) return;
  108. console.log('点击商品:', item.id, 'mode:', item.mode, 'type:', item.type);
  109. // 根据类型跳转到不同页面 - 按照小程序逻辑
  110. if (item.type === 7) {
  111. // 擂台赏跳转到 boxInBox 页面
  112. router.push({ pathname: '/boxInBox', params: { poolId: item.id } } as any);
  113. } else if (item.mode === 'UNLIMITED') {
  114. // 高爆赏/高保赏
  115. router.push({ pathname: '/award-detail', params: { poolId: item.id } } as any);
  116. } else if (item.mode === 'YFS_PRO') {
  117. // 一番赏
  118. router.push({ pathname: '/award-detail-yfs', params: { poolId: item.id } } as any);
  119. } else {
  120. // 其他商品
  121. router.push(`/product/${item.id}` as any);
  122. }
  123. };
  124. const renderItem = ({ item }: { item: PoolItem }) => (
  125. <TouchableOpacity
  126. style={styles.itemContainer}
  127. onPress={() => handleItemPress(item)}
  128. activeOpacity={0.8}
  129. >
  130. <ImageBackground
  131. source={{ uri: Images.box.goodsItemBg }}
  132. style={styles.itemBg}
  133. resizeMode="stretch"
  134. >
  135. <Image
  136. source={{ uri: item.cover }}
  137. style={styles.itemImage}
  138. contentFit="cover"
  139. />
  140. <View style={styles.itemInfo}>
  141. <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
  142. <Text style={styles.itemPrice}>
  143. <Text style={styles.priceUnit}>¥</Text>
  144. {item.price}起
  145. </Text>
  146. </View>
  147. </ImageBackground>
  148. </TouchableOpacity>
  149. );
  150. const renderHeader = () => (
  151. <View>
  152. {/* 顶部主图 */}
  153. <Image
  154. source={{ uri: Images.box.awardMainImg }}
  155. style={styles.mainImage}
  156. contentFit="contain"
  157. />
  158. {/* 弹幕区域 */}
  159. {barrageList.length > 0 && (
  160. <View style={styles.barrageSection}>
  161. <View style={styles.barrageRow}>
  162. {barrageList.slice(0, 3).map((item, index) => (
  163. <View key={item.id || index} style={styles.barrageItem}>
  164. <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
  165. </View>
  166. ))}
  167. </View>
  168. <View style={[styles.barrageRow, { marginLeft: 25 }]}>
  169. {barrageList.slice(3, 6).map((item, index) => (
  170. <View key={item.id || index} style={styles.barrageItem}>
  171. <Text style={styles.barrageText} numberOfLines={1}>{item.content}</Text>
  172. </View>
  173. ))}
  174. </View>
  175. </View>
  176. )}
  177. {/* 分类筛选 */}
  178. <View style={styles.typeSection}>
  179. <View style={styles.typeList}>
  180. {typeList.map((item, index) => (
  181. <TouchableOpacity
  182. key={index}
  183. style={styles.typeItem}
  184. onPress={() => handleTypeChange(index)}
  185. >
  186. <Image
  187. source={{ uri: typeIndex === index ? item.imgOn : item.img }}
  188. style={styles.typeImage}
  189. contentFit="contain"
  190. />
  191. </TouchableOpacity>
  192. ))}
  193. </View>
  194. <TouchableOpacity style={styles.sortBtn} onPress={handlePriceSort}>
  195. <Image
  196. source={{
  197. uri: priceSort === 0
  198. ? Images.box.sortAmount
  199. : priceSort === 1
  200. ? Images.box.sortAmountOnT
  201. : Images.box.sortAmountOnB,
  202. }}
  203. style={styles.sortIcon}
  204. contentFit="contain"
  205. />
  206. </TouchableOpacity>
  207. </View>
  208. </View>
  209. );
  210. const renderFooter = () => {
  211. if (!loading) return null;
  212. return (
  213. <View style={styles.footer}>
  214. <ActivityIndicator size="small" color="#fff" />
  215. <Text style={styles.footerText}>加载中...</Text>
  216. </View>
  217. );
  218. };
  219. const renderEmpty = () => {
  220. if (loading) return null;
  221. return (
  222. <View style={styles.empty}>
  223. <Text style={styles.emptyText}>暂无数据</Text>
  224. </View>
  225. );
  226. };
  227. return (
  228. <View style={styles.container}>
  229. <StatusBar barStyle="light-content" />
  230. <ImageBackground
  231. source={{ uri: Images.common.awardBg }}
  232. style={styles.background}
  233. resizeMode="cover"
  234. >
  235. {/* 顶部搜索栏 */}
  236. <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
  237. <Image
  238. source={{ uri: Images.home.portrait }}
  239. style={styles.logo}
  240. contentFit="contain"
  241. />
  242. <View style={styles.searchBar}>
  243. <Image
  244. source={{ uri: Images.home.search }}
  245. style={styles.searchIcon}
  246. contentFit="contain"
  247. />
  248. <TextInput
  249. style={styles.searchInput}
  250. value={keyword}
  251. onChangeText={setKeyword}
  252. placeholder="搜索"
  253. placeholderTextColor="rgba(255,255,255,0.5)"
  254. returnKeyType="search"
  255. onSubmitEditing={() => loadData(true)}
  256. />
  257. </View>
  258. </View>
  259. {/* 列表 */}
  260. <FlatList
  261. data={list}
  262. renderItem={renderItem}
  263. keyExtractor={(item) => item.id}
  264. ListHeaderComponent={renderHeader}
  265. ListFooterComponent={renderFooter}
  266. ListEmptyComponent={renderEmpty}
  267. contentContainerStyle={styles.listContent}
  268. showsVerticalScrollIndicator={false}
  269. refreshControl={
  270. <RefreshControl
  271. refreshing={refreshing}
  272. onRefresh={handleRefresh}
  273. tintColor="#fff"
  274. />
  275. }
  276. onEndReached={handleLoadMore}
  277. onEndReachedThreshold={0.3}
  278. />
  279. </ImageBackground>
  280. </View>
  281. );
  282. }
  283. const styles = StyleSheet.create({
  284. container: {
  285. flex: 1,
  286. backgroundColor: '#1a1a2e',
  287. },
  288. background: {
  289. flex: 1,
  290. },
  291. header: {
  292. flexDirection: 'row',
  293. alignItems: 'center',
  294. paddingHorizontal: 15,
  295. paddingBottom: 10,
  296. },
  297. logo: {
  298. width: 67,
  299. height: 25,
  300. marginRight: 20,
  301. },
  302. searchBar: {
  303. flex: 1,
  304. flexDirection: 'row',
  305. alignItems: 'center',
  306. backgroundColor: 'rgba(255,255,255,0.38)',
  307. borderRadius: 180,
  308. paddingHorizontal: 15,
  309. height: 28,
  310. },
  311. searchIcon: {
  312. width: 15,
  313. height: 15,
  314. marginRight: 5,
  315. },
  316. searchInput: {
  317. flex: 1,
  318. color: '#fff',
  319. fontSize: 12,
  320. padding: 0,
  321. },
  322. mainImage: {
  323. width: '100%',
  324. height: 395,
  325. },
  326. typeSection: {
  327. flexDirection: 'row',
  328. alignItems: 'center',
  329. paddingHorizontal: 10,
  330. paddingVertical: 10,
  331. },
  332. typeList: {
  333. flex: 1,
  334. flexDirection: 'row',
  335. justifyContent: 'space-around',
  336. },
  337. typeItem: {
  338. width: 75,
  339. height: 30,
  340. },
  341. typeImage: {
  342. width: '100%',
  343. height: '100%',
  344. },
  345. sortBtn: {
  346. width: '10%',
  347. alignItems: 'center',
  348. },
  349. sortIcon: {
  350. width: 20,
  351. height: 20,
  352. },
  353. listContent: {
  354. paddingHorizontal: 10,
  355. paddingBottom: 100,
  356. },
  357. itemContainer: {
  358. marginBottom: 8,
  359. },
  360. itemBg: {
  361. width: '100%',
  362. height: 210,
  363. padding: 8,
  364. },
  365. itemImage: {
  366. width: '100%',
  367. height: 142,
  368. borderRadius: 8,
  369. },
  370. itemInfo: {
  371. paddingHorizontal: 15,
  372. paddingTop: 15,
  373. },
  374. itemName: {
  375. color: '#fff',
  376. fontSize: 14,
  377. },
  378. itemPrice: {
  379. color: '#ff0000',
  380. fontSize: 12,
  381. fontWeight: 'bold',
  382. marginTop: 5,
  383. },
  384. priceUnit: {
  385. fontSize: 12,
  386. marginRight: 2,
  387. },
  388. footer: {
  389. flexDirection: 'row',
  390. justifyContent: 'center',
  391. alignItems: 'center',
  392. paddingVertical: 15,
  393. },
  394. footerText: {
  395. color: 'rgba(255,255,255,0.6)',
  396. fontSize: 12,
  397. marginLeft: 8,
  398. },
  399. empty: {
  400. alignItems: 'center',
  401. paddingVertical: 50,
  402. },
  403. emptyText: {
  404. color: 'rgba(255,255,255,0.6)',
  405. fontSize: 14,
  406. },
  407. barrageSection: {
  408. marginVertical: 10,
  409. paddingHorizontal: 10,
  410. },
  411. barrageRow: {
  412. flexDirection: 'row',
  413. marginBottom: 5,
  414. },
  415. barrageItem: {
  416. backgroundColor: 'rgba(0,0,0,0.5)',
  417. borderRadius: 15,
  418. paddingHorizontal: 12,
  419. paddingVertical: 6,
  420. marginRight: 8,
  421. maxWidth: 150,
  422. },
  423. barrageText: {
  424. color: '#fff',
  425. fontSize: 12,
  426. },
  427. });