138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
|
import { ChallengeType } from '@/services/challengesApi';
|
|
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
|
|
import { logger } from '@/utils/logger';
|
|
import { fetchCompleteSleepData, formatSleepTime } from '@/utils/sleepHealthKit';
|
|
import dayjs from 'dayjs';
|
|
import { Image } from 'expo-image';
|
|
import { router } from 'expo-router';
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
|
|
|
interface SleepCardProps {
|
|
selectedDate?: Date;
|
|
style?: object;
|
|
}
|
|
|
|
const SleepCard: React.FC<SleepCardProps> = ({
|
|
selectedDate,
|
|
style,
|
|
}) => {
|
|
const dispatch = useAppDispatch();
|
|
const challenges = useAppSelector(selectChallengeList);
|
|
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const joinedSleepChallenges = useMemo(
|
|
() => challenges.filter((challenge) => challenge.type === ChallengeType.SLEEP && challenge.isJoined),
|
|
[challenges]
|
|
);
|
|
const lastReportedRef = useRef<{ date: string; value: number | null } | null>(null);
|
|
|
|
// 获取睡眠数据
|
|
useEffect(() => {
|
|
const loadSleepData = async () => {
|
|
if (!selectedDate) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
const data = await fetchCompleteSleepData(selectedDate);
|
|
setSleepDuration(data?.totalSleepTime || null);
|
|
} catch (error) {
|
|
console.error('SleepCard: 获取睡眠数据失败:', error);
|
|
setSleepDuration(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadSleepData();
|
|
}, [selectedDate]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedDate || !sleepDuration || !joinedSleepChallenges.length) {
|
|
return;
|
|
}
|
|
|
|
// 如果当前日期不是今天,不上报
|
|
if (!dayjs(selectedDate).isSame(dayjs(), 'day')) {
|
|
return;
|
|
}
|
|
|
|
const dateKey = dayjs(selectedDate).format('YYYY-MM-DD');
|
|
const lastReport = lastReportedRef.current;
|
|
|
|
if (lastReport && lastReport.date === dateKey && lastReport.value === sleepDuration) {
|
|
return;
|
|
}
|
|
|
|
const reportProgress = async () => {
|
|
const sleepChallenge = joinedSleepChallenges.find((c) => c.type === ChallengeType.SLEEP);
|
|
if (!sleepChallenge) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await dispatch(reportChallengeProgress({ id: sleepChallenge.id, value: sleepDuration })).unwrap();
|
|
} catch (error) {
|
|
logger.warn('SleepCard: 挑战进度上报失败', { error, challengeId: sleepChallenge.id });
|
|
}
|
|
|
|
lastReportedRef.current = { date: dateKey, value: sleepDuration };
|
|
};
|
|
|
|
reportProgress();
|
|
}, [dispatch, joinedSleepChallenges, selectedDate, sleepDuration]);
|
|
|
|
const CardContent = (
|
|
<View style={[styles.container, style]}>
|
|
<View style={styles.cardHeaderRow}>
|
|
<Image
|
|
source={require('@/assets/images/icons/icon-sleep.png')}
|
|
style={styles.titleIcon}
|
|
/>
|
|
<Text style={styles.cardTitle}>睡眠</Text>
|
|
</View>
|
|
<Text style={styles.sleepValue}>
|
|
{loading ? '加载中...' : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<TouchableOpacity onPress={() => router.push(`/sleep-detail?date=${dayjs(selectedDate).format('YYYY-MM-DD')}`)} activeOpacity={0.7}>
|
|
{CardContent}
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
// Container styles will be inherited from parent (FloatingCard)
|
|
},
|
|
cardHeaderRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
},
|
|
titleIcon: {
|
|
width: 16,
|
|
height: 16,
|
|
marginRight: 6,
|
|
resizeMode: 'contain',
|
|
},
|
|
cardTitle: {
|
|
fontSize: 14,
|
|
color: '#192126',
|
|
fontWeight: '600',
|
|
},
|
|
sleepValue: {
|
|
fontSize: 16,
|
|
color: '#1E40AF',
|
|
fontWeight: '700',
|
|
marginTop: 8,
|
|
},
|
|
});
|
|
|
|
export default SleepCard;
|