BoxSelectionModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import { Images } from '@/constants/images';
  2. import { getBoxList } from '@/services/award';
  3. import { Image, ImageBackground } from 'expo-image';
  4. import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
  5. import {
  6. ActivityIndicator,
  7. Dimensions,
  8. FlatList,
  9. Modal,
  10. StyleSheet,
  11. Text,
  12. TouchableOpacity,
  13. View
  14. } from 'react-native';
  15. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  16. interface BoxSelectionModalProps {
  17. poolId: string;
  18. onSelect: (box: any) => void;
  19. }
  20. export interface BoxSelectionModalRef {
  21. show: () => void;
  22. close: () => void;
  23. }
  24. const TABS = [
  25. { title: '全部', value: '' },
  26. { title: '超神款', value: 'A' },
  27. { title: '欧皇款', value: 'B' },
  28. { title: '隐藏款', value: 'C' },
  29. { title: '普通款', value: 'D' },
  30. ];
  31. export const BoxSelectionModal = forwardRef<BoxSelectionModalRef, BoxSelectionModalProps>(
  32. ({ poolId, onSelect }, ref) => {
  33. const [visible, setVisible] = useState(false);
  34. const [activeTab, setActiveTab] = useState(TABS[0]);
  35. const [list, setList] = useState<any[]>([]);
  36. const [loading, setLoading] = useState(false);
  37. const [refreshing, setRefreshing] = useState(false);
  38. const [page, setPage] = useState(1);
  39. const [hasMore, setHasMore] = useState(true);
  40. const loadData = useCallback(async (pageNum = 1, tabValue = '', isRefresh = false) => {
  41. if (isRefresh) setRefreshing(true);
  42. if (pageNum === 1 && !isRefresh) setLoading(true);
  43. try {
  44. const res = await getBoxList(poolId, tabValue as any, pageNum, 20); // Using 20 as pageSize
  45. const newData = res?.records || []; // Assuming standard paginated response
  46. if (pageNum === 1) {
  47. setList(newData);
  48. } else {
  49. setList(prev => [...prev, ...newData]);
  50. }
  51. setHasMore(newData.length >= 20);
  52. setPage(pageNum);
  53. } catch (error) {
  54. console.error('Failed to load boxes:', error);
  55. } finally {
  56. setLoading(false);
  57. setRefreshing(false);
  58. }
  59. }, [poolId]);
  60. useImperativeHandle(ref, () => ({
  61. show: () => {
  62. setVisible(true);
  63. setActiveTab(TABS[0]);
  64. loadData(1, TABS[0].value);
  65. },
  66. close: () => {
  67. setVisible(false);
  68. },
  69. }));
  70. const handleTabChange = (tab: typeof TABS[0]) => {
  71. setActiveTab(tab);
  72. loadData(1, tab.value);
  73. };
  74. const handleLoadMore = () => {
  75. if (!loading && hasMore) {
  76. loadData(page + 1, activeTab.value);
  77. }
  78. };
  79. const handleRefresh = () => {
  80. loadData(1, activeTab.value, true);
  81. }
  82. const renderItem = ({ item, index }: { item: any; index: number }) => {
  83. if (item.leftQuantity <= 0) return null;
  84. return (
  85. <TouchableOpacity
  86. style={styles.item}
  87. onPress={() => {
  88. setVisible(false);
  89. onSelect(item);
  90. }}
  91. >
  92. <View style={styles.itemIndex}>
  93. <Text style={styles.indexText}>{index + 1}</Text>
  94. </View>
  95. <View style={styles.itemContent}>
  96. {/* Left Icon & Count */}
  97. <View style={styles.leftSection}>
  98. <Image source={{ uri: Images.box.detail.boxIcon }} style={styles.boxIcon} contentFit="contain"/>
  99. <Text style={styles.leftText}>剩{item.leftQuantity}发</Text>
  100. </View>
  101. <View style={styles.divider} />
  102. {/* Breakdown */}
  103. <View style={styles.breakdown}>
  104. <BreakdownItem label="A" current={item.leftQuantityA} total={item.quantityA} icon={Images.box.detail.levelTextA} />
  105. <BreakdownItem label="B" current={item.leftQuantityB} total={item.quantityB} icon={Images.box.detail.levelTextB} />
  106. <BreakdownItem label="C" current={item.leftQuantityC} total={item.quantityC} icon={Images.box.detail.levelTextC} />
  107. <BreakdownItem label="D" current={item.leftQuantityD} total={item.quantityD} icon={Images.box.detail.levelTextD} />
  108. </View>
  109. </View>
  110. </TouchableOpacity>
  111. );
  112. };
  113. return (
  114. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  115. <View style={styles.overlay}>
  116. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
  117. <ImageBackground
  118. source={{ uri: Images.box.detail.recordBg }}
  119. style={styles.container}
  120. resizeMode="stretch"
  121. >
  122. {/* Header */}
  123. <View style={styles.header}>
  124. <Image source={{ uri: Images.box.detail.recordTitleLeft }} style={styles.titleDecor} contentFit="contain" />
  125. <Text style={styles.title}>换盒</Text>
  126. <Image source={{ uri: Images.box.detail.recordTitleRight }} style={styles.titleDecor} contentFit="contain" />
  127. <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
  128. <Text style={styles.closeText}>×</Text>
  129. </TouchableOpacity>
  130. </View>
  131. {/* Tabs */}
  132. <View style={styles.tabs}>
  133. {TABS.map(tab => (
  134. <TouchableOpacity
  135. key={tab.value}
  136. style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
  137. onPress={() => handleTabChange(tab)}
  138. >
  139. <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
  140. {tab.title}
  141. </Text>
  142. </TouchableOpacity>
  143. ))}
  144. </View>
  145. {/* List */}
  146. <View style={styles.listContainer}>
  147. <FlatList
  148. data={list}
  149. renderItem={renderItem}
  150. keyExtractor={(item, index) => item.id || String(index)}
  151. contentContainerStyle={styles.listContent}
  152. onEndReached={handleLoadMore}
  153. onEndReachedThreshold={0.5}
  154. refreshing={refreshing}
  155. onRefresh={handleRefresh}
  156. ListEmptyComponent={
  157. !loading ? <Text style={styles.emptyText}>暂无数据</Text> : null
  158. }
  159. ListFooterComponent={loading && !refreshing ? <ActivityIndicator /> : null}
  160. />
  161. </View>
  162. </ImageBackground>
  163. </View>
  164. </Modal>
  165. );
  166. }
  167. );
  168. const BreakdownItem = ({ label, current, total, icon }: { label: string, current: number, total: number, icon: string }) => (
  169. <View style={styles.breakdownItem}>
  170. <Image source={{ uri: icon }} style={styles.levelIcon} contentFit="contain" />
  171. <Text style={styles.breakdownText}>
  172. <Text style={styles.currentNum}>{current}</Text>/{total}
  173. </Text>
  174. </View>
  175. );
  176. const styles = StyleSheet.create({
  177. overlay: {
  178. flex: 1,
  179. backgroundColor: 'rgba(0,0,0,0.5)',
  180. justifyContent: 'flex-end',
  181. },
  182. mask: { flex: 1 },
  183. container: {
  184. height: 600, // wrapperWidth rpx in Vue
  185. paddingTop: 15,
  186. borderTopLeftRadius: 15,
  187. borderTopRightRadius: 15,
  188. overflow: 'hidden',
  189. backgroundColor: '#fff', // Fallback
  190. },
  191. header: {
  192. flexDirection: 'row',
  193. alignItems: 'center',
  194. justifyContent: 'center',
  195. marginBottom: 15,
  196. position: 'relative',
  197. height: 44,
  198. },
  199. title: {
  200. fontSize: 18,
  201. fontWeight: 'bold',
  202. color: '#fff',
  203. marginHorizontal: 10,
  204. textShadowColor: '#000',
  205. textShadowOffset: { width: 1, height: 1 },
  206. textShadowRadius: 1,
  207. },
  208. titleDecor: {
  209. width: 17,
  210. height: 19,
  211. },
  212. closeBtn: {
  213. position: 'absolute',
  214. right: 15,
  215. top: 0,
  216. width: 24,
  217. height: 24,
  218. backgroundColor: '#ebebeb',
  219. borderRadius: 12,
  220. justifyContent: 'center',
  221. alignItems: 'center',
  222. },
  223. closeText: {
  224. fontSize: 18,
  225. color: '#a2a2a2',
  226. marginTop: -2,
  227. },
  228. tabs: {
  229. flexDirection: 'row',
  230. justifyContent: 'space-around',
  231. paddingHorizontal: 15,
  232. marginBottom: 10,
  233. },
  234. tab: {
  235. paddingVertical: 6,
  236. paddingHorizontal: 12,
  237. borderRadius: 15,
  238. backgroundColor: '#f5f5f5',
  239. },
  240. activeTab: {
  241. backgroundColor: '#ffdb4d',
  242. },
  243. tabText: {
  244. fontSize: 12,
  245. color: '#666',
  246. },
  247. activeTabText: {
  248. color: '#333',
  249. fontWeight: 'bold',
  250. },
  251. listContainer: {
  252. flex: 1,
  253. backgroundColor: '#f3f3f3',
  254. marginHorizontal: 15,
  255. marginBottom: 20,
  256. borderWidth: 2,
  257. borderColor: '#000',
  258. padding: 10,
  259. },
  260. listContent: {
  261. paddingBottom: 20,
  262. },
  263. item: {
  264. backgroundColor: '#fff',
  265. borderWidth: 3,
  266. borderColor: '#000',
  267. marginBottom: 10,
  268. flexDirection: 'row',
  269. height: 80,
  270. position: 'relative',
  271. overflow: 'hidden',
  272. },
  273. itemIndex: {
  274. position: 'absolute',
  275. left: 0,
  276. top: 0,
  277. width: 22,
  278. height: 22,
  279. backgroundColor: '#fff',
  280. borderRightWidth: 2,
  281. borderBottomWidth: 2,
  282. borderColor: '#000',
  283. justifyContent: 'center',
  284. alignItems: 'center',
  285. zIndex: 1,
  286. },
  287. indexText: {
  288. fontSize: 12,
  289. fontWeight: 'bold',
  290. },
  291. itemContent: {
  292. flex: 1,
  293. flexDirection: 'row',
  294. alignItems: 'center',
  295. paddingHorizontal: 10,
  296. paddingLeft: 22, // Space for index
  297. },
  298. leftSection: {
  299. width: 60,
  300. alignItems: 'center',
  301. justifyContent: 'center',
  302. },
  303. boxIcon: {
  304. width: 24,
  305. height: 24,
  306. marginBottom: 2,
  307. },
  308. leftText: {
  309. fontSize: 10,
  310. color: '#333',
  311. },
  312. divider: {
  313. width: 1,
  314. height: 40,
  315. backgroundColor: '#dcdad3',
  316. marginHorizontal: 10,
  317. opacity: 0.5,
  318. },
  319. breakdown: {
  320. flex: 1,
  321. flexDirection: 'row',
  322. flexWrap: 'wrap',
  323. },
  324. breakdownItem: {
  325. width: '50%',
  326. flexDirection: 'row',
  327. alignItems: 'center',
  328. marginBottom: 4,
  329. },
  330. levelIcon: {
  331. width: 45,
  332. height: 16,
  333. marginRight: 4,
  334. },
  335. breakdownText: {
  336. fontSize: 12,
  337. color: '#666',
  338. },
  339. currentNum: {
  340. color: '#000',
  341. fontWeight: 'bold',
  342. },
  343. emptyText: {
  344. textAlign: 'center',
  345. marginTop: 20,
  346. color: '#999',
  347. },
  348. });