RecordModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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 => isRefresh ? res : [...prev, ...res]);
  64. setLastId(res[res.length - 1].id);
  65. // Assuming page size is 10 or 20. If less than that, no more data.
  66. // Let's assume default is 10. If strictly < 10, definitely no more.
  67. // If == 10, maybe more.
  68. // Safer: if 0 returned, handled by outside if.
  69. } else {
  70. setHasMore(false);
  71. }
  72. if (res && res.length < 10) {
  73. setHasMore(false);
  74. }
  75. } catch (e) {
  76. console.error('Failed to load records', e);
  77. }
  78. setLoading(false);
  79. setRefreshing(false);
  80. };
  81. const handleTabChange = (tab: any) => {
  82. setActiveTab(tab);
  83. // Reset and reload
  84. setLastId(undefined);
  85. setData([]);
  86. setHasMore(true);
  87. // We can't immediately call loadData due to state update async nature,
  88. // but let's assume useEffect or immediate call with arg works.
  89. // Actually best to useEffect on activeTab change?
  90. // Or specific effect for tab
  91. // Let's do explicit reload in effect or here
  92. // setData is sync-ish in RN usually batching, let's use effect.
  93. };
  94. useEffect(() => {
  95. if (visible) {
  96. setLastId(undefined);
  97. setData([]);
  98. setHasMore(true);
  99. loadData(true);
  100. }
  101. }, [activeTab]);
  102. const renderItem = ({ item }: { item: any }) => (
  103. <View style={styles.item}>
  104. <Image source={{ uri: item.avatar || Images.common.defaultAvatar }} style={styles.avatar} />
  105. <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
  106. {/* Level Icon - Assuming we have images for level text similar to legacy */}
  107. {/* Legacy: :src="LEVEL_MAP[item.level].titleText" */}
  108. {/* We don't have titleText in our config yet, need to map or use text */}
  109. {/* For now use Text with color */}
  110. <View style={[styles.levelTag, { borderColor: LEVEL_MAP[item.level]?.color || '#000' }]}>
  111. <Text style={[styles.levelText, { color: LEVEL_MAP[item.level]?.color || '#000' }]}>
  112. {LEVEL_MAP[item.level]?.title}
  113. </Text>
  114. </View>
  115. <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
  116. <Text style={styles.countText}>×{item.lastCount}</Text>
  117. <Image source={{ uri: item.cover }} style={styles.itemImage} />
  118. </View>
  119. );
  120. return (
  121. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  122. <View style={styles.overlay}>
  123. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
  124. <ImageBackground
  125. source={{ uri: Images.box.detail.recordBg }}
  126. style={styles.container}
  127. resizeMode="stretch"
  128. >
  129. {/* Header */}
  130. <View style={styles.header}>
  131. <Text style={styles.title}>购买记录</Text>
  132. <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
  133. {/* Close Icon or Text */}
  134. <Text style={{ fontSize: 20 }}>×</Text>
  135. </TouchableOpacity>
  136. </View>
  137. {/* Stats Banner */}
  138. <ImageBackground
  139. source={{ uri: Images.box.detail.taggingBg }}
  140. style={styles.statsBanner}
  141. resizeMode="stretch"
  142. >
  143. <View style={styles.statItem}>
  144. <Text style={styles.statNum}>{taggingA}</Text>
  145. <Text style={styles.statLabel}>发未出</Text>
  146. <Text style={[styles.statLevel, { color: LEVEL_MAP.A.color }]}>超神款</Text>
  147. </View>
  148. <View style={styles.statItem}>
  149. <Text style={styles.statNum}>{taggingB}</Text>
  150. <Text style={styles.statLabel}>发未出</Text>
  151. <Text style={[styles.statLevel, { color: LEVEL_MAP.B.color }]}>欧皇款</Text>
  152. </View>
  153. </ImageBackground>
  154. {/* Tabs */}
  155. <View style={styles.tabs}>
  156. {TABS.map(tab => (
  157. <TouchableOpacity
  158. key={tab.value}
  159. style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
  160. onPress={() => handleTabChange(tab)}
  161. >
  162. <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
  163. {tab.title}
  164. </Text>
  165. </TouchableOpacity>
  166. ))}
  167. </View>
  168. {/* List */}
  169. <FlatList
  170. data={data}
  171. renderItem={renderItem}
  172. keyExtractor={(item, index) => item.id || String(index)}
  173. style={styles.list}
  174. contentContainerStyle={{ paddingBottom: 20 }}
  175. onEndReached={() => loadData(false)}
  176. onEndReachedThreshold={0.5}
  177. ListEmptyComponent={
  178. !loading ? <Text style={styles.emptyText}>暂无记录</Text> : null
  179. }
  180. ListFooterComponent={loading ? <ActivityIndicator color="#000" /> : null}
  181. />
  182. </ImageBackground>
  183. </View>
  184. </Modal>
  185. );
  186. });
  187. const styles = StyleSheet.create({
  188. overlay: {
  189. flex: 1,
  190. backgroundColor: 'rgba(0,0,0,0.5)',
  191. justifyContent: 'flex-end',
  192. },
  193. mask: {
  194. flex: 1,
  195. },
  196. container: {
  197. height: height * 0.7,
  198. paddingTop: 0,
  199. backgroundColor: '#fff',
  200. borderTopLeftRadius: 15,
  201. borderTopRightRadius: 15,
  202. overflow: 'hidden',
  203. },
  204. header: {
  205. height: 50,
  206. justifyContent: 'center',
  207. alignItems: 'center',
  208. marginTop: 10,
  209. },
  210. title: {
  211. fontSize: 18,
  212. fontWeight: 'bold',
  213. },
  214. closeBtn: {
  215. position: 'absolute',
  216. right: 20,
  217. top: 15,
  218. width: 30,
  219. height: 30,
  220. alignItems: 'center',
  221. justifyContent: 'center',
  222. backgroundColor: '#eee',
  223. borderRadius: 15,
  224. },
  225. statsBanner: {
  226. width: '90%',
  227. alignSelf: 'center',
  228. height: 50,
  229. flexDirection: 'row',
  230. justifyContent: 'space-around',
  231. alignItems: 'center',
  232. marginBottom: 10,
  233. },
  234. statItem: {
  235. flexDirection: 'row',
  236. alignItems: 'center',
  237. },
  238. statNum: {
  239. fontSize: 16,
  240. fontWeight: 'bold',
  241. color: '#fff',
  242. textShadowColor: '#000',
  243. textShadowOffset: { width: -1, height: -1 },
  244. textShadowRadius: 1,
  245. },
  246. statLabel: {
  247. fontSize: 12,
  248. color: '#928D81',
  249. fontWeight: '300',
  250. marginHorizontal: 8
  251. },
  252. statLevel: { fontSize: 12, fontWeight: 'bold' },
  253. tabs: {
  254. flexDirection: 'row',
  255. justifyContent: 'space-around',
  256. paddingHorizontal: 10,
  257. marginBottom: 10,
  258. backgroundColor: 'rgba(255,255,255,0.1)',
  259. paddingVertical: 5,
  260. },
  261. tab: {
  262. paddingVertical: 5,
  263. paddingHorizontal: 10,
  264. borderRadius: 5,
  265. backgroundColor: '#eee',
  266. },
  267. activeTab: {
  268. backgroundColor: '#FEC433',
  269. },
  270. tabText: { fontSize: 12, color: '#666' },
  271. activeTabText: { color: '#000', fontWeight: 'bold' },
  272. list: {
  273. flex: 1,
  274. paddingHorizontal: 15,
  275. },
  276. item: {
  277. flexDirection: 'row',
  278. alignItems: 'center',
  279. backgroundColor: '#FFF7E3',
  280. marginBottom: 10,
  281. padding: 5,
  282. borderRadius: 8,
  283. height: 60,
  284. },
  285. avatar: { width: 40, height: 40, borderRadius: 20 },
  286. nickname: { flex: 1, marginLeft: 10, fontSize: 12, color: '#333' },
  287. levelTag: {
  288. borderWidth: 1,
  289. paddingHorizontal: 4,
  290. borderRadius: 4,
  291. marginHorizontal: 5
  292. },
  293. levelText: { fontSize: 10, fontWeight: 'bold' },
  294. itemName: { width: 80, fontSize: 12, color: '#FEC433', marginHorizontal: 5 },
  295. countText: { fontSize: 12, color: '#000', fontWeight: 'bold', marginRight: 5 },
  296. itemImage: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#fff' },
  297. emptyText: { textAlign: 'center', marginTop: 20, color: '#999' }
  298. });