RecordModal.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { Image } from 'expo-image';
  2. import React, { forwardRef, useImperativeHandle, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. FlatList,
  6. Modal,
  7. StyleSheet,
  8. Text,
  9. TouchableOpacity,
  10. View,
  11. } from 'react-native';
  12. import { getBuyRecord } from '@/services/award';
  13. interface RecordItem {
  14. id: string;
  15. nickname: string;
  16. avatar: string;
  17. goodsName: string;
  18. cover: string; // API 返回的是 cover 字段
  19. level: string;
  20. createTime: string;
  21. }
  22. interface RecordModalProps {
  23. poolId: string;
  24. }
  25. export interface RecordModalRef {
  26. show: () => void;
  27. close: () => void;
  28. }
  29. const LEVEL_MAP: Record<string, string> = {
  30. A: '超神款',
  31. B: '欧皇款',
  32. C: '隐藏款',
  33. D: '普通款',
  34. };
  35. export const RecordModal = forwardRef<RecordModalRef, RecordModalProps>(
  36. ({ poolId }, ref) => {
  37. const [visible, setVisible] = useState(false);
  38. const [loading, setLoading] = useState(false);
  39. const [records, setRecords] = useState<RecordItem[]>([]);
  40. const [levelFilter, setLevelFilter] = useState<number | undefined>();
  41. useImperativeHandle(ref, () => ({
  42. show: () => {
  43. setVisible(true);
  44. loadRecords();
  45. },
  46. close: () => setVisible(false),
  47. }));
  48. const loadRecords = async (level?: number) => {
  49. setLoading(true);
  50. try {
  51. const res = await getBuyRecord(poolId, undefined, level);
  52. setRecords(res || []);
  53. } catch (error) {
  54. console.error('加载记录失败:', error);
  55. }
  56. setLoading(false);
  57. };
  58. const handleLevelFilter = (level?: number) => {
  59. setLevelFilter(level);
  60. loadRecords(level);
  61. };
  62. const renderItem = ({ item }: { item: RecordItem }) => (
  63. <View style={styles.recordItem}>
  64. <Image
  65. source={{ uri: item.avatar }}
  66. style={styles.avatar}
  67. contentFit="cover"
  68. />
  69. <View style={styles.recordInfo}>
  70. <Text style={styles.nickname} numberOfLines={1}>{item.nickname}</Text>
  71. <Text style={styles.time}>{item.createTime}</Text>
  72. </View>
  73. <View style={styles.goodsInfo}>
  74. <Image
  75. source={{ uri: item.cover }}
  76. style={styles.goodsImage}
  77. contentFit="cover"
  78. />
  79. <View style={styles.goodsDetail}>
  80. <Text style={styles.goodsName} numberOfLines={1}>{item.goodsName}</Text>
  81. <Text style={styles.levelText}>{LEVEL_MAP[item.level] || item.level}</Text>
  82. </View>
  83. </View>
  84. </View>
  85. );
  86. const levels = [
  87. { label: '全部', value: undefined },
  88. { label: '超神款', value: 1 },
  89. { label: '欧皇款', value: 2 },
  90. { label: '隐藏款', value: 3 },
  91. { label: '普通款', value: 4 },
  92. ];
  93. return (
  94. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  95. <View style={styles.overlay}>
  96. <View style={styles.container}>
  97. <View style={styles.header}>
  98. <Text style={styles.title}>购买记录</Text>
  99. <TouchableOpacity onPress={() => setVisible(false)} style={styles.closeBtn}>
  100. <Text style={styles.closeText}>×</Text>
  101. </TouchableOpacity>
  102. </View>
  103. <View style={styles.filterRow}>
  104. {levels.map((item) => (
  105. <TouchableOpacity
  106. key={item.label}
  107. style={[styles.filterBtn, levelFilter === item.value && styles.filterBtnActive]}
  108. onPress={() => handleLevelFilter(item.value)}
  109. >
  110. <Text style={[styles.filterText, levelFilter === item.value && styles.filterTextActive]}>
  111. {item.label}
  112. </Text>
  113. </TouchableOpacity>
  114. ))}
  115. </View>
  116. {loading ? (
  117. <View style={styles.loadingBox}>
  118. <ActivityIndicator size="large" color="#ff6600" />
  119. </View>
  120. ) : (
  121. <FlatList
  122. data={records}
  123. renderItem={renderItem}
  124. keyExtractor={(item, index) => item.id || index.toString()}
  125. contentContainerStyle={styles.listContent}
  126. ListEmptyComponent={
  127. <View style={styles.emptyBox}>
  128. <Text style={styles.emptyText}>暂无记录</Text>
  129. </View>
  130. }
  131. />
  132. )}
  133. </View>
  134. </View>
  135. </Modal>
  136. );
  137. }
  138. );
  139. const styles = StyleSheet.create({
  140. overlay: {
  141. flex: 1,
  142. backgroundColor: 'rgba(0,0,0,0.5)',
  143. justifyContent: 'flex-end',
  144. },
  145. container: {
  146. backgroundColor: '#fff',
  147. borderTopLeftRadius: 20,
  148. borderTopRightRadius: 20,
  149. height: '70%',
  150. },
  151. header: {
  152. flexDirection: 'row',
  153. alignItems: 'center',
  154. justifyContent: 'center',
  155. padding: 15,
  156. borderBottomWidth: 1,
  157. borderBottomColor: '#eee',
  158. },
  159. title: { fontSize: 16, fontWeight: '600', color: '#333' },
  160. closeBtn: { position: 'absolute', right: 15, top: 10 },
  161. closeText: { fontSize: 24, color: '#999' },
  162. filterRow: {
  163. flexDirection: 'row',
  164. paddingHorizontal: 10,
  165. paddingVertical: 10,
  166. borderBottomWidth: 1,
  167. borderBottomColor: '#eee',
  168. },
  169. filterBtn: {
  170. paddingHorizontal: 12,
  171. paddingVertical: 6,
  172. borderRadius: 15,
  173. marginRight: 8,
  174. backgroundColor: '#f5f5f5',
  175. },
  176. filterBtnActive: { backgroundColor: '#ff6600' },
  177. filterText: { fontSize: 12, color: '#666' },
  178. filterTextActive: { color: '#fff' },
  179. listContent: { padding: 15 },
  180. recordItem: {
  181. flexDirection: 'row',
  182. alignItems: 'center',
  183. paddingVertical: 12,
  184. borderBottomWidth: 1,
  185. borderBottomColor: '#f5f5f5',
  186. },
  187. avatar: { width: 40, height: 40, borderRadius: 20 },
  188. recordInfo: { flex: 1, marginLeft: 10 },
  189. nickname: { fontSize: 14, color: '#333' },
  190. time: { fontSize: 11, color: '#999', marginTop: 2 },
  191. goodsInfo: { flexDirection: 'row', alignItems: 'center' },
  192. goodsImage: { width: 50, height: 50, borderRadius: 5 },
  193. goodsDetail: { marginLeft: 8, alignItems: 'flex-end' },
  194. goodsName: { fontSize: 12, color: '#333', maxWidth: 80 },
  195. levelText: { fontSize: 11, color: '#ff6600', marginTop: 2 },
  196. loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  197. emptyBox: { alignItems: 'center', paddingVertical: 50 },
  198. emptyText: { color: '#999', fontSize: 14 },
  199. });