BoxSelectionModal.tsx 13 KB

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