RecordModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import { LEVEL_MAP } from '@/constants/config';
  2. import { Images } from '@/constants/images';
  3. import { countRecordsAfterLastLevel, getBuyRecord } from '@/services/award';
  4. import { Image, ImageBackground } from 'expo-image';
  5. import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
  6. import { ActivityIndicator, Dimensions, FlatList, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  7. const { width, height } = Dimensions.get('window');
  8. const TABS = [
  9. { title: '全部', value: '' },
  10. { title: '超神款', value: 'A' },
  11. { title: '欧皇款', value: 'B' },
  12. { title: '隐藏款', value: 'C' },
  13. { title: '普通款', value: 'D' }
  14. ];
  15. interface RecordModalProps {
  16. poolId: string;
  17. }
  18. export interface RecordModalRef {
  19. show: () => void;
  20. close: () => void;
  21. }
  22. export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(({ poolId }, ref) => {
  23. const [visible, setVisible] = useState(false);
  24. const [data, setData] = useState<any[]>([]);
  25. const [activeTab, setActiveTab] = useState(TABS[0]);
  26. const [loading, setLoading] = useState(false);
  27. const [refreshing, setRefreshing] = useState(false);
  28. const [lastId, setLastId] = useState<string | undefined>(undefined);
  29. const [taggingA, setTaggingA] = useState(0);
  30. const [taggingB, setTaggingB] = useState(0);
  31. useImperativeHandle(ref, () => ({
  32. show: () => {
  33. setVisible(true);
  34. refresh();
  35. },
  36. close: () => setVisible(false)
  37. }));
  38. const refresh = () => {
  39. setLastId(undefined);
  40. setData([]);
  41. loadData(true);
  42. loadStats();
  43. };
  44. const loadStats = async () => {
  45. try {
  46. const resA = await countRecordsAfterLastLevel({ levelEnumList: ['A'], poolId });
  47. setTaggingA(resA.data || 0); // Check API structure: {data: number}
  48. const resB = await countRecordsAfterLastLevel({ levelEnumList: ['B'], poolId });
  49. setTaggingB(resB.data || 0);
  50. } catch (e) {
  51. console.error('Failed to load stats', e);
  52. }
  53. };
  54. const loadData = async (isRefresh = false) => {
  55. if (loading) return;
  56. setLoading(true);
  57. try {
  58. const currentLastId = isRefresh ? undefined : lastId;
  59. const res = await getBuyRecord(poolId, currentLastId, activeTab.value as any);
  60. if (res && res.length > 0) {
  61. setData(prev => isRefresh ? res : [...prev, ...res]);
  62. setLastId(res[res.length - 1].id);
  63. }
  64. } catch (e) {
  65. console.error('Failed to load records', e);
  66. }
  67. setLoading(false);
  68. setRefreshing(false);
  69. };
  70. const handleTabChange = (tab: any) => {
  71. setActiveTab(tab);
  72. // Reset and reload
  73. setLastId(undefined);
  74. setData([]);
  75. // We can't immediately call loadData due to state update async nature,
  76. // but let's assume useEffect or immediate call with arg works.
  77. // Actually best to useEffect on activeTab change?
  78. // Or specific effect for tab
  79. // Let's do explicit reload in effect or here
  80. // setData is sync-ish in RN usually batching, let's use effect.
  81. };
  82. useEffect(() => {
  83. if (visible) {
  84. setLastId(undefined);
  85. setData([]);
  86. loadData(true);
  87. }
  88. }, [activeTab]);
  89. const renderItem = ({ item }: { item: any }) => (
  90. <View style={styles.item}>
  91. <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
  92. <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
  93. {/* Level Icon - Assuming we have images for level text similar to legacy */}
  94. {/* Legacy: :src="LEVEL_MAP[item.level].titleText" */}
  95. {/* We don't have titleText in our config yet, need to map or use text */}
  96. {/* For now use Text with color */}
  97. <View style={[styles.levelTag, { borderColor: LEVEL_MAP[item.level]?.color || '#000' }]}>
  98. <Text style={[styles.levelText, { color: LEVEL_MAP[item.level]?.color || '#000' }]}>
  99. {LEVEL_MAP[item.level]?.title}
  100. </Text>
  101. </View>
  102. <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
  103. <Text style={styles.countText}>×{item.lastCount}</Text>
  104. <Image source={{ uri: item.cover }} style={styles.itemImage} />
  105. </View>
  106. );
  107. return (
  108. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  109. <View style={styles.overlay}>
  110. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
  111. <ImageBackground
  112. source={{ uri: Images.box.detail.recordBg }}
  113. style={styles.container}
  114. resizeMode="stretch"
  115. >
  116. {/* Header */}
  117. <View style={styles.header}>
  118. <Text style={styles.title}>购买记录</Text>
  119. <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
  120. {/* Close Icon or Text */}
  121. <Text style={{ fontSize: 20 }}>×</Text>
  122. </TouchableOpacity>
  123. </View>
  124. {/* Stats Banner */}
  125. <ImageBackground
  126. source={{ uri: Images.box.detail.taggingBg }}
  127. style={styles.statsBanner}
  128. resizeMode="stretch"
  129. >
  130. <View style={styles.statItem}>
  131. <Text style={styles.statNum}>{taggingA}</Text>
  132. <Text style={styles.statLabel}>发未出</Text>
  133. <Text style={[styles.statLevel, { color: LEVEL_MAP.A.color }]}>超神款</Text>
  134. </View>
  135. <View style={styles.statItem}>
  136. <Text style={styles.statNum}>{taggingB}</Text>
  137. <Text style={styles.statLabel}>发未出</Text>
  138. <Text style={[styles.statLevel, { color: LEVEL_MAP.B.color }]}>欧皇款</Text>
  139. </View>
  140. </ImageBackground>
  141. {/* Tabs */}
  142. <View style={styles.tabs}>
  143. {TABS.map(tab => (
  144. <TouchableOpacity
  145. key={tab.value}
  146. style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
  147. onPress={() => handleTabChange(tab)}
  148. >
  149. <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
  150. {tab.title}
  151. </Text>
  152. </TouchableOpacity>
  153. ))}
  154. </View>
  155. {/* List */}
  156. <FlatList
  157. data={data}
  158. renderItem={renderItem}
  159. keyExtractor={(item, index) => item.id || String(index)}
  160. style={styles.list}
  161. contentContainerStyle={{ paddingBottom: 20 }}
  162. onEndReached={() => loadData(false)}
  163. onEndReachedThreshold={0.5}
  164. ListEmptyComponent={
  165. !loading ? <Text style={styles.emptyText}>暂无记录</Text> : null
  166. }
  167. ListFooterComponent={loading ? <ActivityIndicator color="#000" /> : null}
  168. />
  169. </ImageBackground>
  170. </View>
  171. </Modal>
  172. );
  173. });
  174. const styles = StyleSheet.create({
  175. overlay: {
  176. flex: 1,
  177. backgroundColor: 'rgba(0,0,0,0.5)',
  178. justifyContent: 'flex-end',
  179. },
  180. mask: {
  181. flex: 1,
  182. },
  183. container: {
  184. height: height * 0.7,
  185. paddingTop: 0,
  186. backgroundColor: '#fff',
  187. borderTopLeftRadius: 15,
  188. borderTopRightRadius: 15,
  189. overflow: 'hidden',
  190. },
  191. header: {
  192. height: 50,
  193. justifyContent: 'center',
  194. alignItems: 'center',
  195. marginTop: 10,
  196. },
  197. title: {
  198. fontSize: 18,
  199. fontWeight: 'bold',
  200. },
  201. closeBtn: {
  202. position: 'absolute',
  203. right: 20,
  204. top: 15,
  205. width: 30,
  206. height: 30,
  207. alignItems: 'center',
  208. justifyContent: 'center',
  209. backgroundColor: '#eee',
  210. borderRadius: 15,
  211. },
  212. statsBanner: {
  213. width: '90%',
  214. alignSelf: 'center',
  215. height: 50,
  216. flexDirection: 'row',
  217. justifyContent: 'space-around',
  218. alignItems: 'center',
  219. marginBottom: 10,
  220. },
  221. statItem: {
  222. flexDirection: 'row',
  223. alignItems: 'center',
  224. },
  225. statNum: { fontSize: 16, fontWeight: 'bold', color: '#fff', textShadowColor:'#000', textShadowRadius:1 },
  226. statLabel: { fontSize: 12, color: '#ddd', marginHorizontal: 5 },
  227. statLevel: { fontSize: 12, fontWeight: 'bold' },
  228. tabs: {
  229. flexDirection: 'row',
  230. justifyContent: 'space-around',
  231. paddingHorizontal: 10,
  232. marginBottom: 10,
  233. backgroundColor: 'rgba(255,255,255,0.1)',
  234. paddingVertical: 5,
  235. },
  236. tab: {
  237. paddingVertical: 5,
  238. paddingHorizontal: 10,
  239. borderRadius: 5,
  240. backgroundColor: '#eee',
  241. },
  242. activeTab: {
  243. backgroundColor: '#FEC433',
  244. },
  245. tabText: { fontSize: 12, color: '#666' },
  246. activeTabText: { color: '#000', fontWeight: 'bold' },
  247. list: {
  248. flex: 1,
  249. paddingHorizontal: 15,
  250. },
  251. item: {
  252. flexDirection: 'row',
  253. alignItems: 'center',
  254. backgroundColor: '#FFF7E3',
  255. marginBottom: 10,
  256. padding: 5,
  257. borderRadius: 8,
  258. height: 60,
  259. },
  260. avatar: { width: 40, height: 40, borderRadius: 20 },
  261. nickname: { flex: 1, marginLeft: 10, fontSize: 12, color: '#333' },
  262. levelTag: {
  263. borderWidth: 1,
  264. paddingHorizontal: 4,
  265. borderRadius: 4,
  266. marginHorizontal: 5
  267. },
  268. levelText: { fontSize: 10, fontWeight: 'bold' },
  269. itemName: { width: 80, fontSize: 12, color: '#FEC433', marginHorizontal: 5 },
  270. countText: { fontSize: 12, color: '#000', fontWeight: 'bold', marginRight: 5 },
  271. itemImage: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#fff' },
  272. emptyText: { textAlign: 'center', marginTop: 20, color: '#999' }
  273. });