BoxChooseModal.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import { Image } from 'expo-image';
  2. import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. ImageBackground,
  6. Modal,
  7. ScrollView,
  8. StyleSheet,
  9. Text,
  10. TouchableOpacity,
  11. View,
  12. } from 'react-native';
  13. import { Images } from '@/constants/images';
  14. import { getBoxList } from '@/services/award';
  15. // 等级筛选标签
  16. const TABS = [
  17. { title: '全部', value: '' },
  18. { title: '超神款', value: 'A' },
  19. { title: '欧皇款', value: 'B' },
  20. { title: '隐藏款', value: 'C' },
  21. { title: '普通款', value: 'D' },
  22. ];
  23. interface BoxItem {
  24. number: string;
  25. quantity: number;
  26. leftQuantity: number;
  27. quantityA: number;
  28. quantityB: number;
  29. quantityC: number;
  30. quantityD: number;
  31. leftQuantityA: number;
  32. leftQuantityB: number;
  33. leftQuantityC: number;
  34. leftQuantityD: number;
  35. }
  36. interface BoxChooseModalProps {
  37. poolId: string;
  38. onChoose: (boxNumber: string) => void;
  39. }
  40. export interface BoxChooseModalRef {
  41. show: () => void;
  42. close: () => void;
  43. }
  44. export const BoxChooseModal = forwardRef<BoxChooseModalRef, BoxChooseModalProps>(
  45. ({ poolId, onChoose }, ref) => {
  46. const [visible, setVisible] = useState(false);
  47. const [loading, setLoading] = useState(false);
  48. const [currentTab, setCurrentTab] = useState(TABS[0]);
  49. const [boxList, setBoxList] = useState<BoxItem[]>([]);
  50. const [pageNum, setPageNum] = useState(1);
  51. const [hasMore, setHasMore] = useState(true);
  52. const loadData = useCallback(async (page: number, level?: string) => {
  53. if (page === 1) setLoading(true);
  54. try {
  55. const levelValue = level === '' ? undefined : level === 'A' ? 1 : level === 'B' ? 2 : level === 'C' ? 3 : level === 'D' ? 4 : undefined;
  56. const res = await getBoxList(poolId, levelValue, page, 20);
  57. if (res && res.records) {
  58. const newList = res.records.filter((item: BoxItem) => item.leftQuantity > 0);
  59. if (page === 1) {
  60. setBoxList(newList);
  61. } else {
  62. setBoxList(prev => [...prev, ...newList]);
  63. }
  64. setHasMore(res.records.length >= 20);
  65. setPageNum(page);
  66. }
  67. } catch (error) {
  68. console.error('加载盒子列表失败:', error);
  69. } finally {
  70. setLoading(false);
  71. }
  72. }, [poolId]);
  73. useImperativeHandle(ref, () => ({
  74. show: () => {
  75. setVisible(true);
  76. setCurrentTab(TABS[0]);
  77. setPageNum(1);
  78. loadData(1, '');
  79. },
  80. close: () => {
  81. setVisible(false);
  82. setBoxList([]);
  83. },
  84. }));
  85. const close = () => {
  86. setVisible(false);
  87. };
  88. const clickTab = (tab: typeof TABS[0]) => {
  89. setCurrentTab(tab);
  90. setPageNum(1);
  91. loadData(1, tab.value);
  92. };
  93. const loadMore = () => {
  94. if (!loading && hasMore) {
  95. loadData(pageNum + 1, currentTab.value);
  96. }
  97. };
  98. const choose = (item: BoxItem) => {
  99. if (item.leftQuantity <= 0) return;
  100. onChoose(item.number);
  101. close();
  102. };
  103. const handleScroll = (event: any) => {
  104. const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
  105. const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 50;
  106. if (isCloseToBottom) {
  107. loadMore();
  108. }
  109. };
  110. return (
  111. <Modal visible={visible} transparent animationType="slide" onRequestClose={close}>
  112. <View style={styles.overlay}>
  113. <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={close} />
  114. <ImageBackground source={{ uri: Images.box.detail.recordBg }} style={styles.container} resizeMode="cover">
  115. {/* 标题 */}
  116. <View style={styles.titleSection}>
  117. <Text style={styles.title}>换盒</Text>
  118. <TouchableOpacity style={styles.closeBtn} onPress={close}>
  119. <Text style={styles.closeText}>×</Text>
  120. </TouchableOpacity>
  121. </View>
  122. {/* 标签页 */}
  123. <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsScroll}>
  124. <View style={styles.tabs}>
  125. {TABS.map((tab) => (
  126. <TouchableOpacity
  127. key={tab.value}
  128. style={[styles.tabItem, currentTab.value === tab.value && styles.tabItemActive]}
  129. onPress={() => clickTab(tab)}
  130. >
  131. <Text style={[styles.tabText, currentTab.value === tab.value && styles.tabTextActive]}>
  132. {tab.title}
  133. </Text>
  134. </TouchableOpacity>
  135. ))}
  136. </View>
  137. </ScrollView>
  138. {/* 盒子列表 */}
  139. <ScrollView
  140. style={styles.listScroll}
  141. showsVerticalScrollIndicator={false}
  142. onScroll={handleScroll}
  143. scrollEventThrottle={16}
  144. >
  145. <View style={styles.listContainer}>
  146. {loading && pageNum === 1 ? (
  147. <View style={styles.loadingBox}>
  148. <ActivityIndicator color="#FFC900" size="large" />
  149. </View>
  150. ) : boxList.length === 0 ? (
  151. <View style={styles.emptyBox}>
  152. <Text style={styles.emptyText}>暂无可用盒子</Text>
  153. </View>
  154. ) : (
  155. boxList.map((item, index) => (
  156. <TouchableOpacity
  157. key={item.number}
  158. style={styles.boxItem}
  159. onPress={() => choose(item)}
  160. activeOpacity={0.8}
  161. >
  162. <View style={styles.itemIndex}>
  163. <Text style={styles.itemIndexText}>{index + 1}</Text>
  164. </View>
  165. <View style={styles.itemContent}>
  166. <View style={styles.leftSection}>
  167. <Image
  168. source={{ uri: Images.box.detail.boxIcon }}
  169. style={styles.boxIcon}
  170. contentFit="contain"
  171. />
  172. <Text style={styles.leftText}>剩{item.leftQuantity}发</Text>
  173. </View>
  174. <View style={styles.divider} />
  175. <View style={styles.levelList}>
  176. <View style={styles.levelBox}>
  177. <Image source={{ uri: Images.box.detail.levelTextA }} style={styles.levelIcon} contentFit="contain" />
  178. <View style={styles.numBox}>
  179. <Text style={styles.currentNum}>{item.leftQuantityA}</Text>
  180. <Text style={styles.totalNum}>/{item.quantityA}</Text>
  181. </View>
  182. </View>
  183. <View style={styles.levelBox}>
  184. <Image source={{ uri: Images.box.detail.levelTextB }} style={styles.levelIcon} contentFit="contain" />
  185. <View style={styles.numBox}>
  186. <Text style={styles.currentNum}>{item.leftQuantityB}</Text>
  187. <Text style={styles.totalNum}>/{item.quantityB}</Text>
  188. </View>
  189. </View>
  190. <View style={styles.levelBox}>
  191. <Image source={{ uri: Images.box.detail.levelTextC }} style={styles.levelIcon} contentFit="contain" />
  192. <View style={styles.numBox}>
  193. <Text style={styles.currentNum}>{item.leftQuantityC}</Text>
  194. <Text style={styles.totalNum}>/{item.quantityC}</Text>
  195. </View>
  196. </View>
  197. <View style={styles.levelBox}>
  198. <Image source={{ uri: Images.box.detail.levelTextD }} style={styles.levelIcon} contentFit="contain" />
  199. <View style={styles.numBox}>
  200. <Text style={styles.currentNum}>{item.leftQuantityD}</Text>
  201. <Text style={styles.totalNum}>/{item.quantityD}</Text>
  202. </View>
  203. </View>
  204. </View>
  205. </View>
  206. </TouchableOpacity>
  207. ))
  208. )}
  209. {loading && pageNum > 1 && (
  210. <View style={styles.loadMoreBox}>
  211. <ActivityIndicator color="#FFC900" size="small" />
  212. </View>
  213. )}
  214. </View>
  215. </ScrollView>
  216. </ImageBackground>
  217. </View>
  218. </Modal>
  219. );
  220. }
  221. );
  222. const styles = StyleSheet.create({
  223. overlay: {
  224. flex: 1,
  225. backgroundColor: 'rgba(0,0,0,0.5)',
  226. justifyContent: 'flex-end',
  227. },
  228. mask: { flex: 1 },
  229. container: {
  230. borderTopLeftRadius: 15,
  231. borderTopRightRadius: 15,
  232. paddingTop: 15,
  233. paddingBottom: 34,
  234. maxHeight: '80%',
  235. },
  236. titleSection: {
  237. alignItems: 'center',
  238. paddingVertical: 15,
  239. position: 'relative',
  240. },
  241. title: {
  242. fontSize: 16,
  243. fontWeight: 'bold',
  244. color: '#fff',
  245. textShadowColor: '#000',
  246. textShadowOffset: { width: 1, height: 1 },
  247. textShadowRadius: 2,
  248. },
  249. closeBtn: {
  250. position: 'absolute',
  251. right: 15,
  252. top: 10,
  253. width: 24,
  254. height: 24,
  255. backgroundColor: '#ebebeb',
  256. borderRadius: 12,
  257. justifyContent: 'center',
  258. alignItems: 'center',
  259. },
  260. closeText: { fontSize: 18, color: '#a2a2a2', marginTop: -2 },
  261. tabsScroll: {
  262. maxHeight: 40,
  263. marginHorizontal: 10,
  264. marginBottom: 10,
  265. },
  266. tabs: {
  267. flexDirection: 'row',
  268. },
  269. tabItem: {
  270. paddingHorizontal: 12,
  271. paddingVertical: 6,
  272. backgroundColor: 'rgba(255,255,255,0.2)',
  273. borderRadius: 15,
  274. marginRight: 8,
  275. },
  276. tabItemActive: {
  277. backgroundColor: '#FFC900',
  278. },
  279. tabText: {
  280. fontSize: 12,
  281. color: '#fff',
  282. },
  283. tabTextActive: {
  284. color: '#000',
  285. fontWeight: 'bold',
  286. },
  287. listScroll: {
  288. height: 400,
  289. marginHorizontal: 10,
  290. },
  291. listContainer: {
  292. backgroundColor: '#f3f3f3',
  293. borderWidth: 2,
  294. borderColor: '#000',
  295. padding: 15,
  296. borderRadius: 4,
  297. },
  298. loadingBox: {
  299. height: 200,
  300. justifyContent: 'center',
  301. alignItems: 'center',
  302. },
  303. emptyBox: {
  304. height: 200,
  305. justifyContent: 'center',
  306. alignItems: 'center',
  307. },
  308. emptyText: {
  309. fontSize: 14,
  310. color: '#999',
  311. },
  312. boxItem: {
  313. backgroundColor: '#fff',
  314. borderWidth: 3,
  315. borderColor: '#000',
  316. borderRadius: 4,
  317. marginBottom: 10,
  318. position: 'relative',
  319. shadowColor: '#FFC900',
  320. shadowOffset: { width: 0, height: 3 },
  321. shadowOpacity: 1,
  322. shadowRadius: 0,
  323. elevation: 3,
  324. },
  325. itemIndex: {
  326. position: 'absolute',
  327. left: 0,
  328. top: 0,
  329. width: 22,
  330. height: 22,
  331. borderWidth: 1.5,
  332. borderColor: '#000',
  333. backgroundColor: '#fff',
  334. justifyContent: 'center',
  335. alignItems: 'center',
  336. zIndex: 1,
  337. },
  338. itemIndexText: {
  339. fontSize: 14,
  340. fontWeight: 'bold',
  341. color: '#000',
  342. },
  343. itemContent: {
  344. flexDirection: 'row',
  345. alignItems: 'center',
  346. padding: 12,
  347. },
  348. leftSection: {
  349. width: 74,
  350. alignItems: 'center',
  351. },
  352. boxIcon: {
  353. width: 24,
  354. height: 24,
  355. },
  356. leftText: {
  357. fontSize: 12,
  358. color: '#000',
  359. marginTop: 4,
  360. },
  361. divider: {
  362. width: 1,
  363. height: 40,
  364. backgroundColor: '#dcdad3',
  365. opacity: 0.5,
  366. marginRight: 10,
  367. },
  368. levelList: {
  369. flex: 1,
  370. flexDirection: 'row',
  371. flexWrap: 'wrap',
  372. },
  373. levelBox: {
  374. width: '50%',
  375. flexDirection: 'row',
  376. alignItems: 'center',
  377. marginBottom: 4,
  378. },
  379. levelIcon: {
  380. width: 45,
  381. height: 16,
  382. marginRight: 4,
  383. },
  384. numBox: {
  385. flexDirection: 'row',
  386. alignItems: 'center',
  387. },
  388. currentNum: {
  389. fontSize: 13,
  390. fontWeight: '500',
  391. color: '#000',
  392. },
  393. totalNum: {
  394. fontSize: 11,
  395. color: '#666',
  396. },
  397. loadMoreBox: {
  398. paddingVertical: 15,
  399. alignItems: 'center',
  400. },
  401. });