| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- import { Image } from 'expo-image';
- import { useRouter } from 'expo-router';
- import React, { useCallback, useEffect, useState } from 'react';
- import {
- Alert,
- ImageBackground,
- ScrollView,
- StatusBar,
- StyleSheet,
- Text,
- TouchableOpacity,
- View,
- } from 'react-native';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { Images } from '@/constants/images';
- import { getCreditRecord, getWalletInfo, signIn } from '@/services/user';
- interface DayInfo {
- text: string;
- date: string;
- isFutureDate: boolean;
- istoday: boolean;
- isSignIn: boolean;
- }
- interface RecordItem {
- id: string;
- credit: number;
- createTime: string;
- }
- export default function IntegralScreen() {
- const router = useRouter();
- const insets = useSafeAreaInsets();
-
- const [integral, setIntegral] = useState(0);
- const [daysIntegral, setDaysIntegral] = useState(0);
- const [istodaySignIn, setIstodaySignIn] = useState(false);
- const [record, setRecord] = useState<RecordItem[]>([]);
- const [dataInfo, setDataInfo] = useState<DayInfo[]>([]);
- const [todayIntegral, setTodayIntegral] = useState(1);
- const [tomorrowIntegral, setTomorrowIntegral] = useState(1);
- const [isTooltip, setIsTooltip] = useState(false);
- // 格式化数字为两位
- const padWithZeros = (number: number, length: number) => {
- return String(number).padStart(length, '0');
- };
- // 设置日期数据
- const setDate = useCallback(() => {
- const now = new Date();
- const dataInfoArr: DayInfo[] = [];
- // 前4天
- for (let i = -4; i <= 0; i++) {
- const date = new Date(now);
- date.setDate(date.getDate() + i);
- const m = date.getMonth() + 1;
- const d = date.getDate();
- const y = date.getFullYear();
-
- dataInfoArr.push({
- text: `${m}.${d}`,
- date: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
- isFutureDate: false,
- istoday: i === 0,
- isSignIn: false,
- });
- }
- // 明天
- const tomorrow = new Date(now);
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tm = tomorrow.getMonth() + 1;
- const td = tomorrow.getDate();
- const ty = tomorrow.getFullYear();
-
- dataInfoArr.push({
- text: `${tm}.${td}`,
- date: `${ty}-${padWithZeros(tm, 2)}-${padWithZeros(td, 2)}`,
- isFutureDate: true,
- istoday: false,
- isSignIn: false,
- });
- setDataInfo(dataInfoArr);
- return dataInfoArr;
- }, []);
- // 获取积分信息
- const getInfo = useCallback(async () => {
- try {
- const info = await getWalletInfo('USER_CREDIT');
- setIntegral(info?.balance || 0);
- } catch (error) {
- console.error('获取积分失败:', error);
- }
- }, []);
- // 获取签到记录
- const getData = useCallback(async (dateInfo: DayInfo[]) => {
- try {
- const fourDaysAgo = new Date();
- fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
- const m = fourDaysAgo.getMonth() + 1;
- const d = fourDaysAgo.getDate();
- const y = fourDaysAgo.getFullYear();
-
- const param = {
- createTime: `${y}-${padWithZeros(m, 2)}-${padWithZeros(d, 2)}`,
- };
- const res = await getCreditRecord(param);
- const recordData = res?.data || [];
- setRecord(recordData);
- // 今天日期
- const now = new Date();
- const todayStr = `${now.getFullYear()}-${padWithZeros(now.getMonth() + 1, 2)}-${padWithZeros(now.getDate(), 2)}`;
- // 更新签到状态
- const updatedDataInfo = dateInfo.map(item => {
- const newItem = { ...item };
-
- // 检查是否已签到
- for (const rec of recordData) {
- const createTime = rec.createTime?.slice(0, 10);
- if (createTime === item.date) {
- newItem.isSignIn = true;
- if (item.date === todayStr) {
- setIstodaySignIn(true);
- }
- }
- }
-
- return newItem;
- });
- setDataInfo(updatedDataInfo);
- // 计算签到积分
- let total = 0;
- for (const rec of recordData) {
- if (rec.credit > 0) {
- total += rec.credit;
- }
- }
- setDaysIntegral(total);
- // 计算今天和明天的积分
- if (recordData.length > 0) {
- const lastCredit = recordData[0]?.credit || 1;
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- const yesterdayStr = `${yesterday.getFullYear()}-${padWithZeros(yesterday.getMonth() + 1, 2)}-${padWithZeros(yesterday.getDate(), 2)}`;
- const lastRecordDate = recordData[0]?.createTime?.slice(0, 10);
- if (yesterdayStr === lastRecordDate) {
- setTodayIntegral(lastCredit + 1);
- setTomorrowIntegral(lastCredit + 2);
- } else {
- setTodayIntegral(lastCredit);
- setTomorrowIntegral(Math.min(lastCredit + 1, 7));
- }
- }
- } catch (error) {
- console.error('获取签到记录失败:', error);
- }
- }, []);
- // 初始化
- useEffect(() => {
- const dateInfo = setDate();
- getInfo();
- getData(dateInfo);
- }, [setDate, getInfo, getData]);
- // 签到
- const handleSignIn = async () => {
- if (istodaySignIn) {
- Alert.alert('提示', '今天已签到');
- return;
- }
- try {
- const res = await signIn();
- if (res?.code === 0) {
- Alert.alert('提示', '签到成功');
- setIstodaySignIn(true);
- const dateInfo = setDate();
- getInfo();
- getData(dateInfo);
- } else {
- Alert.alert('提示', res?.msg || '签到失败');
- }
- } catch (error) {
- console.error('签到失败:', error);
- Alert.alert('提示', '签到失败');
- }
- };
- const handleBack = () => {
- router.back();
- };
- return (
- <View style={styles.container}>
- <StatusBar barStyle="dark-content" />
-
- {/* 顶部导航 */}
- <View style={[styles.header, { paddingTop: insets.top }]}>
- <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
- <Text style={styles.backIcon}>‹</Text>
- </TouchableOpacity>
- <Text style={styles.title}>我的积分</Text>
- <View style={styles.placeholder} />
- </View>
- <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
- {/* 头部积分展示 */}
- <ImageBackground
- source={{ uri: Images.integral?.head || Images.common.commonBg }}
- style={styles.headerBg}
- resizeMode="cover"
- >
- <Text style={styles.integralNum}>{integral}</Text>
- <TouchableOpacity
- style={styles.allBox}
- onPress={() => setIsTooltip(!isTooltip)}
- >
- <Text style={styles.allText}>所有积分</Text>
- <Image
- source={{ uri: Images.integral?.greetings }}
- style={styles.infoIcon}
- contentFit="contain"
- />
- </TouchableOpacity>
- <View style={styles.todayBox}>
- <Text style={styles.todayText}>
- 今日已获得<Text style={styles.todayNum}>{istodaySignIn ? todayIntegral : 0}</Text>积分
- </Text>
- </View>
- </ImageBackground>
- <View style={styles.content}>
- {/* 签到面板 */}
- <View style={styles.panel}>
- <View style={styles.panelTitle}>
- <View style={styles.panelTitleLeft}>
- <Image
- source={{ uri: Images.integral?.goldCoins }}
- style={styles.goldIcon}
- contentFit="contain"
- />
- <Text style={styles.panelTitleText}>签到领积分</Text>
- </View>
- <Text style={[styles.panelTitleRight, istodaySignIn && styles.signedText]}>
- {istodaySignIn ? '今日已签到' : '今日未签到'}
- </Text>
- </View>
- <Text style={styles.explain}>
- 已签到{record.filter(r => r.credit > 0).length}天,共获得<Text style={styles.highlightText}>{daysIntegral}</Text>积分
- </Text>
- {/* 签到进度 */}
- <View style={styles.progressBox}>
- <View style={styles.lineBox}>
- {[1, 2, 3, 4, 5].map((_, index) => (
- <View key={index} style={styles.lineSection}>
- <View style={[styles.line, styles.lineOn]} />
- </View>
- ))}
- </View>
- <View style={styles.daysBox}>
- {dataInfo.map((item, index) => (
- <View key={index} style={styles.dayItem}>
- <TouchableOpacity
- style={styles.dayCircleBox}
- onPress={item.istoday && !istodaySignIn ? handleSignIn : undefined}
- activeOpacity={item.istoday && !istodaySignIn ? 0.7 : 1}
- >
- {item.istoday && !istodaySignIn ? (
- <ImageBackground
- source={{ uri: Images.integral?.basisBg }}
- style={styles.dayCircleBg}
- resizeMode="contain"
- >
- <View style={[styles.dayCircle, styles.todayCircle]}>
- <Text style={styles.todayCircleText}>+{todayIntegral}</Text>
- </View>
- </ImageBackground>
- ) : item.isFutureDate ? (
- <View style={[styles.dayCircle, styles.futureCircle]}>
- <Text style={styles.futureText}>+{tomorrowIntegral}</Text>
- </View>
- ) : item.isSignIn ? (
- <View style={[styles.dayCircle, styles.signedCircle]}>
- <Text style={styles.checkIcon}>✓</Text>
- </View>
- ) : (
- <View style={[styles.dayCircle, styles.missedCircle]}>
- <Text style={styles.missedIcon}>✗</Text>
- </View>
- )}
- </TouchableOpacity>
- <Text style={[styles.dayText, item.istoday && styles.todayDayText]}>
- {item.istoday ? '今天' : item.text}
- </Text>
- </View>
- ))}
- </View>
- </View>
- </View>
- {/* 积分明细 */}
- <View style={styles.listSection}>
- <Text style={styles.listTitle}>积分明细</Text>
- {record.map((item, index) => (
- <View key={item.id || index} style={styles.listItem}>
- <View style={styles.listItemLeft}>
- <Text style={styles.listItemTitle}>
- {item.credit > 0 ? '积分签到获得' : '大转盘消费'}
- </Text>
- <Text style={styles.listItemTime}>{item.createTime}</Text>
- </View>
- <Text style={[
- styles.listItemCredit,
- item.credit > 0 ? styles.creditPositive : styles.creditNegative
- ]}>
- {item.credit > 0 ? '+' : ''}{item.credit}
- </Text>
- </View>
- ))}
- {record.length === 0 && (
- <View style={styles.emptyBox}>
- <Text style={styles.emptyText}>暂无积分记录</Text>
- </View>
- )}
- </View>
- </View>
- </ScrollView>
- </View>
- );
- }
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#F8FAFB',
- },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 10,
- height: 80,
- backgroundColor: 'transparent',
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- zIndex: 100,
- },
- backBtn: {
- width: 40,
- height: 40,
- justifyContent: 'center',
- alignItems: 'center',
- },
- backIcon: {
- fontSize: 32,
- color: '#000',
- fontWeight: 'bold',
- },
- title: {
- fontSize: 15,
- fontWeight: 'bold',
- color: '#000',
- },
- placeholder: {
- width: 40,
- },
- scrollView: {
- flex: 1,
- },
- headerBg: {
- width: '100%',
- height: 283,
- paddingTop: 100,
- alignItems: 'center',
- },
- integralNum: {
- fontSize: 48,
- fontWeight: '400',
- color: '#5B460F',
- textAlign: 'center',
- },
- allBox: {
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: 7,
- marginBottom: 12,
- },
- allText: {
- fontSize: 12,
- color: '#C8B177',
- },
- infoIcon: {
- width: 16,
- height: 16,
- marginLeft: 4,
- },
- todayBox: {
- backgroundColor: 'rgba(0,0,0,0.08)',
- borderRadius: 217,
- paddingHorizontal: 15,
- paddingVertical: 5,
- },
- todayText: {
- fontSize: 12,
- color: '#8A794F',
- },
- todayNum: {
- fontWeight: '800',
- color: '#5B460F',
- },
- content: {
- paddingHorizontal: 10,
- marginTop: -50,
- },
- panel: {
- backgroundColor: '#fff',
- borderRadius: 15,
- padding: 12,
- },
- panelTitle: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingHorizontal: 10,
- },
- panelTitleLeft: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- goldIcon: {
- width: 24,
- height: 24,
- marginRight: 6,
- },
- panelTitleText: {
- fontSize: 16,
- fontWeight: '700',
- color: '#3D3D3D',
- },
- panelTitleRight: {
- fontSize: 12,
- color: '#FC7D2E',
- },
- signedText: {
- color: '#999',
- },
- explain: {
- fontSize: 12,
- color: '#999',
- paddingHorizontal: 10,
- marginTop: 7,
- marginBottom: 11,
- },
- highlightText: {
- color: '#FC7D2E',
- },
- progressBox: {
- paddingTop: 20,
- },
- lineBox: {
- flexDirection: 'row',
- paddingHorizontal: 10,
- },
- lineSection: {
- flex: 1,
- },
- line: {
- height: 1,
- backgroundColor: '#9E9E9E',
- },
- lineOn: {
- backgroundColor: '#FC7D2E',
- },
- daysBox: {
- flexDirection: 'row',
- marginTop: -25,
- },
- dayItem: {
- flex: 1,
- alignItems: 'center',
- },
- dayCircleBox: {
- width: 46,
- height: 46,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: 3,
- },
- dayCircleBg: {
- width: 46,
- height: 46,
- justifyContent: 'center',
- alignItems: 'center',
- },
- dayCircle: {
- width: 32,
- height: 32,
- borderRadius: 16,
- justifyContent: 'center',
- alignItems: 'center',
- },
- todayCircle: {
- backgroundColor: '#FC7D2E',
- },
- todayCircleText: {
- color: '#fff',
- fontSize: 12,
- fontWeight: 'bold',
- },
- futureCircle: {
- backgroundColor: '#F4F6F8',
- borderWidth: 1,
- borderColor: 'rgba(226,226,226,0.5)',
- },
- futureText: {
- color: '#505050',
- fontSize: 12,
- },
- signedCircle: {
- backgroundColor: '#FFE7C4',
- },
- checkIcon: {
- color: '#FC7D2E',
- fontSize: 16,
- fontWeight: 'bold',
- },
- missedCircle: {
- backgroundColor: '#F4F6F8',
- },
- missedIcon: {
- color: '#999',
- fontSize: 16,
- },
- dayText: {
- fontSize: 12,
- color: '#9E9E9E',
- },
- todayDayText: {
- color: '#FC7D2E',
- },
- listSection: {
- backgroundColor: '#fff',
- borderRadius: 15,
- padding: 12,
- marginTop: 10,
- marginBottom: 30,
- },
- listTitle: {
- fontSize: 16,
- fontWeight: '700',
- color: '#3D3D3D',
- marginBottom: 10,
- },
- listItem: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 14,
- borderBottomWidth: 1,
- borderBottomColor: 'rgba(0,0,0,0.05)',
- },
- listItemLeft: {},
- listItemTitle: {
- fontSize: 14,
- color: '#333',
- marginBottom: 3,
- },
- listItemTime: {
- fontSize: 12,
- color: '#999',
- },
- listItemCredit: {
- fontSize: 18,
- fontWeight: '700',
- },
- creditPositive: {
- color: '#588CFF',
- },
- creditNegative: {
- color: '#FC7D2E',
- },
- emptyBox: {
- paddingVertical: 50,
- alignItems: 'center',
- },
- emptyText: {
- fontSize: 14,
- color: '#999',
- },
- });
|