index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import { useLocalSearchParams, useRouter } from 'expo-router';
  2. import React, { useCallback, useEffect, useRef, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. Alert,
  6. Dimensions,
  7. ImageBackground,
  8. ScrollView,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TouchableOpacity,
  13. View
  14. } from 'react-native';
  15. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  16. import { RecordModal, RecordModalRef } from '@/app/award-detail/components/RecordModal';
  17. import { BoxSelectionModal, BoxSelectionModalRef } from '@/components/award-detail-yfs/BoxSelectionModal';
  18. import FirstLast from '@/components/award-detail-yfs/FirstLast';
  19. import { NumSelectionModal, NumSelectionModalRef } from '@/components/award-detail-yfs/NumSelectionModal';
  20. import ProductListYfs from '@/components/award-detail-yfs/ProductListYfs';
  21. import ProductSwiper from '@/components/award-detail-yfs/ProductSwiper';
  22. import PurchaseBar from '@/components/award-detail-yfs/PurchaseBar';
  23. import { RuleModal, RuleModalRef } from '@/components/award-detail-yfs/RuleModal';
  24. import { Images } from '@/constants/images';
  25. import { getBoxDetail, getNextBox, getPoolDetail, getPreBox, lockBox, poolIn, poolOut, unlockBox } from '@/services/award';
  26. import { CheckoutModal, CheckoutModalRef } from '../award-detail/components/CheckoutModal';
  27. const { width } = Dimensions.get('window');
  28. const styles = StyleSheet.create({
  29. container: { flex: 1, backgroundColor: '#1a1a2e' },
  30. background: { flex: 1 },
  31. loading: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
  32. nav: {
  33. flexDirection: 'row',
  34. alignItems: 'center',
  35. justifyContent: 'space-between',
  36. paddingHorizontal: 15,
  37. height: 90,
  38. zIndex: 100,
  39. position: 'absolute',
  40. top: 0,
  41. left: 0,
  42. right: 0,
  43. },
  44. backBtn: { width: 40 },
  45. backText: { color: '#fff', fontSize: 24 },
  46. navTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', maxWidth: '60%' },
  47. placeholder: { width: 40 },
  48. scrollView: { flex: 1 },
  49. detailWrapper: {
  50. position: 'relative',
  51. minHeight: 800,
  52. paddingBottom: 100,
  53. },
  54. mainGoodsSection: {
  55. width: '100%',
  56. height: 504,
  57. position: 'relative',
  58. zIndex: 1,
  59. },
  60. mainGoodsSectionBtext: {
  61. width: '100%',
  62. height: 74,
  63. marginTop: -10,
  64. zIndex: 2,
  65. },
  66. positionBgLeftBg: {
  67. position: 'absolute',
  68. left: 0,
  69. top: 225,
  70. width: 32,
  71. height: 188,
  72. zIndex: 2,
  73. },
  74. positionBgRightBg: {
  75. position: 'absolute',
  76. right: 0,
  77. top: 225,
  78. width: 32,
  79. height: 188,
  80. zIndex: 2,
  81. },
  82. positionBut: {
  83. position: 'absolute',
  84. zIndex: 10,
  85. width: 35,
  86. height: 34,
  87. },
  88. btnBg: {
  89. width: '100%',
  90. height: '100%',
  91. justifyContent: 'center',
  92. alignItems: 'center',
  93. },
  94. slantedL: {
  95. color: '#fff',
  96. fontSize: 12,
  97. fontWeight: 'bold',
  98. transform: [{ rotate: '14deg' }],
  99. marginTop: -2,
  100. textShadowColor: '#000',
  101. textShadowOffset: { width: 1, height: 1 },
  102. textShadowRadius: 1,
  103. },
  104. slantedR: {
  105. color: '#fff',
  106. fontSize: 12,
  107. fontWeight: 'bold',
  108. transform: [{ rotate: '-16deg' }],
  109. marginTop: -2,
  110. textShadowColor: '#000',
  111. textShadowOffset: { width: 1, height: 1 },
  112. textShadowRadius: 1,
  113. },
  114. positionRule: {
  115. top: 256,
  116. left: 0,
  117. },
  118. positionStore: {
  119. top: 256,
  120. right: 0,
  121. },
  122. positionRecord: {
  123. top: 300,
  124. left: 0,
  125. },
  126. positionRefresh: {
  127. top: 345,
  128. right: 0,
  129. },
  130. bottomBar: {
  131. position: 'absolute',
  132. bottom: 0,
  133. left: 0,
  134. right: 0,
  135. zIndex: 200,
  136. }
  137. });
  138. export default function AwardDetailYfsScreen() {
  139. const { poolId } = useLocalSearchParams<{ poolId: string }>();
  140. const router = useRouter();
  141. const insets = useSafeAreaInsets();
  142. const [loading, setLoading] = useState(true);
  143. const [data, setData] = useState<any>(null);
  144. const [scrollTop, setScrollTop] = useState(0);
  145. const [currentBox, setCurrentBox] = useState<any>(null);
  146. const boxRef = useRef<BoxSelectionModalRef>(null);
  147. const numRef = useRef<NumSelectionModalRef>(null);
  148. const checkoutRef = useRef<CheckoutModalRef>(null);
  149. const ruleRef = useRef<RuleModalRef>(null);
  150. const recordRef = useRef<RecordModalRef>(null);
  151. const getSafePoolId = () => Array.isArray(poolId) ? poolId[0] : poolId;
  152. const loadBox = async (boxNum?: string) => {
  153. try {
  154. const id = getSafePoolId();
  155. console.log(`[DEBUG-ICHIBAN] loadBox called for pool: ${id}, boxNum: ${boxNum}`);
  156. const res = await getBoxDetail(id, boxNum);
  157. console.log(`[DEBUG-ICHIBAN] getBoxDetail res:`, res ? `ID: ${res.number}, UsedStatKeys: ${Object.keys(res.usedStat || {})}` : 'null');
  158. if (res) setCurrentBox(res);
  159. } catch (error) {
  160. console.error('[DEBUG-ICHIBAN] Failed to load box', error);
  161. }
  162. };
  163. const loadData = useCallback(async () => {
  164. if (!poolId) return;
  165. setLoading(true);
  166. try {
  167. const safePoolId = getSafePoolId(); // calling getSafePoolId inside loadData too to be sure
  168. console.log(`[DEBUG-ICHIBAN] loadData called for pool: ${safePoolId}`);
  169. const res = await getPoolDetail(safePoolId);
  170. setData(res);
  171. console.log(`[DEBUG-ICHIBAN] Pool detail loaded, calling loadBox...`);
  172. await loadBox(); // Initial box
  173. } catch (error) {
  174. console.error('[DEBUG-ICHIBAN] Failed to load detail', error);
  175. } finally {
  176. setLoading(false);
  177. // Assuming setRefreshing is defined elsewhere or will be added by the user
  178. // setRefreshing(false);
  179. }
  180. }, [poolId]);
  181. useEffect(() => {
  182. if (poolId) {
  183. poolIn(poolId);
  184. return () => {
  185. poolOut(poolId);
  186. };
  187. }
  188. }, [poolId]);
  189. const handleBoxSelect = (box: any) => {
  190. // Must load full detail to get usedStat
  191. loadBox(box.number);
  192. };
  193. const handlePrevBox = async () => {
  194. if (!currentBox) return;
  195. try {
  196. const res = await getPreBox(getSafePoolId(), currentBox.number);
  197. if (res) setCurrentBox(res);
  198. else Alert.alert('提示', '已经是第一盒了');
  199. } catch (e) { Alert.alert('提示', '切换失败'); }
  200. };
  201. const handleNextBox = async () => {
  202. if (!currentBox) return;
  203. try {
  204. const res = await getNextBox(getSafePoolId(), currentBox.number);
  205. if (res) setCurrentBox(res);
  206. else Alert.alert('提示', '已经是最后一盒了');
  207. } catch (e) { Alert.alert('提示', '切换失败'); }
  208. };
  209. const handlePay = ({ previewRes, chooseNum, boxNum }: any) => {
  210. checkoutRef.current?.show(chooseNum.length, previewRes, boxNum, chooseNum);
  211. };
  212. useEffect(() => {
  213. loadData();
  214. }, [loadData]);
  215. const headerBg = scrollTop > 50 ? '#333' : 'transparent';
  216. if (loading) {
  217. return (
  218. <View style={styles.loading}>
  219. <ActivityIndicator color="#fff" />
  220. </View>
  221. );
  222. }
  223. return (
  224. <View style={styles.container}>
  225. <StatusBar barStyle="light-content" />
  226. <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
  227. {/* Navigation Bar */}
  228. <View style={[styles.nav, { paddingTop: insets.top, backgroundColor: headerBg }]}>
  229. <TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
  230. <Text style={styles.backText}>←</Text>
  231. </TouchableOpacity>
  232. <Text style={styles.navTitle} numberOfLines={1}>{data?.name || '详情'}</Text>
  233. <View style={styles.placeholder} />
  234. </View>
  235. <ScrollView
  236. style={styles.scrollView}
  237. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  238. scrollEventThrottle={16}
  239. showsVerticalScrollIndicator={false}
  240. >
  241. <View style={styles.detailWrapper}>
  242. {/* Main Goods Section */}
  243. <ImageBackground
  244. source={{ uri: Images.box.detail.mainGoodsSection }}
  245. style={styles.mainGoodsSection}
  246. resizeMode="stretch"
  247. >
  248. <View style={{ height: 180 }} />
  249. <ProductSwiper
  250. box={currentBox} // Pass current box
  251. products={data?.luckGoodsList?.map((item: any) => ({
  252. cover: item.spu.cover,
  253. name: item.spu.name,
  254. level: item.level,
  255. quantity: item.quantity,
  256. id: item.id, // Ensure IDs are passed
  257. spu: item.spu
  258. })) || []}
  259. />
  260. <ImageBackground
  261. source={{ uri: Images.box.detail.positionBgleftBg }}
  262. style={styles.positionBgLeftBg}
  263. resizeMode="contain"
  264. />
  265. <ImageBackground
  266. source={{ uri: Images.box.detail.positionBgRightBg }}
  267. style={styles.positionBgRightBg}
  268. resizeMode="contain"
  269. />
  270. </ImageBackground>
  271. {/* Bottom Text Graphic */}
  272. <ImageBackground
  273. source={{ uri: Images.box.detail.mainGoodsSectionBtext }}
  274. style={styles.mainGoodsSectionBtext}
  275. resizeMode="contain"
  276. />
  277. {/* FirstLast (Box Info & Nav) */}
  278. <FirstLast
  279. data={data}
  280. box={currentBox}
  281. onPrevBox={handlePrevBox}
  282. onNextBox={handleNextBox}
  283. onChangeBox={() => boxRef.current?.show()}
  284. onProductClick={(spu) => console.log(spu)}
  285. />
  286. {/* Product List (A/B/C/D) */}
  287. <ProductListYfs
  288. products={data?.luckGoodsList || []}
  289. poolId={getSafePoolId()}
  290. box={currentBox}
  291. />
  292. {/* Floating Buttons */}
  293. <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
  294. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  295. <Text style={styles.slantedL}>规则</Text>
  296. </ImageBackground>
  297. </TouchableOpacity>
  298. <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
  299. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  300. <Text style={styles.slantedL}>记录</Text>
  301. </ImageBackground>
  302. </TouchableOpacity>
  303. <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store')}>
  304. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  305. <Text style={styles.slantedR}>仓库</Text>
  306. </ImageBackground>
  307. </TouchableOpacity>
  308. {/* Lock Button */}
  309. <TouchableOpacity style={[styles.positionBut, { top: 300, right: 0 }]} onPress={() => {
  310. if (!currentBox) return;
  311. const isLocked = currentBox.lockTime && new Date(currentBox.lockTime).getTime() > Date.now();
  312. Alert.alert('提示', isLocked ? '是否解锁盒子?' : '是否锁定盒子(锁定后他人无法购买)?', [
  313. { text: '取消', style: 'cancel' },
  314. { text: '确定', onPress: async () => {
  315. try {
  316. const poolId = getSafePoolId();
  317. let success = false;
  318. if (isLocked) {
  319. success = await unlockBox(poolId, currentBox.number);
  320. } else {
  321. success = await lockBox(poolId, currentBox.number);
  322. }
  323. if (success) {
  324. Alert.alert('成功', isLocked ? '解锁成功' : '锁定成功');
  325. loadBox(currentBox.number); // Refresh
  326. }
  327. // System handles error msg via interceptor, no need for manual alert
  328. } catch (e) {
  329. // console.error(e); // Silent error
  330. }
  331. }}
  332. ]);
  333. }}>
  334. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  335. <Text style={styles.slantedR}>{currentBox?.lockTime && new Date(currentBox.lockTime).getTime() > Date.now() ? '解锁' : '锁盒'}</Text>
  336. </ImageBackground>
  337. </TouchableOpacity>
  338. <TouchableOpacity style={[styles.positionBut, styles.positionRefresh]} onPress={() => loadBox(currentBox?.number)}>
  339. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  340. <Text style={styles.slantedR}>刷新</Text>
  341. </ImageBackground>
  342. </TouchableOpacity>
  343. </View>
  344. <View style={{ height: 100 }} />
  345. </ScrollView>
  346. <View style={styles.bottomBar}>
  347. <PurchaseBar
  348. price={data?.price || 0}
  349. specialPrice={data?.specialPrice}
  350. specialPriceFive={data?.specialPriceFive}
  351. onBuy={(count) => {
  352. if (!currentBox) {
  353. boxRef.current?.show();
  354. } else {
  355. numRef.current?.show(currentBox);
  356. }
  357. }}
  358. onBuyMany={() => {
  359. if (!currentBox) {
  360. boxRef.current?.show();
  361. } else {
  362. numRef.current?.show(currentBox);
  363. }
  364. }}
  365. />
  366. </View>
  367. {/* Modals */}
  368. <BoxSelectionModal
  369. ref={boxRef}
  370. poolId={getSafePoolId()}
  371. onSelect={handleBoxSelect}
  372. />
  373. <NumSelectionModal
  374. ref={numRef}
  375. poolId={getSafePoolId()}
  376. onPay={handlePay}
  377. />
  378. <CheckoutModal
  379. ref={checkoutRef}
  380. poolId={getSafePoolId()}
  381. data={data}
  382. onSuccess={(res) => {
  383. console.log('Success', res);
  384. loadData(); // Refresh
  385. }}
  386. />
  387. <RuleModal ref={ruleRef} />
  388. <RecordModal ref={recordRef} poolId={getSafePoolId()} />
  389. </ImageBackground>
  390. </View>
  391. );
  392. }