wish.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import { Images } from '@/constants/images';
  2. import Service from '@/services/weal';
  3. import { useRouter } from 'expo-router';
  4. import React, { useCallback, useEffect, useRef, useState } from 'react';
  5. import {
  6. Alert,
  7. Dimensions,
  8. Image,
  9. ImageBackground,
  10. ScrollView,
  11. StatusBar,
  12. StyleSheet,
  13. Text,
  14. TouchableOpacity,
  15. View,
  16. } from 'react-native';
  17. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  18. import { WishMaterialModal, WishMaterialModalRef } from './components/WishMaterialModal';
  19. import { WishRecordModal, WishRecordModalRef } from './components/WishRecordModal';
  20. import { WishRuleModal, WishRuleModalRef } from './components/WishRuleModal';
  21. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  22. const CDN_BASE = 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart';
  23. const wishImages = {
  24. bg: `${CDN_BASE}/common/wishBg.png`,
  25. title: `${CDN_BASE}/welfare/wishTitle.png`,
  26. rule: `${CDN_BASE}/welfare/wishRule.png`,
  27. addBg: `${CDN_BASE}/welfare/addBg.png`,
  28. addLiBg: `${CDN_BASE}/welfare/addLiBg.png`,
  29. addClose: `${CDN_BASE}/welfare/addClose.png`,
  30. addSectionBg: `${CDN_BASE}/welfare/toys/addSectionBg.png`,
  31. add: `${CDN_BASE}/welfare/add.png`,
  32. left: `${CDN_BASE}/box/detail/left.png`,
  33. right: `${CDN_BASE}/box/detail/right.png`,
  34. progressBar: `${CDN_BASE}/welfare/toys/wishProgressBar.png`,
  35. };
  36. interface WishItem {
  37. id: string;
  38. name: string;
  39. spu: { cover: string };
  40. quantity: number;
  41. completeQuantity: number;
  42. }
  43. interface GoodsItem {
  44. id: string;
  45. spu: { cover: string };
  46. magicAmount: number;
  47. }
  48. export default function WishScreen() {
  49. const router = useRouter();
  50. const insets = useSafeAreaInsets();
  51. const [tableData, setTableData] = useState<WishItem[]>([]);
  52. const [active, setActive] = useState(0);
  53. const [goodsList, setGoodsList] = useState<GoodsItem[]>([]);
  54. const [progress, setProgress] = useState(0);
  55. const [magicAmount, setMagicAmount] = useState(0);
  56. // Modals
  57. const ruleRef = useRef<WishRuleModalRef>(null);
  58. const recordRef = useRef<WishRecordModalRef>(null);
  59. const materialRef = useRef<WishMaterialModalRef>(null);
  60. const loadData = useCallback(async () => {
  61. try {
  62. const data = await Service.getWishList();
  63. // Ensure data is array
  64. const list = Array.isArray(data) ? data : (data?.records || []);
  65. if (list && list.length > 0) {
  66. setTableData(list);
  67. }
  68. } catch (error) {
  69. console.error('加载祈愿数据失败:', error);
  70. }
  71. }, []);
  72. useEffect(() => {
  73. loadData();
  74. }, [loadData]);
  75. // Update progress when active item changes or goodsList changes
  76. useEffect(() => {
  77. const updateProgress = async () => {
  78. if (tableData.length > 0 && tableData[active]) {
  79. const current = tableData[active];
  80. if (goodsList.length === 0) {
  81. setProgress(0);
  82. setMagicAmount(0);
  83. } else {
  84. // Call preview to get progress
  85. const inventoryIds = goodsList.map(g => g.id);
  86. try {
  87. const res = await Service.wishPreview({
  88. substituteGoodsId: current.id,
  89. inventoryIds
  90. });
  91. if (res) {
  92. let p = (res.progress || 0) * 100;
  93. if (p > 100) p = 100;
  94. setProgress(p);
  95. setMagicAmount(res.magicAmount || 0);
  96. }
  97. } catch (e) {
  98. console.error(e);
  99. }
  100. }
  101. } else {
  102. setProgress(0);
  103. setMagicAmount(0);
  104. }
  105. };
  106. updateProgress();
  107. }, [tableData, active, goodsList]);
  108. const handlePrev = () => {
  109. if (active > 0) {
  110. setActive(active - 1);
  111. setGoodsList([]);
  112. }
  113. };
  114. const handleNext = () => {
  115. if (active < tableData.length - 1) {
  116. setActive(active + 1);
  117. setGoodsList([]);
  118. }
  119. };
  120. const currentItem = tableData[active];
  121. const serverRemaining = currentItem ? currentItem.quantity - currentItem.completeQuantity : 0;
  122. // Visual remaining doesn't update until submit in this flow
  123. const remaining = serverRemaining;
  124. const removeGoods = (item: GoodsItem) => {
  125. setGoodsList(goodsList.filter((g) => g.id !== item.id));
  126. };
  127. const openMaterialModal = () => {
  128. if (!currentItem) return;
  129. materialRef.current?.show((selected) => {
  130. // Add selected items to goodsList
  131. setGoodsList(prev => {
  132. const newIds = new Set(prev.map(i => i.id));
  133. const toAdd = selected.filter(i => !newIds.has(i.id));
  134. return [...prev, ...toAdd];
  135. });
  136. });
  137. };
  138. const submitWish = async () => {
  139. try {
  140. const inventoryIds = goodsList.map(g => g.id);
  141. console.log('Submitting Wish:', { substituteGoodsId: currentItem.id, inventoryIds });
  142. const res = await Service.wishSubmit({
  143. substituteGoodsId: currentItem.id,
  144. inventoryIds
  145. });
  146. console.log('Submit Res:', res);
  147. // Fix: Use loose equality since backend returns string "0"
  148. if (res.code == 0 || res.success) {
  149. Alert.alert('成功', '点亮成功!');
  150. setGoodsList([]);
  151. loadData();
  152. } else {
  153. Alert.alert('提交失败', `Code: ${res.code}, Msg: ${res.msg}`);
  154. }
  155. } catch (error) {
  156. console.error('LightUp Error:', error);
  157. Alert.alert('执行异常', String(error));
  158. }
  159. };
  160. const handleLightUp = async () => {
  161. if (!currentItem) return;
  162. if (progress < 100) {
  163. if (goodsList.length === 0) {
  164. Alert.alert('提示', '请先添加材料');
  165. return;
  166. }
  167. Alert.alert('提示', '进度未满100%,无法点亮');
  168. return;
  169. }
  170. Alert.alert(
  171. '确认点亮',
  172. '是否确认消耗所选材料进行点亮?',
  173. [
  174. { text: '取消', style: 'cancel' },
  175. {
  176. text: '确认',
  177. onPress: () => {
  178. submitWish();
  179. }
  180. }
  181. ]
  182. );
  183. };
  184. return (
  185. <View style={styles.container}>
  186. <StatusBar barStyle="light-content" />
  187. <ImageBackground source={{ uri: wishImages.bg }} style={styles.background} resizeMode="cover">
  188. {/* 头部导航 */}
  189. <View style={[styles.header, { paddingTop: insets.top }]}>
  190. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  191. <Text style={styles.backText}>←</Text>
  192. </TouchableOpacity>
  193. <Text style={styles.title}>祈愿</Text>
  194. <View style={styles.placeholder} />
  195. </View>
  196. {/* 规则按钮 */}
  197. <TouchableOpacity style={[styles.ruleBtn, { top: insets.top + 160 }]} onPress={() => ruleRef.current?.show()}>
  198. <Image source={{ uri: wishImages.rule }} style={styles.ruleBtnImg} resizeMode="contain" />
  199. </TouchableOpacity>
  200. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  201. <View style={{ height: insets.top + 50 }} />
  202. {/* 标题图片 */}
  203. <View style={styles.titleBox}>
  204. <Image source={{ uri: wishImages.title }} style={styles.titleImg} resizeMode="contain" />
  205. </View>
  206. {/* 卡片轮播区域 */}
  207. <View style={styles.swiperBox}>
  208. {/* 左箭头 */}
  209. {active > 0 && (
  210. <TouchableOpacity style={styles.prevBtn} onPress={handlePrev}>
  211. <Image source={{ uri: wishImages.left }} style={styles.arrowImg} resizeMode="contain" />
  212. </TouchableOpacity>
  213. )}
  214. {/* 卡片 */}
  215. <View style={styles.cardBox}>
  216. {currentItem ? (
  217. <View style={styles.card}>
  218. <View style={styles.imgBox}>
  219. <Image source={{ uri: currentItem.spu?.cover }} style={styles.spuImage} resizeMode="contain" />
  220. {/* 仅剩标签 */}
  221. <View style={styles.remainingBox}>
  222. <Text style={styles.remainingText}>仅剩:{remaining}</Text>
  223. </View>
  224. </View>
  225. <Text style={styles.cardName} numberOfLines={1}>
  226. {currentItem.name}
  227. </Text>
  228. </View>
  229. ) : (
  230. <View style={styles.card}>
  231. <View style={styles.imgBox}>
  232. <Text style={styles.emptyText}>暂无祈愿商品</Text>
  233. </View>
  234. </View>
  235. )}
  236. </View>
  237. {/* 右箭头 */}
  238. {active < tableData.length - 1 && (
  239. <TouchableOpacity style={styles.nextBtn} onPress={handleNext}>
  240. <Image source={{ uri: wishImages.right }} style={styles.arrowImg} resizeMode="contain" />
  241. </TouchableOpacity>
  242. )}
  243. </View>
  244. {/* 指示点 */}
  245. {tableData.length > 1 && (
  246. <View style={styles.dotsBox}>
  247. {tableData.map((_, i) => (
  248. <View key={i} style={[styles.dot, i === active && styles.dotActive]} />
  249. ))}
  250. </View>
  251. )}
  252. {/* 进度条区域 */}
  253. <View style={styles.progressSection}>
  254. <View style={styles.progressLabelBox}>
  255. <Text style={styles.progressLabel}>进度:</Text>
  256. </View>
  257. {/* Show Magic Return if 100% and magicAmount > 0 */}
  258. {progress >= 100 && magicAmount > 0 && (
  259. <View style={styles.magicBubble}>
  260. <Text style={styles.magicText}>返还: {magicAmount}果实</Text>
  261. </View>
  262. )}
  263. <View style={styles.progressBar}>
  264. <View style={[styles.progressFill, { width: `${progress}%` }]} />
  265. </View>
  266. <Text style={styles.progressText}>{progress > 0 ? progress.toFixed(1) : progress}%</Text>
  267. </View>
  268. {/* 材料添加区域 */}
  269. <ImageBackground source={{ uri: wishImages.addSectionBg }} style={styles.addSection} resizeMode="stretch">
  270. <Text style={styles.addTitle}>材料添加</Text>
  271. <View style={styles.addMain}>
  272. {/* 添加按钮 */}
  273. <TouchableOpacity style={styles.addBtn} onPress={openMaterialModal}>
  274. <ImageBackground source={{ uri: wishImages.addBg }} style={styles.addBtnBg} resizeMode="contain">
  275. <Image source={{ uri: wishImages.add }} style={styles.addIcon} resizeMode="contain" />
  276. <Text style={styles.addBtnText}>添加</Text>
  277. </ImageBackground>
  278. </TouchableOpacity>
  279. {/* 已添加的材料列表 */}
  280. <View style={styles.addCenter}>
  281. <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.addScrollContent}>
  282. {goodsList.map((item, index) => (
  283. <View key={item.id || index} style={styles.addItem}>
  284. <ImageBackground source={{ uri: wishImages.addLiBg }} style={styles.addItemBg} resizeMode="contain">
  285. <Image source={{ uri: item.spu?.cover }} style={styles.addItemImg} resizeMode="cover" />
  286. </ImageBackground>
  287. <TouchableOpacity style={styles.closeBtn} onPress={() => removeGoods(item)}>
  288. <Image source={{ uri: wishImages.addClose }} style={styles.closeIcon} resizeMode="contain" />
  289. </TouchableOpacity>
  290. </View>
  291. ))}
  292. </ScrollView>
  293. </View>
  294. </View>
  295. </ImageBackground>
  296. {/* 底部按钮 */}
  297. <View style={styles.bottomContainer}>
  298. {/* Record Link (Optional, if design has it outside standard flow, adding it near bottom or top)
  299. Wait, original design had "Rule" on left, maybe "Record" is not prominently displayed or inside Rule?
  300. Let's stick to current design.
  301. */}
  302. <TouchableOpacity style={styles.bottomBtn} onPress={handleLightUp}>
  303. <ImageBackground
  304. source={{ uri: progress >= 100 || goodsList.length > 0 ? Images.common.butBgL : Images.common.butBgHui }}
  305. style={styles.bottomBtnBg}
  306. resizeMode="contain"
  307. >
  308. <Text style={styles.bottomBtnText}>{progress >= 100 ? '点亮心愿' : (goodsList.length > 0 ? '确认添加' : '等待参与')}</Text>
  309. </ImageBackground>
  310. </TouchableOpacity>
  311. </View>
  312. <Text style={styles.bottomTip}>*本活动最终解释权归本平台所有</Text>
  313. <View style={{ height: 100 }} />
  314. </ScrollView>
  315. <WishRuleModal ref={ruleRef} />
  316. <WishRecordModal ref={recordRef} />
  317. <WishMaterialModal ref={materialRef} />
  318. </ImageBackground>
  319. </View>
  320. );
  321. }
  322. const styles = StyleSheet.create({
  323. container: { flex: 1, backgroundColor: '#1a1a2e' },
  324. background: { flex: 1 },
  325. header: {
  326. flexDirection: 'row',
  327. alignItems: 'center',
  328. justifyContent: 'space-between',
  329. paddingHorizontal: 10,
  330. paddingBottom: 10,
  331. position: 'absolute',
  332. top: 0,
  333. left: 0,
  334. right: 0,
  335. zIndex: 100,
  336. },
  337. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  338. backText: { color: '#fff', fontSize: 20 },
  339. title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  340. placeholder: { width: 40 },
  341. scrollView: { flex: 1 },
  342. // 规则按钮
  343. ruleBtn: { position: 'absolute', left: 0, zIndex: 99 },
  344. ruleBtnImg: { width: 39, height: 20 },
  345. // 标题
  346. titleBox: { alignItems: 'center', marginBottom: 10 },
  347. titleImg: { width: 288, height: 86 },
  348. // 轮播区域
  349. swiperBox: {
  350. flexDirection: 'row',
  351. alignItems: 'center',
  352. justifyContent: 'center',
  353. paddingHorizontal: 20,
  354. minHeight: 350,
  355. position: 'relative',
  356. },
  357. prevBtn: { position: 'absolute', left: 15, zIndex: 10 },
  358. nextBtn: { position: 'absolute', right: 15, zIndex: 10 },
  359. arrowImg: { width: 51, height: 49 },
  360. // 卡片
  361. cardBox: { alignItems: 'center' },
  362. card: { alignItems: 'center' },
  363. imgBox: {
  364. width: 215,
  365. height: 284,
  366. backgroundColor: '#FFF7D8',
  367. borderWidth: 4.5,
  368. borderColor: '#000',
  369. justifyContent: 'center',
  370. alignItems: 'center',
  371. position: 'relative',
  372. },
  373. spuImage: { width: 200, height: 260 },
  374. emptyText: { color: '#999', fontSize: 14 },
  375. remainingBox: {
  376. position: 'absolute',
  377. bottom: '15%',
  378. backgroundColor: '#fff',
  379. borderWidth: 3,
  380. borderColor: '#000',
  381. paddingHorizontal: 15,
  382. paddingVertical: 5,
  383. },
  384. remainingText: { fontSize: 13, fontWeight: 'bold', color: '#000' },
  385. cardName: {
  386. color: '#D0D0D0',
  387. fontSize: 12,
  388. marginTop: 13,
  389. textAlign: 'center',
  390. maxWidth: 200,
  391. },
  392. // 指示点
  393. dotsBox: { flexDirection: 'row', justifyContent: 'center', marginTop: 10 },
  394. dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#666', marginHorizontal: 3 },
  395. dotActive: { backgroundColor: '#fff' },
  396. // 进度条
  397. progressSection: {
  398. flexDirection: 'row',
  399. alignItems: 'center',
  400. justifyContent: 'center',
  401. marginTop: 5,
  402. paddingHorizontal: 20,
  403. position: 'relative',
  404. zIndex: 20,
  405. },
  406. progressLabelBox: { width: 40 },
  407. progressLabel: { color: '#fff', fontSize: 12 },
  408. magicBubble: {
  409. position: 'absolute',
  410. right: 0,
  411. top: -24,
  412. zIndex: 10,
  413. backgroundColor: '#f44336',
  414. borderRadius: 4,
  415. paddingHorizontal: 8,
  416. paddingVertical: 3,
  417. },
  418. magicText: { color: '#fff', fontSize: 10 },
  419. progressBar: {
  420. flex: 1,
  421. height: 14,
  422. backgroundColor: '#FFEABE',
  423. borderWidth: 2,
  424. borderColor: '#000',
  425. marginHorizontal: 5,
  426. position: 'relative',
  427. },
  428. progressFill: {
  429. height: '100%',
  430. backgroundColor: '#FFAD00',
  431. borderRightWidth: 2,
  432. borderRightColor: '#000',
  433. },
  434. progressText: { color: '#fff', fontSize: 12, width: 40, textAlign: 'right' },
  435. // 材料添加区域
  436. addSection: {
  437. width: SCREEN_WIDTH - 20,
  438. height: 124,
  439. marginHorizontal: 10,
  440. marginTop: 10,
  441. paddingTop: 10,
  442. },
  443. addTitle: {
  444. color: '#fff',
  445. fontSize: 14,
  446. textAlign: 'center',
  447. fontWeight: '400',
  448. textShadowColor: '#000',
  449. textShadowOffset: { width: 1, height: 1 },
  450. textShadowRadius: 1,
  451. marginBottom: 7,
  452. },
  453. addMain: {
  454. flexDirection: 'row',
  455. alignItems: 'center',
  456. paddingHorizontal: 25,
  457. },
  458. addBtn: { marginRight: 10 },
  459. addBtnBg: {
  460. width: 42,
  461. height: 42,
  462. justifyContent: 'center',
  463. alignItems: 'center',
  464. paddingTop: 5,
  465. },
  466. addIcon: { width: 16, height: 16 },
  467. addBtnText: { fontSize: 12, color: '#000', fontWeight: '500' },
  468. addCenter: {
  469. flex: 1,
  470. height: 60,
  471. backgroundColor: '#FFFBEA',
  472. paddingTop: 7,
  473. paddingLeft: 20,
  474. },
  475. addScrollContent: { paddingRight: 10 },
  476. addItem: { position: 'relative', marginRight: 10 },
  477. addItemBg: { width: 42, height: 42, justifyContent: 'center', alignItems: 'center' },
  478. addItemImg: { width: '80%', height: '80%' },
  479. closeBtn: { position: 'absolute', right: 0, top: 0, width: 13, height: 13 },
  480. closeIcon: { width: '100%', height: '100%' },
  481. // 底部按钮
  482. bottomContainer: { alignItems: 'center', marginTop: -10 },
  483. bottomBtn: { alignItems: 'center' },
  484. bottomBtnBg: {
  485. width: 160,
  486. height: 60,
  487. justifyContent: 'center',
  488. alignItems: 'center',
  489. paddingTop: 4,
  490. },
  491. bottomBtnText: {
  492. color: '#fff',
  493. fontSize: 15,
  494. fontWeight: 'bold',
  495. textShadowColor: '#000',
  496. textShadowOffset: { width: 1, height: 1 },
  497. textShadowRadius: 1,
  498. },
  499. bottomTip: {
  500. color: 'rgba(255,255,255,0.67)',
  501. fontSize: 10,
  502. textAlign: 'center',
  503. marginTop: 5,
  504. },
  505. });