boxList.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useState } from 'react';
  4. import {
  5. Alert,
  6. ImageBackground,
  7. ScrollView,
  8. StatusBar,
  9. StyleSheet,
  10. Text,
  11. TouchableOpacity,
  12. View,
  13. } from 'react-native';
  14. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  15. import { Images } from '@/constants/images';
  16. import { getMyBoxes, openBox } from '@/services/award';
  17. interface BoxItem {
  18. id: string;
  19. boxName: string;
  20. boxCover: string;
  21. }
  22. export default function BoxListScreen() {
  23. const router = useRouter();
  24. const insets = useSafeAreaInsets();
  25. const [list, setList] = useState<BoxItem[]>([]);
  26. const [selectedIds, setSelectedIds] = useState<string[]>([]);
  27. const [loading, setLoading] = useState(false);
  28. const loadData = useCallback(async () => {
  29. try {
  30. setLoading(true);
  31. const res = await getMyBoxes();
  32. // 确保返回的是数组
  33. setList(Array.isArray(res) ? res : []);
  34. } catch (error) {
  35. console.error('获取宝箱列表失败:', error);
  36. setList([]);
  37. } finally {
  38. setLoading(false);
  39. }
  40. }, []);
  41. useEffect(() => {
  42. loadData();
  43. }, [loadData]);
  44. const toggleSelect = (item: BoxItem) => {
  45. if (!item.id) return;
  46. setSelectedIds(prev => {
  47. const index = prev.indexOf(item.id);
  48. if (index > -1) {
  49. return prev.filter(id => id !== item.id);
  50. } else {
  51. return [...prev, item.id];
  52. }
  53. });
  54. };
  55. const isSelected = (id: string) => selectedIds.includes(id);
  56. const isAllSelected = list.length > 0 && selectedIds.length === list.length;
  57. const selectAll = () => {
  58. if (isAllSelected) {
  59. setSelectedIds([]);
  60. } else {
  61. setSelectedIds(list.map(item => item.id));
  62. }
  63. };
  64. const openSelectedBoxes = async () => {
  65. if (selectedIds.length === 0) {
  66. Alert.alert('提示', '请先选择宝箱');
  67. return;
  68. }
  69. try {
  70. const boxIds = selectedIds.join(',');
  71. const res = await openBox({ boxIds });
  72. if (res.code === 0) {
  73. // TODO: 显示开箱结果弹窗
  74. Alert.alert('开箱成功', '恭喜获得奖品!', [
  75. { text: '确定', onPress: () => {
  76. setSelectedIds([]);
  77. loadData();
  78. }}
  79. ]);
  80. } else {
  81. Alert.alert('提示', res.msg || '开箱失败');
  82. }
  83. } catch (error) {
  84. console.error('开箱失败:', error);
  85. Alert.alert('提示', '开箱失败,请重试');
  86. }
  87. };
  88. const handleBack = () => {
  89. router.back();
  90. };
  91. return (
  92. <View style={styles.container}>
  93. <StatusBar barStyle="light-content" />
  94. <ImageBackground
  95. source={{ uri: Images.mine.kaixinMineBg }}
  96. style={styles.background}
  97. resizeMode="cover"
  98. >
  99. {/* 顶部导航 */}
  100. <View style={[styles.header, { paddingTop: insets.top }]}>
  101. <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
  102. <Text style={styles.backIcon}>‹</Text>
  103. </TouchableOpacity>
  104. <Text style={styles.title}>宝箱</Text>
  105. <View style={styles.placeholder} />
  106. </View>
  107. {/* 内容区域 */}
  108. <View style={styles.wrapper}>
  109. <View style={styles.contentBox}>
  110. <ScrollView
  111. showsVerticalScrollIndicator={false}
  112. contentContainerStyle={styles.scrollContent}
  113. >
  114. <View style={styles.itemList}>
  115. {list.map((item, index) => (
  116. <TouchableOpacity
  117. key={item.id || index}
  118. style={styles.item}
  119. onPress={() => toggleSelect(item)}
  120. activeOpacity={0.8}
  121. >
  122. <View style={[
  123. styles.itemCenter,
  124. isSelected(item.id) && styles.itemSelected
  125. ]}>
  126. <View style={styles.imgBox}>
  127. <Image
  128. source={{ uri: item.boxCover }}
  129. style={styles.boxImage}
  130. contentFit="cover"
  131. />
  132. {isSelected(item.id) && (
  133. <View style={styles.selectMask}>
  134. <Text style={styles.checkIcon}>✓</Text>
  135. </View>
  136. )}
  137. </View>
  138. <View style={styles.textBox}>
  139. <Text style={styles.boxName} numberOfLines={1}>
  140. {item.boxName}
  141. </Text>
  142. </View>
  143. </View>
  144. </TouchableOpacity>
  145. ))}
  146. </View>
  147. {list.length === 0 && !loading && (
  148. <View style={styles.empty}>
  149. <Text style={styles.emptyText}>暂无宝箱</Text>
  150. </View>
  151. )}
  152. </ScrollView>
  153. </View>
  154. </View>
  155. {/* 底部按钮 */}
  156. <View style={[styles.bottomBtnBox, { paddingBottom: insets.bottom + 80 }]}>
  157. <View style={styles.btnGroup}>
  158. <TouchableOpacity
  159. style={styles.selectAllBtn}
  160. onPress={selectAll}
  161. >
  162. <ImageBackground
  163. source={{ uri: Images.common.loginBtn }}
  164. style={styles.btnBg}
  165. resizeMode="stretch"
  166. >
  167. <Text style={styles.btnText}>
  168. {isAllSelected ? '取消全选' : '全选'}
  169. </Text>
  170. </ImageBackground>
  171. </TouchableOpacity>
  172. <TouchableOpacity
  173. style={[
  174. styles.openBtn,
  175. selectedIds.length === 0 && styles.openBtnDisabled
  176. ]}
  177. onPress={openSelectedBoxes}
  178. disabled={selectedIds.length === 0}
  179. >
  180. <ImageBackground
  181. source={{ uri: Images.common.loginBtn }}
  182. style={styles.btnBg}
  183. resizeMode="stretch"
  184. >
  185. <Text style={styles.btnText}>
  186. 开启宝箱 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
  187. </Text>
  188. </ImageBackground>
  189. </TouchableOpacity>
  190. </View>
  191. </View>
  192. </ImageBackground>
  193. </View>
  194. );
  195. }
  196. const styles = StyleSheet.create({
  197. container: {
  198. flex: 1,
  199. backgroundColor: '#1a1a2e',
  200. },
  201. background: {
  202. flex: 1,
  203. },
  204. header: {
  205. flexDirection: 'row',
  206. alignItems: 'center',
  207. justifyContent: 'space-between',
  208. paddingHorizontal: 10,
  209. height: 80,
  210. zIndex: 100,
  211. },
  212. backBtn: {
  213. width: 40,
  214. height: 40,
  215. justifyContent: 'center',
  216. alignItems: 'center',
  217. },
  218. backIcon: {
  219. fontSize: 32,
  220. color: '#fff',
  221. fontWeight: 'bold',
  222. },
  223. title: {
  224. fontSize: 15,
  225. fontWeight: 'bold',
  226. color: '#fff',
  227. textAlign: 'center',
  228. },
  229. placeholder: {
  230. width: 40,
  231. },
  232. wrapper: {
  233. flex: 1,
  234. padding: 10,
  235. },
  236. contentBox: {
  237. flex: 1,
  238. backgroundColor: '#fff',
  239. borderRadius: 12,
  240. borderWidth: 1,
  241. borderColor: '#000',
  242. padding: 10,
  243. },
  244. scrollContent: {
  245. paddingBottom: 60,
  246. },
  247. itemList: {
  248. flexDirection: 'row',
  249. flexWrap: 'wrap',
  250. },
  251. item: {
  252. width: '33.33%',
  253. height: 110,
  254. padding: 5,
  255. },
  256. itemCenter: {
  257. width: '100%',
  258. height: '100%',
  259. alignItems: 'center',
  260. },
  261. itemSelected: {
  262. transform: [{ scale: 0.95 }],
  263. opacity: 0.8,
  264. },
  265. imgBox: {
  266. width: '85%',
  267. height: '75%',
  268. position: 'relative',
  269. },
  270. boxImage: {
  271. width: '100%',
  272. height: '100%',
  273. borderRadius: 8,
  274. },
  275. selectMask: {
  276. position: 'absolute',
  277. top: 0,
  278. left: 0,
  279. right: 0,
  280. bottom: 0,
  281. backgroundColor: 'rgba(0, 0, 0, 0.4)',
  282. borderRadius: 8,
  283. justifyContent: 'center',
  284. alignItems: 'center',
  285. },
  286. checkIcon: {
  287. fontSize: 30,
  288. color: '#fff',
  289. fontWeight: 'bold',
  290. },
  291. textBox: {
  292. width: '100%',
  293. alignItems: 'center',
  294. marginTop: 4,
  295. },
  296. boxName: {
  297. fontSize: 13,
  298. color: '#000',
  299. },
  300. empty: {
  301. flex: 1,
  302. justifyContent: 'center',
  303. alignItems: 'center',
  304. paddingVertical: 100,
  305. },
  306. emptyText: {
  307. fontSize: 14,
  308. color: '#999',
  309. },
  310. bottomBtnBox: {
  311. position: 'absolute',
  312. bottom: 0,
  313. left: 0,
  314. right: 0,
  315. padding: 10,
  316. },
  317. btnGroup: {
  318. flexDirection: 'row',
  319. justifyContent: 'space-between',
  320. alignItems: 'center',
  321. width: '90%',
  322. alignSelf: 'center',
  323. },
  324. selectAllBtn: {
  325. width: '30%',
  326. height: 44,
  327. marginRight: 10,
  328. },
  329. openBtn: {
  330. flex: 1,
  331. height: 44,
  332. },
  333. openBtnDisabled: {
  334. opacity: 0.6,
  335. },
  336. btnBg: {
  337. width: '100%',
  338. height: '100%',
  339. justifyContent: 'center',
  340. alignItems: 'center',
  341. },
  342. btnText: {
  343. fontSize: 14,
  344. fontWeight: 'bold',
  345. color: '#000',
  346. },
  347. });