Files
digital-pilates/components/statistic/SleepCard.tsx
richarjiang 2dca3253e6 feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本
- 新增useI18n钩子函数统一管理翻译
- 完善中英文翻译资源,覆盖统计、用药、通知设置等模块
- 优化Tab布局使用翻译键值替代静态文本
- 更新药品管理、个人资料编辑等页面的多语言支持
2025-11-13 11:09:55 +08:00

140 lines
4.1 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 { useTranslation } from 'react-i18next';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface SleepCardProps {
selectedDate?: Date;
style?: object;
}
const SleepCard: React.FC<SleepCardProps> = ({
selectedDate,
style,
}) => {
const { t } = useTranslation();
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 && challenge.status === 'ongoing'),
[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: Failed to get sleep data:', 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: Challenge progress report failed', { 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}>{t('statistics.components.sleep.title')}</Text>
</View>
<Text style={styles.sleepValue}>
{loading ? t('statistics.components.sleep.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;