record.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. TouchableOpacity,
  13. View,
  14. } from 'react-native';
  15. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  16. import { Images } from '@/constants/images';
  17. import { getWealRecord } from '@/services/dimension';
  18. interface RecordItem {
  19. id: string;
  20. name: string;
  21. type: string;
  22. goodsQuantity: number;
  23. officialFlag: number;
  24. leftTime: number;
  25. user: { avatar: string; username: string };
  26. luckRoomGoodsList: { spu: { cover: string } }[];
  27. participatingList: any[];
  28. }
  29. export default function WealRecordScreen() {
  30. const router = useRouter();
  31. const insets = useSafeAreaInsets();
  32. const [loading, setLoading] = useState(true);
  33. const [refreshing, setRefreshing] = useState(false);
  34. const [list, setList] = useState<RecordItem[]>([]);
  35. const [pageNum, setPageNum] = useState(1);
  36. const [hasMore, setHasMore] = useState(true);
  37. const loadData = useCallback(async (isRefresh = false) => {
  38. if (isRefresh) {
  39. setRefreshing(true);
  40. setPageNum(1);
  41. } else {
  42. setLoading(true);
  43. }
  44. try {
  45. const page = isRefresh ? 1 : pageNum;
  46. const data = await getWealRecord(page, 10);
  47. if (data) {
  48. if (page === 1) {
  49. setList(data);
  50. } else {
  51. setList(prev => [...prev, ...data]);
  52. }
  53. setHasMore(data.length === 10);
  54. }
  55. } catch (error) {
  56. console.error('加载记录失败:', error);
  57. }
  58. setLoading(false);
  59. setRefreshing(false);
  60. }, [pageNum]);
  61. useEffect(() => {
  62. loadData();
  63. }, [loadData]);
  64. const handleLoadMore = () => {
  65. if (!hasMore || loading || refreshing) return;
  66. setPageNum(prev => prev + 1);
  67. };
  68. const isItemType = (type: string) => {
  69. const map: Record<string, string> = {
  70. COMMON: '福利房',
  71. PASSWORD: '口令房',
  72. EUROPEAN_GAS: '欧气房',
  73. ACHIEVEMENT: '成就房',
  74. };
  75. return map[type] || type;
  76. };
  77. return (
  78. <View style={styles.container}>
  79. <StatusBar barStyle="light-content" />
  80. <ImageBackground source={{ uri: Images.common.commonBg }} style={styles.background} resizeMode="cover">
  81. {/* 导航 */}
  82. <View style={[styles.nav, { paddingTop: insets.top }]}>
  83. <TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
  84. <Text style={styles.backText}>←</Text>
  85. </TouchableOpacity>
  86. <Text style={styles.navTitle}>参与记录</Text>
  87. <View style={styles.placeholder} />
  88. </View>
  89. <ScrollView
  90. style={styles.scrollView}
  91. refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} tintColor="#fff" />}
  92. onScroll={({ nativeEvent }) => {
  93. const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
  94. if (layoutMeasurement.height + contentOffset.y >= contentSize.height - 20) {
  95. handleLoadMore();
  96. }
  97. }}
  98. scrollEventThrottle={400}
  99. >
  100. <View style={{ height: 20 }} />
  101. <View style={styles.list}>
  102. {list.length === 0 && !loading ? (
  103. <View style={styles.empty}><Text style={styles.emptyText}>暂无记录</Text></View>
  104. ) : list.map((item, index) => (
  105. <TouchableOpacity
  106. key={index}
  107. style={[styles.itemWrapper, item.leftTime < 1 && styles.grayscale]}
  108. activeOpacity={0.8}
  109. onPress={() => router.push({ pathname: '/dimension/detail', params: { id: item.id } } as any)}
  110. >
  111. <ImageBackground source={{ uri: Images.welfare.roomItemBg }} style={styles.item} resizeMode="stretch">
  112. {item.officialFlag === 1 && (
  113. <View style={styles.officialBadge}>
  114. <Image source={{ uri: Images.welfare.official }} style={styles.officialImg} contentFit="contain" />
  115. </View>
  116. )}
  117. <ImageBackground source={{ uri: Images.welfare.roomItemImgBg }} style={styles.roomCover} resizeMode="contain">
  118. <Image source={{ uri: item.luckRoomGoodsList[0]?.spu?.cover }} style={styles.roomCoverImg} contentFit="cover" />
  119. </ImageBackground>
  120. <View style={styles.roomInfo}>
  121. <View style={styles.roomTop}>
  122. <Text style={styles.roomName} numberOfLines={1}>{item.name}</Text>
  123. <Text style={styles.roomTypeLabel}>{isItemType(item.type)}</Text>
  124. </View>
  125. <View style={styles.roomBottom}>
  126. <View style={styles.userInfo}>
  127. <Image source={{ uri: item.user.avatar }} style={styles.userAvatar} />
  128. <Text style={styles.userName} numberOfLines={1}>{item.user.username}</Text>
  129. </View>
  130. <Text style={styles.goodsNum}>共{item.goodsQuantity}件赠品</Text>
  131. <View style={styles.participantBox}>
  132. <Image source={{ uri: Images.welfare.participationIcon }} style={styles.participantIcon} />
  133. <Text style={styles.participantNum}>{item.participatingList.length}</Text>
  134. </View>
  135. </View>
  136. </View>
  137. </ImageBackground>
  138. </TouchableOpacity>
  139. ))}
  140. </View>
  141. {loading && <ActivityIndicator color="#fff" style={{ marginVertical: 20 }} />}
  142. <View style={{ height: 100 }} />
  143. </ScrollView>
  144. </ImageBackground>
  145. </View>
  146. );
  147. }
  148. const styles = StyleSheet.create({
  149. container: { flex: 1, backgroundColor: '#1a1a2e' },
  150. background: { flex: 1 },
  151. nav: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15, height: 90 },
  152. backBtn: { width: 40 },
  153. backText: { color: '#fff', fontSize: 24 },
  154. navTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  155. placeholder: { width: 40 },
  156. scrollView: { flex: 1 },
  157. list: { paddingHorizontal: 15 },
  158. itemWrapper: { marginBottom: 6 },
  159. grayscale: { opacity: 0.6 }, // 模拟原项目的置灰效果
  160. item: { width: '100%', height: 84, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16 },
  161. officialBadge: { position: 'absolute', right: 0, top: 0, width: 48, height: 22 },
  162. officialImg: { width: '100%', height: '100%' },
  163. roomCover: { width: 58, height: 58, justifyContent: 'center', alignItems: 'center', marginRight: 14 },
  164. roomCoverImg: { width: 44, height: 44 },
  165. roomInfo: { flex: 1 },
  166. roomTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 },
  167. roomName: { flex: 1, color: '#fff', fontSize: 14, textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1 },
  168. roomTypeLabel: { color: '#2E0000', fontSize: 12 },
  169. roomBottom: { flexDirection: 'row', alignItems: 'center' },
  170. userInfo: { flexDirection: 'row', alignItems: 'center', width: '45%' },
  171. userAvatar: { width: 24, height: 24, borderRadius: 2, backgroundColor: '#FFDD00', borderWidth: 1.5, borderColor: '#000', marginRight: 5 },
  172. userName: { color: '#2E0000', fontSize: 12, fontWeight: 'bold', maxWidth: 60 },
  173. goodsNum: { color: '#2E0000', fontSize: 10, width: '35%' },
  174. participantBox: { flexDirection: 'row', alignItems: 'center', width: '20%' },
  175. participantIcon: { width: 14, height: 14 },
  176. participantNum: { color: '#2E0000', fontSize: 12, marginLeft: 3 },
  177. empty: { alignItems: 'center', marginTop: 100 },
  178. emptyText: { color: '#999', fontSize: 14 },
  179. });