swipe.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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. 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 {
  17. RecordModal,
  18. RecordModalRef,
  19. } from "@/app/treasure-hunt/components/RecordModal";
  20. import {
  21. RuleModal,
  22. RuleModalRef,
  23. } from "@/app/treasure-hunt/components/RuleModal";
  24. import { Images } from "@/constants/images";
  25. import { getPoolDetail } from "@/services/award";
  26. const { width: SCREEN_WIDTH } = Dimensions.get("window");
  27. interface ProductItem {
  28. id: string;
  29. name: string;
  30. cover: string;
  31. level: string;
  32. probability: number;
  33. quantity?: number;
  34. spu?: {
  35. id: string;
  36. cover: string;
  37. name: string;
  38. marketPrice?: number;
  39. parameter?: string;
  40. brandName?: string;
  41. worksName?: string;
  42. pic?: string;
  43. };
  44. }
  45. interface PoolData {
  46. id: string;
  47. name: string;
  48. price: number;
  49. luckGoodsList: ProductItem[];
  50. recommendedLuckPool?: any[];
  51. }
  52. export default function AwardDetailSwipeScreen() {
  53. const { poolId, index } = useLocalSearchParams<{
  54. poolId: string;
  55. index: string;
  56. }>();
  57. const router = useRouter();
  58. const insets = useSafeAreaInsets();
  59. const [loading, setLoading] = useState(true);
  60. const [data, setData] = useState<PoolData | null>(null);
  61. const [products, setProducts] = useState<ProductItem[]>([]);
  62. const [currentIndex, setCurrentIndex] = useState(parseInt(index || "0", 10));
  63. const ruleRef = useRef<RuleModalRef>(null);
  64. const recordRef = useRef<RecordModalRef>(null);
  65. console.log(
  66. `[DEBUG-SWIPE] Init. PoolId: ${poolId}, ParamIndex: ${index}, StateIndex: ${currentIndex}`,
  67. );
  68. const loadData = useCallback(async () => {
  69. if (!poolId) return;
  70. setLoading(true);
  71. try {
  72. const detail = await getPoolDetail(poolId);
  73. if (detail) {
  74. setData(detail);
  75. setProducts(detail.luckGoodsList || []);
  76. }
  77. } catch (error) {
  78. console.error("加载数据失败:", error);
  79. }
  80. setLoading(false);
  81. }, [poolId]);
  82. useEffect(() => {
  83. loadData();
  84. }, [poolId]);
  85. const handlePrev = () => {
  86. if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
  87. };
  88. const handleNext = () => {
  89. if (currentIndex < products.length - 1) setCurrentIndex(currentIndex + 1);
  90. };
  91. const parseParameter = (paramStr?: string) => {
  92. if (!paramStr) return [];
  93. try {
  94. return JSON.parse(paramStr);
  95. } catch {
  96. return [];
  97. }
  98. };
  99. if (loading) {
  100. return (
  101. <View style={[styles.loadingContainer, { paddingTop: insets.top }]}>
  102. <ActivityIndicator size="large" color="#ff6600" />
  103. </View>
  104. );
  105. }
  106. if (!data || products.length === 0) {
  107. return (
  108. <View style={[styles.loadingContainer, { paddingTop: insets.top }]}>
  109. <Text style={styles.errorText}>商品不存在</Text>
  110. <TouchableOpacity style={styles.backBtn2} onPress={() => router.back()}>
  111. <Text style={styles.backBtn2Text}>返回</Text>
  112. </TouchableOpacity>
  113. </View>
  114. );
  115. }
  116. const currentProduct = products[currentIndex];
  117. const params = parseParameter(currentProduct?.spu?.parameter);
  118. const detailPics = currentProduct?.spu?.pic
  119. ? currentProduct.spu.pic.split(",").filter(Boolean)
  120. : [];
  121. const recommendList = data.recommendedLuckPool || [];
  122. return (
  123. <View style={styles.container}>
  124. <StatusBar barStyle="dark-content" />
  125. {/* 顶部导航 */}
  126. <View style={[styles.header, { paddingTop: insets.top }]}>
  127. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  128. <Text style={styles.backText}>{"<"}</Text>
  129. </TouchableOpacity>
  130. <View style={styles.placeholder} />
  131. </View>
  132. <ScrollView
  133. style={styles.scrollView}
  134. showsVerticalScrollIndicator={false}
  135. >
  136. <View style={styles.detailWrapper}>
  137. {/* 侧边按钮 - 规则 */}
  138. <TouchableOpacity
  139. style={[styles.positionBut, styles.positionRule]}
  140. onPress={() => ruleRef.current?.show()}
  141. >
  142. <ImageBackground
  143. source={{ uri: Images.box.detail.positionBgLeft }}
  144. style={styles.positionButBg}
  145. resizeMode="contain"
  146. >
  147. <Text style={styles.positionButText}>规则</Text>
  148. </ImageBackground>
  149. </TouchableOpacity>
  150. {/* 侧边按钮 - 记录 */}
  151. <TouchableOpacity
  152. style={[styles.positionBut, styles.positionRecord]}
  153. onPress={() => recordRef.current?.show()}
  154. >
  155. <ImageBackground
  156. source={{ uri: Images.box.detail.positionBgLeft }}
  157. style={styles.positionButBg}
  158. resizeMode="contain"
  159. >
  160. <Text style={styles.positionButText}>记录</Text>
  161. </ImageBackground>
  162. </TouchableOpacity>
  163. {/* 侧边按钮 - 仓库 */}
  164. <TouchableOpacity
  165. style={[styles.positionBut, styles.positionStore]}
  166. onPress={() => router.push("/cloud-warehouse" as any)}
  167. >
  168. <ImageBackground
  169. source={{ uri: Images.box.detail.positionBgRight }}
  170. style={styles.positionButBg}
  171. resizeMode="contain"
  172. >
  173. <Text style={styles.positionButTextR}>仓库</Text>
  174. </ImageBackground>
  175. </TouchableOpacity>
  176. {/* 商品图片区域 */}
  177. <View style={styles.imageSection}>
  178. <Image
  179. source={{
  180. uri: currentProduct?.spu?.cover || currentProduct?.cover,
  181. }}
  182. style={styles.productImage}
  183. contentFit="contain"
  184. />
  185. {/* 左右切换按钮 */}
  186. {currentIndex > 0 && (
  187. <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
  188. <Text style={styles.arrowText}>{"<"}</Text>
  189. </TouchableOpacity>
  190. )}
  191. {currentIndex < products.length - 1 && (
  192. <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
  193. <Text style={styles.arrowText}>{">"}</Text>
  194. </TouchableOpacity>
  195. )}
  196. </View>
  197. {/* 价格和名称区域 */}
  198. <View style={styles.priceSection}>
  199. <View style={styles.priceRow}>
  200. <Text style={styles.priceText}>
  201. ¥{currentProduct?.spu?.marketPrice || data.price}
  202. </Text>
  203. </View>
  204. <Text style={styles.productName}>{currentProduct?.name}</Text>
  205. </View>
  206. {/* 参数区域 */}
  207. <View style={styles.paramSection}>
  208. <View style={styles.paramHeader}>
  209. <Text style={styles.paramTitle}>参数</Text>
  210. </View>
  211. {params.length > 0 && (
  212. <ScrollView
  213. horizontal
  214. showsHorizontalScrollIndicator={false}
  215. style={styles.paramScroll}
  216. >
  217. {params.map(
  218. (param: { label: string; value: string }, idx: number) => (
  219. <View key={idx} style={styles.paramItem}>
  220. <Text style={styles.paramLabel}>{param.label}</Text>
  221. <Text style={styles.paramValue}>{param.value}</Text>
  222. </View>
  223. ),
  224. )}
  225. </ScrollView>
  226. )}
  227. {currentProduct?.spu?.worksName && (
  228. <View style={styles.paramRow}>
  229. <Text style={styles.paramRowLabel}>IP</Text>
  230. <Text style={styles.paramRowValue}>
  231. {currentProduct.spu.worksName}
  232. </Text>
  233. </View>
  234. )}
  235. {currentProduct?.spu?.brandName && (
  236. <View style={styles.paramRow}>
  237. <Text style={styles.paramRowLabel}>品牌</Text>
  238. <Text style={styles.paramRowValue}>
  239. {currentProduct.spu.brandName}
  240. </Text>
  241. </View>
  242. )}
  243. </View>
  244. {/* 放心购 正品保障 */}
  245. <View style={styles.guaranteeSection}>
  246. <Text style={styles.guaranteeTitle}>放心购 正品保障</Text>
  247. <Text style={styles.guaranteeText}>
  248. 不支持七天无理由退换货 包邮
  249. </Text>
  250. </View>
  251. {/* 商品推荐 */}
  252. {recommendList.length > 0 && (
  253. <View style={styles.recommendSection}>
  254. <Text style={styles.recommendTitle}>商品推荐</Text>
  255. <ScrollView
  256. horizontal
  257. showsHorizontalScrollIndicator={false}
  258. contentContainerStyle={styles.recommendScroll}
  259. >
  260. {recommendList.map((item: any) => (
  261. <TouchableOpacity
  262. key={item.id}
  263. style={styles.recommendItem}
  264. activeOpacity={0.8}
  265. onPress={() =>
  266. router.push({
  267. pathname: "/treasure-hunt",
  268. params: { poolId: item.id },
  269. } as any)
  270. }
  271. >
  272. <Image
  273. source={{ uri: item.cover }}
  274. style={styles.recommendImage}
  275. contentFit="contain"
  276. />
  277. <Text style={styles.recommendName} numberOfLines={1}>
  278. {item.name}
  279. </Text>
  280. <Text style={styles.recommendPrice}>¥{item.price}</Text>
  281. </TouchableOpacity>
  282. ))}
  283. </ScrollView>
  284. </View>
  285. )}
  286. {/* 商品详情 */}
  287. <View style={styles.detailSection}>
  288. <Text style={styles.detailTitle}>商品详情</Text>
  289. {detailPics.length > 0 ? (
  290. detailPics.map((pic, idx) => (
  291. <Image
  292. key={idx}
  293. source={{ uri: pic }}
  294. style={styles.detailImage}
  295. contentFit="contain"
  296. />
  297. ))
  298. ) : (
  299. <View style={styles.detailContent}>
  300. <Text style={styles.detailHeading}>商城购买须知!</Text>
  301. <Text style={styles.detailSubTitle}>商城现货</Text>
  302. <Text style={styles.detailText}>
  303. 商城所售现货商品均为全新正版商品。手办模玩非艺术品,因厂商品控差异导致的微小瑕疵属于正常情况,官图仅供参考,具体以实物为准。
  304. </Text>
  305. <Text style={styles.detailSubTitle}>新品预定</Text>
  306. <Text style={styles.detailText}>
  307. 预定商品的总价=定金+尾款,在预定期限内支付定金后,商品到货并补齐尾款后,超级商城才会发货相应商品预定订单确认成功后,定金不可退。
  308. {"\n"}
  309. 商品页面显示的商品制作完成时间及预计补款时间,都是按照官方预估的时间推测,具体到货时间请以实际出货为准。如因厂商、海关等因素造成延期的,不接受以此原因申请定金退款,请耐心等待。
  310. </Text>
  311. <Text style={styles.detailSubTitle}>预售补款</Text>
  312. <Text style={styles.detailText}>
  313. 商品到货后超级商城会通过您在预定时预留的号码进行短信通知请自行留意。为防止错过补款通知,可添加商城客服,并备注所购商品进入对应社群,社群会同步推送新品咨询及补款通知。
  314. </Text>
  315. </View>
  316. )}
  317. </View>
  318. </View>
  319. </ScrollView>
  320. {/* 弹窗组件 */}
  321. <RuleModal ref={ruleRef} />
  322. <RecordModal ref={recordRef} poolId={poolId as string} />
  323. </View>
  324. );
  325. }
  326. const styles = StyleSheet.create({
  327. container: { flex: 1, backgroundColor: "#f5f5f5" },
  328. loadingContainer: {
  329. flex: 1,
  330. backgroundColor: "#f5f5f5",
  331. justifyContent: "center",
  332. alignItems: "center",
  333. },
  334. errorText: { color: "#999", fontSize: 16 },
  335. backBtn2: {
  336. marginTop: 20,
  337. backgroundColor: "#ff6600",
  338. paddingHorizontal: 20,
  339. paddingVertical: 10,
  340. borderRadius: 8,
  341. },
  342. backBtn2Text: { color: "#fff", fontSize: 14 },
  343. header: {
  344. flexDirection: "row",
  345. alignItems: "center",
  346. justifyContent: "space-between",
  347. paddingHorizontal: 10,
  348. paddingBottom: 10,
  349. backgroundColor: "transparent",
  350. position: "absolute",
  351. top: 0,
  352. left: 0,
  353. right: 0,
  354. zIndex: 10,
  355. },
  356. backBtn: {
  357. width: 40,
  358. height: 40,
  359. justifyContent: "center",
  360. alignItems: "center",
  361. },
  362. backText: { color: "#333", fontSize: 24, fontWeight: "bold" },
  363. placeholder: { width: 40 },
  364. scrollView: { flex: 1 },
  365. // 图片区域
  366. imageSection: {
  367. paddingTop: 80,
  368. paddingBottom: 20,
  369. alignItems: "center",
  370. position: "relative",
  371. },
  372. productImage: {
  373. width: SCREEN_WIDTH * 0.7,
  374. height: 350,
  375. },
  376. prevBtn: {
  377. position: "absolute",
  378. left: 10,
  379. top: "50%",
  380. width: 36,
  381. height: 36,
  382. backgroundColor: "rgba(0,0,0,0.3)",
  383. borderRadius: 18,
  384. justifyContent: "center",
  385. alignItems: "center",
  386. },
  387. nextBtn: {
  388. position: "absolute",
  389. right: 10,
  390. top: "50%",
  391. width: 36,
  392. height: 36,
  393. backgroundColor: "rgba(0,0,0,0.3)",
  394. borderRadius: 18,
  395. justifyContent: "center",
  396. alignItems: "center",
  397. },
  398. arrowText: { color: "#fff", fontSize: 18, fontWeight: "bold" },
  399. detailWrapper: {
  400. position: "relative",
  401. },
  402. // 侧边悬浮按钮
  403. positionBut: {
  404. position: "absolute",
  405. left: 0,
  406. zIndex: 100,
  407. width: 35,
  408. height: 35,
  409. },
  410. positionRule: {
  411. top: 256,
  412. left: 0,
  413. },
  414. positionRecord: {
  415. top: 300,
  416. left: 0,
  417. },
  418. positionStore: {
  419. top: 256,
  420. right: 0,
  421. },
  422. positionButBg: {
  423. width: 35,
  424. height: 34,
  425. justifyContent: "center",
  426. alignItems: "center",
  427. },
  428. positionButText: {
  429. color: "#fff",
  430. fontSize: 12,
  431. fontWeight: "bold",
  432. transform: [{ rotate: "14deg" }],
  433. marginTop: -2,
  434. textShadowColor: "rgba(0,0,0,0.5)",
  435. textShadowOffset: { width: 1, height: 1 },
  436. textShadowRadius: 1,
  437. },
  438. positionButTextR: {
  439. color: "#fff",
  440. fontSize: 12,
  441. fontWeight: "bold",
  442. transform: [{ rotate: "-16deg" }],
  443. marginTop: -2,
  444. textShadowColor: "rgba(0,0,0,0.5)",
  445. textShadowOffset: { width: 1, height: 1 },
  446. textShadowRadius: 1,
  447. },
  448. // 价格区域
  449. priceSection: {
  450. backgroundColor: "#fff",
  451. paddingHorizontal: 16,
  452. paddingVertical: 12,
  453. },
  454. priceRow: {
  455. flexDirection: "row",
  456. alignItems: "baseline",
  457. },
  458. priceText: {
  459. fontSize: 24,
  460. color: "#ff4444",
  461. fontWeight: "bold",
  462. },
  463. productName: {
  464. fontSize: 16,
  465. color: "#333",
  466. marginTop: 8,
  467. lineHeight: 22,
  468. },
  469. // 参数区域
  470. paramSection: {
  471. backgroundColor: "#fff",
  472. marginTop: 10,
  473. paddingHorizontal: 16,
  474. paddingVertical: 12,
  475. },
  476. paramHeader: {
  477. flexDirection: "row",
  478. alignItems: "center",
  479. paddingBottom: 10,
  480. borderBottomWidth: 1,
  481. borderBottomColor: "#eee",
  482. },
  483. paramTitle: {
  484. fontSize: 14,
  485. color: "#666",
  486. },
  487. paramScroll: {
  488. marginTop: 10,
  489. },
  490. paramItem: {
  491. paddingHorizontal: 12,
  492. borderRightWidth: 1,
  493. borderRightColor: "#eee",
  494. },
  495. paramLabel: { fontSize: 14, color: "#666" },
  496. paramValue: { fontSize: 14, color: "#333", marginTop: 4 },
  497. paramRow: {
  498. flexDirection: "row",
  499. alignItems: "center",
  500. paddingTop: 12,
  501. },
  502. paramRowLabel: {
  503. fontSize: 14,
  504. color: "#666",
  505. width: 50,
  506. },
  507. paramRowValue: {
  508. fontSize: 14,
  509. color: "#333",
  510. flex: 1,
  511. },
  512. // 放心购区域
  513. guaranteeSection: {
  514. backgroundColor: "#fff",
  515. marginTop: 10,
  516. paddingHorizontal: 16,
  517. paddingVertical: 12,
  518. flexDirection: "row",
  519. alignItems: "center",
  520. justifyContent: "space-between",
  521. },
  522. guaranteeTitle: {
  523. fontSize: 14,
  524. color: "#333",
  525. fontWeight: "bold",
  526. },
  527. guaranteeText: {
  528. fontSize: 12,
  529. color: "#999",
  530. },
  531. // 商品推荐
  532. recommendSection: {
  533. backgroundColor: "#1a1a1a",
  534. marginTop: 10,
  535. paddingHorizontal: 16,
  536. paddingVertical: 16,
  537. },
  538. recommendTitle: {
  539. fontSize: 16,
  540. fontWeight: "bold",
  541. color: "#fff",
  542. marginBottom: 12,
  543. },
  544. recommendScroll: {
  545. paddingRight: 10,
  546. },
  547. recommendItem: {
  548. width: 90,
  549. marginRight: 12,
  550. },
  551. recommendImage: {
  552. width: 90,
  553. height: 90,
  554. backgroundColor: "#fff",
  555. borderRadius: 4,
  556. },
  557. recommendName: {
  558. fontSize: 12,
  559. color: "#fff",
  560. marginTop: 6,
  561. },
  562. recommendPrice: {
  563. fontSize: 14,
  564. color: "#ff4444",
  565. fontWeight: "bold",
  566. marginTop: 4,
  567. },
  568. // 商品详情
  569. detailSection: {
  570. backgroundColor: "#1a1a1a",
  571. marginTop: 10,
  572. paddingHorizontal: 16,
  573. paddingVertical: 16,
  574. },
  575. detailTitle: {
  576. fontSize: 16,
  577. fontWeight: "bold",
  578. color: "#fff",
  579. marginBottom: 16,
  580. },
  581. detailImage: {
  582. width: SCREEN_WIDTH - 32,
  583. height: 400,
  584. marginBottom: 10,
  585. },
  586. detailContent: {
  587. backgroundColor: "#fff",
  588. borderRadius: 8,
  589. padding: 16,
  590. },
  591. detailHeading: {
  592. fontSize: 18,
  593. fontWeight: "bold",
  594. color: "#333",
  595. textAlign: "center",
  596. marginBottom: 20,
  597. },
  598. detailSubTitle: {
  599. fontSize: 16,
  600. fontWeight: "bold",
  601. color: "#333",
  602. marginTop: 16,
  603. marginBottom: 8,
  604. paddingLeft: 10,
  605. borderLeftWidth: 3,
  606. borderLeftColor: "#333",
  607. },
  608. detailText: {
  609. fontSize: 14,
  610. color: "#666",
  611. lineHeight: 22,
  612. },
  613. });