index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. import { Image } from "expo-image";
  2. import { useLocalSearchParams, useRouter } from "expo-router";
  3. import React, { useCallback, useEffect, useRef, useState } from "react";
  4. import {
  5. ActivityIndicator,
  6. Animated,
  7. Dimensions,
  8. ImageBackground,
  9. ScrollView,
  10. StatusBar,
  11. StyleSheet,
  12. Text,
  13. TouchableOpacity,
  14. View,
  15. } from "react-native";
  16. import { useSafeAreaInsets } from "react-native-safe-area-context";
  17. import { Images } from "@/constants/images";
  18. import { getPoolDetail, poolIn, poolOut, previewOrder } from "@/services/award";
  19. import { CheckoutModal } from "./components/CheckoutModal";
  20. import { ExplainSection } from "./components/ExplainSection";
  21. import { ProductList } from "./components/ProductList";
  22. import { RecordModal } from "./components/RecordModal";
  23. import { RuleModal } from "./components/RuleModal";
  24. const { width: SCREEN_WIDTH } = Dimensions.get("window");
  25. interface PoolData {
  26. id: string;
  27. name: string;
  28. cover: string;
  29. price: number;
  30. specialPrice?: number;
  31. specialPriceFive?: number;
  32. demoFlag?: number;
  33. luckGoodsList: ProductItem[];
  34. luckGoodsLevelProbabilityList?: any[];
  35. }
  36. interface ProductItem {
  37. id: string;
  38. name: string;
  39. cover: string;
  40. level: string;
  41. probability: number;
  42. price?: number;
  43. }
  44. export default function AwardDetailScreen() {
  45. const { poolId } = useLocalSearchParams<{ poolId: string }>();
  46. const router = useRouter();
  47. const insets = useSafeAreaInsets();
  48. const [loading, setLoading] = useState(true);
  49. const [data, setData] = useState<PoolData | null>(null);
  50. const [products, setProducts] = useState<ProductItem[]>([]);
  51. const [currentIndex, setCurrentIndex] = useState(0);
  52. const [scrollTop, setScrollTop] = useState(0);
  53. const checkoutRef = useRef<any>(null);
  54. const recordRef = useRef<any>(null);
  55. const ruleRef = useRef<any>(null);
  56. const floatAnim = useRef(new Animated.Value(0)).current;
  57. useEffect(() => {
  58. Animated.loop(
  59. Animated.sequence([
  60. Animated.timing(floatAnim, {
  61. toValue: 10,
  62. duration: 1500,
  63. useNativeDriver: true,
  64. }),
  65. Animated.timing(floatAnim, {
  66. toValue: -10,
  67. duration: 1500,
  68. useNativeDriver: true,
  69. }),
  70. ]),
  71. ).start();
  72. }, []);
  73. const loadData = useCallback(async () => {
  74. if (!poolId) return;
  75. setLoading(true);
  76. try {
  77. const detail = await getPoolDetail(poolId);
  78. if (detail) {
  79. setData(detail);
  80. setProducts(detail.luckGoodsList || []);
  81. }
  82. } catch (error) {
  83. console.error("加载数据失败:", error);
  84. }
  85. setLoading(false);
  86. }, [poolId]);
  87. useEffect(() => {
  88. loadData();
  89. if (poolId) poolIn(poolId);
  90. return () => {
  91. if (poolId) poolOut(poolId);
  92. };
  93. }, [poolId]);
  94. const handlePay = async (num: number) => {
  95. if (!poolId || !data) return;
  96. try {
  97. const preview = await previewOrder(poolId, num);
  98. if (preview) checkoutRef.current?.show(num, preview);
  99. } catch (error) {
  100. console.error("预览订单失败:", error);
  101. }
  102. };
  103. const handleSuccess = () => {
  104. setTimeout(() => loadData(), 500);
  105. };
  106. const handleProductPress = (index: number) => {
  107. router.push({
  108. pathname: "/treasure-hunt/swipe" as any,
  109. params: { poolId, index },
  110. });
  111. };
  112. const handlePrev = () => {
  113. if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
  114. };
  115. const handleNext = () => {
  116. if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
  117. };
  118. const getLevelName = (level: string) => {
  119. const map: Record<string, string> = {
  120. A: "超神款",
  121. B: "欧皇款",
  122. C: "隐藏款",
  123. D: "普通款",
  124. };
  125. return map[level] || level;
  126. };
  127. const ignoreRatio0 = (val: number) => {
  128. const str = String(val);
  129. const match = str.match(/^(\d+\.?\d*?)0*$/);
  130. return match ? match[1].replace(/\.$/, "") : str;
  131. };
  132. if (loading) {
  133. return (
  134. <View style={styles.loadingContainer}>
  135. <ActivityIndicator size="large" color="#fff" />
  136. </View>
  137. );
  138. }
  139. if (!data) {
  140. return (
  141. <View style={styles.loadingContainer}>
  142. <Text style={styles.errorText}>奖池不存在</Text>
  143. <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
  144. <Text style={styles.backBtn2Text}>返回</Text>
  145. </TouchableOpacity>
  146. </View>
  147. );
  148. }
  149. const currentProduct = products[currentIndex];
  150. const headerBg = scrollTop > 0 ? "#333" : "transparent";
  151. return (
  152. <View style={styles.container}>
  153. <StatusBar barStyle="light-content" />
  154. <ImageBackground
  155. source={{ uri: Images.common.indexBg }}
  156. style={styles.background}
  157. resizeMode="cover"
  158. >
  159. {/* 顶部导航 */}
  160. <View
  161. style={[
  162. styles.header,
  163. { paddingTop: insets.top, backgroundColor: headerBg },
  164. ]}
  165. >
  166. <TouchableOpacity
  167. style={styles.backBtn}
  168. onPress={() => router.back()}
  169. >
  170. <Text style={styles.backText}>{"<"}</Text>
  171. </TouchableOpacity>
  172. <Text style={styles.headerTitle} numberOfLines={1}>
  173. {data.name}
  174. </Text>
  175. <View style={styles.placeholder} />
  176. </View>
  177. <ScrollView
  178. style={styles.scrollView}
  179. showsVerticalScrollIndicator={false}
  180. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  181. scrollEventThrottle={16}
  182. >
  183. {/* 主商品展示区域 */}
  184. <ImageBackground
  185. source={{ uri: Images.box.detail.mainGoodsSection }}
  186. style={styles.mainGoodsSection}
  187. resizeMode="cover"
  188. >
  189. <View style={{ height: 72 + insets.top }} />
  190. {/* 商品轮播区域 */}
  191. <View style={styles.mainSwiper}>
  192. {currentProduct && (
  193. <>
  194. <TouchableOpacity
  195. onPress={() => handleProductPress(currentIndex)}
  196. activeOpacity={0.9}
  197. >
  198. <Animated.View
  199. style={[
  200. styles.productImageBox,
  201. { transform: [{ translateY: floatAnim }] },
  202. ]}
  203. >
  204. <Image
  205. source={{ uri: currentProduct.cover }}
  206. style={styles.productImage}
  207. contentFit="contain"
  208. />
  209. </Animated.View>
  210. </TouchableOpacity>
  211. {/* 等级信息 */}
  212. <ImageBackground
  213. source={{ uri: Images.box.detail.detailsBut }}
  214. style={styles.detailsBut}
  215. resizeMode="contain"
  216. >
  217. <View style={styles.detailsText}>
  218. <Text style={styles.levelText}>
  219. {getLevelName(currentProduct.level)}
  220. </Text>
  221. <Text style={styles.probabilityText}>
  222. ({ignoreRatio0(currentProduct.probability)}%)
  223. </Text>
  224. </View>
  225. </ImageBackground>
  226. {/* 商品名称 */}
  227. <ImageBackground
  228. source={{ uri: Images.box.detail.nameBg }}
  229. style={styles.goodsNameBg}
  230. resizeMode="contain"
  231. >
  232. <Text style={styles.goodsNameText} numberOfLines={6}>
  233. {currentProduct.name}
  234. </Text>
  235. </ImageBackground>
  236. </>
  237. )}
  238. {/* 左右切换按钮 */}
  239. {currentIndex > 0 && (
  240. <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
  241. <Image
  242. source={{ uri: Images.box.detail.left }}
  243. style={styles.arrowImg}
  244. contentFit="contain"
  245. />
  246. </TouchableOpacity>
  247. )}
  248. {currentIndex < products.length - 1 && (
  249. <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
  250. <Image
  251. source={{ uri: Images.box.detail.right }}
  252. style={styles.arrowImg}
  253. contentFit="contain"
  254. />
  255. </TouchableOpacity>
  256. )}
  257. </View>
  258. {/* 左侧装饰 */}
  259. <Image
  260. source={{ uri: Images.box.detail.positionBgleftBg }}
  261. style={styles.positionBgleftBg}
  262. contentFit="contain"
  263. />
  264. {/* 右侧装饰 */}
  265. <Image
  266. source={{ uri: Images.box.detail.positionBgRightBg }}
  267. style={styles.positionBgRightBg}
  268. contentFit="contain"
  269. />
  270. </ImageBackground>
  271. {/* 底部装饰文字 */}
  272. <Image
  273. source={{ uri: Images.box.detail.mainGoodsSectionBtext }}
  274. style={styles.mainGoodsSectionBtext}
  275. contentFit="cover"
  276. />
  277. {/* 侧边按钮 - 规则 */}
  278. <TouchableOpacity
  279. style={[styles.positionBut, styles.positionRule]}
  280. onPress={() => ruleRef.current?.show()}
  281. >
  282. <ImageBackground
  283. source={{ uri: Images.box.detail.positionBgLeft }}
  284. style={styles.positionButBg}
  285. resizeMode="contain"
  286. >
  287. <Text style={styles.positionButText}>规则</Text>
  288. </ImageBackground>
  289. </TouchableOpacity>
  290. {/* 侧边按钮 - 记录 */}
  291. <TouchableOpacity
  292. style={[styles.positionBut, styles.positionRecord]}
  293. onPress={() => recordRef.current?.show()}
  294. >
  295. <ImageBackground
  296. source={{ uri: Images.box.detail.positionBgLeft }}
  297. style={styles.positionButBg}
  298. resizeMode="contain"
  299. >
  300. <Text style={styles.positionButText}>记录</Text>
  301. </ImageBackground>
  302. </TouchableOpacity>
  303. {/* 侧边按钮 - 客服 */}
  304. <TouchableOpacity
  305. style={[styles.positionBut, styles.positionService]}
  306. onPress={() => kefuRef.current?.open()}
  307. >
  308. <ImageBackground
  309. source={{ uri: Images.box.detail.positionBgRight }}
  310. style={styles.positionButBg}
  311. resizeMode="contain"
  312. >
  313. <Text style={styles.positionButTextR}>客服</Text>
  314. </ImageBackground>
  315. </TouchableOpacity>
  316. {/* 侧边按钮 - 仓库 */}
  317. <TouchableOpacity
  318. style={[styles.positionBut, styles.positionStore]}
  319. onPress={() => router.push("/cloud-warehouse" as any)}
  320. >
  321. <ImageBackground
  322. source={{ uri: Images.box.detail.positionBgRight }}
  323. style={styles.positionButBg}
  324. resizeMode="contain"
  325. >
  326. <Text style={styles.positionButTextR}>仓库</Text>
  327. </ImageBackground>
  328. </TouchableOpacity>
  329. {/* 商品列表 */}
  330. <ProductList
  331. products={products}
  332. levelList={data.luckGoodsLevelProbabilityList}
  333. poolId={poolId!}
  334. price={data.price}
  335. />
  336. {/* 说明文字 */}
  337. <ExplainSection poolId={poolId!} />
  338. <View style={{ height: 150 }} />
  339. </ScrollView>
  340. {/* 底部购买栏 */}
  341. <ImageBackground
  342. source={{ uri: Images.box.detail.boxDetailBott }}
  343. style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}
  344. resizeMode="cover"
  345. >
  346. <View style={styles.bottomBtns}>
  347. <TouchableOpacity
  348. style={styles.btnItem}
  349. onPress={() => handlePay(1)}
  350. activeOpacity={0.8}
  351. >
  352. <ImageBackground
  353. source={{ uri: Images.common.butBgV }}
  354. style={styles.btnBg}
  355. resizeMode="contain"
  356. >
  357. <Text style={styles.btnText}>购买一盒</Text>
  358. <Text style={styles.btnPrice}>
  359. (¥{data.specialPrice || data.price})
  360. </Text>
  361. </ImageBackground>
  362. </TouchableOpacity>
  363. <TouchableOpacity
  364. style={styles.btnItem}
  365. onPress={() => handlePay(5)}
  366. activeOpacity={0.8}
  367. >
  368. <ImageBackground
  369. source={{ uri: Images.common.butBgL }}
  370. style={styles.btnBg}
  371. resizeMode="contain"
  372. >
  373. <Text style={styles.btnText}>购买五盒</Text>
  374. <Text style={styles.btnPrice}>
  375. (¥{data.specialPriceFive || data.price * 5})
  376. </Text>
  377. </ImageBackground>
  378. </TouchableOpacity>
  379. <TouchableOpacity
  380. style={styles.btnItem}
  381. onPress={() => checkoutRef.current?.showFreedom()}
  382. activeOpacity={0.8}
  383. >
  384. <ImageBackground
  385. source={{ uri: Images.common.butBgH }}
  386. style={styles.btnBg}
  387. resizeMode="contain"
  388. >
  389. <Text style={styles.btnText}>购买多盒</Text>
  390. </ImageBackground>
  391. </TouchableOpacity>
  392. </View>
  393. </ImageBackground>
  394. </ImageBackground>
  395. <CheckoutModal
  396. ref={checkoutRef}
  397. data={data}
  398. poolId={poolId!}
  399. onSuccess={handleSuccess}
  400. />
  401. <RecordModal ref={recordRef} poolId={poolId!} />
  402. <RuleModal ref={ruleRef} />
  403. </View>
  404. );
  405. }
  406. const styles = StyleSheet.create({
  407. container: { flex: 1, backgroundColor: "#1a1a2e" },
  408. background: { flex: 1 },
  409. loadingContainer: {
  410. flex: 1,
  411. backgroundColor: "#1a1a2e",
  412. justifyContent: "center",
  413. alignItems: "center",
  414. },
  415. errorText: { color: "#999", fontSize: 16 },
  416. backBtn2: {
  417. marginTop: 20,
  418. backgroundColor: "#ff6600",
  419. paddingHorizontal: 20,
  420. paddingVertical: 10,
  421. borderRadius: 8,
  422. },
  423. backBtn2Text: { color: "#fff", fontSize: 14 },
  424. // 顶部导航
  425. header: {
  426. flexDirection: "row",
  427. alignItems: "center",
  428. justifyContent: "space-between",
  429. paddingHorizontal: 10,
  430. paddingBottom: 10,
  431. position: "absolute",
  432. top: 0,
  433. left: 0,
  434. right: 0,
  435. zIndex: 100,
  436. },
  437. backBtn: {
  438. width: 40,
  439. height: 40,
  440. justifyContent: "center",
  441. alignItems: "center",
  442. },
  443. backText: { color: "#fff", fontSize: 16, fontWeight: "bold" },
  444. headerTitle: {
  445. color: "#fff",
  446. fontSize: 15,
  447. fontWeight: "bold",
  448. flex: 1,
  449. textAlign: "center",
  450. width: 250,
  451. },
  452. placeholder: { width: 40 },
  453. scrollView: { flex: 1 },
  454. // 主商品展示区域
  455. mainGoodsSection: {
  456. width: SCREEN_WIDTH,
  457. height: 504,
  458. position: "relative",
  459. },
  460. mainSwiper: {
  461. position: "relative",
  462. width: "100%",
  463. height: 375,
  464. alignItems: "center",
  465. justifyContent: "center",
  466. marginTop: -50,
  467. },
  468. productImageBox: {
  469. width: 200,
  470. height: 300,
  471. justifyContent: "center",
  472. alignItems: "center",
  473. },
  474. productImage: {
  475. width: 200,
  476. height: 300,
  477. },
  478. detailsBut: {
  479. width: 120,
  480. height: 45,
  481. justifyContent: "center",
  482. alignItems: "center",
  483. marginTop: -30,
  484. },
  485. detailsText: {
  486. flexDirection: "row",
  487. alignItems: "center",
  488. },
  489. levelText: {
  490. fontSize: 14,
  491. color: "#FBC400",
  492. fontWeight: "bold",
  493. },
  494. probabilityText: {
  495. fontSize: 10,
  496. color: "#FBC400",
  497. marginLeft: 3,
  498. },
  499. goodsNameBg: {
  500. position: "absolute",
  501. left: 47,
  502. top: 53,
  503. width: 43,
  504. height: 100,
  505. paddingTop: 8,
  506. justifyContent: "flex-start",
  507. alignItems: "center",
  508. },
  509. goodsNameText: {
  510. fontSize: 12,
  511. fontWeight: "bold",
  512. color: "#000",
  513. writingDirection: "ltr",
  514. width: 20,
  515. textAlign: "center",
  516. },
  517. prevBtn: { position: "absolute", left: 35, top: "40%" },
  518. nextBtn: { position: "absolute", right: 35, top: "40%" },
  519. arrowImg: { width: 33, height: 38 },
  520. // 装饰图片
  521. positionBgleftBg: {
  522. position: "absolute",
  523. left: 0,
  524. top: 225,
  525. width: 32,
  526. height: 188,
  527. },
  528. positionBgRightBg: {
  529. position: "absolute",
  530. right: 0,
  531. top: 225,
  532. width: 32,
  533. height: 188,
  534. },
  535. mainGoodsSectionBtext: {
  536. width: SCREEN_WIDTH,
  537. height: 74,
  538. marginTop: -10,
  539. },
  540. // 侧边按钮
  541. positionBut: {
  542. position: "absolute",
  543. zIndex: 10,
  544. width: 35,
  545. height: 34,
  546. },
  547. positionButBg: {
  548. width: 35,
  549. height: 34,
  550. justifyContent: "center",
  551. alignItems: "center",
  552. },
  553. positionButText: {
  554. fontSize: 12,
  555. fontWeight: "bold",
  556. color: "#fff",
  557. transform: [{ rotate: "14deg" }],
  558. textShadowColor: "#000",
  559. textShadowOffset: { width: 1, height: 1 },
  560. textShadowRadius: 1,
  561. },
  562. positionButTextR: {
  563. fontSize: 12,
  564. fontWeight: "bold",
  565. color: "#fff",
  566. transform: [{ rotate: "-16deg" }],
  567. textShadowColor: "#000",
  568. textShadowOffset: { width: 1, height: 1 },
  569. textShadowRadius: 1,
  570. },
  571. positionRule: { top: 256, left: 0 },
  572. positionRecord: { top: 300, left: 0 },
  573. positionStore: { top: 256, right: 0 },
  574. positionService: { top: 300, right: 0 },
  575. // 底部购买栏
  576. bottomBar: {
  577. position: "absolute",
  578. bottom: 0,
  579. left: 0,
  580. right: 0,
  581. height: 69,
  582. paddingHorizontal: 5,
  583. },
  584. bottomBtns: {
  585. flexDirection: "row",
  586. height: 64,
  587. alignItems: "center",
  588. justifyContent: "space-around",
  589. },
  590. btnItem: {
  591. flex: 1,
  592. marginHorizontal: 6,
  593. },
  594. btnBg: {
  595. width: "100%",
  596. height: 54,
  597. justifyContent: "center",
  598. alignItems: "center",
  599. },
  600. btnText: {
  601. fontSize: 14,
  602. fontWeight: "bold",
  603. color: "#fff",
  604. },
  605. btnPrice: {
  606. fontSize: 9,
  607. color: "#fff",
  608. },
  609. });