mine.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. import { KefuPopup, KefuPopupRef } from "@/components/mine/KefuPopup";
  2. import { MenuCell } from "@/components/mine/MenuCell";
  3. import { Colors } from "@/constants/Colors";
  4. import { Images } from "@/constants/images";
  5. import { getMagicIndex } from "@/services/award";
  6. import { getParamConfig, getUserInfo, UserInfo } from "@/services/user";
  7. import * as Clipboard from "expo-clipboard";
  8. import { Image } from "expo-image";
  9. import { useFocusEffect, useRouter } from "expo-router";
  10. import React, { useCallback, useRef, useState } from "react";
  11. import {
  12. Alert,
  13. ScrollView,
  14. StatusBar,
  15. StyleSheet,
  16. Text,
  17. TouchableOpacity,
  18. View,
  19. } from "react-native";
  20. import { useSafeAreaInsets } from "react-native-safe-area-context";
  21. interface IndexData {
  22. couponCount?: number;
  23. inventoryCount?: number;
  24. magicBalance?: number;
  25. treasureBoxCount?: number;
  26. }
  27. export default function MineScreen() {
  28. const router = useRouter();
  29. const insets = useSafeAreaInsets();
  30. const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
  31. const [indexData, setIndexData] = useState<IndexData | null>(null);
  32. const [inviteShow, setInviteShow] = useState(false);
  33. const [filingInfo, setFilingInfo] = useState<{
  34. state: number;
  35. data: string;
  36. } | null>(null);
  37. const [showWallet, setShowWallet] = useState(false);
  38. const [showExchange, setShowExchange] = useState(false);
  39. const kefuRef = useRef<KefuPopupRef>(null);
  40. const loadData = useCallback(async () => {
  41. try {
  42. const [info, magicData] = await Promise.all([
  43. getUserInfo(),
  44. getMagicIndex(),
  45. ]);
  46. setUserInfo(info);
  47. setIndexData(magicData);
  48. const inviteConfig = await getParamConfig("invite_config");
  49. setInviteShow(inviteConfig?.state !== 0);
  50. const filingConfig = await getParamConfig("beian_icp");
  51. if (filingConfig) {
  52. setFilingInfo({ state: filingConfig.state, data: filingConfig.data });
  53. }
  54. const walletConfig = await getParamConfig("wallet_recharge_show");
  55. setShowWallet(walletConfig?.state === 1);
  56. const exchangeConfig = await getParamConfig("harry_exchange_show");
  57. setShowExchange(exchangeConfig?.state === 1);
  58. } catch (error) {
  59. console.error("获取数据失败:", error);
  60. }
  61. }, []);
  62. useFocusEffect(
  63. useCallback(() => {
  64. loadData();
  65. }, [loadData]),
  66. );
  67. const handleLogin = () => {
  68. if (!userInfo) {
  69. router.push("/login" as any);
  70. }
  71. };
  72. const handleCopy = async (text: string) => {
  73. await Clipboard.setStringAsync(text);
  74. Alert.alert("提示", "复制成功");
  75. };
  76. const handleMenuPress = (route: string) => {
  77. if (!userInfo) {
  78. router.push("/login" as any);
  79. return;
  80. }
  81. if (route) {
  82. router.push(route as any);
  83. }
  84. };
  85. const handleMenuItemPress = (type: string) => {
  86. if (!userInfo && type !== "4_4") {
  87. router.push("/login" as any);
  88. return;
  89. }
  90. switch (type) {
  91. case "1_1": // 钱包
  92. router.push("/wallet" as any);
  93. break;
  94. case "2_0": // 全部订单
  95. router.push("/orders" as any);
  96. break;
  97. case "6_1": // 兑换码
  98. router.push("/exchange" as any);
  99. break;
  100. case "4_4": // 联系客服
  101. kefuRef.current?.open();
  102. break;
  103. case "4_3": // 地址
  104. router.push("/address" as any);
  105. break;
  106. case "4_9": // 意见反馈
  107. router.push("/feedback" as any);
  108. break;
  109. case "4_5": // 设置
  110. router.push("/setting" as any);
  111. break;
  112. case "4_10": // 实名认证
  113. router.push("/certification" as any);
  114. break;
  115. default:
  116. break;
  117. }
  118. };
  119. const showNumber = (key: keyof IndexData) => {
  120. if (!indexData) return "-";
  121. if (typeof indexData[key] === "undefined") return "-";
  122. return bigNumberTransform(indexData[key]!);
  123. };
  124. const bigNumberTransform = (value: number) => {
  125. const newValue = ["", "", ""];
  126. let fr = 1000;
  127. let num = 3;
  128. let text1 = "";
  129. let fm = 1;
  130. let tempValue = value;
  131. while (tempValue / fr >= 1) {
  132. fr *= 10;
  133. num += 1;
  134. }
  135. if (num <= 4) {
  136. newValue[0] = parseInt(String(value / 1000)) + "";
  137. newValue[1] = "千";
  138. } else if (num <= 8) {
  139. text1 = (num - 4) / 3 > 1 ? "千万" : "万";
  140. fm = text1 === "万" ? 10000 : 10000000;
  141. if (value % fm === 0) {
  142. newValue[0] = parseInt(String(value / fm)) + "";
  143. } else {
  144. newValue[0] = String(Math.floor((value / fm) * 10) / 10);
  145. }
  146. newValue[1] = text1;
  147. } else if (num <= 16) {
  148. text1 = (num - 8) / 3 > 1 ? "千亿" : "亿";
  149. text1 = (num - 8) / 4 > 1 ? "万亿" : text1;
  150. text1 = (num - 8) / 7 > 1 ? "千万亿" : text1;
  151. fm = 1;
  152. if (text1 === "亿") {
  153. fm = 100000000;
  154. } else if (text1 === "千亿") {
  155. fm = 100000000000;
  156. } else if (text1 === "万亿") {
  157. fm = 1000000000000;
  158. } else if (text1 === "千万亿") {
  159. fm = 1000000000000000;
  160. }
  161. if (value % fm === 0) {
  162. newValue[0] = parseInt(String(value / fm)) + "";
  163. } else {
  164. newValue[0] = String(Math.floor((value / fm) * 10) / 10);
  165. }
  166. newValue[1] = text1;
  167. }
  168. if (value < 1000) {
  169. newValue[0] = String(value);
  170. newValue[1] = "";
  171. }
  172. return newValue.join("");
  173. };
  174. // Stat Card Component
  175. const StatCard = ({ num, label, onPress }: any) => (
  176. <TouchableOpacity style={styles.dataItem} onPress={onPress}>
  177. <Text style={styles.dataNum}>{num}</Text>
  178. <Text style={styles.dataLabel}>{label}</Text>
  179. <View style={styles.dataLine} />
  180. </TouchableOpacity>
  181. );
  182. return (
  183. <View style={styles.container}>
  184. <StatusBar barStyle="light-content" />
  185. {/* Scroll View */}
  186. <ScrollView
  187. style={styles.scrollView}
  188. contentContainerStyle={{
  189. paddingTop: insets.top + 20,
  190. paddingBottom: 100,
  191. }}
  192. showsVerticalScrollIndicator={false}
  193. >
  194. {/* Header Title */}
  195. <View style={styles.header}>
  196. <Text style={styles.title}>DATA CENTER</Text>
  197. <Text style={styles.subTitle}>个人档案</Text>
  198. </View>
  199. {/* User Info Card */}
  200. <View style={styles.userCard}>
  201. <View style={styles.userContent}>
  202. <TouchableOpacity
  203. style={styles.avatarContainer}
  204. onPress={handleLogin}
  205. activeOpacity={0.8}
  206. >
  207. <Image
  208. source={{
  209. uri: userInfo?.avatar || Images.common.defaultAvatar,
  210. }}
  211. style={styles.avatar}
  212. contentFit="cover"
  213. />
  214. <View style={styles.avatarBorder} />
  215. </TouchableOpacity>
  216. <View style={styles.userInfo}>
  217. <View style={styles.nicknameRow}>
  218. <View style={styles.nicknameLeft}>
  219. <Text style={styles.nickname} numberOfLines={1}>
  220. {userInfo?.nickname || "未授权访问"}
  221. </Text>
  222. {userInfo && (
  223. <TouchableOpacity
  224. onPress={() => handleMenuPress("/certification")}
  225. style={[
  226. styles.realNameBadge,
  227. userInfo.realNameFlag === 1
  228. ? styles.realNameBadgeDone
  229. : styles.realNameBadgeTodo,
  230. ]}
  231. activeOpacity={0.7}
  232. >
  233. <Text
  234. style={[
  235. styles.realNameBadgeText,
  236. userInfo.realNameFlag === 1
  237. ? styles.realNameBadgeTextDone
  238. : styles.realNameBadgeTextTodo,
  239. ]}
  240. >
  241. {userInfo.realNameFlag === 1 ? "已实名" : "未实名"}
  242. </Text>
  243. </TouchableOpacity>
  244. )}
  245. </View>
  246. {userInfo && (
  247. <TouchableOpacity
  248. onPress={() => handleMenuPress("/profile")}
  249. style={styles.editBtn}
  250. >
  251. <Text style={styles.editBtnText}>EDIT</Text>
  252. </TouchableOpacity>
  253. )}
  254. </View>
  255. {userInfo ? (
  256. <View style={styles.idRow}>
  257. <TouchableOpacity
  258. style={styles.idItem}
  259. onPress={() =>
  260. handleCopy(userInfo.username || userInfo.id || "")
  261. }
  262. >
  263. <Text style={styles.idText}>
  264. ID: {userInfo.username || userInfo.id}
  265. </Text>
  266. <View style={styles.copyBadge}>
  267. <Text style={styles.copyText}>COPY</Text>
  268. </View>
  269. </TouchableOpacity>
  270. {(userInfo.mobile || userInfo.phone) && (
  271. <Text style={styles.phoneText}>
  272. Mobile: {userInfo.mobile || userInfo.phone}
  273. </Text>
  274. )}
  275. </View>
  276. ) : (
  277. <TouchableOpacity onPress={handleLogin}>
  278. <Text style={styles.loginTip}>点击获取访问权限</Text>
  279. </TouchableOpacity>
  280. )}
  281. </View>
  282. </View>
  283. {/* Tech Decoration */}
  284. <View style={styles.techDecoTR} />
  285. <View style={styles.techDecoBL} />
  286. </View>
  287. {/* Stats Grid */}
  288. <View style={styles.statsContainer}>
  289. <StatCard
  290. num={showNumber("couponCount")}
  291. label="优惠券"
  292. onPress={() => handleMenuPress("/coupon")}
  293. />
  294. <StatCard
  295. num={showNumber("inventoryCount")}
  296. label="仓库"
  297. onPress={() => handleMenuPress("/store")}
  298. />
  299. <StatCard
  300. num={showNumber("magicBalance")}
  301. label="果实"
  302. onPress={() => handleMenuPress("/magic")}
  303. />
  304. <StatCard
  305. num={showNumber("treasureBoxCount")}
  306. label="宝箱"
  307. onPress={() => handleMenuPress("/boxInBox/boxList")}
  308. />
  309. </View>
  310. {/* Shortcuts */}
  311. <View style={styles.shortcutsContainer}>
  312. <View style={styles.shortcutsHeader}>
  313. <Text style={styles.shortcutsTitle}>QUICK ACCESS</Text>
  314. </View>
  315. <View style={styles.shortcutsGrid}>
  316. {inviteShow && userInfo && (
  317. <TouchableOpacity
  318. style={styles.shortcutItem}
  319. onPress={() => Alert.alert("提示", "暂未开放")}
  320. >
  321. <View style={styles.shortcutIconBox}>
  322. <Image
  323. source={Images.mine.invite}
  324. style={styles.shortcutIcon}
  325. contentFit="contain"
  326. />
  327. </View>
  328. <Text style={styles.shortcutText}>邀新有礼</Text>
  329. </TouchableOpacity>
  330. )}
  331. <TouchableOpacity
  332. style={styles.shortcutItem}
  333. onPress={() => handleMenuPress("/message")}
  334. >
  335. <View style={styles.shortcutIconBox}>
  336. <Image
  337. source={Images.mine.message}
  338. style={styles.shortcutIcon}
  339. contentFit="contain"
  340. />
  341. </View>
  342. <Text style={styles.shortcutText}>系统消息</Text>
  343. </TouchableOpacity>
  344. <TouchableOpacity
  345. style={styles.shortcutItem}
  346. onPress={() => handleMenuPress("/orders")}
  347. >
  348. <View style={styles.shortcutIconBox}>
  349. <Image
  350. source={Images.mine.kaixinOrders}
  351. style={styles.shortcutIcon}
  352. contentFit="contain"
  353. />
  354. </View>
  355. <Text style={styles.shortcutText}>宝箱订单</Text>
  356. </TouchableOpacity>
  357. </View>
  358. </View>
  359. {/* Orders Entry */}
  360. <View style={styles.orderContainer}>
  361. <View style={styles.shortcutsHeader}>
  362. <Text style={styles.shortcutsTitle}>ORDERS</Text>
  363. </View>
  364. <View style={styles.orderGrid}>
  365. <TouchableOpacity
  366. style={styles.orderBtn}
  367. onPress={() => handleMenuPress("/orders/shop?active=1")}
  368. >
  369. <Text style={styles.orderBtnText}>待发货</Text>
  370. <View style={styles.orderBtnLine} />
  371. </TouchableOpacity>
  372. <TouchableOpacity
  373. style={styles.orderBtn}
  374. onPress={() => handleMenuPress("/orders/shop?active=4")}
  375. >
  376. <Text style={styles.orderBtnText}>已发货</Text>
  377. <View style={styles.orderBtnLine} />
  378. </TouchableOpacity>
  379. </View>
  380. </View>
  381. {/* Menu Section */}
  382. <View style={styles.menuSection}>
  383. <MenuCell onItemPress={handleMenuItemPress} showWallet={showWallet} showExchange={showExchange} />
  384. </View>
  385. {/* Footer Info */}
  386. {filingInfo && filingInfo.state !== 0 && (
  387. <View style={styles.filingBox}>
  388. <Text style={styles.filingText}>{filingInfo.data}</Text>
  389. </View>
  390. )}
  391. </ScrollView>
  392. <KefuPopup ref={kefuRef} />
  393. </View>
  394. );
  395. }
  396. const styles = StyleSheet.create({
  397. container: {
  398. flex: 1,
  399. backgroundColor: Colors.darkBg,
  400. },
  401. scrollView: {
  402. flex: 1,
  403. },
  404. header: {
  405. paddingHorizontal: 20,
  406. marginBottom: 20,
  407. },
  408. title: {
  409. color: "#fff",
  410. fontSize: 24,
  411. fontWeight: "bold",
  412. fontStyle: "italic",
  413. letterSpacing: 2,
  414. textShadowColor: Colors.neonBlue,
  415. textShadowRadius: 10,
  416. },
  417. subTitle: {
  418. color: Colors.neonBlue,
  419. fontSize: 12,
  420. letterSpacing: 4,
  421. },
  422. // User Card
  423. userCard: {
  424. marginHorizontal: 15,
  425. padding: 20,
  426. backgroundColor: "rgba(255, 255, 255, 0.05)",
  427. borderRadius: 12,
  428. borderWidth: 1,
  429. borderColor: Colors.neonBlue,
  430. position: "relative",
  431. marginBottom: 20,
  432. },
  433. userContent: {
  434. flexDirection: "row",
  435. alignItems: "center",
  436. },
  437. avatarContainer: {
  438. position: "relative",
  439. marginRight: 15,
  440. },
  441. avatar: {
  442. width: 60,
  443. height: 60,
  444. borderRadius: 30,
  445. borderWidth: 2,
  446. borderColor: "#fff",
  447. },
  448. avatarBorder: {
  449. position: "absolute",
  450. top: -4,
  451. left: -4,
  452. right: -4,
  453. bottom: -4,
  454. borderRadius: 34,
  455. borderWidth: 1,
  456. borderColor: Colors.neonPink,
  457. borderStyle: "dashed",
  458. },
  459. userInfo: {
  460. flex: 1,
  461. },
  462. nicknameRow: {
  463. flexDirection: "row",
  464. alignItems: "center",
  465. justifyContent: "space-between",
  466. marginBottom: 8,
  467. },
  468. nickname: {
  469. color: "#fff",
  470. fontSize: 18,
  471. fontWeight: "bold",
  472. maxWidth: 140,
  473. },
  474. nicknameLeft: {
  475. flexDirection: "row",
  476. alignItems: "center",
  477. flex: 1,
  478. flexWrap: "wrap",
  479. },
  480. realNameBadge: {
  481. marginLeft: 8,
  482. paddingHorizontal: 6,
  483. paddingVertical: 2,
  484. borderRadius: 4,
  485. borderWidth: 1,
  486. },
  487. realNameBadgeDone: {
  488. borderColor: Colors.neonBlue,
  489. backgroundColor: "rgba(0, 243, 255, 0.12)",
  490. },
  491. realNameBadgeTodo: {
  492. borderColor: Colors.neonPink,
  493. backgroundColor: "rgba(255, 64, 129, 0.12)",
  494. },
  495. realNameBadgeText: {
  496. fontSize: 10,
  497. fontWeight: "600",
  498. },
  499. realNameBadgeTextDone: {
  500. color: Colors.neonBlue,
  501. },
  502. realNameBadgeTextTodo: {
  503. color: Colors.neonPink,
  504. },
  505. editBtn: {
  506. paddingHorizontal: 10,
  507. paddingVertical: 4,
  508. backgroundColor: Colors.neonBlue,
  509. borderRadius: 4,
  510. },
  511. editBtnText: {
  512. color: "#000",
  513. fontSize: 10,
  514. fontWeight: "bold",
  515. },
  516. idRow: {
  517. alignItems: "flex-start",
  518. },
  519. idItem: {
  520. flexDirection: "row",
  521. alignItems: "center",
  522. marginBottom: 4,
  523. },
  524. idText: {
  525. color: Colors.textSecondary,
  526. fontSize: 12,
  527. marginRight: 8,
  528. },
  529. copyBadge: {
  530. backgroundColor: "rgba(255, 255, 255, 0.1)",
  531. paddingHorizontal: 4,
  532. borderRadius: 2,
  533. },
  534. copyText: {
  535. color: "#fff",
  536. fontSize: 8,
  537. },
  538. phoneText: {
  539. color: Colors.textTertiary,
  540. fontSize: 12,
  541. },
  542. loginTip: {
  543. color: Colors.neonPink,
  544. fontSize: 14,
  545. textDecorationLine: "underline",
  546. },
  547. techDecoTR: {
  548. position: "absolute",
  549. top: 0,
  550. right: 0,
  551. width: 20,
  552. height: 20,
  553. borderTopWidth: 2,
  554. borderRightWidth: 2,
  555. borderColor: Colors.neonBlue,
  556. },
  557. techDecoBL: {
  558. position: "absolute",
  559. bottom: 0,
  560. left: 0,
  561. width: 20,
  562. height: 20,
  563. borderBottomWidth: 2,
  564. borderLeftWidth: 2,
  565. borderColor: Colors.neonBlue,
  566. },
  567. // Stats
  568. statsContainer: {
  569. flexDirection: "row",
  570. justifyContent: "space-between",
  571. marginHorizontal: 15,
  572. marginBottom: 20,
  573. backgroundColor: Colors.darkCard,
  574. borderRadius: 8,
  575. padding: 15,
  576. borderWidth: 1,
  577. borderColor: "rgba(255, 255, 255, 0.1)",
  578. },
  579. dataItem: {
  580. alignItems: "center",
  581. flex: 1,
  582. },
  583. dataNum: {
  584. color: Colors.neonBlue,
  585. fontSize: 18,
  586. fontWeight: "bold",
  587. textShadowColor: Colors.neonBlue,
  588. textShadowRadius: 5,
  589. },
  590. dataLabel: {
  591. color: Colors.textSecondary,
  592. fontSize: 12,
  593. marginTop: 4,
  594. },
  595. dataLine: {
  596. width: 20,
  597. height: 2,
  598. backgroundColor: "rgba(255, 255, 255, 0.1)",
  599. marginTop: 8,
  600. },
  601. // Shortcuts
  602. shortcutsContainer: {
  603. marginHorizontal: 15,
  604. marginBottom: 20,
  605. },
  606. shortcutsHeader: {
  607. borderLeftWidth: 3,
  608. borderLeftColor: Colors.neonPink,
  609. paddingLeft: 10,
  610. marginBottom: 10,
  611. },
  612. shortcutsTitle: {
  613. color: "#fff",
  614. fontSize: 14,
  615. fontWeight: "bold",
  616. letterSpacing: 2,
  617. },
  618. shortcutsGrid: {
  619. flexDirection: "row",
  620. backgroundColor: Colors.darkCard,
  621. borderRadius: 8,
  622. padding: 15,
  623. },
  624. shortcutItem: {
  625. flex: 1,
  626. alignItems: "center",
  627. },
  628. shortcutIconBox: {
  629. width: 48,
  630. height: 48,
  631. borderRadius: 24,
  632. backgroundColor: "rgba(255, 255, 255, 0.05)",
  633. justifyContent: "center",
  634. alignItems: "center",
  635. marginBottom: 8,
  636. borderWidth: 1,
  637. borderColor: "rgba(255, 255, 255, 0.1)",
  638. },
  639. shortcutIcon: {
  640. width: 24,
  641. height: 24,
  642. tintColor: Colors.neonBlue, // Apply theme color
  643. },
  644. shortcutText: {
  645. color: Colors.textSecondary,
  646. fontSize: 12,
  647. },
  648. // Orders
  649. orderContainer: {
  650. marginHorizontal: 15,
  651. marginBottom: 20,
  652. },
  653. orderGrid: {
  654. flexDirection: "row",
  655. gap: 15,
  656. },
  657. orderBtn: {
  658. flex: 1,
  659. height: 60,
  660. backgroundColor: Colors.darkCard,
  661. borderRadius: 8,
  662. justifyContent: "center",
  663. alignItems: "center",
  664. borderWidth: 1,
  665. borderColor: "rgba(255, 255, 255, 0.1)",
  666. position: "relative",
  667. overflow: "hidden",
  668. },
  669. orderBtnText: {
  670. color: "#fff",
  671. fontSize: 14,
  672. fontWeight: "bold",
  673. letterSpacing: 2,
  674. },
  675. orderBtnLine: {
  676. position: "absolute",
  677. bottom: 0,
  678. left: 0,
  679. right: 0,
  680. height: 2,
  681. backgroundColor: Colors.neonBlue,
  682. },
  683. // Menu
  684. menuSection: {
  685. marginHorizontal: 15,
  686. backgroundColor: Colors.darkCard,
  687. borderRadius: 8,
  688. paddingVertical: 5,
  689. borderWidth: 1,
  690. borderColor: "rgba(255, 255, 255, 0.1)",
  691. },
  692. // Footer
  693. filingBox: {
  694. alignItems: "center",
  695. marginTop: 20,
  696. opacity: 0.5,
  697. },
  698. filingText: {
  699. color: Colors.textTertiary,
  700. fontSize: 10,
  701. textAlign: "center",
  702. },
  703. });