room.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useRef, useState } from 'react';
  4. import {
  5. ActivityIndicator,
  6. Dimensions,
  7. ImageBackground,
  8. RefreshControl,
  9. ScrollView,
  10. StatusBar,
  11. StyleSheet,
  12. Text,
  13. TextInput,
  14. TouchableOpacity,
  15. View,
  16. Platform,
  17. } from 'react-native';
  18. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  19. import { Images } from '@/constants/images';
  20. import { getRoomTypePermission, getWealList } from '@/services/dimension';
  21. import { RuleModal, RuleModalRef } from './components/RuleModal';
  22. interface RoomItem {
  23. id: string;
  24. name: string;
  25. type: string;
  26. goodsQuantity: number;
  27. officialFlag: number;
  28. user: { avatar: string; username: string };
  29. luckRoomGoodsList: { spu: { cover: string } }[];
  30. participatingList: any[];
  31. mode?: string;
  32. }
  33. export default function RoomScreen() {
  34. const router = useRouter();
  35. const insets = useSafeAreaInsets();
  36. const [loading, setLoading] = useState(true);
  37. const [refreshing, setRefreshing] = useState(false);
  38. const [list, setList] = useState<RoomItem[]>([]);
  39. const [searchVal, setSearchVal] = useState('');
  40. const [type, setType] = useState('');
  41. const [typeIndex, setTypeIndex] = useState(0);
  42. const [scrollTop, setScrollTop] = useState(0);
  43. const [roomTab, setRoomTab] = useState([
  44. { name: '全部', value: '' }
  45. ]);
  46. const ruleRef = useRef<RuleModalRef>(null);
  47. const loadPermission = async () => {
  48. try {
  49. const permission = await getRoomTypePermission();
  50. let tabs = [{ name: '全部', value: '' }];
  51. if (permission && permission.roomConfig !== 0) {
  52. tabs.push({ name: '福利房', value: 'COMMON' });
  53. tabs.push({ name: '口令房', value: 'PASSWORD' });
  54. }
  55. if (permission && permission.roomGAS !== 0) {
  56. tabs.push({ name: '欧气房', value: 'EUROPEAN_GAS' });
  57. }
  58. if (permission && permission.roomCjf !== 0) {
  59. tabs.push({ name: '成就房', value: 'ACHIEVEMENT' });
  60. }
  61. setRoomTab(tabs);
  62. } catch (error) {
  63. console.error('获取房间类型权限失败:', error);
  64. }
  65. };
  66. const loadData = useCallback(async (isRefresh = false) => {
  67. if (isRefresh) setRefreshing(true);
  68. else setLoading(true);
  69. try {
  70. const data = await getWealList(1, 20, searchVal, type);
  71. setList(data || []);
  72. } catch (error) {
  73. console.error('加载房间列表失败:', error);
  74. }
  75. setLoading(false);
  76. setRefreshing(false);
  77. }, [type, searchVal]);
  78. useEffect(() => {
  79. loadPermission();
  80. }, []);
  81. useEffect(() => {
  82. loadData();
  83. }, [type]);
  84. const handleSearch = () => {
  85. loadData();
  86. };
  87. const handleTypeChange = (item: any, index: number) => {
  88. setTypeIndex(index);
  89. setType(item.value);
  90. };
  91. const handleRoomPress = (item: RoomItem) => {
  92. if (item.mode === 'YFS_PRO') {
  93. router.push({ pathname: '/award-detail-yfs', params: { id: item.id } } as any);
  94. } else {
  95. router.push({ pathname: '/dimension/detail', params: { id: item.id } } as any);
  96. }
  97. };
  98. const isItemType = (type: string) => {
  99. const map: Record<string, string> = {
  100. COMMON: '福利房',
  101. PASSWORD: '口令房',
  102. EUROPEAN_GAS: '欧气房',
  103. ACHIEVEMENT: '成就房',
  104. };
  105. return map[type] || type;
  106. };
  107. const headerBg = scrollTop > 0 ? '#333' : 'transparent';
  108. return (
  109. <View style={styles.container}>
  110. <StatusBar barStyle="light-content" />
  111. <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
  112. {/* 顶部导航 */}
  113. <View style={[styles.header, { paddingTop: insets.top, backgroundColor: headerBg }]}>
  114. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  115. <Text style={styles.backText}>←</Text>
  116. </TouchableOpacity>
  117. <Text style={styles.title}>房间</Text>
  118. <View style={styles.placeholder} />
  119. </View>
  120. {/* 头部背景 */}
  121. <ImageBackground source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headBg} resizeMode="cover" />
  122. <ScrollView
  123. style={styles.scrollView}
  124. showsVerticalScrollIndicator={false}
  125. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  126. scrollEventThrottle={16}
  127. refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} tintColor="#fff" />}
  128. >
  129. <View style={{ height: 90 + insets.top }} />
  130. {/* iOS 特有免责声明 (应 Apple 审核要求显眼展示) */}
  131. {Platform.OS === 'ios' && (
  132. <View style={{ paddingHorizontal: 15, marginBottom: 15, alignItems: 'center' }}>
  133. <Text style={{ color: 'rgba(255,255,255,0.4)', fontSize: 10, textAlign: 'center' }}>
  134. 特别声明:本平台所有活动与苹果公司(Apple Inc.)无关
  135. </Text>
  136. </View>
  137. )}
  138. {/* 搜索栏和功能按钮 */}
  139. <View style={styles.topSection}>
  140. <ImageBackground source={{ uri: Images.welfare.roomInputBg }} style={styles.searchBox} resizeMode="stretch">
  141. <Image source={{ uri: Images.home.search2 }} style={styles.searchIcon} contentFit="contain" />
  142. <TextInput
  143. style={styles.searchInput}
  144. placeholder="搜索"
  145. placeholderTextColor="#6C6C6C"
  146. value={searchVal}
  147. onChangeText={setSearchVal}
  148. onSubmitEditing={handleSearch}
  149. returnKeyType="search"
  150. />
  151. </ImageBackground>
  152. <View style={styles.funcBtns}>
  153. <TouchableOpacity style={styles.funcItem} onPress={() => router.push('/dimension/create' as any)}>
  154. <Image source={{ uri: Images.welfare.roomIcon0 }} style={styles.funcIcon} contentFit="contain" />
  155. <Text style={styles.funcText}>创建</Text>
  156. </TouchableOpacity>
  157. <TouchableOpacity style={styles.funcItem} onPress={() => router.push('/dimension/record' as any)}>
  158. <Image source={{ uri: Images.welfare.roomIcon1 }} style={styles.funcIcon} contentFit="contain" />
  159. <Text style={styles.funcText}>我的</Text>
  160. </TouchableOpacity>
  161. <TouchableOpacity style={styles.funcItem} onPress={() => ruleRef.current?.show()}>
  162. <Image source={{ uri: Images.welfare.roomIcon2 }} style={styles.funcIcon} contentFit="contain" />
  163. <Text style={styles.funcText}>玩法</Text>
  164. </TouchableOpacity>
  165. </View>
  166. </View>
  167. {/* 类型切换 */}
  168. <View style={styles.typeContainer}>
  169. <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.typeSection}>
  170. {roomTab.map((item, index) => (
  171. <TouchableOpacity
  172. key={index}
  173. style={styles.typeItem}
  174. onPress={() => handleTypeChange(item, index)}
  175. >
  176. <Text style={[styles.typeText, typeIndex === index && styles.typeTextActive]}>{item.name}</Text>
  177. {typeIndex === index && (
  178. <>
  179. <Image source={{ uri: Images.mine.typeSelectIconT }} style={styles.typeSelectIconT} />
  180. <Image source={{ uri: Images.mine.typeSelectIconB }} style={styles.typeSelectIconB} />
  181. </>
  182. )}
  183. </TouchableOpacity>
  184. ))}
  185. </ScrollView>
  186. </View>
  187. {/* 房间列表 */}
  188. {loading ? (
  189. <ActivityIndicator size="large" color="#fff" style={{ marginTop: 50 }} />
  190. ) : list.length === 0 ? (
  191. <View style={styles.emptyBox}>
  192. <Text style={styles.emptyText}>暂无房间</Text>
  193. </View>
  194. ) : (
  195. <View style={styles.roomList}>
  196. {list.map((item) => (
  197. <TouchableOpacity key={item.id} onPress={() => handleRoomPress(item)} activeOpacity={0.8}>
  198. <ImageBackground source={{ uri: Images.welfare.roomItemBg }} style={styles.roomItem} resizeMode="stretch">
  199. {/* 官方标签 */}
  200. {item.officialFlag === 1 && (
  201. <View style={styles.officialBadge}>
  202. <Image source={{ uri: Images.welfare.official }} style={styles.officialImg} contentFit="contain" />
  203. </View>
  204. )}
  205. {/* 成就房标签 */}
  206. {item.type === 'ACHIEVEMENT' && (
  207. <View style={styles.mustBeBadge}>
  208. <Image source={{ uri: Images.welfare.mustBe }} style={styles.mustBeImg} contentFit="contain" />
  209. </View>
  210. )}
  211. {/* 商品图片 */}
  212. <ImageBackground source={{ uri: Images.welfare.roomItemImgBg }} style={styles.roomCover} resizeMode="contain">
  213. {item.luckRoomGoodsList?.[0]?.spu?.cover && (
  214. <Image source={{ uri: item.luckRoomGoodsList[0].spu.cover }} style={styles.roomCoverImg} contentFit="cover" />
  215. )}
  216. </ImageBackground>
  217. {/* 房间信息 */}
  218. <View style={styles.roomInfo}>
  219. <View style={styles.roomTop}>
  220. <Text style={styles.roomName} numberOfLines={1}>{item.name}</Text>
  221. <Text style={styles.roomTypeLabel}>{isItemType(item.type)}</Text>
  222. </View>
  223. <View style={styles.roomBottom}>
  224. <View style={styles.userInfo}>
  225. {item.user?.avatar && (
  226. <Image source={{ uri: item.user.avatar }} style={styles.userAvatar} contentFit="cover" />
  227. )}
  228. <Text style={styles.userName} numberOfLines={1}>{item.user?.username || '匿名'}</Text>
  229. </View>
  230. <Text style={styles.goodsNum}>共{item.goodsQuantity}件赠品</Text>
  231. <View style={styles.participantBox}>
  232. <Image source={{ uri: Images.welfare.participationIcon }} style={styles.participantIcon} contentFit="contain" />
  233. <Text style={styles.participantNum}>{item.participatingList?.length || 0}</Text>
  234. </View>
  235. </View>
  236. </View>
  237. </ImageBackground>
  238. </TouchableOpacity>
  239. ))}
  240. </View>
  241. )}
  242. <View style={{ height: 100 }} />
  243. </ScrollView>
  244. <RuleModal ref={ruleRef} />
  245. </ImageBackground>
  246. </View>
  247. );
  248. }
  249. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  250. const styles = StyleSheet.create({
  251. container: { flex: 1, backgroundColor: '#1a1a2e' },
  252. background: { flex: 1 },
  253. header: {
  254. flexDirection: 'row',
  255. alignItems: 'center',
  256. justifyContent: 'space-between',
  257. paddingHorizontal: 15,
  258. paddingBottom: 10,
  259. position: 'absolute',
  260. top: 0,
  261. left: 0,
  262. right: 0,
  263. zIndex: 100,
  264. height: 90,
  265. },
  266. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  267. backText: { color: '#fff', fontSize: 20 },
  268. title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  269. placeholder: { width: 40 },
  270. headBg: { position: 'absolute', top: 0, left: 0, right: 0, height: 215 },
  271. scrollView: { flex: 1 },
  272. // 顶部搜索和功能区
  273. topSection: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, marginTop: 10, marginBottom: 15 },
  274. searchBox: { flex: 1, height: 37, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15 },
  275. searchIcon: { width: 19, height: 19, marginRight: 8 },
  276. searchInput: { flex: 1, fontSize: 12, color: '#000', padding: 0 },
  277. funcBtns: { flexDirection: 'row', marginLeft: 10 },
  278. funcItem: { alignItems: 'center', marginLeft: 12 },
  279. funcIcon: { width: 16, height: 16 },
  280. funcText: { fontSize: 10, color: '#DFDFDF', marginTop: 2 },
  281. // 类型切换
  282. typeContainer: { paddingHorizontal: 15, marginBottom: 40 },
  283. typeSection: {},
  284. typeItem: { marginRight: 15, width: 55, height: 38, justifyContent: 'center', alignItems: 'center', position: 'relative' },
  285. typeText: { fontSize: 12, color: '#DFDFDF' },
  286. typeTextActive: {
  287. color: '#e79018',
  288. fontWeight: 'bold',
  289. fontSize: 15,
  290. textShadowColor: '#000',
  291. textShadowOffset: { width: 1, height: 1 },
  292. textShadowRadius: 1
  293. },
  294. typeSelectIconT: {
  295. position: 'absolute',
  296. right: -5,
  297. top: 5,
  298. zIndex: 2,
  299. width: 28,
  300. height: 10,
  301. },
  302. typeSelectIconB: {
  303. position: 'absolute',
  304. left: 0,
  305. bottom: 0,
  306. zIndex: 2,
  307. width: 24,
  308. height: 14,
  309. },
  310. // 房间列表
  311. roomList: { paddingHorizontal: 0, alignItems: 'center' },
  312. roomItem: { width: SCREEN_WIDTH - 12, height: 84, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 6, position: 'relative' },
  313. officialBadge: { position: 'absolute', right: 0, top: 0, width: 48, height: 22 },
  314. officialImg: { width: '100%', height: '100%' },
  315. mustBeBadge: { position: 'absolute', left: 0, top: 0, width: 51, height: 51 },
  316. mustBeImg: { width: '100%', height: '100%' },
  317. roomCover: { width: 58, height: 58, justifyContent: 'center', alignItems: 'center', marginRight: 14 },
  318. roomCoverImg: { width: 44, height: 44 },
  319. roomInfo: { flex: 1 },
  320. roomTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 },
  321. roomName: {
  322. flex: 1,
  323. color: '#fff',
  324. fontSize: 14,
  325. fontWeight: '400',
  326. marginRight: 10,
  327. textShadowColor: '#000',
  328. textShadowOffset: { width: 1, height: 1 },
  329. textShadowRadius: 1
  330. },
  331. roomTypeLabel: { color: '#2E0000', fontSize: 12 },
  332. roomBottom: { flexDirection: 'row', alignItems: 'center' },
  333. userInfo: { flexDirection: 'row', alignItems: 'center', width: '45%' },
  334. userAvatar: { width: 24, height: 24, borderRadius: 2, backgroundColor: '#FFDD00', borderWidth: 1.5, borderColor: '#000', marginRight: 5 },
  335. userName: { color: '#2E0000', fontSize: 12, fontWeight: 'bold', maxWidth: 60 },
  336. goodsNum: { color: '#2E0000', fontSize: 10, width: '35%' },
  337. participantBox: { flexDirection: 'row', alignItems: 'center', width: '20%' },
  338. participantIcon: { width: 14, height: 14 },
  339. participantNum: { color: '#2E0000', fontSize: 12, marginLeft: 3 },
  340. emptyBox: { alignItems: 'center', paddingTop: 50 },
  341. emptyText: { color: '#999', fontSize: 14 },
  342. });