RecordModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. const [hasMore, setHasMore] = useState(true);
  32. useImperativeHandle(ref, () => ({
  33. show: () => {
  34. setVisible(true);
  35. refresh();
  36. },
  37. close: () => setVisible(false)
  38. }));
  39. const refresh = () => {
  40. setLastId(undefined);
  41. setData([]);
  42. setHasMore(true);
  43. loadData(true);
  44. loadStats();
  45. };
  46. const loadStats = async () => {
  47. try {
  48. const resA = await countRecordsAfterLastLevel({ levelEnumList: ['A'], poolId });
  49. setTaggingA(resA.data || 0); // Check API structure: {data: number}
  50. const resB = await countRecordsAfterLastLevel({ levelEnumList: ['B'], poolId });
  51. setTaggingB(resB.data || 0);
  52. } catch (e) {
  53. console.error('Failed to load stats', e);
  54. }
  55. };
  56. const loadData = async (isRefresh = false) => {
  57. if (loading || (!isRefresh && !hasMore)) return;
  58. setLoading(true);
  59. try {
  60. const currentLastId = isRefresh ? undefined : lastId;
  61. const res = await getBuyRecord(poolId, currentLastId, activeTab.value as any);
  62. if (res && res.length > 0) {
  63. setData(prev => {
  64. if (isRefresh) return res;
  65. // Deduplicate based on ID
  66. const existingIds = new Set(prev.map(item => item.id));
  67. const newItems = res.filter(item => !existingIds.has(item.id));
  68. return [...prev, ...newItems];
  69. });
  70. setLastId(res[res.length - 1].id);
  71. // Assuming page size is 10 or 20.
  72. } else {
  73. setHasMore(false);
  74. }
  75. if (res && res.length < 10) {
  76. setHasMore(false);
  77. }
  78. } catch (e) {
  79. console.error('Failed to load records', e);
  80. }
  81. setLoading(false);
  82. setRefreshing(false);
  83. };
  84. const handleTabChange = (tab: any) => {
  85. setActiveTab(tab);
  86. // Reset and reload
  87. setLastId(undefined);
  88. setData([]);
  89. setHasMore(true);
  90. };
  91. useEffect(() => {
  92. if (visible) {
  93. setLastId(undefined);
  94. setData([]);
  95. setHasMore(true);
  96. loadData(true);
  97. }
  98. }, [activeTab]);
  99. const renderItem = ({ item }: { item: any }) => (
  100. <View style={styles.item}>
  101. <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
  102. <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
  103. {/* Level Icon */}
  104. <View style={[styles.levelTag, { borderColor: LEVEL_MAP[item.level]?.color || '#000' }]}>
  105. <Text style={[styles.levelText, { color: LEVEL_MAP[item.level]?.color || '#000' }]}>
  106. {LEVEL_MAP[item.level]?.title}
  107. </Text>
  108. </View>
  109. <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
  110. <Text style={styles.countText}>×{item.lastCount}</Text>
  111. <Image source={{ uri: item.cover }} style={styles.itemImage} />
  112. </View>
  113. );
  114. return (
  115. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  116. <View style={styles.overlay}>
  117. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
  118. <ImageBackground
  119. source={{ uri: Images.box.detail.recordBg }}
  120. style={styles.container}
  121. resizeMode="stretch"
  122. >
  123. {/* Header */}
  124. <View style={styles.header}>
  125. <Text style={styles.title}>购买记录</Text>
  126. <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
  127. {/* Close Icon or Text */}
  128. <Text style={{ fontSize: 20 }}>×</Text>
  129. </TouchableOpacity>
  130. </View>
  131. {/* Stats Banner */}
  132. <ImageBackground
  133. source={{ uri: Images.box.detail.taggingBg }}
  134. style={styles.statsBanner}
  135. resizeMode="stretch"
  136. >
  137. <View style={styles.statItem}>
  138. <Text style={styles.statNum}>{taggingA}</Text>
  139. <Text style={styles.statLabel}>发未出</Text>
  140. <Image source={{ uri: LEVEL_MAP.A.titleText }} style={{ width: 45, height: 16 }} contentFit="contain" />
  141. </View>
  142. <View style={styles.statItem}>
  143. <Text style={styles.statNum}>{taggingB}</Text>
  144. <Text style={styles.statLabel}>发未出</Text>
  145. <Image source={{ uri: LEVEL_MAP.B.titleText }} style={{ width: 45, height: 16 }} contentFit="contain" />
  146. </View>
  147. </ImageBackground>
  148. {/* Tabs */}
  149. <View style={styles.tabs}>
  150. {TABS.map(tab => (
  151. <TouchableOpacity
  152. key={tab.value}
  153. style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
  154. onPress={() => handleTabChange(tab)}
  155. >
  156. <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
  157. {tab.title}
  158. </Text>
  159. </TouchableOpacity>
  160. ))}
  161. </View>
  162. {/* List */}
  163. <FlatList
  164. data={data}
  165. renderItem={renderItem}
  166. keyExtractor={(item, index) => (item.id ? `${item.id}_${index}` : String(index))}
  167. style={styles.list}
  168. contentContainerStyle={{ paddingBottom: 20 }}
  169. onEndReached={() => loadData(false)}
  170. onEndReachedThreshold={0.5}
  171. ListEmptyComponent={
  172. !loading ? <Text style={styles.emptyText}>暂无记录</Text> : null
  173. }
  174. ListFooterComponent={loading ? <ActivityIndicator color="#000" /> : null}
  175. />
  176. </ImageBackground>
  177. </View>
  178. </Modal>
  179. );
  180. });
  181. const styles = StyleSheet.create({
  182. overlay: {
  183. flex: 1,
  184. backgroundColor: 'rgba(0,0,0,0.5)',
  185. justifyContent: 'flex-end',
  186. },
  187. mask: {
  188. flex: 1,
  189. },
  190. container: {
  191. height: height * 0.7,
  192. paddingTop: 0,
  193. backgroundColor: '#fff',
  194. borderTopLeftRadius: 15,
  195. borderTopRightRadius: 15,
  196. overflow: 'hidden',
  197. },
  198. header: {
  199. height: 50,
  200. justifyContent: 'center',
  201. alignItems: 'center',
  202. marginTop: 10,
  203. },
  204. title: {
  205. fontSize: 18,
  206. fontWeight: 'bold',
  207. },
  208. closeBtn: {
  209. position: 'absolute',
  210. right: 20,
  211. top: 15,
  212. width: 30,
  213. height: 30,
  214. alignItems: 'center',
  215. justifyContent: 'center',
  216. backgroundColor: '#eee',
  217. borderRadius: 15,
  218. },
  219. statsBanner: {
  220. width: '90%',
  221. alignSelf: 'center',
  222. height: 50,
  223. flexDirection: 'row',
  224. justifyContent: 'space-around',
  225. alignItems: 'center',
  226. marginBottom: 10,
  227. },
  228. statItem: {
  229. flexDirection: 'row',
  230. alignItems: 'center',
  231. },
  232. statNum: {
  233. fontSize: 24,
  234. fontWeight: '900',
  235. color: '#000',
  236. textShadowColor: '#fff',
  237. textShadowOffset: { width: 1, height: 1 },
  238. textShadowRadius: 1,
  239. },
  240. statLabel: {
  241. fontSize: 12,
  242. color: '#928D81',
  243. fontWeight: '300',
  244. marginHorizontal: 8
  245. },
  246. statLevel: { fontSize: 12, fontWeight: 'bold' },
  247. tabs: {
  248. flexDirection: 'row',
  249. justifyContent: 'space-around',
  250. paddingHorizontal: 10,
  251. marginBottom: 10,
  252. backgroundColor: 'rgba(255,255,255,0.1)',
  253. paddingVertical: 5,
  254. },
  255. tab: {
  256. paddingVertical: 5,
  257. paddingHorizontal: 10,
  258. borderRadius: 5,
  259. backgroundColor: '#eee',
  260. },
  261. activeTab: {
  262. backgroundColor: '#FEC433',
  263. },
  264. tabText: { fontSize: 12, color: '#666' },
  265. activeTabText: { color: '#000', fontWeight: 'bold' },
  266. list: {
  267. flex: 1,
  268. paddingHorizontal: 15,
  269. },
  270. item: {
  271. flexDirection: 'row',
  272. alignItems: 'center',
  273. backgroundColor: '#FFF7E3',
  274. marginBottom: 10,
  275. padding: 5,
  276. borderRadius: 8,
  277. height: 60,
  278. },
  279. avatar: { width: 40, height: 40, borderRadius: 20 },
  280. nickname: { flex: 1, marginLeft: 10, fontSize: 12, color: '#333' },
  281. levelTag: {
  282. borderWidth: 1,
  283. paddingHorizontal: 4,
  284. borderRadius: 4,
  285. marginHorizontal: 5
  286. },
  287. levelText: { fontSize: 10, fontWeight: 'bold' },
  288. itemName: { width: 80, fontSize: 12, color: '#FEC433', marginHorizontal: 5 },
  289. countText: { fontSize: 12, color: '#000', fontWeight: 'bold', marginRight: 5 },
  290. itemImage: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#fff' },
  291. emptyText: { textAlign: 'center', marginTop: 20, color: '#999' }
  292. });