feat: Enhance Oxygen Saturation Card with health permissions and loading state management
feat(i18n): Add common translations and mood-related strings in English and Chinese fix(i18n): Update metabolism titles for consistency in health translations chore: Update Podfile.lock to include SDWebImage 5.21.4 and other dependency versions refactor(moodCheckins): Improve mood configuration retrieval with optional translation support refactor(sleepHealthKit): Replace useI18n with direct i18n import for sleep quality descriptions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { ChallengeType } from '@/services/challengesApi';
|
||||
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
|
||||
import { ActivityRingsData, fetchActivityRingsForDate } from '@/utils/health';
|
||||
@@ -26,6 +27,7 @@ export function FitnessRingsCard({
|
||||
selectedDate,
|
||||
resetToken,
|
||||
}: FitnessRingsCardProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const challenges = useAppSelector(selectChallengeList);
|
||||
const [activityData, setActivityData] = useState<ActivityRingsData | null>(null);
|
||||
@@ -135,6 +137,24 @@ export function FitnessRingsCard({
|
||||
const exerciseProgress = Math.min(1, Math.max(0, exerciseMinutes / exerciseMinutesGoal));
|
||||
const standProgress = Math.min(1, Math.max(0, standHours / standHoursGoal));
|
||||
|
||||
const units = useMemo(
|
||||
() => ({
|
||||
kcal: t('statistics.components.fitness.kcal'),
|
||||
minutes: t('statistics.components.fitness.minutes'),
|
||||
hours: t('statistics.components.fitness.hours'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const fitnessRows = useMemo(
|
||||
() => [
|
||||
{ key: 'active', value: Math.round(activeCalories), goal: activeCaloriesGoal, unit: units.kcal },
|
||||
{ key: 'exercise', value: Math.round(exerciseMinutes), goal: exerciseMinutesGoal, unit: units.minutes },
|
||||
{ key: 'stand', value: Math.round(standHours), goal: standHoursGoal, unit: units.hours },
|
||||
],
|
||||
[activeCalories, activeCaloriesGoal, exerciseMinutes, exerciseMinutesGoal, standHours, standHoursGoal, units]
|
||||
);
|
||||
|
||||
const handlePress = () => {
|
||||
router.push(ROUTES.FITNESS_RINGS_DETAIL);
|
||||
};
|
||||
@@ -191,47 +211,23 @@ export function FitnessRingsCard({
|
||||
|
||||
{/* 右侧数据显示 */}
|
||||
<View style={styles.dataContainer}>
|
||||
<View style={styles.dataRow}>
|
||||
<Text style={styles.dataText}>
|
||||
{loading ? (
|
||||
<Text style={styles.dataValue}>--</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataValue}>{Math.round(activeCalories)}</Text>
|
||||
<Text style={styles.dataGoal}>/{activeCaloriesGoal}</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.dataUnit}>千卡</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataRow}>
|
||||
<Text style={styles.dataText}>
|
||||
{loading ? (
|
||||
<Text style={styles.dataValue}>--</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataValue}>{Math.round(exerciseMinutes)}</Text>
|
||||
<Text style={styles.dataGoal}>/{exerciseMinutesGoal}</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.dataUnit}>分钟</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataRow}>
|
||||
<Text style={styles.dataText}>
|
||||
{loading ? (
|
||||
<Text style={styles.dataValue}>--</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataValue}>{Math.round(standHours)}</Text>
|
||||
<Text style={styles.dataGoal}>/{standHoursGoal}</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.dataUnit}>小时</Text>
|
||||
</View>
|
||||
{fitnessRows.map((row) => (
|
||||
<View key={row.key} style={styles.dataRow}>
|
||||
<Text style={styles.dataText}>
|
||||
{loading ? (
|
||||
<Text style={styles.dataValue}>--</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataValue}>{row.value}</Text>
|
||||
<Text style={styles.dataGoal}>
|
||||
{t('statistics.components.fitnessRings.goal', { goal: row.goal })}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.dataUnit}>{row.unit}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface MoodCardProps {
|
||||
|
||||
export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
|
||||
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType, t) : null;
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,4 +122,4 @@ const styles = StyleSheet.create({
|
||||
marginTop: 22,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
@@ -8,7 +9,9 @@ interface MoodHistoryCardProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function MoodHistoryCard({ moodCheckins, title = '心情记录' }: MoodHistoryCardProps) {
|
||||
export function MoodHistoryCard({ moodCheckins, title }: MoodHistoryCardProps) {
|
||||
const { t } = useI18n();
|
||||
const defaultTitle = t('mood.history.title');
|
||||
// 计算心情统计
|
||||
const moodStats = React.useMemo(() => {
|
||||
const stats = {
|
||||
@@ -26,7 +29,7 @@ export function MoodHistoryCard({ moodCheckins, title = '心情记录' }: MoodHi
|
||||
|
||||
// 计算心情分布
|
||||
moodCheckins.forEach(checkin => {
|
||||
const moodLabel = getMoodConfig(checkin.moodType)?.label || checkin.moodType;
|
||||
const moodLabel = getMoodConfig(checkin.moodType, t)?.label || checkin.moodType;
|
||||
stats.moodDistribution[moodLabel] = (stats.moodDistribution[moodLabel] || 0) + 1;
|
||||
});
|
||||
|
||||
@@ -45,11 +48,11 @@ export function MoodHistoryCard({ moodCheckins, title = '心情记录' }: MoodHi
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.title}>{title || defaultTitle}</Text>
|
||||
|
||||
{moodCheckins.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyText}>暂无心情记录</Text>
|
||||
<Text style={styles.emptyText}>{t('mood.history.noRecords')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
@@ -57,36 +60,36 @@ export function MoodHistoryCard({ moodCheckins, title = '心情记录' }: MoodHi
|
||||
<View style={styles.statsContainer}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{moodStats.total}</Text>
|
||||
<Text style={styles.statLabel}>总记录</Text>
|
||||
<Text style={styles.statLabel}>{t('mood.history.totalRecords')}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{moodStats.averageIntensity}</Text>
|
||||
<Text style={styles.statLabel}>平均强度</Text>
|
||||
<Text style={styles.statLabel}>{t('mood.history.averageIntensity')}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{moodStats.mostFrequentMood}</Text>
|
||||
<Text style={styles.statLabel}>最常见</Text>
|
||||
<Text style={styles.statLabel}>{t('mood.history.mostFrequent')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 最近记录 */}
|
||||
<View style={styles.recentContainer}>
|
||||
<Text style={styles.sectionTitle}>最近记录</Text>
|
||||
<Text style={styles.sectionTitle}>{t('mood.history.recentRecords')}</Text>
|
||||
{recentMoods.map((checkin, index) => {
|
||||
const moodConfig = getMoodConfig(checkin.moodType);
|
||||
const moodConfig = getMoodConfig(checkin.moodType, t);
|
||||
return (
|
||||
<View key={checkin.id} style={styles.moodItem}>
|
||||
<View style={styles.moodInfo}>
|
||||
<Text style={styles.moodEmoji}>{moodConfig?.emoji}</Text>
|
||||
<Text style={styles.moodEmoji}>😊</Text>
|
||||
<View style={styles.moodDetails}>
|
||||
<Text style={styles.moodLabel}>{moodConfig?.label}</Text>
|
||||
<Text style={styles.moodDate}>
|
||||
{dayjs(checkin.createdAt).format('MM月DD日 HH:mm')}
|
||||
{dayjs(checkin.createdAt).format(t('mood.history.dateTimeFormat'))}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.moodIntensity}>
|
||||
<Text style={styles.intensityText}>强度 {checkin.intensity}</Text>
|
||||
<Text style={styles.intensityText}>{t('mood.history.intensity')} {checkin.intensity}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
@@ -38,6 +39,7 @@ export default function MoodIntensitySlider({
|
||||
width = 320,
|
||||
height = 16, // 更粗的进度条
|
||||
}: MoodIntensitySliderProps) {
|
||||
const { t } = useTranslation();
|
||||
const thumbSize = 32; // 合适的触摸区域
|
||||
const translateX = useSharedValue(0);
|
||||
const isDragging = useSharedValue(0);
|
||||
@@ -175,8 +177,8 @@ export default function MoodIntensitySlider({
|
||||
|
||||
{/* 标签 */}
|
||||
<View style={[styles.labelsContainer, { width: width }]}>
|
||||
<Text style={styles.labelText}>轻微</Text>
|
||||
<Text style={styles.labelText}>强烈</Text>
|
||||
<Text style={styles.labelText}>{t('mood.edit.intensityLow')}</Text>
|
||||
<Text style={styles.labelText}>{t('mood.edit.intensityHigh')}</Text>
|
||||
</View>
|
||||
|
||||
{/* 刻度 */}
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useI18n } from '../hooks/useI18n';
|
||||
import { useNotifications } from '../hooks/useNotifications';
|
||||
import { ThemedText } from './ThemedText';
|
||||
import { ThemedView } from './ThemedView';
|
||||
|
||||
export const NotificationTest: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
isInitialized,
|
||||
permissionStatus,
|
||||
@@ -95,8 +97,8 @@ export const NotificationTest: React.FC = () => {
|
||||
|
||||
const handleSendMoodCheckinReminder = async () => {
|
||||
try {
|
||||
await sendMoodCheckinReminder('心情打卡', '记得记录今天的心情状态哦');
|
||||
Alert.alert('成功', '心情打卡提醒已发送');
|
||||
await sendMoodCheckinReminder(t('notifications.moodReminder.title'), t('notifications.moodReminder.body'));
|
||||
Alert.alert(t('common.success'), t('notifications.moodReminder.sent'));
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '发送心情打卡提醒失败');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { fetchOxygenSaturation } from '@/utils/health';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { ensureHealthPermissions, fetchOxygenSaturation } from '@/utils/health';
|
||||
import { HealthKitUtils } from '@/utils/healthKit';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import HealthDataCard from './HealthDataCard';
|
||||
|
||||
@@ -15,42 +16,52 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
selectedDate
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isFocused = useIsFocused();
|
||||
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
// 获取血氧饱和度数据 - 在页面聚焦、日期变化时触发
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const loadOxygenSaturationData = async () => {
|
||||
const dateToUse = selectedDate || new Date();
|
||||
useEffect(() => {
|
||||
const loadOxygenSaturationData = async () => {
|
||||
const dateToUse = selectedDate || new Date();
|
||||
|
||||
// 防止重复请求
|
||||
if (loadingRef.current) return;
|
||||
if (!isFocused) return;
|
||||
if (!HealthKitUtils.isAvailable()) {
|
||||
setOxygenSaturation(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
// 防止重复请求
|
||||
if (loadingRef.current) return;
|
||||
|
||||
const options = {
|
||||
startDate: dayjs(dateToUse).startOf('day').toDate().toISOString(),
|
||||
endDate: dayjs(dateToUse).endOf('day').toDate().toISOString()
|
||||
};
|
||||
try {
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
|
||||
const data = await fetchOxygenSaturation(options);
|
||||
setOxygenSaturation(data);
|
||||
} catch (error) {
|
||||
console.error('OxygenSaturationCard: Failed to get blood oxygen data:', error);
|
||||
const hasPermission = await ensureHealthPermissions();
|
||||
if (!hasPermission) {
|
||||
setOxygenSaturation(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loadOxygenSaturationData();
|
||||
}, [selectedDate])
|
||||
);
|
||||
const options = {
|
||||
startDate: dayjs(dateToUse).startOf('day').toDate().toISOString(),
|
||||
endDate: dayjs(dateToUse).endOf('day').toDate().toISOString()
|
||||
};
|
||||
|
||||
const data = await fetchOxygenSaturation(options);
|
||||
setOxygenSaturation(data);
|
||||
} catch (error) {
|
||||
console.error('OxygenSaturationCard: Failed to get blood oxygen data:', error);
|
||||
setOxygenSaturation(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
loadOxygenSaturationData();
|
||||
}, [isFocused, selectedDate]);
|
||||
|
||||
return (
|
||||
<HealthDataCard
|
||||
@@ -62,4 +73,4 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default OxygenSaturationCard;
|
||||
export default OxygenSaturationCard;
|
||||
|
||||
Reference in New Issue
Block a user