detail.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  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. Alert,
  7. ImageBackground,
  8. Modal,
  9. ScrollView,
  10. Share,
  11. StatusBar,
  12. StyleSheet,
  13. Text,
  14. TextInput,
  15. TouchableOpacity,
  16. View,
  17. } from "react-native";
  18. import { useSafeAreaInsets } from "react-native-safe-area-context";
  19. import { Images } from "@/constants/images";
  20. import {
  21. getWealDetail,
  22. getWinningRecord,
  23. joinWealRoom,
  24. } from "@/services/dimension";
  25. const TYPE_MAP: any = {
  26. COMMON: { title: "福利房" },
  27. PASSWORD: { title: "口令房" },
  28. ACHIEVEMENT: { title: "成就房" },
  29. EUROPEAN_GAS: { title: "欧气房" },
  30. HONOR_ROLL: { title: "荣耀榜" },
  31. };
  32. export default function WealDetailScreen() {
  33. const { id } = useLocalSearchParams<{ id: string }>();
  34. const router = useRouter();
  35. const insets = useSafeAreaInsets();
  36. const [loading, setLoading] = useState(true);
  37. const [data, setData] = useState<any>(null);
  38. const [leftTime, setLeftTime] = useState(0);
  39. const [scrollTop, setScrollTop] = useState(0);
  40. const [winVisible, setWinVisible] = useState(false);
  41. const [winRecords, setWinRecords] = useState([]);
  42. const [joinVisible, setJoinVisible] = useState(false);
  43. const [password, setPassword] = useState("");
  44. const timerRef = useRef<any>(null);
  45. const loadData = useCallback(
  46. async (showLoading = false) => {
  47. if (showLoading) setLoading(true);
  48. try {
  49. const res = await getWealDetail(id as string);
  50. if (res) {
  51. setData(res);
  52. setLeftTime(res.leftTime);
  53. }
  54. } catch (error) {
  55. console.error("加载详情失败:", error);
  56. }
  57. setLoading(false);
  58. },
  59. [id],
  60. );
  61. useEffect(() => {
  62. loadData(true);
  63. return () => stopTimer();
  64. }, [loadData]);
  65. useEffect(() => {
  66. if (data?.status === 1 && leftTime > 0) {
  67. startTimer();
  68. } else {
  69. stopTimer();
  70. }
  71. }, [data, leftTime]);
  72. const startTimer = () => {
  73. stopTimer();
  74. timerRef.current = setInterval(() => {
  75. setLeftTime((prev) => {
  76. if (prev <= 0) {
  77. stopTimer();
  78. loadData();
  79. return 0;
  80. }
  81. return prev - 1000;
  82. });
  83. }, 1000);
  84. };
  85. const stopTimer = () => {
  86. if (timerRef.current) {
  87. clearInterval(timerRef.current);
  88. timerRef.current = null;
  89. }
  90. };
  91. const formatLeftTime = () => {
  92. if (leftTime <= 0) return "00:00:00";
  93. let second = Math.floor(leftTime / 1000);
  94. const d = Math.floor(second / (24 * 3600));
  95. second %= 24 * 3600;
  96. const h = Math.floor(second / 3600);
  97. second %= 3600;
  98. const m = Math.floor(second / 60);
  99. const s = second % 60;
  100. let res = "";
  101. if (d > 0) res += `${d}天`;
  102. res += `${h.toString().padStart(2, "0")}时${m.toString().padStart(2, "0")}分${s.toString().padStart(2, "0")}秒`;
  103. return res;
  104. };
  105. const handleJoin = async () => {
  106. if (data.status !== 1 || data.myParticipatedFlag === 1) return;
  107. if (data.type === "PASSWORD") {
  108. setJoinVisible(true);
  109. } else {
  110. try {
  111. const res = await joinWealRoom(id as string, "");
  112. if (res.success) {
  113. Alert.alert("提示", "加入成功");
  114. loadData();
  115. } else {
  116. if (
  117. res.msg &&
  118. (res.msg.includes("口令") || res.msg.includes("密码"))
  119. ) {
  120. setJoinVisible(true);
  121. } else {
  122. Alert.alert("错误", res.msg || "加入失败");
  123. }
  124. }
  125. } catch (error) {
  126. Alert.alert("错误", "请求异常");
  127. }
  128. }
  129. };
  130. const handleJoinWithPassword = async () => {
  131. if (!password) return;
  132. try {
  133. const res = await joinWealRoom(id as string, password);
  134. if (res.success) {
  135. Alert.alert("提示", "加入成功");
  136. setJoinVisible(false);
  137. setPassword("");
  138. loadData();
  139. } else {
  140. Alert.alert("错误", res.msg || "口令错误");
  141. }
  142. } catch (error) {
  143. Alert.alert("错误", "请求异常");
  144. }
  145. };
  146. const handleShare = async () => {
  147. try {
  148. const result = await Share.share({
  149. message: `快来参与福利房:${data?.name},房间ID:${id}`,
  150. title: "福利房分享",
  151. });
  152. if (result.action === Share.sharedAction) {
  153. if (result.activityType) {
  154. // shared with activity type of result.activityType
  155. } else {
  156. // shared
  157. }
  158. } else if (result.action === Share.dismissedAction) {
  159. // dismissed
  160. }
  161. } catch (error: any) {
  162. Alert.alert(error.message);
  163. }
  164. };
  165. const showWinRecords = async () => {
  166. try {
  167. const res = await getWinningRecord(id as string);
  168. setWinRecords(res || []);
  169. setWinVisible(true);
  170. } catch (error) {
  171. console.error("获取中奖记录失败:", error);
  172. }
  173. };
  174. if (loading) {
  175. return (
  176. <View style={styles.loading}>
  177. <ActivityIndicator color="#fff" />
  178. </View>
  179. );
  180. }
  181. const headerBg = scrollTop > 50 ? "#333" : "transparent";
  182. return (
  183. <View style={styles.container}>
  184. <StatusBar barStyle="light-content" />
  185. <ImageBackground
  186. source={{ uri: Images.mine.kaixinMineBg }}
  187. style={styles.background}
  188. resizeMode="cover"
  189. >
  190. {/* 导航 */}
  191. <View
  192. style={[
  193. styles.nav,
  194. { paddingTop: insets.top, backgroundColor: headerBg },
  195. ]}
  196. >
  197. <TouchableOpacity
  198. onPress={() => router.back()}
  199. style={styles.backBtn}
  200. >
  201. <Text style={styles.backText}>←</Text>
  202. </TouchableOpacity>
  203. <Text style={styles.navTitle}>
  204. {TYPE_MAP[data.type]?.title || "详情"}
  205. </Text>
  206. <TouchableOpacity onPress={handleShare} style={styles.backBtn}>
  207. <Image
  208. source={{ uri: Images.mine.invite }}
  209. style={{ width: 20, height: 20 }}
  210. contentFit="contain"
  211. />
  212. </TouchableOpacity>
  213. </View>
  214. <ScrollView
  215. style={styles.scrollView}
  216. onScroll={(e) => setScrollTop(e.nativeEvent.contentOffset.y)}
  217. scrollEventThrottle={16}
  218. showsVerticalScrollIndicator={false}
  219. >
  220. <ImageBackground
  221. source={{ uri: Images.mine.kaixinMineHeadBg }}
  222. style={styles.headerBg}
  223. resizeMode="cover"
  224. />
  225. <View style={styles.content}>
  226. <View style={styles.roomInfo}>
  227. <Text style={styles.roomName}>{data.name}</Text>
  228. <View style={styles.roomType}>
  229. <Text style={styles.roomTypeText}>
  230. {TYPE_MAP[data.type]?.title}
  231. </Text>
  232. </View>
  233. <Text style={styles.roomDesc} numberOfLines={1}>
  234. {data.description}
  235. </Text>
  236. </View>
  237. {/* 中奖记录按钮 */}
  238. {data.prizeMode !== 1 && data.type !== "EUROPEAN_GAS" && (
  239. <TouchableOpacity
  240. style={styles.recordBtn}
  241. onPress={showWinRecords}
  242. >
  243. <ImageBackground
  244. source={{ uri: Images.welfare.detail.record }}
  245. style={styles.recordBtnBg}
  246. resizeMode="contain"
  247. >
  248. <Image
  249. source={{ uri: Images.welfare.detail.recordIcon }}
  250. style={styles.recordIcon}
  251. contentFit="contain"
  252. />
  253. <Text style={styles.recordBtnText}>中奖记录</Text>
  254. </ImageBackground>
  255. </TouchableOpacity>
  256. )}
  257. {/* 赠品池 */}
  258. <View style={styles.goodsCard}>
  259. <View style={styles.cardHeader}>
  260. <Text style={styles.cardTitle}>赠品池</Text>
  261. <Text style={styles.cardNum}>
  262. {data.luckRoomGoodsList?.length}件赠品
  263. </Text>
  264. </View>
  265. <View style={styles.goodsList}>
  266. {data.luckRoomGoodsList?.map((item: any, index: number) => (
  267. <View key={index} style={styles.goodsItem}>
  268. <Image
  269. source={{ uri: item.spu.cover }}
  270. style={styles.goodsImg}
  271. contentFit="contain"
  272. />
  273. <Text style={styles.goodsName} numberOfLines={1}>
  274. {item.spu.name}
  275. </Text>
  276. <View style={styles.goodsCountTag}>
  277. <Text style={styles.goodsCountText}>
  278. 数量:{item.quantity}
  279. </Text>
  280. </View>
  281. </View>
  282. ))}
  283. </View>
  284. </View>
  285. {/* 参与度 */}
  286. <View style={styles.participantSection}>
  287. <View style={styles.cardHeader}>
  288. <Text style={styles.cardTitle}>参与度</Text>
  289. <Text style={styles.cardNum}>
  290. {data.participatingList?.length}个玩家
  291. </Text>
  292. </View>
  293. <View style={[styles.userList, { overflow: "hidden" }]}>
  294. {data.participatingList?.map((user: any, index: number) => (
  295. <View key={index} style={styles.userItem}>
  296. <Image
  297. source={{ uri: user.avatar }}
  298. style={styles.userAvatar}
  299. />
  300. <Text style={styles.userName} numberOfLines={1}>
  301. {user.nickname}
  302. </Text>
  303. </View>
  304. ))}
  305. </View>
  306. </View>
  307. </View>
  308. <View style={{ height: 120 }} />
  309. </ScrollView>
  310. {/* 底部按钮栏 */}
  311. <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
  312. {data.status === 1 && (
  313. <View style={styles.timerRow}>
  314. <Text style={styles.timerLabel}>倒计时:</Text>
  315. <Text style={styles.timerValue}>{formatLeftTime()}</Text>
  316. </View>
  317. )}
  318. <TouchableOpacity
  319. style={[
  320. styles.joinBtn,
  321. (data.myParticipatedFlag === 1 || data.status !== 1) &&
  322. styles.joinBtnDisabled,
  323. ]}
  324. onPress={handleJoin}
  325. disabled={data.myParticipatedFlag === 1 || data.status !== 1}
  326. >
  327. <ImageBackground
  328. source={{
  329. uri: data.status === 1 ? Images.common.loginBtn : undefined,
  330. }}
  331. style={styles.joinBtnBg}
  332. resizeMode="stretch"
  333. >
  334. <Text style={styles.joinBtnText}>
  335. {data.status !== 1
  336. ? "已开奖"
  337. : data.myParticipatedFlag === 1
  338. ? data.participateMode === 1 && data.myAuditStatus === 0
  339. ? "待审核"
  340. : data.participateMode === 1 && data.myAuditStatus === 1
  341. ? "审核不通过"
  342. : "等待开赏"
  343. : "加入房间,即可参与"}
  344. </Text>
  345. </ImageBackground>
  346. </TouchableOpacity>
  347. </View>
  348. {/* 口令弹窗 */}
  349. <Modal visible={joinVisible} transparent animationType="fade">
  350. <View style={styles.modalOverlay}>
  351. <View style={styles.modalContent}>
  352. <Text style={styles.modalTitle}>请输入房间口令</Text>
  353. <TextInput
  354. style={styles.modalInput}
  355. value={password}
  356. onChangeText={setPassword}
  357. placeholder="请输入口令"
  358. />
  359. <View style={styles.modalBtns}>
  360. <TouchableOpacity
  361. style={styles.modalBtn}
  362. onPress={() => setJoinVisible(false)}
  363. >
  364. <Text style={styles.modalBtnText}>取消</Text>
  365. </TouchableOpacity>
  366. <TouchableOpacity
  367. style={[styles.modalBtn, styles.modalBtnConfirm]}
  368. onPress={handleJoinWithPassword}
  369. >
  370. <Text style={styles.modalBtnText}>确认加入</Text>
  371. </TouchableOpacity>
  372. </View>
  373. </View>
  374. </View>
  375. </Modal>
  376. {/* 中奖记录弹窗 */}
  377. <Modal visible={winVisible} transparent animationType="slide">
  378. <View style={styles.winOverlay}>
  379. <View style={styles.winContent}>
  380. <View style={styles.winHeader}>
  381. <Text style={styles.winTitle}>中奖记录</Text>
  382. <TouchableOpacity
  383. onPress={() => setWinVisible(false)}
  384. style={styles.winClose}
  385. >
  386. <Text style={styles.winCloseText}>×</Text>
  387. </TouchableOpacity>
  388. </View>
  389. <ScrollView style={styles.winList}>
  390. {winRecords.length === 0 ? (
  391. <View style={styles.empty}>
  392. <Text style={{ color: "#999" }}>暂无记录</Text>
  393. </View>
  394. ) : (
  395. winRecords.map((item: any, index: number) => (
  396. <View key={index} style={styles.winItem}>
  397. <Image
  398. source={{ uri: item.avatar }}
  399. style={styles.winAvatar}
  400. />
  401. <Text style={styles.winUser}>{item.nickname}</Text>
  402. <Text style={styles.winGot}>获得了</Text>
  403. <Text style={styles.winGoods} numberOfLines={1}>
  404. {item.spu.name}
  405. </Text>
  406. <Image
  407. source={{ uri: item.spu.cover }}
  408. style={styles.winGoodsImg}
  409. contentFit="contain"
  410. />
  411. </View>
  412. ))
  413. )}
  414. </ScrollView>
  415. </View>
  416. </View>
  417. </Modal>
  418. </ImageBackground>
  419. </View>
  420. );
  421. }
  422. const styles = StyleSheet.create({
  423. container: { flex: 1 },
  424. background: { flex: 1 },
  425. loading: {
  426. flex: 1,
  427. backgroundColor: "#1a1a2e",
  428. justifyContent: "center",
  429. alignItems: "center",
  430. },
  431. nav: {
  432. flexDirection: "row",
  433. alignItems: "center",
  434. justifyContent: "space-between",
  435. paddingHorizontal: 15,
  436. height: 90,
  437. zIndex: 100,
  438. },
  439. backBtn: { width: 40 },
  440. backText: { color: "#fff", fontSize: 24 },
  441. navTitle: { color: "#fff", fontSize: 16, fontWeight: "bold" },
  442. placeholder: { width: 40 },
  443. scrollView: { flex: 1 },
  444. headerBg: { width: "100%", height: 180, position: "absolute", top: 0 },
  445. content: { paddingHorizontal: 15, marginTop: 90 },
  446. roomInfo: { alignItems: "center", marginBottom: 20 },
  447. roomName: {
  448. color: "#fff",
  449. fontSize: 22,
  450. fontWeight: "bold",
  451. textShadowColor: "#000",
  452. textShadowOffset: { width: 1, height: 1 },
  453. textShadowRadius: 2,
  454. },
  455. roomType: {
  456. marginTop: 8,
  457. paddingVertical: 4,
  458. paddingHorizontal: 12,
  459. backgroundColor: "rgba(255,255,255,0.2)",
  460. borderRadius: 10,
  461. },
  462. roomTypeText: { color: "#fff", fontSize: 12 },
  463. roomDesc: { marginTop: 10, color: "#BEBBB3", fontSize: 12 },
  464. recordBtn: { position: "absolute", right: 0, top: 0, zIndex: 10 },
  465. recordBtnBg: {
  466. width: 78,
  467. height: 26,
  468. justifyContent: "center",
  469. alignItems: "center",
  470. flexDirection: "row",
  471. },
  472. recordIcon: { width: 16, height: 16, marginRight: 2 },
  473. recordBtnText: {
  474. color: "#fff",
  475. fontSize: 12,
  476. fontWeight: "bold",
  477. textShadowColor: "#000",
  478. textShadowOffset: { width: 1, height: 1 },
  479. textShadowRadius: 1,
  480. },
  481. goodsCard: {
  482. marginTop: 25,
  483. backgroundColor: "rgba(255,255,255,0.1)",
  484. borderRadius: 15,
  485. padding: 15,
  486. },
  487. cardHeader: { flexDirection: "row", alignItems: "center", marginBottom: 15 },
  488. cardTitle: { color: "#fff", fontSize: 18, fontWeight: "bold" },
  489. cardNum: { color: "#eee", fontSize: 12, marginLeft: 10 },
  490. goodsList: {
  491. flexDirection: "row",
  492. flexWrap: "wrap",
  493. justifyContent: "space-between",
  494. },
  495. goodsItem: {
  496. width: "31%",
  497. aspectRatio: 0.8,
  498. backgroundColor: "rgba(0,0,0,0.3)",
  499. borderRadius: 10,
  500. padding: 8,
  501. marginBottom: 10,
  502. alignItems: "center",
  503. },
  504. goodsImg: { width: "80%", height: "60%" },
  505. goodsName: { color: "#fff", fontSize: 10, marginTop: 5 },
  506. goodsCountTag: {
  507. position: "absolute",
  508. left: 0,
  509. bottom: 10,
  510. backgroundColor: "#FFDD00",
  511. paddingHorizontal: 5,
  512. borderTopRightRadius: 5,
  513. borderBottomRightRadius: 5,
  514. },
  515. goodsCountText: { color: "#000", fontSize: 8, fontWeight: "bold" },
  516. participantSection: {
  517. marginTop: 20,
  518. backgroundColor: "rgba(255,255,255,0.1)",
  519. borderRadius: 15,
  520. padding: 15,
  521. },
  522. userList: { flexDirection: "row" },
  523. userItem: { alignItems: "center", marginRight: 15, width: 50 },
  524. userAvatar: {
  525. width: 40,
  526. height: 40,
  527. borderRadius: 20,
  528. borderWidth: 1,
  529. borderColor: "#fff",
  530. },
  531. userName: {
  532. color: "#fff",
  533. fontSize: 8,
  534. marginTop: 5,
  535. width: "100%",
  536. textAlign: "center",
  537. },
  538. bottomBar: {
  539. position: "absolute",
  540. left: 0,
  541. right: 0,
  542. bottom: 0,
  543. backgroundColor: "rgba(0,0,0,0.8)",
  544. paddingHorizontal: 15,
  545. paddingTop: 10,
  546. },
  547. timerRow: {
  548. flexDirection: "row",
  549. alignItems: "center",
  550. justifyContent: "center",
  551. marginBottom: 10,
  552. },
  553. timerLabel: { color: "#fff", fontSize: 12 },
  554. timerValue: { color: "#fdf685", fontSize: 12, fontWeight: "bold" },
  555. joinBtn: {
  556. height: 45,
  557. borderRadius: 22.5,
  558. width: "100%",
  559. overflow: "hidden",
  560. },
  561. joinBtnDisabled: { backgroundColor: "#666" },
  562. joinBtnBg: {
  563. width: "100%",
  564. height: "100%",
  565. justifyContent: "center",
  566. alignItems: "center",
  567. },
  568. joinBtnText: { color: "#fff", fontSize: 14, fontWeight: "bold" },
  569. modalOverlay: {
  570. flex: 1,
  571. backgroundColor: "rgba(0,0,0,0.6)",
  572. justifyContent: "center",
  573. alignItems: "center",
  574. },
  575. modalContent: {
  576. width: "80%",
  577. backgroundColor: "#fff",
  578. borderRadius: 15,
  579. padding: 20,
  580. alignItems: "center",
  581. },
  582. modalTitle: { fontSize: 18, fontWeight: "bold", marginBottom: 20 },
  583. modalInput: {
  584. width: "100%",
  585. height: 45,
  586. backgroundColor: "#f5f5f5",
  587. borderRadius: 8,
  588. paddingHorizontal: 15,
  589. marginBottom: 20,
  590. },
  591. modalBtns: {
  592. flexDirection: "row",
  593. justifyContent: "space-between",
  594. width: "100%",
  595. },
  596. modalBtn: {
  597. flex: 0.45,
  598. height: 40,
  599. justifyContent: "center",
  600. alignItems: "center",
  601. borderRadius: 20,
  602. backgroundColor: "#eee",
  603. },
  604. modalBtnConfirm: { backgroundColor: "#e79018" },
  605. modalBtnText: { fontWeight: "bold" },
  606. winOverlay: {
  607. flex: 1,
  608. backgroundColor: "rgba(0,0,0,0.5)",
  609. justifyContent: "flex-end",
  610. },
  611. winContent: {
  612. backgroundColor: "#fff",
  613. height: "60%",
  614. borderTopLeftRadius: 20,
  615. borderTopRightRadius: 20,
  616. padding: 20,
  617. },
  618. winHeader: {
  619. flexDirection: "row",
  620. justifyContent: "space-between",
  621. alignItems: "center",
  622. marginBottom: 20,
  623. },
  624. winTitle: { fontSize: 18, fontWeight: "bold" },
  625. winClose: {
  626. width: 30,
  627. height: 30,
  628. backgroundColor: "#eee",
  629. borderRadius: 15,
  630. justifyContent: "center",
  631. alignItems: "center",
  632. },
  633. winCloseText: { fontSize: 20, color: "#999" },
  634. winList: { flex: 1 },
  635. winItem: {
  636. flexDirection: "row",
  637. alignItems: "center",
  638. paddingVertical: 10,
  639. borderBottomWidth: 1,
  640. borderBottomColor: "#f5f5f5",
  641. },
  642. winAvatar: { width: 30, height: 30, borderRadius: 15 },
  643. winUser: { marginLeft: 10, fontSize: 12, width: 60 },
  644. winGot: { fontSize: 12, color: "#999", marginHorizontal: 5 },
  645. winGoods: { flex: 1, fontSize: 12 },
  646. winGoodsImg: { width: 30, height: 30, marginLeft: 10 },
  647. empty: { alignItems: "center", padding: 30 },
  648. });