wish.tsx 19 KB

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