index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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. onProductClick={(product) => {
  292. const list = data?.luckGoodsList || [];
  293. const getPId = (p: any) => String(p.id || p.spu?.id || p.spuId || '');
  294. const targetId = getPId(product);
  295. const index = list.findIndex((p: any) => getPId(p) === targetId);
  296. console.log(`[DEBUG-NAV] Clicked ${targetId} (${product.name}). Found index: ${index}`);
  297. if (index !== -1) {
  298. router.push({
  299. pathname: '/award-detail/swipe',
  300. params: { poolId: getSafePoolId(), index }
  301. });
  302. }
  303. }}
  304. />
  305. {/* Floating Buttons */}
  306. <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
  307. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  308. <Text style={styles.slantedL}>规则</Text>
  309. </ImageBackground>
  310. </TouchableOpacity>
  311. <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
  312. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  313. <Text style={styles.slantedL}>记录</Text>
  314. </ImageBackground>
  315. </TouchableOpacity>
  316. <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store')}>
  317. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  318. <Text style={styles.slantedR}>仓库</Text>
  319. </ImageBackground>
  320. </TouchableOpacity>
  321. {/* Lock Button */}
  322. <TouchableOpacity style={[styles.positionBut, { top: 300, right: 0 }]} onPress={() => {
  323. if (!currentBox) return;
  324. const isLocked = currentBox.lockTime && new Date(currentBox.lockTime).getTime() > Date.now();
  325. Alert.alert('提示', isLocked ? '是否解锁盒子?' : '是否锁定盒子(锁定后他人无法购买)?', [
  326. { text: '取消', style: 'cancel' },
  327. { text: '确定', onPress: async () => {
  328. try {
  329. const poolId = getSafePoolId();
  330. let success = false;
  331. if (isLocked) {
  332. success = await unlockBox(poolId, currentBox.number);
  333. } else {
  334. success = await lockBox(poolId, currentBox.number);
  335. }
  336. if (success) {
  337. Alert.alert('成功', isLocked ? '解锁成功' : '锁定成功');
  338. loadBox(currentBox.number); // Refresh
  339. }
  340. // System handles error msg via interceptor, no need for manual alert
  341. } catch (e) {
  342. // console.error(e); // Silent error
  343. }
  344. }}
  345. ]);
  346. }}>
  347. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  348. <Text style={styles.slantedR}>{currentBox?.lockTime && new Date(currentBox.lockTime).getTime() > Date.now() ? '解锁' : '锁盒'}</Text>
  349. </ImageBackground>
  350. </TouchableOpacity>
  351. <TouchableOpacity style={[styles.positionBut, styles.positionRefresh]} onPress={() => loadBox(currentBox?.number)}>
  352. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  353. <Text style={styles.slantedR}>刷新</Text>
  354. </ImageBackground>
  355. </TouchableOpacity>
  356. </View>
  357. <View style={{ height: 100 }} />
  358. </ScrollView>
  359. <View style={styles.bottomBar}>
  360. <PurchaseBar
  361. price={data?.price || 0}
  362. specialPrice={data?.specialPrice}
  363. specialPriceFive={data?.specialPriceFive}
  364. onBuy={(count) => {
  365. if (!currentBox) {
  366. boxRef.current?.show();
  367. } else {
  368. numRef.current?.show(currentBox);
  369. }
  370. }}
  371. onBuyMany={() => {
  372. if (!currentBox) {
  373. boxRef.current?.show();
  374. } else {
  375. numRef.current?.show(currentBox);
  376. }
  377. }}
  378. />
  379. </View>
  380. {/* Modals */}
  381. <BoxSelectionModal
  382. ref={boxRef}
  383. poolId={getSafePoolId()}
  384. onSelect={handleBoxSelect}
  385. />
  386. <NumSelectionModal
  387. ref={numRef}
  388. poolId={getSafePoolId()}
  389. onPay={handlePay}
  390. />
  391. <CheckoutModal
  392. ref={checkoutRef}
  393. poolId={getSafePoolId()}
  394. data={data}
  395. onSuccess={(res) => {
  396. console.log('Success', res);
  397. loadData(); // Refresh
  398. }}
  399. />
  400. <RuleModal ref={ruleRef} />
  401. <RecordModal ref={recordRef} poolId={getSafePoolId()} />
  402. </ImageBackground>
  403. </View>
  404. );
  405. }