wish.tsx 17 KB

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