room.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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. ImageBackground,
  7. RefreshControl,
  8. ScrollView,
  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 { get } from '@/services/http';
  19. interface RoomItem {
  20. id: string;
  21. name: string;
  22. type: string;
  23. goodsQuantity: number;
  24. officialFlag: number;
  25. user: { avatar: string; username: string };
  26. luckRoomGoodsList: { spu: { cover: string } }[];
  27. participatingList: any[];
  28. }
  29. const roomTypes = [
  30. { name: '全部', value: '' },
  31. { name: '福利营', value: 'COMMON' },
  32. { name: '欧气营', value: 'EUROPEAN_GAS' },
  33. { name: '口令营', value: 'PASSWORD' },
  34. { name: '成就营', value: 'ACHIEVEMENT' },
  35. { name: '荣耀榜', value: 'GLORY' },
  36. ];
  37. const getTypeLabel = (type: string) => {
  38. const map: Record<string, string> = {
  39. COMMON: '福利营',
  40. PASSWORD: '口令营',
  41. EUROPEAN_GAS: '欧气营',
  42. ACHIEVEMENT: '成就营',
  43. };
  44. return map[type] || type;
  45. };
  46. export default function RoomScreen() {
  47. const router = useRouter();
  48. const insets = useSafeAreaInsets();
  49. const [loading, setLoading] = useState(true);
  50. const [refreshing, setRefreshing] = useState(false);
  51. const [list, setList] = useState<RoomItem[]>([]);
  52. const [searchVal, setSearchVal] = useState('');
  53. const [typeIndex, setTypeIndex] = useState(0);
  54. const [scrollTop, setScrollTop] = useState(0);
  55. const loadData = useCallback(async (isRefresh = false) => {
  56. if (isRefresh) setRefreshing(true);
  57. else setLoading(true);
  58. try {
  59. const type = roomTypes[typeIndex].value;
  60. const res = await get('/api/luckRoom/list', {
  61. pageNum: 1,
  62. pageSize: 20,
  63. keyword: searchVal,
  64. type: type || undefined,
  65. });
  66. setList(res.data?.records || res.data || []);
  67. } catch (error) {
  68. console.error('加载房间列表失败:', error);
  69. }
  70. setLoading(false);
  71. setRefreshing(false);
  72. }, [typeIndex, searchVal]);
  73. useEffect(() => {
  74. loadData();
  75. }, [typeIndex]);
  76. const handleSearch = () => {
  77. loadData();
  78. };
  79. const handleTypeChange = (index: number) => {
  80. setTypeIndex(index);
  81. };
  82. const handleRoomPress = (item: RoomItem) => {
  83. router.push({ pathname: '/weal/detail', params: { id: item.id } } as any);
  84. };
  85. const headerBg = scrollTop > 0 ? '#333' : 'transparent';
  86. return (
  87. <View style={styles.container}>
  88. <StatusBar barStyle="light-content" />
  89. <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
  90. {/* 顶部导航 */}
  91. <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
  92. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  93. <Text style={styles.backText}>←</Text>
  94. </TouchableOpacity>
  95. <Text style={styles.title}>福利</Text>
  96. <View style={styles.placeholder} />
  97. </View>
  98. {/* 头部背景 */}
  99. <ImageBackground source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headBg} resizeMode="cover" />
  100. <ScrollView
  101. style={styles.scrollView}
  102. showsVerticalScrollIndicator={false}
  103. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  104. scrollEventThrottle={16}
  105. refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} tintColor="#fff" />}
  106. >
  107. <View style={{ height: 90 + insets.top }} />
  108. {/* 搜索栏和功能按钮 */}
  109. <View style={styles.topSection}>
  110. <ImageBackground source={{ uri: Images.welfare.roomInputBg }} style={styles.searchBox} resizeMode="stretch">
  111. <Image source={{ uri: Images.home.search2 }} style={styles.searchIcon} contentFit="contain" />
  112. <TextInput
  113. style={styles.searchInput}
  114. placeholder="搜索"
  115. placeholderTextColor="#6C6C6C"
  116. value={searchVal}
  117. onChangeText={setSearchVal}
  118. onSubmitEditing={handleSearch}
  119. returnKeyType="search"
  120. />
  121. </ImageBackground>
  122. <View style={styles.funcBtns}>
  123. <TouchableOpacity style={styles.funcItem}>
  124. <Image source={{ uri: Images.welfare.roomIcon0 }} style={styles.funcIcon} contentFit="contain" />
  125. <Text style={styles.funcText}>创建</Text>
  126. </TouchableOpacity>
  127. <TouchableOpacity style={styles.funcItem}>
  128. <Image source={{ uri: Images.welfare.roomIcon1 }} style={styles.funcIcon} contentFit="contain" />
  129. <Text style={styles.funcText}>我的</Text>
  130. </TouchableOpacity>
  131. <TouchableOpacity style={styles.funcItem}>
  132. <Image source={{ uri: Images.welfare.roomIcon2 }} style={styles.funcIcon} contentFit="contain" />
  133. <Text style={styles.funcText}>玩法</Text>
  134. </TouchableOpacity>
  135. </View>
  136. </View>
  137. {/* 类型切换 */}
  138. <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.typeSection}>
  139. {roomTypes.map((item, index) => (
  140. <TouchableOpacity
  141. key={index}
  142. style={styles.typeItem}
  143. onPress={() => handleTypeChange(index)}
  144. >
  145. <Text style={[styles.typeText, typeIndex === index && styles.typeTextActive]}>{item.name}</Text>
  146. </TouchableOpacity>
  147. ))}
  148. </ScrollView>
  149. {/* 房间列表 */}
  150. {loading ? (
  151. <ActivityIndicator size="large" color="#fff" style={{ marginTop: 50 }} />
  152. ) : list.length === 0 ? (
  153. <View style={styles.emptyBox}>
  154. <Text style={styles.emptyText}>暂无房间</Text>
  155. </View>
  156. ) : (
  157. <View style={styles.roomList}>
  158. {list.map((item) => (
  159. <TouchableOpacity key={item.id} onPress={() => handleRoomPress(item)} activeOpacity={0.8}>
  160. <ImageBackground source={{ uri: Images.welfare.roomItemBg }} style={styles.roomItem} resizeMode="stretch">
  161. {/* 官方标签 */}
  162. {item.officialFlag === 1 && (
  163. <View style={styles.officialBadge}>
  164. <Image source={{ uri: Images.welfare.official }} style={styles.officialImg} contentFit="contain" />
  165. </View>
  166. )}
  167. {/* 成就房标签 */}
  168. {item.type === 'ACHIEVEMENT' && (
  169. <View style={styles.mustBeBadge}>
  170. <Image source={{ uri: Images.welfare.mustBe }} style={styles.mustBeImg} contentFit="contain" />
  171. </View>
  172. )}
  173. {/* 商品图片 */}
  174. <ImageBackground source={{ uri: Images.welfare.roomItemImgBg }} style={styles.roomCover} resizeMode="contain">
  175. {item.luckRoomGoodsList?.[0]?.spu?.cover && (
  176. <Image source={{ uri: item.luckRoomGoodsList[0].spu.cover }} style={styles.roomCoverImg} contentFit="cover" />
  177. )}
  178. </ImageBackground>
  179. {/* 房间信息 */}
  180. <View style={styles.roomInfo}>
  181. <View style={styles.roomTop}>
  182. <Text style={styles.roomName} numberOfLines={1}>{item.name}</Text>
  183. <Text style={styles.roomType}>{getTypeLabel(item.type)}</Text>
  184. </View>
  185. <View style={styles.roomBottom}>
  186. <View style={styles.userInfo}>
  187. {item.user?.avatar && (
  188. <Image source={{ uri: item.user.avatar }} style={styles.userAvatar} contentFit="cover" />
  189. )}
  190. <Text style={styles.userName} numberOfLines={1}>{item.user?.username || '匿名'}</Text>
  191. </View>
  192. <Text style={styles.goodsNum}>共{item.goodsQuantity}件赠品</Text>
  193. <View style={styles.participantBox}>
  194. <Image source={{ uri: Images.welfare.participationIcon }} style={styles.participantIcon} contentFit="contain" />
  195. <Text style={styles.participantNum}>{item.participatingList?.length || 0}</Text>
  196. </View>
  197. </View>
  198. </View>
  199. </ImageBackground>
  200. </TouchableOpacity>
  201. ))}
  202. </View>
  203. )}
  204. <View style={{ height: 100 }} />
  205. </ScrollView>
  206. </ImageBackground>
  207. </View>
  208. );
  209. }
  210. const styles = StyleSheet.create({
  211. container: { flex: 1, backgroundColor: '#1a1a2e' },
  212. background: { flex: 1 },
  213. header: {
  214. flexDirection: 'row',
  215. alignItems: 'center',
  216. justifyContent: 'space-between',
  217. paddingHorizontal: 10,
  218. paddingBottom: 10,
  219. position: 'absolute',
  220. top: 0,
  221. left: 0,
  222. right: 0,
  223. zIndex: 100,
  224. },
  225. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  226. backText: { color: '#fff', fontSize: 20 },
  227. title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  228. placeholder: { width: 40 },
  229. headBg: { position: 'absolute', top: 0, left: 0, right: 0, height: 215 },
  230. scrollView: { flex: 1 },
  231. // 顶部搜索和功能区
  232. topSection: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, marginBottom: 15 },
  233. searchBox: { flex: 1, height: 37, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15 },
  234. searchIcon: { width: 19, height: 19, marginRight: 8 },
  235. searchInput: { flex: 1, fontSize: 12, color: '#000', padding: 0 },
  236. funcBtns: { flexDirection: 'row', marginLeft: 10 },
  237. funcItem: { alignItems: 'center', marginLeft: 12 },
  238. funcIcon: { width: 16, height: 16 },
  239. funcText: { fontSize: 10, color: '#DFDFDF', marginTop: 2 },
  240. // 类型切换
  241. typeSection: { paddingHorizontal: 10, marginBottom: 40 },
  242. typeItem: { marginRight: 15, paddingVertical: 8 },
  243. typeText: { fontSize: 12, color: '#DFDFDF' },
  244. typeTextActive: { color: '#e79018', fontWeight: 'bold', fontSize: 15, textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
  245. // 房间列表
  246. roomList: { paddingHorizontal: 10 },
  247. roomItem: { width: '100%', height: 84, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 6, position: 'relative' },
  248. officialBadge: { position: 'absolute', right: 0, top: 0, width: 48, height: 22 },
  249. officialImg: { width: '100%', height: '100%' },
  250. mustBeBadge: { position: 'absolute', left: 0, top: 0, width: 51, height: 51 },
  251. mustBeImg: { width: '100%', height: '100%' },
  252. roomCover: { width: 58, height: 58, justifyContent: 'center', alignItems: 'center', marginRight: 14 },
  253. roomCoverImg: { width: 44, height: 44 },
  254. roomInfo: { flex: 1 },
  255. roomTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 },
  256. roomName: { flex: 1, color: '#fff', fontSize: 14, fontWeight: '400', marginRight: 10, textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
  257. roomType: { color: '#2E0000', fontSize: 12 },
  258. roomBottom: { flexDirection: 'row', alignItems: 'center' },
  259. userInfo: { flexDirection: 'row', alignItems: 'center', width: '45%' },
  260. userAvatar: { width: 24, height: 24, borderRadius: 2, backgroundColor: '#FFDD00', borderWidth: 1.5, borderColor: '#000', marginRight: 5 },
  261. userName: { color: '#2E0000', fontSize: 12, fontWeight: 'bold', maxWidth: 60 },
  262. goodsNum: { color: '#2E0000', fontSize: 10, width: '35%' },
  263. participantBox: { flexDirection: 'row', alignItems: 'center', width: '20%' },
  264. participantIcon: { width: 14, height: 14 },
  265. participantNum: { color: '#2E0000', fontSize: 12, marginLeft: 3 },
  266. emptyBox: { alignItems: 'center', paddingTop: 50 },
  267. emptyText: { color: '#999', fontSize: 14 },
  268. });