index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useState } from 'react';
  4. import {
  5. Alert,
  6. ImageBackground,
  7. ScrollView,
  8. StatusBar,
  9. StyleSheet,
  10. Text,
  11. TouchableOpacity,
  12. View,
  13. } from 'react-native';
  14. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  15. import { Images } from '@/constants/images';
  16. import { getCreditRecord, getWalletInfo, signIn } from '@/services/user';
  17. interface DayInfo {
  18. text: string;
  19. date: string;
  20. isFutureDate: boolean;
  21. istoday: boolean;
  22. isSignIn: boolean;
  23. }
  24. interface RecordItem {
  25. id: string;
  26. credit: number;
  27. createTime: string;
  28. }
  29. export default function IntegralScreen() {
  30. const router = useRouter();
  31. const insets = useSafeAreaInsets();
  32. const [integral, setIntegral] = useState(0);
  33. const [daysIntegral, setDaysIntegral] = useState(0);
  34. const [istodaySignIn, setIstodaySignIn] = useState(false);
  35. const [record, setRecord] = useState<RecordItem[]>([]);
  36. const [dataInfo, setDataInfo] = useState<DayInfo[]>([]);
  37. const [todayIntegral, setTodayIntegral] = useState(1);
  38. const [tomorrowIntegral, setTomorrowIntegral] = useState(1);
  39. const [isTooltip, setIsTooltip] = useState(false);
  40. // 格式化数字为两位
  41. const padWithZeros = (number: number, length: number) => {
  42. return String(number).padStart(length, '0');
  43. };
  44. // 设置日期数据
  45. const setDate = useCallback(() => {
  46. const now = new Date();
  47. const dataInfoArr: DayInfo[] = [];
  48. // 前4天
  49. for (let i = -4; i <= 0; i++) {
  50. const date = new Date(now);
  51. date.setDate(date.getDate() + i);
  52. const m = date.getMonth() + 1;
  53. const d = date.getDate();
  54. const y = date.getFullYear();
  55. dataInfoArr.push({
  56. text: `${m}.${d}`,
  57. date: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
  58. isFutureDate: false,
  59. istoday: i === 0,
  60. isSignIn: false,
  61. });
  62. }
  63. // 明天
  64. const tomorrow = new Date(now);
  65. tomorrow.setDate(tomorrow.getDate() + 1);
  66. const tm = tomorrow.getMonth() + 1;
  67. const td = tomorrow.getDate();
  68. const ty = tomorrow.getFullYear();
  69. dataInfoArr.push({
  70. text: `${tm}.${td}`,
  71. date: `${ty}-${padWithZeros(tm, 2)}-${padWithZeros(td, 2)}`,
  72. isFutureDate: true,
  73. istoday: false,
  74. isSignIn: false,
  75. });
  76. setDataInfo(dataInfoArr);
  77. return dataInfoArr;
  78. }, []);
  79. // 获取积分信息
  80. const getInfo = useCallback(async () => {
  81. try {
  82. const info = await getWalletInfo('USER_CREDIT');
  83. setIntegral(info?.balance || 0);
  84. } catch (error) {
  85. console.error('获取积分失败:', error);
  86. }
  87. }, []);
  88. // 获取签到记录
  89. const getData = useCallback(async (dateInfo: DayInfo[]) => {
  90. try {
  91. const fourDaysAgo = new Date();
  92. fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
  93. const m = fourDaysAgo.getMonth() + 1;
  94. const d = fourDaysAgo.getDate();
  95. const y = fourDaysAgo.getFullYear();
  96. const param = {
  97. createTime: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
  98. };
  99. const res = await getCreditRecord(param);
  100. const recordData = res?.data || [];
  101. setRecord(recordData);
  102. // 今天日期
  103. const now = new Date();
  104. const todayStr = `${now.getFullYear()}-${padWithZeros(now.getMonth() + 1, 2)}-${padWithZeros(now.getDate(), 2)}`;
  105. // 更新签到状态
  106. const updatedDataInfo = dateInfo.map(item => {
  107. const newItem = { ...item };
  108. // 检查是否已签到
  109. for (const rec of recordData) {
  110. const createTime = rec.createTime?.slice(0, 10);
  111. if (createTime === item.date) {
  112. newItem.isSignIn = true;
  113. if (item.date === todayStr) {
  114. setIstodaySignIn(true);
  115. }
  116. }
  117. }
  118. return newItem;
  119. });
  120. setDataInfo(updatedDataInfo);
  121. // 计算签到积分
  122. let total = 0;
  123. for (const rec of recordData) {
  124. if (rec.credit > 0) {
  125. total += rec.credit;
  126. }
  127. }
  128. setDaysIntegral(total);
  129. // 计算今天和明天的积分
  130. if (recordData.length > 0) {
  131. const lastCredit = recordData[0]?.credit || 1;
  132. const yesterday = new Date();
  133. yesterday.setDate(yesterday.getDate() - 1);
  134. const yesterdayStr = `${yesterday.getFullYear()}-${padWithZeros(yesterday.getMonth() + 1, 2)}-${padWithZeros(yesterday.getDate(), 2)}`;
  135. const lastRecordDate = recordData[0]?.createTime?.slice(0, 10);
  136. if (yesterdayStr === lastRecordDate) {
  137. setTodayIntegral(lastCredit + 1);
  138. setTomorrowIntegral(lastCredit + 2);
  139. } else {
  140. setTodayIntegral(lastCredit);
  141. setTomorrowIntegral(Math.min(lastCredit + 1, 7));
  142. }
  143. }
  144. } catch (error) {
  145. console.error('获取签到记录失败:', error);
  146. }
  147. }, []);
  148. // 初始化
  149. useEffect(() => {
  150. const dateInfo = setDate();
  151. getInfo();
  152. getData(dateInfo);
  153. }, [setDate, getInfo, getData]);
  154. // 签到
  155. const handleSignIn = async () => {
  156. if (istodaySignIn) {
  157. Alert.alert('提示', '今天已签到');
  158. return;
  159. }
  160. try {
  161. const res = await signIn();
  162. if (res?.code === 0) {
  163. Alert.alert('提示', '签到成功');
  164. setIstodaySignIn(true);
  165. const dateInfo = setDate();
  166. getInfo();
  167. getData(dateInfo);
  168. } else {
  169. Alert.alert('提示', res?.msg || '签到失败');
  170. }
  171. } catch (error) {
  172. console.error('签到失败:', error);
  173. Alert.alert('提示', '签到失败');
  174. }
  175. };
  176. const handleBack = () => {
  177. router.back();
  178. };
  179. return (
  180. <View style={styles.container}>
  181. <StatusBar barStyle="dark-content" />
  182. {/* 顶部导航 */}
  183. <View style={[styles.header, { paddingTop: insets.top }]}>
  184. <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
  185. <Text style={styles.backIcon}>‹</Text>
  186. </TouchableOpacity>
  187. <Text style={styles.title}>我的积分</Text>
  188. <View style={styles.placeholder} />
  189. </View>
  190. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  191. {/* 头部积分展示 */}
  192. <ImageBackground
  193. source={{ uri: Images.integral?.head || Images.common.commonBg }}
  194. style={styles.headerBg}
  195. resizeMode="cover"
  196. >
  197. <Text style={styles.integralNum}>{integral}</Text>
  198. <TouchableOpacity
  199. style={styles.allBox}
  200. onPress={() => setIsTooltip(!isTooltip)}
  201. >
  202. <Text style={styles.allText}>所有积分</Text>
  203. <Image
  204. source={{ uri: Images.integral?.greetings }}
  205. style={styles.infoIcon}
  206. contentFit="contain"
  207. />
  208. </TouchableOpacity>
  209. <View style={styles.todayBox}>
  210. <Text style={styles.todayText}>
  211. 今日已获得<Text style={styles.todayNum}>{istodaySignIn ? todayIntegral : 0}</Text>积分
  212. </Text>
  213. </View>
  214. </ImageBackground>
  215. <View style={styles.content}>
  216. {/* 签到面板 */}
  217. <View style={styles.panel}>
  218. <View style={styles.panelTitle}>
  219. <View style={styles.panelTitleLeft}>
  220. <Image
  221. source={{ uri: Images.integral?.goldCoins }}
  222. style={styles.goldIcon}
  223. contentFit="contain"
  224. />
  225. <Text style={styles.panelTitleText}>签到领积分</Text>
  226. </View>
  227. <Text style={[styles.panelTitleRight, istodaySignIn && styles.signedText]}>
  228. {istodaySignIn ? '今日已签到' : '今日未签到'}
  229. </Text>
  230. </View>
  231. <Text style={styles.explain}>
  232. 已签到{record.filter(r => r.credit > 0).length}天,共获得<Text style={styles.highlightText}>{daysIntegral}</Text>积分
  233. </Text>
  234. {/* 签到进度 */}
  235. <View style={styles.progressBox}>
  236. <View style={styles.lineBox}>
  237. {[1, 2, 3, 4, 5].map((_, index) => (
  238. <View key={index} style={styles.lineSection}>
  239. <View style={[styles.line, styles.lineOn]} />
  240. </View>
  241. ))}
  242. </View>
  243. <View style={styles.daysBox}>
  244. {dataInfo.map((item, index) => (
  245. <View key={index} style={styles.dayItem}>
  246. <TouchableOpacity
  247. style={styles.dayCircleBox}
  248. onPress={item.istoday && !istodaySignIn ? handleSignIn : undefined}
  249. activeOpacity={item.istoday && !istodaySignIn ? 0.7 : 1}
  250. >
  251. {item.istoday && !istodaySignIn ? (
  252. <ImageBackground
  253. source={{ uri: Images.integral?.basisBg }}
  254. style={styles.dayCircleBg}
  255. resizeMode="contain"
  256. >
  257. <View style={[styles.dayCircle, styles.todayCircle]}>
  258. <Text style={styles.todayCircleText}>+{todayIntegral}</Text>
  259. </View>
  260. </ImageBackground>
  261. ) : item.isFutureDate ? (
  262. <View style={[styles.dayCircle, styles.futureCircle]}>
  263. <Text style={styles.futureText}>+{tomorrowIntegral}</Text>
  264. </View>
  265. ) : item.isSignIn ? (
  266. <View style={[styles.dayCircle, styles.signedCircle]}>
  267. <Text style={styles.checkIcon}>✓</Text>
  268. </View>
  269. ) : (
  270. <View style={[styles.dayCircle, styles.missedCircle]}>
  271. <Text style={styles.missedIcon}>✗</Text>
  272. </View>
  273. )}
  274. </TouchableOpacity>
  275. <Text style={[styles.dayText, item.istoday && styles.todayDayText]}>
  276. {item.istoday ? '今天' : item.text}
  277. </Text>
  278. </View>
  279. ))}
  280. </View>
  281. </View>
  282. </View>
  283. {/* 积分明细 */}
  284. <View style={styles.listSection}>
  285. <Text style={styles.listTitle}>积分明细</Text>
  286. {record.map((item, index) => (
  287. <View key={item.id || index} style={styles.listItem}>
  288. <View style={styles.listItemLeft}>
  289. <Text style={styles.listItemTitle}>
  290. {item.credit > 0 ? '积分签到获得' : '大转盘消费'}
  291. </Text>
  292. <Text style={styles.listItemTime}>{item.createTime}</Text>
  293. </View>
  294. <Text style={[
  295. styles.listItemCredit,
  296. item.credit > 0 ? styles.creditPositive : styles.creditNegative
  297. ]}>
  298. {item.credit > 0 ? '+' : ''}{item.credit}
  299. </Text>
  300. </View>
  301. ))}
  302. {record.length === 0 && (
  303. <View style={styles.emptyBox}>
  304. <Text style={styles.emptyText}>暂无积分记录</Text>
  305. </View>
  306. )}
  307. </View>
  308. </View>
  309. </ScrollView>
  310. </View>
  311. );
  312. }
  313. const styles = StyleSheet.create({
  314. container: {
  315. flex: 1,
  316. backgroundColor: '#F8FAFB',
  317. },
  318. header: {
  319. flexDirection: 'row',
  320. alignItems: 'center',
  321. justifyContent: 'space-between',
  322. paddingHorizontal: 10,
  323. height: 80,
  324. backgroundColor: 'transparent',
  325. position: 'absolute',
  326. top: 0,
  327. left: 0,
  328. right: 0,
  329. zIndex: 100,
  330. },
  331. backBtn: {
  332. width: 40,
  333. height: 40,
  334. justifyContent: 'center',
  335. alignItems: 'center',
  336. },
  337. backIcon: {
  338. fontSize: 32,
  339. color: '#000',
  340. fontWeight: 'bold',
  341. },
  342. title: {
  343. fontSize: 15,
  344. fontWeight: 'bold',
  345. color: '#000',
  346. },
  347. placeholder: {
  348. width: 40,
  349. },
  350. scrollView: {
  351. flex: 1,
  352. },
  353. headerBg: {
  354. width: '100%',
  355. height: 283,
  356. paddingTop: 100,
  357. alignItems: 'center',
  358. },
  359. integralNum: {
  360. fontSize: 48,
  361. fontWeight: '400',
  362. color: '#5B460F',
  363. textAlign: 'center',
  364. },
  365. allBox: {
  366. flexDirection: 'row',
  367. alignItems: 'center',
  368. marginTop: 7,
  369. marginBottom: 12,
  370. },
  371. allText: {
  372. fontSize: 12,
  373. color: '#C8B177',
  374. },
  375. infoIcon: {
  376. width: 16,
  377. height: 16,
  378. marginLeft: 4,
  379. },
  380. todayBox: {
  381. backgroundColor: 'rgba(0,0,0,0.08)',
  382. borderRadius: 217,
  383. paddingHorizontal: 15,
  384. paddingVertical: 5,
  385. },
  386. todayText: {
  387. fontSize: 12,
  388. color: '#8A794F',
  389. },
  390. todayNum: {
  391. fontWeight: '800',
  392. color: '#5B460F',
  393. },
  394. content: {
  395. paddingHorizontal: 10,
  396. marginTop: -50,
  397. },
  398. panel: {
  399. backgroundColor: '#fff',
  400. borderRadius: 15,
  401. padding: 12,
  402. },
  403. panelTitle: {
  404. flexDirection: 'row',
  405. justifyContent: 'space-between',
  406. alignItems: 'center',
  407. paddingHorizontal: 10,
  408. },
  409. panelTitleLeft: {
  410. flexDirection: 'row',
  411. alignItems: 'center',
  412. },
  413. goldIcon: {
  414. width: 24,
  415. height: 24,
  416. marginRight: 6,
  417. },
  418. panelTitleText: {
  419. fontSize: 16,
  420. fontWeight: '700',
  421. color: '#3D3D3D',
  422. },
  423. panelTitleRight: {
  424. fontSize: 12,
  425. color: '#FC7D2E',
  426. },
  427. signedText: {
  428. color: '#999',
  429. },
  430. explain: {
  431. fontSize: 12,
  432. color: '#999',
  433. paddingHorizontal: 10,
  434. marginTop: 7,
  435. marginBottom: 11,
  436. },
  437. highlightText: {
  438. color: '#FC7D2E',
  439. },
  440. progressBox: {
  441. paddingTop: 20,
  442. },
  443. lineBox: {
  444. flexDirection: 'row',
  445. paddingHorizontal: 10,
  446. },
  447. lineSection: {
  448. flex: 1,
  449. },
  450. line: {
  451. height: 1,
  452. backgroundColor: '#9E9E9E',
  453. },
  454. lineOn: {
  455. backgroundColor: '#FC7D2E',
  456. },
  457. daysBox: {
  458. flexDirection: 'row',
  459. marginTop: -25,
  460. },
  461. dayItem: {
  462. flex: 1,
  463. alignItems: 'center',
  464. },
  465. dayCircleBox: {
  466. width: 46,
  467. height: 46,
  468. justifyContent: 'center',
  469. alignItems: 'center',
  470. marginBottom: 3,
  471. },
  472. dayCircleBg: {
  473. width: 46,
  474. height: 46,
  475. justifyContent: 'center',
  476. alignItems: 'center',
  477. },
  478. dayCircle: {
  479. width: 32,
  480. height: 32,
  481. borderRadius: 16,
  482. justifyContent: 'center',
  483. alignItems: 'center',
  484. },
  485. todayCircle: {
  486. backgroundColor: '#FC7D2E',
  487. },
  488. todayCircleText: {
  489. color: '#fff',
  490. fontSize: 12,
  491. fontWeight: 'bold',
  492. },
  493. futureCircle: {
  494. backgroundColor: '#F4F6F8',
  495. borderWidth: 1,
  496. borderColor: 'rgba(226,226,226,0.5)',
  497. },
  498. futureText: {
  499. color: '#505050',
  500. fontSize: 12,
  501. },
  502. signedCircle: {
  503. backgroundColor: '#FFE7C4',
  504. },
  505. checkIcon: {
  506. color: '#FC7D2E',
  507. fontSize: 16,
  508. fontWeight: 'bold',
  509. },
  510. missedCircle: {
  511. backgroundColor: '#F4F6F8',
  512. },
  513. missedIcon: {
  514. color: '#999',
  515. fontSize: 16,
  516. },
  517. dayText: {
  518. fontSize: 12,
  519. color: '#9E9E9E',
  520. },
  521. todayDayText: {
  522. color: '#FC7D2E',
  523. },
  524. listSection: {
  525. backgroundColor: '#fff',
  526. borderRadius: 15,
  527. padding: 12,
  528. marginTop: 10,
  529. marginBottom: 30,
  530. },
  531. listTitle: {
  532. fontSize: 16,
  533. fontWeight: '700',
  534. color: '#3D3D3D',
  535. marginBottom: 10,
  536. },
  537. listItem: {
  538. flexDirection: 'row',
  539. justifyContent: 'space-between',
  540. alignItems: 'center',
  541. paddingVertical: 14,
  542. borderBottomWidth: 1,
  543. borderBottomColor: 'rgba(0,0,0,0.05)',
  544. },
  545. listItemLeft: {},
  546. listItemTitle: {
  547. fontSize: 14,
  548. color: '#333',
  549. marginBottom: 3,
  550. },
  551. listItemTime: {
  552. fontSize: 12,
  553. color: '#999',
  554. },
  555. listItemCredit: {
  556. fontSize: 18,
  557. fontWeight: '700',
  558. },
  559. creditPositive: {
  560. color: '#588CFF',
  561. },
  562. creditNegative: {
  563. color: '#FC7D2E',
  564. },
  565. emptyBox: {
  566. paddingVertical: 50,
  567. alignItems: 'center',
  568. },
  569. emptyText: {
  570. fontSize: 14,
  571. color: '#999',
  572. },
  573. });