swipe.tsx 16 KB

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