index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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, poolIn, poolOut } 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. const res = await getBoxDetail(id, boxNum);
  156. if (res) setCurrentBox(res);
  157. } catch (error) {
  158. console.error('Failed to load box', error);
  159. }
  160. };
  161. const loadData = useCallback(async () => {
  162. if (!poolId) return;
  163. setLoading(true);
  164. try {
  165. const safePoolId = getSafePoolId();
  166. const res = await getPoolDetail(safePoolId);
  167. setData(res);
  168. await loadBox(); // Initial box
  169. } catch (error) {
  170. console.error('Failed to load detail:', error);
  171. }
  172. setLoading(false);
  173. }, [poolId]);
  174. useEffect(() => {
  175. if (poolId) {
  176. poolIn(poolId);
  177. return () => {
  178. poolOut(poolId);
  179. };
  180. }
  181. }, [poolId]);
  182. const handleBoxSelect = (box: any) => {
  183. setCurrentBox(box);
  184. };
  185. const handlePrevBox = async () => {
  186. if (!currentBox) return;
  187. try {
  188. const res = await getPreBox(getSafePoolId(), currentBox.number);
  189. if (res) setCurrentBox(res);
  190. else Alert.alert('提示', '已经是第一盒了');
  191. } catch (e) { Alert.alert('提示', '切换失败'); }
  192. };
  193. const handleNextBox = async () => {
  194. if (!currentBox) return;
  195. try {
  196. const res = await getNextBox(getSafePoolId(), currentBox.number);
  197. if (res) setCurrentBox(res);
  198. else Alert.alert('提示', '已经是最后一盒了');
  199. } catch (e) { Alert.alert('提示', '切换失败'); }
  200. };
  201. const handlePay = ({ previewRes, chooseNum, boxNum }: any) => {
  202. checkoutRef.current?.show(chooseNum.length, previewRes, boxNum, chooseNum);
  203. };
  204. useEffect(() => {
  205. loadData();
  206. }, [loadData]);
  207. const headerBg = scrollTop > 50 ? '#333' : 'transparent';
  208. if (loading) {
  209. return (
  210. <View style={styles.loading}>
  211. <ActivityIndicator color="#fff" />
  212. </View>
  213. );
  214. }
  215. return (
  216. <View style={styles.container}>
  217. <StatusBar barStyle="light-content" />
  218. <ImageBackground source={{ uri: Images.common.indexBg }} style={styles.background} resizeMode="cover">
  219. {/* Navigation Bar */}
  220. <View style={[styles.nav, { paddingTop: insets.top, backgroundColor: headerBg }]}>
  221. <TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
  222. <Text style={styles.backText}>←</Text>
  223. </TouchableOpacity>
  224. <Text style={styles.navTitle} numberOfLines={1}>{data?.name || '详情'}</Text>
  225. <View style={styles.placeholder} />
  226. </View>
  227. <ScrollView
  228. style={styles.scrollView}
  229. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  230. scrollEventThrottle={16}
  231. showsVerticalScrollIndicator={false}
  232. >
  233. <View style={styles.detailWrapper}>
  234. {/* Main Goods Section */}
  235. <ImageBackground
  236. source={{ uri: Images.box.detail.mainGoodsSection }}
  237. style={styles.mainGoodsSection}
  238. resizeMode="stretch"
  239. >
  240. <View style={{ height: 180 }} />
  241. <ProductSwiper
  242. products={data?.luckGoodsList?.map((item: any) => {
  243. let prob = '0%';
  244. const total = data.leftQuantity || 1;
  245. if (item.level === 'A') prob = ((data.leftQuantityA / total) * 100).toFixed(4) + '%';
  246. else if (item.level === 'B') prob = ((data.leftQuantityB / total) * 100).toFixed(4) + '%';
  247. else if (item.level === 'C') prob = ((data.leftQuantityC / total) * 100).toFixed(4) + '%';
  248. else if (item.level === 'D') prob = ((data.leftQuantityD / total) * 100).toFixed(4) + '%';
  249. return {
  250. cover: item.spu.cover,
  251. name: item.spu.name,
  252. level: item.level,
  253. probability: prob,
  254. quantity: item.quantity
  255. };
  256. }) || []}
  257. />
  258. <ImageBackground
  259. source={{ uri: Images.box.detail.positionBgleftBg }}
  260. style={styles.positionBgLeftBg}
  261. resizeMode="contain"
  262. />
  263. <ImageBackground
  264. source={{ uri: Images.box.detail.positionBgRightBg }}
  265. style={styles.positionBgRightBg}
  266. resizeMode="contain"
  267. />
  268. </ImageBackground>
  269. {/* Bottom Text Graphic */}
  270. <ImageBackground
  271. source={{ uri: Images.box.detail.mainGoodsSectionBtext }}
  272. style={styles.mainGoodsSectionBtext}
  273. resizeMode="contain"
  274. />
  275. {/* FirstLast (Box Info & Nav) */}
  276. <FirstLast
  277. data={data}
  278. box={currentBox}
  279. onPrevBox={handlePrevBox}
  280. onNextBox={handleNextBox}
  281. onChangeBox={() => boxRef.current?.show()}
  282. onProductClick={(spu) => console.log(spu)}
  283. />
  284. {/* Product List (A/B/C/D) */}
  285. <ProductListYfs
  286. products={data?.luckGoodsList || []}
  287. poolId={getSafePoolId()}
  288. box={currentBox}
  289. />
  290. {/* Floating Buttons */}
  291. <TouchableOpacity style={[styles.positionBut, styles.positionRule]} onPress={() => ruleRef.current?.show()}>
  292. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  293. <Text style={styles.slantedL}>规则</Text>
  294. </ImageBackground>
  295. </TouchableOpacity>
  296. <TouchableOpacity style={[styles.positionBut, styles.positionRecord]} onPress={() => recordRef.current?.show()}>
  297. <ImageBackground source={{ uri: Images.box.detail.positionBgLeft }} style={styles.btnBg} resizeMode="contain">
  298. <Text style={styles.slantedL}>记录</Text>
  299. </ImageBackground>
  300. </TouchableOpacity>
  301. <TouchableOpacity style={[styles.positionBut, styles.positionStore]} onPress={() => router.push('/store')}>
  302. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  303. <Text style={styles.slantedR}>仓库</Text>
  304. </ImageBackground>
  305. </TouchableOpacity>
  306. <TouchableOpacity style={[styles.positionBut, styles.positionRefresh]} onPress={() => loadBox(currentBox?.number)}>
  307. <ImageBackground source={{ uri: Images.box.detail.positionBgRight }} style={styles.btnBg} resizeMode="contain">
  308. <Text style={styles.slantedR}>刷新</Text>
  309. </ImageBackground>
  310. </TouchableOpacity>
  311. </View>
  312. <View style={{ height: 100 }} />
  313. </ScrollView>
  314. <View style={styles.bottomBar}>
  315. <PurchaseBar
  316. price={data?.price || 0}
  317. specialPrice={data?.specialPrice}
  318. specialPriceFive={data?.specialPriceFive}
  319. onBuy={(count) => {
  320. if (!currentBox) {
  321. boxRef.current?.show();
  322. } else {
  323. numRef.current?.show(currentBox);
  324. }
  325. }}
  326. onBuyMany={() => {
  327. if (!currentBox) {
  328. boxRef.current?.show();
  329. } else {
  330. numRef.current?.show(currentBox);
  331. }
  332. }}
  333. />
  334. </View>
  335. {/* Modals */}
  336. <BoxSelectionModal
  337. ref={boxRef}
  338. poolId={getSafePoolId()}
  339. onSelect={handleBoxSelect}
  340. />
  341. <NumSelectionModal
  342. ref={numRef}
  343. poolId={getSafePoolId()}
  344. onPay={handlePay}
  345. />
  346. <CheckoutModal
  347. ref={checkoutRef}
  348. poolId={getSafePoolId()}
  349. data={data}
  350. onSuccess={(res) => {
  351. console.log('Success', res);
  352. loadData(); // Refresh
  353. }}
  354. />
  355. <RuleModal ref={ruleRef} />
  356. <RecordModal ref={recordRef} poolId={getSafePoolId()} />
  357. </ImageBackground>
  358. </View>
  359. );
  360. }