feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
@@ -3,6 +3,7 @@ import { GlassContainer, GlassView, isLiquidGlassAvailable } from 'expo-glass-ef
|
|||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { Tabs, usePathname } from 'expo-router';
|
import { Tabs, usePathname } from 'expo-router';
|
||||||
import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs';
|
import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||||
@@ -16,18 +17,19 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
|||||||
// Tab configuration
|
// Tab configuration
|
||||||
type TabConfig = {
|
type TabConfig = {
|
||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
titleKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAB_CONFIGS: Record<string, TabConfig> = {
|
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||||
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
statistics: { icon: 'chart.pie.fill', titleKey: 'statistics.tabs.health' },
|
||||||
medications: { icon: 'pills.fill', title: '用药' },
|
medications: { icon: 'pills.fill', titleKey: 'statistics.tabs.medications' },
|
||||||
fasting: { icon: 'timer', title: '断食' },
|
fasting: { icon: 'timer', titleKey: 'statistics.tabs.fasting' },
|
||||||
challenges: { icon: 'trophy.fill', title: '挑战' },
|
challenges: { icon: 'trophy.fill', titleKey: 'statistics.tabs.challenges' },
|
||||||
personal: { icon: 'person.fill', title: '个人' },
|
personal: { icon: 'person.fill', titleKey: 'statistics.tabs.personal' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -96,7 +98,7 @@ export default function TabLayout() {
|
|||||||
}}
|
}}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{tabConfig.title}
|
{t(tabConfig.titleKey)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -175,24 +177,24 @@ export default function TabLayout() {
|
|||||||
if (glassEffectAvailable) {
|
if (glassEffectAvailable) {
|
||||||
return <NativeTabs>
|
return <NativeTabs>
|
||||||
<NativeTabs.Trigger name="statistics">
|
<NativeTabs.Trigger name="statistics">
|
||||||
<Label>健康</Label>
|
<Label>{t('statistics.tabs.health')}</Label>
|
||||||
<Icon sf="chart.pie.fill" drawable="custom_android_drawable" />
|
<Icon sf="chart.pie.fill" drawable="custom_android_drawable" />
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="medications">
|
<NativeTabs.Trigger name="medications">
|
||||||
<Icon sf="pills.fill" drawable="custom_android_drawable" />
|
<Icon sf="pills.fill" drawable="custom_android_drawable" />
|
||||||
<Label>用药</Label>
|
<Label>{t('statistics.tabs.medications')}</Label>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="fasting">
|
<NativeTabs.Trigger name="fasting">
|
||||||
<Icon sf="timer" drawable="custom_android_drawable" />
|
<Icon sf="timer" drawable="custom_android_drawable" />
|
||||||
<Label>断食</Label>
|
<Label>{t('statistics.tabs.fasting')}</Label>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="challenges">
|
<NativeTabs.Trigger name="challenges">
|
||||||
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
||||||
<Label>挑战</Label>
|
<Label>{t('statistics.tabs.challenges')}</Label>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="personal">
|
<NativeTabs.Trigger name="personal">
|
||||||
<Icon sf="person.fill" drawable="custom_settings_drawable" />
|
<Icon sf="person.fill" drawable="custom_settings_drawable" />
|
||||||
<Label>我的</Label>
|
<Label>{t('statistics.tabs.personal')}</Label>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
</NativeTabs>
|
</NativeTabs>
|
||||||
}
|
}
|
||||||
@@ -203,11 +205,11 @@ export default function TabLayout() {
|
|||||||
screenOptions={({ route }) => getScreenOptions(route.name)}
|
screenOptions={({ route }) => getScreenOptions(route.name)}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
<Tabs.Screen name="statistics" options={{ title: t('statistics.tabs.health') }} />
|
||||||
<Tabs.Screen name="medications" options={{ title: '用药' }} />
|
<Tabs.Screen name="medications" options={{ title: t('statistics.tabs.medications') }} />
|
||||||
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
|
<Tabs.Screen name="fasting" options={{ title: t('statistics.tabs.fasting') }} />
|
||||||
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
|
<Tabs.Screen name="challenges" options={{ title: t('statistics.tabs.challenges') }} />
|
||||||
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
<Tabs.Screen name="personal" options={{ title: t('statistics.tabs.personal') }} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DateSelector } from '@/components/DateSelector';
|
|
||||||
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||||
|
import { DateSelector } from '@/components/DateSelector';
|
||||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
@@ -17,6 +17,7 @@ import { Image } from 'expo-image';
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -32,6 +33,7 @@ type MedicationFilter = 'all' | 'taken' | 'missed';
|
|||||||
type ThemeColors = (typeof Colors)[keyof typeof Colors];
|
type ThemeColors = (typeof Colors)[keyof typeof Colors];
|
||||||
|
|
||||||
export default function MedicationsScreen() {
|
export default function MedicationsScreen() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
@@ -147,8 +149,8 @@ export default function MedicationsScreen() {
|
|||||||
|
|
||||||
const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME;
|
const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME;
|
||||||
const headerDateLabel = selectedDate.isSame(dayjs(), 'day')
|
const headerDateLabel = selectedDate.isSame(dayjs(), 'day')
|
||||||
? `今天,${selectedDate.format('M月D日')}`
|
? t('medications.dateFormats.today', { date: selectedDate.format('M月D日') })
|
||||||
: selectedDate.format('M月D日 dddd');
|
: t('medications.dateFormats.other', { date: selectedDate.format('M月D日 dddd') });
|
||||||
|
|
||||||
const emptyState = filteredMedications.length === 0;
|
const emptyState = filteredMedications.length === 0;
|
||||||
|
|
||||||
@@ -178,9 +180,9 @@ export default function MedicationsScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<View>
|
||||||
<ThemedText style={styles.greeting}>你好,{displayName}</ThemedText>
|
<ThemedText style={styles.greeting}>{t('medications.greeting', { name: displayName })}</ThemedText>
|
||||||
<ThemedText style={[styles.welcome, { color: colors.textMuted }]}>
|
<ThemedText style={[styles.welcome, { color: colors.textMuted }]}>
|
||||||
欢迎来到用药助手!
|
{t('medications.welcome')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
@@ -239,15 +241,10 @@ export default function MedicationsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.sectionSpacing}>
|
<View style={styles.sectionSpacing}>
|
||||||
<ThemedText style={styles.sectionHeader}>今日用药</ThemedText>
|
<ThemedText style={styles.sectionHeader}>{t('medications.todayMedications')}</ThemedText>
|
||||||
<View style={[styles.segmentedControl, { backgroundColor: colors.surface }]}>
|
<View style={[styles.segmentedControl, { backgroundColor: colors.surface }]}>
|
||||||
{(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => {
|
{(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => {
|
||||||
const isActive = activeFilter === filter;
|
const isActive = activeFilter === filter;
|
||||||
const labelMap: Record<MedicationFilter, string> = {
|
|
||||||
all: '全部',
|
|
||||||
taken: '已服用',
|
|
||||||
missed: '未服用',
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={filter}
|
key={filter}
|
||||||
@@ -263,7 +260,7 @@ export default function MedicationsScreen() {
|
|||||||
{ color: isActive ? colors.onPrimary : colors.textSecondary },
|
{ color: isActive ? colors.onPrimary : colors.textSecondary },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{labelMap[filter]}
|
{t(`medications.filters.${filter}`)}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -295,9 +292,9 @@ export default function MedicationsScreen() {
|
|||||||
style={styles.emptyIllustration}
|
style={styles.emptyIllustration}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
<ThemedText style={styles.emptyTitle}>今日暂无用药安排</ThemedText>
|
<ThemedText style={styles.emptyTitle}>{t('medications.emptyState.title')}</ThemedText>
|
||||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
||||||
还未添加任何用药计划,快来补充吧。
|
{t('medications.emptyState.subtitle')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import dayjs from 'dayjs';
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
Image,
|
Image,
|
||||||
@@ -55,6 +56,7 @@ const FloatingCard = ({ children, style }: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ExploreScreen() {
|
export default function ExploreScreen() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||||
|
|
||||||
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||||||
@@ -351,7 +353,7 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* 右边文字区域 */}
|
{/* 右边文字区域 */}
|
||||||
<View style={styles.headerTextContainer}>
|
<View style={styles.headerTextContainer}>
|
||||||
<Text style={styles.headerTitle}>Out Live</Text>
|
<Text style={styles.headerTitle}>{t('statistics.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 开发环境调试按钮 */}
|
{/* 开发环境调试按钮 */}
|
||||||
@@ -360,7 +362,7 @@ export default function ExploreScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.debugButton}
|
style={styles.debugButton}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
console.log('🔧 手动触发后台任务测试...');
|
console.log('🔧 Manual background task test...');
|
||||||
await BackgroundTaskManager.getInstance().triggerTaskForTesting();
|
await BackgroundTaskManager.getInstance().triggerTaskForTesting();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -370,7 +372,7 @@ export default function ExploreScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.debugButton, styles.hrvTestButton]}
|
style={[styles.debugButton, styles.hrvTestButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
console.log('🫀 测试HRV数据获取...');
|
console.log('🫀 Testing HRV data fetch...');
|
||||||
await testHRVDataFetch();
|
await testHRVDataFetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -407,7 +409,7 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* 身体指标section标题 */}
|
{/* 身体指标section标题 */}
|
||||||
<View style={styles.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
<Text style={styles.sectionTitle}>身体指标</Text>
|
<Text style={styles.sectionTitle}>{t('statistics.sections.bodyMetrics')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 真正瀑布流布局 */}
|
{/* 真正瀑布流布局 */}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet';
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import InfoCard from '@/components/ui/InfoCard';
|
import InfoCard from '@/components/ui/InfoCard';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_LABELS, FORM_OPTIONS } from '@/constants/Medication';
|
import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_OPTIONS } from '@/constants/Medication';
|
||||||
import { ROUTES } from '@/constants/Routes';
|
import { ROUTES } from '@/constants/Routes';
|
||||||
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { useVipService } from '@/hooks/useVipService';
|
import { useVipService } from '@/hooks/useVipService';
|
||||||
import { medicationNotificationService } from '@/services/medicationNotifications';
|
import { medicationNotificationService } from '@/services/medicationNotifications';
|
||||||
import {
|
import {
|
||||||
@@ -59,6 +60,7 @@ type RecordsSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function MedicationDetailScreen() {
|
export default function MedicationDetailScreen() {
|
||||||
|
const { t } = useI18n();
|
||||||
const params = useLocalSearchParams<{ medicationId?: string }>();
|
const params = useLocalSearchParams<{ medicationId?: string }>();
|
||||||
const medicationId = Array.isArray(params.medicationId)
|
const medicationId = Array.isArray(params.medicationId)
|
||||||
? params.medicationId[0]
|
? params.medicationId[0]
|
||||||
@@ -198,7 +200,7 @@ export default function MedicationDetailScreen() {
|
|||||||
console.error('加载药品详情失败', err);
|
console.error('加载药品详情失败', err);
|
||||||
console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err);
|
console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err);
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setError('暂时无法获取该药品的信息,请稍后重试。');
|
setError(t('medications.detail.error.title'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -295,7 +297,7 @@ export default function MedicationDetailScreen() {
|
|||||||
console.log('[MEDICATION_DETAIL] voice error', error);
|
console.log('[MEDICATION_DETAIL] voice error', error);
|
||||||
setDictationActive(false);
|
setDictationActive(false);
|
||||||
setDictationLoading(false);
|
setDictationLoading(false);
|
||||||
Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试');
|
Alert.alert(t('medications.add.note.voiceError'), t('medications.add.note.voiceErrorMessage'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -354,7 +356,7 @@ export default function MedicationDetailScreen() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[MEDICATION_DETAIL] unable to start dictation', error);
|
console.log('[MEDICATION_DETAIL] unable to start dictation', error);
|
||||||
setDictationLoading(false);
|
setDictationLoading(false);
|
||||||
Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试');
|
Alert.alert(t('medications.add.note.voiceStartError'), t('medications.add.note.voiceStartErrorMessage'));
|
||||||
}
|
}
|
||||||
}, [dictationActive, dictationLoading, isDictationSupported]);
|
}, [dictationActive, dictationLoading, isDictationSupported]);
|
||||||
|
|
||||||
@@ -400,7 +402,7 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('切换药品状态失败', err);
|
console.error('切换药品状态失败', err);
|
||||||
Alert.alert('操作失败', '切换提醒状态时出现问题,请稍后重试。');
|
Alert.alert(t('medications.detail.toggleError.title'), t('medications.detail.toggleError.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setUpdatePending(false);
|
setUpdatePending(false);
|
||||||
}
|
}
|
||||||
@@ -430,13 +432,13 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停用药物失败', error);
|
console.error('停用药物失败', error);
|
||||||
Alert.alert('操作失败', '停用药物时发生问题,请稍后重试。');
|
Alert.alert(t('medications.detail.deactivate.error.title'), t('medications.detail.deactivate.error.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setDeactivateLoading(false);
|
setDeactivateLoading(false);
|
||||||
}
|
}
|
||||||
}, [dispatch, medication, deactivateLoading]);
|
}, [dispatch, medication, deactivateLoading]);
|
||||||
|
|
||||||
const formLabel = medication ? FORM_LABELS[medication.form] : '';
|
const formLabel = medication ? t(`medications.manage.formLabels.${medication.form}`) : '';
|
||||||
const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--';
|
const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--';
|
||||||
const startDateLabel = medication
|
const startDateLabel = medication
|
||||||
? dayjs(medication.startDate).format('YYYY年M月D日')
|
? dayjs(medication.startDate).format('YYYY年M月D日')
|
||||||
@@ -454,24 +456,24 @@ export default function MedicationDetailScreen() {
|
|||||||
return `${startDate} - ${endDate}`;
|
return `${startDate} - ${endDate}`;
|
||||||
} else {
|
} else {
|
||||||
// 没有结束日期,显示长期
|
// 没有结束日期,显示长期
|
||||||
return `${startDate} - 长期`;
|
return `${startDate} - ${t('medications.detail.plan.longTerm')}`;
|
||||||
}
|
}
|
||||||
}, [medication]);
|
}, [medication, t]);
|
||||||
|
|
||||||
const reminderTimes = medication?.medicationTimes?.length
|
const reminderTimes = medication?.medicationTimes?.length
|
||||||
? medication.medicationTimes.join('、')
|
? medication.medicationTimes.join('、')
|
||||||
: '尚未设置';
|
: t('medications.manage.reminderNotSet');
|
||||||
const frequencyLabel = useMemo(() => {
|
const frequencyLabel = useMemo(() => {
|
||||||
if (!medication) return '--';
|
if (!medication) return '--';
|
||||||
switch (medication.repeatPattern) {
|
switch (medication.repeatPattern) {
|
||||||
case 'daily':
|
case 'daily':
|
||||||
return `每日 ${medication.timesPerDay} 次`;
|
return `${t('medications.manage.frequency.daily')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||||||
case 'weekly':
|
case 'weekly':
|
||||||
return `每周 ${medication.timesPerDay} 次`;
|
return `${t('medications.manage.frequency.weekly')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||||||
default:
|
default:
|
||||||
return `自定义 · ${medication.timesPerDay} 次/日`;
|
return `${t('medications.manage.frequency.custom')} · ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||||||
}
|
}
|
||||||
}, [medication]);
|
}, [medication, t]);
|
||||||
|
|
||||||
const handleOpenNoteModal = useCallback(() => {
|
const handleOpenNoteModal = useCallback(() => {
|
||||||
setNoteDraft(medication?.note ?? '');
|
setNoteDraft(medication?.note ?? '');
|
||||||
@@ -493,20 +495,20 @@ export default function MedicationDetailScreen() {
|
|||||||
closeNoteModal();
|
closeNoteModal();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存备注失败', err);
|
console.error('保存备注失败', err);
|
||||||
Alert.alert('保存失败', '提交备注时出现问题,请稍后重试。');
|
Alert.alert(t('medications.detail.note.saveError.title'), t('medications.detail.note.saveError.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setNoteSaving(false);
|
setNoteSaving(false);
|
||||||
}
|
}
|
||||||
}, [closeNoteModal, dispatch, medication, noteDraft]);
|
}, [closeNoteModal, dispatch, medication, noteDraft]);
|
||||||
|
|
||||||
const statusLabel = medication?.isActive ? '提醒已开启' : '提醒已关闭';
|
const statusLabel = medication?.isActive ? t('medications.detail.status.enabled') : t('medications.detail.status.disabled');
|
||||||
const noteText = medication?.note?.trim() ? medication.note : '暂无备注信息';
|
const noteText = medication?.note?.trim() ? medication.note : t('medications.detail.note.noNote');
|
||||||
const dayStreakText =
|
const dayStreakText =
|
||||||
typeof summary.startedDays === 'number'
|
typeof summary.startedDays === 'number'
|
||||||
? `已坚持 ${summary.startedDays} 天`
|
? t('medications.detail.overview.startedDays', { days: summary.startedDays })
|
||||||
: medication
|
: medication
|
||||||
? `开始于 ${dayjs(medication.startDate).format('YYYY年M月D日')}`
|
? t('medications.detail.overview.startDate', { date: dayjs(medication.startDate).format('YYYY年M月D日') })
|
||||||
: '暂无开始日期';
|
: t('medications.detail.overview.noStartDate');
|
||||||
|
|
||||||
const handleDeleteMedication = useCallback(async () => {
|
const handleDeleteMedication = useCallback(async () => {
|
||||||
if (!medication || deleteLoading) {
|
if (!medication || deleteLoading) {
|
||||||
@@ -534,7 +536,7 @@ export default function MedicationDetailScreen() {
|
|||||||
router.back();
|
router.back();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('删除药品失败', err);
|
console.error('删除药品失败', err);
|
||||||
Alert.alert('删除失败', '移除该药品时出现问题,请稍后再试。');
|
Alert.alert(t('medications.detail.delete.error.title'), t('medications.detail.delete.error.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
}
|
}
|
||||||
@@ -550,21 +552,26 @@ export default function MedicationDetailScreen() {
|
|||||||
if (!medication) return;
|
if (!medication) return;
|
||||||
|
|
||||||
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
|
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
|
||||||
let message = `开始服药日期:${startDate}`;
|
let message;
|
||||||
|
|
||||||
if (medication.endDate) {
|
if (medication.endDate) {
|
||||||
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
|
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
|
||||||
message += `\n结束服药日期:${endDate}`;
|
message = t('medications.detail.plan.periodMessage', {
|
||||||
|
startDate,
|
||||||
|
endDateInfo: t('medications.detail.plan.periodMessage', { endDate })
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
message += `\n服药计划:长期服药`;
|
message = t('medications.detail.plan.periodMessage', {
|
||||||
|
startDate,
|
||||||
|
endDateInfo: t('medications.detail.plan.longTermPlan')
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.alert('服药周期', message);
|
Alert.alert(t('medications.detail.sections.plan'), message);
|
||||||
}, [medication]);
|
}, [medication, t]);
|
||||||
|
|
||||||
const handleTimePress = useCallback(() => {
|
const handleTimePress = useCallback(() => {
|
||||||
Alert.alert('服药时间', `设置的时间:${reminderTimes}`);
|
Alert.alert(t('medications.detail.plan.time'), t('medications.detail.plan.timeMessage', { times: reminderTimes }));
|
||||||
}, [reminderTimes]);
|
}, [reminderTimes, t]);
|
||||||
|
|
||||||
const handleDosagePress = useCallback(() => {
|
const handleDosagePress = useCallback(() => {
|
||||||
if (!medication) return;
|
if (!medication) return;
|
||||||
@@ -621,7 +628,7 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('更新剂量失败', err);
|
console.error('更新剂量失败', err);
|
||||||
Alert.alert('更新失败', '更新剂量时出现问题,请稍后重试。');
|
Alert.alert(t('medications.detail.updateErrors.dosage'), t('medications.detail.updateErrors.dosageMessage'));
|
||||||
} finally {
|
} finally {
|
||||||
setUpdatePending(false);
|
setUpdatePending(false);
|
||||||
}
|
}
|
||||||
@@ -656,7 +663,7 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('更新剂型失败', err);
|
console.error('更新剂型失败', err);
|
||||||
Alert.alert('更新失败', '更新剂型时出现问题,请稍后重试。');
|
Alert.alert(t('medications.detail.updateErrors.form'), t('medications.detail.updateErrors.formMessage'));
|
||||||
} finally {
|
} finally {
|
||||||
setUpdatePending(false);
|
setUpdatePending(false);
|
||||||
}
|
}
|
||||||
@@ -723,27 +730,27 @@ export default function MedicationDetailScreen() {
|
|||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
console.error('[MEDICATION] AI 分析失败:', error);
|
console.error('[MEDICATION] AI 分析失败:', error);
|
||||||
|
|
||||||
let errorMessage = 'AI 分析失败,请稍后重试';
|
let errorMessage = t('medications.detail.aiAnalysis.error.message');
|
||||||
|
|
||||||
// 解析服务端返回的错误信息
|
// 解析服务端返回的错误信息
|
||||||
if (error?.message) {
|
if (error?.message) {
|
||||||
if (error.message.includes('[ERROR]')) {
|
if (error.message.includes('[ERROR]')) {
|
||||||
errorMessage = error.message.replace('[ERROR]', '').trim();
|
errorMessage = error.message.replace('[ERROR]', '').trim();
|
||||||
} else if (error.message.includes('无权访问')) {
|
} else if (error.message.includes('无权访问')) {
|
||||||
errorMessage = '无权访问此药物';
|
errorMessage = t('medications.detail.aiAnalysis.error.forbidden');
|
||||||
} else if (error.message.includes('不存在')) {
|
} else if (error.message.includes('不存在')) {
|
||||||
errorMessage = '药物不存在';
|
errorMessage = t('medications.detail.aiAnalysis.error.notFound');
|
||||||
}
|
}
|
||||||
} else if (error?.status === 401) {
|
} else if (error?.status === 401) {
|
||||||
errorMessage = '请先登录';
|
errorMessage = t('medications.detail.aiAnalysis.error.unauthorized');
|
||||||
} else if (error?.status === 403) {
|
} else if (error?.status === 403) {
|
||||||
errorMessage = '无权访问此药物';
|
errorMessage = t('medications.detail.aiAnalysis.error.forbidden');
|
||||||
} else if (error?.status === 404) {
|
} else if (error?.status === 404) {
|
||||||
errorMessage = '药物不存在';
|
errorMessage = t('medications.detail.aiAnalysis.error.notFound');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Alert 弹窗显示错误
|
// 使用 Alert 弹窗显示错误
|
||||||
Alert.alert('分析失败', errorMessage);
|
Alert.alert(t('medications.detail.aiAnalysis.error.title'), errorMessage);
|
||||||
|
|
||||||
// 清空内容和加载状态
|
// 清空内容和加载状态
|
||||||
setAiAnalysisContent('');
|
setAiAnalysisContent('');
|
||||||
@@ -756,7 +763,7 @@ export default function MedicationDetailScreen() {
|
|||||||
console.error('[MEDICATION] AI 分析异常:', error);
|
console.error('[MEDICATION] AI 分析异常:', error);
|
||||||
|
|
||||||
// 使用 Alert 弹窗显示错误
|
// 使用 Alert 弹窗显示错误
|
||||||
Alert.alert('分析失败', '发起分析请求失败,请检查网络连接');
|
Alert.alert(t('medications.detail.aiAnalysis.error.title'), t('medications.detail.aiAnalysis.error.networkError'));
|
||||||
|
|
||||||
// 清空内容和加载状态
|
// 清空内容和加载状态
|
||||||
setAiAnalysisContent('');
|
setAiAnalysisContent('');
|
||||||
@@ -789,10 +796,10 @@ export default function MedicationDetailScreen() {
|
|||||||
<View style={styles.decorativeCircle1} />
|
<View style={styles.decorativeCircle1} />
|
||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
<HeaderBar title="药品详情" variant="minimal" transparent />
|
<HeaderBar title={t('medications.detail.title')} variant="minimal" transparent />
|
||||||
<View style={[styles.centered, { paddingTop: insets.top + 72, paddingHorizontal: 24 }]}>
|
<View style={[styles.centered, { paddingTop: insets.top + 72, paddingHorizontal: 24 }]}>
|
||||||
<ThemedText style={styles.emptyTitle}>未找到药品信息</ThemedText>
|
<ThemedText style={styles.emptyTitle}>{t('medications.detail.notFound.title')}</ThemedText>
|
||||||
<ThemedText style={styles.emptySubtitle}>请从用药列表重新进入此页面。</ThemedText>
|
<ThemedText style={styles.emptySubtitle}>{t('medications.detail.notFound.subtitle')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -815,17 +822,17 @@ export default function MedicationDetailScreen() {
|
|||||||
<View style={styles.decorativeCircle1} />
|
<View style={styles.decorativeCircle1} />
|
||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
<HeaderBar title="药品详情" variant="minimal" transparent />
|
<HeaderBar title={t('medications.detail.title')} variant="minimal" transparent />
|
||||||
{isLoadingState ? (
|
{isLoadingState ? (
|
||||||
<View style={[styles.centered, { paddingTop: insets.top + 48 }]}>
|
<View style={[styles.centered, { paddingTop: insets.top + 48 }]}>
|
||||||
<ActivityIndicator color={colors.primary} />
|
<ActivityIndicator color={colors.primary} />
|
||||||
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>正在载入...</Text>
|
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>{t('medications.detail.loading')}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<View style={[styles.centered, { paddingHorizontal: 24, paddingTop: insets.top + 72 }]}>
|
<View style={[styles.centered, { paddingHorizontal: 24, paddingTop: insets.top + 72 }]}>
|
||||||
<ThemedText style={styles.emptyTitle}>{error}</ThemedText>
|
<ThemedText style={styles.emptyTitle}>{error}</ThemedText>
|
||||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
||||||
请检查网络后重试,或返回上一页。
|
{t('medications.detail.error.subtitle')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : medication ? (
|
) : medication ? (
|
||||||
@@ -878,10 +885,10 @@ export default function MedicationDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Section title="服药计划" color={colors.text}>
|
<Section title={t('medications.detail.sections.plan')} color={colors.text}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
label="服药周期"
|
label={t('medications.detail.plan.period')}
|
||||||
value={medicationPeriodLabel}
|
value={medicationPeriodLabel}
|
||||||
icon="calendar-outline"
|
icon="calendar-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
@@ -889,7 +896,7 @@ export default function MedicationDetailScreen() {
|
|||||||
onPress={handleStartDatePress}
|
onPress={handleStartDatePress}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
label="用药时间"
|
label={t('medications.detail.plan.time')}
|
||||||
value={reminderTimes}
|
value={reminderTimes}
|
||||||
icon="time-outline"
|
icon="time-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
@@ -904,7 +911,7 @@ export default function MedicationDetailScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.fullCardLeading}>
|
<View style={styles.fullCardLeading}>
|
||||||
<Ionicons name="repeat-outline" size={18} color={colors.primary} />
|
<Ionicons name="repeat-outline" size={18} color={colors.primary} />
|
||||||
<Text style={[styles.fullCardLabel, { color: colors.text }]}>频率</Text>
|
<Text style={[styles.fullCardLabel, { color: colors.text }]}>{t('medications.detail.plan.frequency')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.fullCardTrailing}>
|
<View style={styles.fullCardTrailing}>
|
||||||
<Text style={[styles.fullCardValue, { color: colors.text }]}>{frequencyLabel}</Text>
|
<Text style={[styles.fullCardValue, { color: colors.text }]}>{frequencyLabel}</Text>
|
||||||
@@ -913,10 +920,10 @@ export default function MedicationDetailScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="剂量与形式" color={colors.text}>
|
<Section title={t('medications.detail.sections.dosage')} color={colors.text}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
label="每次剂量"
|
label={t('medications.detail.dosage.label')}
|
||||||
value={dosageLabel}
|
value={dosageLabel}
|
||||||
icon="medkit-outline"
|
icon="medkit-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
@@ -924,7 +931,7 @@ export default function MedicationDetailScreen() {
|
|||||||
onPress={handleDosagePress}
|
onPress={handleDosagePress}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
label="剂型"
|
label={t('medications.detail.dosage.form')}
|
||||||
value={formLabel}
|
value={formLabel}
|
||||||
icon="cube-outline"
|
icon="cube-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
@@ -934,7 +941,7 @@ export default function MedicationDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="备注" color={colors.text}>
|
<Section title={t('medications.detail.sections.note')} color={colors.text}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.noteCard, { backgroundColor: colors.surface }]}
|
style={[styles.noteCard, { backgroundColor: colors.surface }]}
|
||||||
activeOpacity={0.92}
|
activeOpacity={0.92}
|
||||||
@@ -942,7 +949,7 @@ export default function MedicationDetailScreen() {
|
|||||||
>
|
>
|
||||||
<Ionicons name="document-text-outline" size={20} color={colors.primary} />
|
<Ionicons name="document-text-outline" size={20} color={colors.primary} />
|
||||||
<View style={styles.noteBody}>
|
<View style={styles.noteBody}>
|
||||||
<Text style={[styles.noteLabel, { color: colors.text }]}>药品备注</Text>
|
<Text style={[styles.noteLabel, { color: colors.text }]}>{t('medications.detail.note.label')}</Text>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.noteValue,
|
styles.noteValue,
|
||||||
@@ -956,17 +963,17 @@ export default function MedicationDetailScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="服药概览" color={colors.text}>
|
<Section title={t('medications.detail.sections.overview')} color={colors.text}>
|
||||||
<View style={[styles.summaryCard, { backgroundColor: colors.surface }]}>
|
<View style={[styles.summaryCard, { backgroundColor: colors.surface }]}>
|
||||||
<View style={styles.summaryIcon}>
|
<View style={styles.summaryIcon}>
|
||||||
<Ionicons name="tablet-portrait-outline" size={22} color={colors.primary} />
|
<Ionicons name="tablet-portrait-outline" size={22} color={colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.summaryBody}>
|
<View style={styles.summaryBody}>
|
||||||
<Text style={[styles.summaryHighlight, { color: colors.text }]}>
|
<Text style={[styles.summaryHighlight, { color: colors.text }]}>
|
||||||
{summaryLoading ? '统计中...' : `累计服药 ${summary.takenCount} 次`}
|
{summaryLoading ? t('medications.detail.overview.calculating') : t('medications.detail.overview.takenCount', { count: summary.takenCount })}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.summaryMeta, { color: colors.textSecondary }]}>
|
<Text style={[styles.summaryMeta, { color: colors.textSecondary }]}>
|
||||||
{summaryLoading ? '正在计算坚持天数' : dayStreakText}
|
{summaryLoading ? t('medications.detail.overview.calculatingDays') : dayStreakText}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -974,13 +981,13 @@ export default function MedicationDetailScreen() {
|
|||||||
|
|
||||||
{/* AI 分析结果展示 - 移动到底部 */}
|
{/* AI 分析结果展示 - 移动到底部 */}
|
||||||
{(aiAnalysisContent || aiAnalysisLoading) && (
|
{(aiAnalysisContent || aiAnalysisLoading) && (
|
||||||
<Section title="AI 用药分析" color={colors.text}>
|
<Section title={t('medications.detail.sections.aiAnalysis')} color={colors.text}>
|
||||||
<View style={[styles.aiAnalysisCard, { backgroundColor: colors.surface }]}>
|
<View style={[styles.aiAnalysisCard, { backgroundColor: colors.surface }]}>
|
||||||
{aiAnalysisLoading && !aiAnalysisContent && (
|
{aiAnalysisLoading && !aiAnalysisContent && (
|
||||||
<View style={styles.aiAnalysisLoading}>
|
<View style={styles.aiAnalysisLoading}>
|
||||||
<ActivityIndicator color={colors.primary} size="small" />
|
<ActivityIndicator color={colors.primary} size="small" />
|
||||||
<Text style={[styles.aiAnalysisLoadingText, { color: colors.textSecondary }]}>
|
<Text style={[styles.aiAnalysisLoadingText, { color: colors.textSecondary }]}>
|
||||||
正在分析用药信息...
|
{t('medications.detail.aiAnalysis.analyzing')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -1102,7 +1109,7 @@ export default function MedicationDetailScreen() {
|
|||||||
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
||||||
)}
|
)}
|
||||||
<Text style={styles.aiAnalysisButtonText}>
|
<Text style={styles.aiAnalysisButtonText}>
|
||||||
{aiAnalysisLoading ? '分析中...' : 'AI 分析'}
|
{aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')}
|
||||||
</Text>
|
</Text>
|
||||||
</GlassView>
|
</GlassView>
|
||||||
) : (
|
) : (
|
||||||
@@ -1113,7 +1120,7 @@ export default function MedicationDetailScreen() {
|
|||||||
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
||||||
)}
|
)}
|
||||||
<Text style={styles.aiAnalysisButtonText}>
|
<Text style={styles.aiAnalysisButtonText}>
|
||||||
{aiAnalysisLoading ? '分析中...' : 'AI 分析'}
|
{aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -1161,7 +1168,7 @@ export default function MedicationDetailScreen() {
|
|||||||
<View style={[styles.modalCard, { backgroundColor: colors.surface }]}>
|
<View style={[styles.modalCard, { backgroundColor: colors.surface }]}>
|
||||||
<View style={styles.modalHandle} />
|
<View style={styles.modalHandle} />
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={[styles.modalTitle, { color: colors.text }]}>编辑备注</Text>
|
<Text style={[styles.modalTitle, { color: colors.text }]}>{t('medications.detail.note.edit')}</Text>
|
||||||
<TouchableOpacity onPress={closeNoteModal} hitSlop={12}>
|
<TouchableOpacity onPress={closeNoteModal} hitSlop={12}>
|
||||||
<Ionicons name="close" size={20} color={colors.textSecondary} />
|
<Ionicons name="close" size={20} color={colors.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -1181,7 +1188,7 @@ export default function MedicationDetailScreen() {
|
|||||||
numberOfLines={6}
|
numberOfLines={6}
|
||||||
value={noteDraft}
|
value={noteDraft}
|
||||||
onChangeText={setNoteDraft}
|
onChangeText={setNoteDraft}
|
||||||
placeholder="记录注意事项、医生叮嘱或自定义提醒"
|
placeholder={t('medications.detail.note.placeholder')}
|
||||||
placeholderTextColor={colors.textMuted}
|
placeholderTextColor={colors.textMuted}
|
||||||
style={[styles.noteEditorInput, { color: colors.text }]}
|
style={[styles.noteEditorInput, { color: colors.text }]}
|
||||||
textAlignVertical="center"
|
textAlignVertical="center"
|
||||||
@@ -1213,7 +1220,7 @@ export default function MedicationDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
{!isDictationSupported && (
|
{!isDictationSupported && (
|
||||||
<Text style={[styles.voiceHint, { color: colors.textMuted }]}>
|
<Text style={[styles.voiceHint, { color: colors.textMuted }]}>
|
||||||
当前设备暂不支持语音转文字,可直接输入备注
|
{t('medications.detail.note.voiceNotSupported')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<View style={styles.modalActionContainer}>
|
<View style={styles.modalActionContainer}>
|
||||||
@@ -1232,7 +1239,7 @@ export default function MedicationDetailScreen() {
|
|||||||
{noteSaving ? (
|
{noteSaving ? (
|
||||||
<ActivityIndicator color={colors.onPrimary} size="small" />
|
<ActivityIndicator color={colors.onPrimary} size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}>保存</Text>
|
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}>{t('medications.detail.note.save')}</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -1253,12 +1260,12 @@ export default function MedicationDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
选择剂量
|
{t('medications.detail.dosage.selectDosage')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View style={styles.pickerRow}>
|
<View style={styles.pickerRow}>
|
||||||
<View style={styles.pickerColumn}>
|
<View style={styles.pickerColumn}>
|
||||||
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
剂量值
|
{t('medications.detail.dosage.dosageValue')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Picker
|
<Picker
|
||||||
selectedValue={dosageValuePicker}
|
selectedValue={dosageValuePicker}
|
||||||
@@ -1277,7 +1284,7 @@ export default function MedicationDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.pickerColumn}>
|
<View style={styles.pickerColumn}>
|
||||||
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
单位
|
{t('medications.detail.dosage.unit')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Picker
|
<Picker
|
||||||
selectedValue={dosageUnitPicker}
|
selectedValue={dosageUnitPicker}
|
||||||
@@ -1301,7 +1308,7 @@ export default function MedicationDetailScreen() {
|
|||||||
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
>
|
>
|
||||||
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
||||||
取消
|
{t('medications.detail.pickers.cancel')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -1309,7 +1316,7 @@ export default function MedicationDetailScreen() {
|
|||||||
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
>
|
>
|
||||||
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||||||
确定
|
{t('medications.detail.pickers.confirm')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@@ -1328,7 +1335,7 @@ export default function MedicationDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
选择剂型
|
{t('medications.detail.dosage.selectForm')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Picker
|
<Picker
|
||||||
selectedValue={formPicker}
|
selectedValue={formPicker}
|
||||||
@@ -1350,7 +1357,7 @@ export default function MedicationDetailScreen() {
|
|||||||
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
>
|
>
|
||||||
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
||||||
取消
|
{t('medications.detail.pickers.cancel')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -1358,7 +1365,7 @@ export default function MedicationDetailScreen() {
|
|||||||
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
>
|
>
|
||||||
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||||||
确定
|
{t('medications.detail.pickers.confirm')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@@ -1370,10 +1377,10 @@ export default function MedicationDetailScreen() {
|
|||||||
visible={deleteSheetVisible}
|
visible={deleteSheetVisible}
|
||||||
onClose={() => setDeleteSheetVisible(false)}
|
onClose={() => setDeleteSheetVisible(false)}
|
||||||
onConfirm={handleDeleteMedication}
|
onConfirm={handleDeleteMedication}
|
||||||
title={`删除 ${medication.name}?`}
|
title={t('medications.detail.delete.title', { name: medication.name })}
|
||||||
description="删除后将清除与该药品相关的提醒与历史记录,且无法恢复。"
|
description={t('medications.detail.delete.description')}
|
||||||
confirmText="删除"
|
confirmText={t('medications.detail.delete.confirm')}
|
||||||
cancelText="取消"
|
cancelText={t('medications.detail.delete.cancel')}
|
||||||
destructive
|
destructive
|
||||||
loading={deleteLoading}
|
loading={deleteLoading}
|
||||||
/>
|
/>
|
||||||
@@ -1384,10 +1391,10 @@ export default function MedicationDetailScreen() {
|
|||||||
visible={deactivateSheetVisible}
|
visible={deactivateSheetVisible}
|
||||||
onClose={() => setDeactivateSheetVisible(false)}
|
onClose={() => setDeactivateSheetVisible(false)}
|
||||||
onConfirm={handleDeactivateMedication}
|
onConfirm={handleDeactivateMedication}
|
||||||
title={`停用 ${medication.name}?`}
|
title={t('medications.detail.deactivate.title', { name: medication.name })}
|
||||||
description="停用后,当天已生成的用药计划会一并删除,且无法恢复。"
|
description={t('medications.detail.deactivate.description')}
|
||||||
confirmText="确认停用"
|
confirmText={t('medications.detail.deactivate.confirm')}
|
||||||
cancelText="取消"
|
cancelText={t('medications.detail.deactivate.cancel')}
|
||||||
destructive
|
destructive
|
||||||
loading={deactivateLoading}
|
loading={deactivateLoading}
|
||||||
/>
|
/>
|
||||||
@@ -1415,7 +1422,7 @@ export default function MedicationDetailScreen() {
|
|||||||
style={styles.imageViewerFooterButton}
|
style={styles.imageViewerFooterButton}
|
||||||
onPress={() => setShowImagePreview(false)}
|
onPress={() => setShowImagePreview(false)}
|
||||||
>
|
>
|
||||||
<Text style={styles.imageViewerFooterButtonText}>关闭</Text>
|
<Text style={styles.imageViewerFooterButtonText}>{t('medications.detail.imageViewer.close')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import {
|
import {
|
||||||
fetchMedications,
|
fetchMedications,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
selectMedicationsLoading,
|
selectMedicationsLoading,
|
||||||
updateMedicationAction,
|
updateMedicationAction,
|
||||||
} from '@/store/medicationsSlice';
|
} from '@/store/medicationsSlice';
|
||||||
|
import { selectUserProfile } from '@/store/userSlice';
|
||||||
import type { Medication, MedicationForm } from '@/types/medication';
|
import type { Medication, MedicationForm } from '@/types/medication';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -33,25 +35,12 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
|
|
||||||
type FilterType = 'all' | 'active' | 'inactive';
|
type FilterType = 'all' | 'active' | 'inactive';
|
||||||
|
|
||||||
const FORM_LABELS: Record<MedicationForm, string> = {
|
// 这些常量将在组件内部定义,以便使用翻译函数
|
||||||
capsule: '胶囊',
|
|
||||||
pill: '药片',
|
|
||||||
injection: '注射',
|
|
||||||
spray: '喷雾',
|
|
||||||
drop: '滴剂',
|
|
||||||
syrup: '糖浆',
|
|
||||||
other: '其他',
|
|
||||||
};
|
|
||||||
|
|
||||||
const FILTER_CONFIG: { key: FilterType; label: string }[] = [
|
|
||||||
{ key: 'all', label: '全部' },
|
|
||||||
{ key: 'active', label: '进行中' },
|
|
||||||
{ key: 'inactive', label: '已停用' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
||||||
|
|
||||||
export default function ManageMedicationsScreen() {
|
export default function ManageMedicationsScreen() {
|
||||||
|
const { t } = useI18n();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colors = Colors[theme];
|
const colors = Colors[theme];
|
||||||
@@ -59,6 +48,7 @@ export default function ManageMedicationsScreen() {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const medications = useAppSelector(selectMedications);
|
const medications = useAppSelector(selectMedications);
|
||||||
const loading = useAppSelector(selectMedicationsLoading);
|
const loading = useAppSelector(selectMedicationsLoading);
|
||||||
|
const userProfile = useAppSelector(selectUserProfile);
|
||||||
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
||||||
const [pendingMedicationId, setPendingMedicationId] = useState<string | null>(null);
|
const [pendingMedicationId, setPendingMedicationId] = useState<string | null>(null);
|
||||||
const [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false);
|
const [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false);
|
||||||
@@ -121,7 +111,7 @@ export default function ManageMedicationsScreen() {
|
|||||||
).unwrap();
|
).unwrap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新药物状态失败', error);
|
console.error('更新药物状态失败', error);
|
||||||
Alert.alert('操作失败', '切换药物状态时发生问题,请稍后重试。');
|
Alert.alert(t('medications.manage.toggleError.title'), t('medications.manage.toggleError.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setPendingMedicationId(null);
|
setPendingMedicationId(null);
|
||||||
}
|
}
|
||||||
@@ -144,7 +134,7 @@ export default function ManageMedicationsScreen() {
|
|||||||
).unwrap();
|
).unwrap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停用药物失败', error);
|
console.error('停用药物失败', error);
|
||||||
Alert.alert('操作失败', '停用药物时发生问题,请稍后重试。');
|
Alert.alert(t('medications.manage.deactivate.error.title'), t('medications.manage.deactivate.error.message'));
|
||||||
} finally {
|
} finally {
|
||||||
setDeactivateLoading(false);
|
setDeactivateLoading(false);
|
||||||
setMedicationToDeactivate(null);
|
setMedicationToDeactivate(null);
|
||||||
@@ -153,14 +143,25 @@ export default function ManageMedicationsScreen() {
|
|||||||
|
|
||||||
// 创建独立的药品卡片组件,使用 React.memo 优化渲染
|
// 创建独立的药品卡片组件,使用 React.memo 优化渲染
|
||||||
const MedicationCard = React.memo(({ medication, onPress }: { medication: Medication; onPress: () => void }) => {
|
const MedicationCard = React.memo(({ medication, onPress }: { medication: Medication; onPress: () => void }) => {
|
||||||
|
// 使用翻译函数获取剂型标签
|
||||||
|
const FORM_LABELS: Record<MedicationForm, string> = {
|
||||||
|
capsule: t('medications.manage.formLabels.capsule'),
|
||||||
|
pill: t('medications.manage.formLabels.pill'),
|
||||||
|
injection: t('medications.manage.formLabels.injection'),
|
||||||
|
spray: t('medications.manage.formLabels.spray'),
|
||||||
|
drop: t('medications.manage.formLabels.drop'),
|
||||||
|
syrup: t('medications.manage.formLabels.syrup'),
|
||||||
|
other: t('medications.manage.formLabels.other'),
|
||||||
|
};
|
||||||
|
|
||||||
const dosageLabel = `${medication.dosageValue} ${medication.dosageUnit || ''} ${FORM_LABELS[medication.form] ?? ''}`.trim();
|
const dosageLabel = `${medication.dosageValue} ${medication.dosageUnit || ''} ${FORM_LABELS[medication.form] ?? ''}`.trim();
|
||||||
const frequencyLabel = `${medication.repeatPattern === 'daily' ? '每日' : medication.repeatPattern === 'weekly' ? '每周' : '自定义'} | ${dosageLabel}`;
|
const frequencyLabel = `${medication.repeatPattern === 'daily' ? t('medications.manage.frequency.daily') : medication.repeatPattern === 'weekly' ? t('medications.manage.frequency.weekly') : t('medications.manage.frequency.custom')} | ${dosageLabel}`;
|
||||||
const startDateLabel = dayjs(medication.startDate).isValid()
|
const startDateLabel = dayjs(medication.startDate).isValid()
|
||||||
? dayjs(medication.startDate).format('M月D日')
|
? dayjs(medication.startDate).format('M月D日')
|
||||||
: '未知日期';
|
: t('medications.manage.unknownDate');
|
||||||
const reminderLabel = medication.medicationTimes?.length
|
const reminderLabel = medication.medicationTimes?.length
|
||||||
? medication.medicationTimes.join('、')
|
? medication.medicationTimes.join('、')
|
||||||
: `${medication.timesPerDay} 次/日`;
|
: `${medication.timesPerDay} ${t('medications.manage.cardMeta.reminderNotSet')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -180,7 +181,7 @@ export default function ManageMedicationsScreen() {
|
|||||||
{frequencyLabel}
|
{frequencyLabel}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={[styles.cardMeta, { color: colors.textMuted }]}>
|
<ThemedText style={[styles.cardMeta, { color: colors.textMuted }]}>
|
||||||
{`开始于 ${startDateLabel} | 提醒:${reminderLabel}`}
|
{t('medications.manage.cardMeta', { date: startDateLabel, reminder: reminderLabel })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -250,26 +251,27 @@ export default function ManageMedicationsScreen() {
|
|||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="药品管理"
|
title={t('medications.manage.title')}
|
||||||
onBack={() => router.back()}
|
onBack={() => router.back()}
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
transparent
|
transparent
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={{ paddingTop: safeAreaTop }} />
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.content,
|
styles.content,
|
||||||
{ paddingBottom: insets.bottom + 32 },
|
{
|
||||||
|
paddingTop: safeAreaTop , // HeaderBar高度 + 额外间距
|
||||||
|
paddingBottom: insets.bottom + 32
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.pageHeader}>
|
<View style={styles.pageHeader}>
|
||||||
<View>
|
<View>
|
||||||
<ThemedText style={styles.title}>我的用药</ThemedText>
|
<ThemedText style={styles.title}>{t('medications.greeting', { name: userProfile.name || '朋友' })}</ThemedText>
|
||||||
<ThemedText style={[styles.subtitle, { color: colors.textMuted }]}>
|
<ThemedText style={[styles.subtitle, { color: colors.textMuted }]}>
|
||||||
管理所有药品的状态与提醒
|
{t('medications.manage.subtitle')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -294,7 +296,11 @@ export default function ManageMedicationsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.segmented, { backgroundColor: colors.surface }]}>
|
<View style={[styles.segmented, { backgroundColor: colors.surface }]}>
|
||||||
{FILTER_CONFIG.map((filter) => {
|
{[
|
||||||
|
{ key: 'all' as FilterType, label: t('medications.manage.filters.all') },
|
||||||
|
{ key: 'active' as FilterType, label: t('medications.manage.filters.active') },
|
||||||
|
{ key: 'inactive' as FilterType, label: t('medications.manage.filters.inactive') },
|
||||||
|
].map((filter) => {
|
||||||
const isActive = filter.key === activeFilter;
|
const isActive = filter.key === activeFilter;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -341,14 +347,14 @@ export default function ManageMedicationsScreen() {
|
|||||||
{listLoading ? (
|
{listLoading ? (
|
||||||
<View style={[styles.loading, { backgroundColor: colors.surface }]}>
|
<View style={[styles.loading, { backgroundColor: colors.surface }]}>
|
||||||
<ActivityIndicator color={colors.primary} />
|
<ActivityIndicator color={colors.primary} />
|
||||||
<ThemedText style={styles.loadingText}>正在载入药品信息...</ThemedText>
|
<ThemedText style={styles.loadingText}>{t('medications.manage.loading')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : filteredMedications.length === 0 ? (
|
) : filteredMedications.length === 0 ? (
|
||||||
<View style={[styles.empty, { backgroundColor: colors.surface }]}>
|
<View style={[styles.empty, { backgroundColor: colors.surface }]}>
|
||||||
<Image source={DEFAULT_IMAGE} style={styles.emptyImage} contentFit="contain" />
|
<Image source={DEFAULT_IMAGE} style={styles.emptyImage} contentFit="contain" />
|
||||||
<ThemedText style={styles.emptyTitle}>暂无药品</ThemedText>
|
<ThemedText style={styles.emptyTitle}>{t('medications.manage.empty.title')}</ThemedText>
|
||||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textSecondary }]}>
|
<ThemedText style={[styles.emptySubtitle, { color: colors.textSecondary }]}>
|
||||||
还没有相关药品记录,点击右上角添加
|
{t('medications.manage.empty.subtitle')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@@ -365,10 +371,10 @@ export default function ManageMedicationsScreen() {
|
|||||||
setMedicationToDeactivate(null);
|
setMedicationToDeactivate(null);
|
||||||
}}
|
}}
|
||||||
onConfirm={handleDeactivateMedication}
|
onConfirm={handleDeactivateMedication}
|
||||||
title={`停用 ${medicationToDeactivate.name}?`}
|
title={t('medications.manage.deactivate.title', { name: medicationToDeactivate.name })}
|
||||||
description="停用后,当天已生成的用药计划会一并删除,且无法恢复。"
|
description={t('medications.manage.deactivate.description')}
|
||||||
confirmText="确认停用"
|
confirmText={t('medications.manage.deactivate.confirm')}
|
||||||
cancelText="取消"
|
cancelText={t('medications.manage.deactivate.cancel')}
|
||||||
destructive
|
destructive
|
||||||
loading={deactivateLoading}
|
loading={deactivateLoading}
|
||||||
/>
|
/>
|
||||||
@@ -419,7 +425,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 26,
|
fontSize: 24,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import {
|
import {
|
||||||
getMedicationReminderEnabled,
|
getMedicationReminderEnabled,
|
||||||
@@ -18,6 +19,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
export default function NotificationSettingsScreen() {
|
export default function NotificationSettingsScreen() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
|
const { t } = useI18n();
|
||||||
const { requestPermission, sendNotification } = useNotifications();
|
const { requestPermission, sendNotification } = useNotifications();
|
||||||
const isLgAvailable = isLiquidGlassAvailable();
|
const isLgAvailable = isLiquidGlassAvailable();
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ export default function NotificationSettingsScreen() {
|
|||||||
setNotificationEnabledState(notification);
|
setNotificationEnabledState(notification);
|
||||||
setMedicationReminderEnabledState(medicationReminder);
|
setMedicationReminderEnabledState(medicationReminder);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载通知设置失败:', error);
|
console.error('Failed to load notification settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -62,25 +64,25 @@ export default function NotificationSettingsScreen() {
|
|||||||
|
|
||||||
// 发送测试通知
|
// 发送测试通知
|
||||||
await sendNotification({
|
await sendNotification({
|
||||||
title: '通知已开启',
|
title: t('notificationSettings.alerts.notificationsEnabled.title'),
|
||||||
body: '您将收到应用通知和提醒',
|
body: t('notificationSettings.alerts.notificationsEnabled.body'),
|
||||||
sound: true,
|
sound: true,
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 系统权限被拒绝,不更新用户偏好设置
|
// 系统权限被拒绝,不更新用户偏好设置
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'权限被拒绝',
|
t('notificationSettings.alerts.permissionDenied.title'),
|
||||||
'请在系统设置中开启通知权限,然后再尝试开启推送功能',
|
t('notificationSettings.alerts.permissionDenied.message'),
|
||||||
[
|
[
|
||||||
{ text: '取消', style: 'cancel' },
|
{ text: t('notificationSettings.alerts.permissionDenied.cancel'), style: 'cancel' },
|
||||||
{ text: '去设置', onPress: () => Linking.openSettings() }
|
{ text: t('notificationSettings.alerts.permissionDenied.goToSettings'), onPress: () => Linking.openSettings() }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('开启推送通知失败:', error);
|
console.error('Failed to enable push notifications:', error);
|
||||||
Alert.alert('错误', '请求通知权限失败');
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.message'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -91,8 +93,8 @@ export default function NotificationSettingsScreen() {
|
|||||||
await setMedicationReminderEnabled(false);
|
await setMedicationReminderEnabled(false);
|
||||||
setMedicationReminderEnabledState(false);
|
setMedicationReminderEnabledState(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('关闭推送通知失败:', error);
|
console.error('Failed to disable push notifications:', error);
|
||||||
Alert.alert('错误', '保存设置失败');
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -106,15 +108,15 @@ export default function NotificationSettingsScreen() {
|
|||||||
if (value) {
|
if (value) {
|
||||||
// 发送测试通知
|
// 发送测试通知
|
||||||
await sendNotification({
|
await sendNotification({
|
||||||
title: '药品提醒已开启',
|
title: t('notificationSettings.alerts.medicationReminderEnabled.title'),
|
||||||
body: '您将在用药时间收到提醒通知',
|
body: t('notificationSettings.alerts.medicationReminderEnabled.body'),
|
||||||
sound: true,
|
sound: true,
|
||||||
priority: 'high',
|
priority: 'high',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('设置药品提醒失败:', error);
|
console.error('Failed to set medication reminder:', error);
|
||||||
Alert.alert('错误', '保存设置失败');
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.medicationReminderFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,7 +185,7 @@ export default function NotificationSettingsScreen() {
|
|||||||
end={{ x: 0, y: 1 }}
|
end={{ x: 0, y: 1 }}
|
||||||
/>
|
/>
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<Text style={styles.loadingText}>加载中...</Text>
|
<Text style={styles.loadingText}>{t('notificationSettings.loading')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -217,16 +219,16 @@ export default function NotificationSettingsScreen() {
|
|||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<ThemedText style={styles.title}>通知设置</ThemedText>
|
<ThemedText style={styles.title}>{t('notificationSettings.title')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 通知设置部分 */}
|
{/* 通知设置部分 */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>通知设置</Text>
|
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.notifications')}</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<SwitchItem
|
<SwitchItem
|
||||||
title="消息推送"
|
title={t('notificationSettings.items.pushNotifications.title')}
|
||||||
description="开启后将接收应用通知"
|
description={t('notificationSettings.items.pushNotifications.description')}
|
||||||
value={notificationEnabled}
|
value={notificationEnabled}
|
||||||
onValueChange={handleNotificationToggle}
|
onValueChange={handleNotificationToggle}
|
||||||
/>
|
/>
|
||||||
@@ -235,11 +237,11 @@ export default function NotificationSettingsScreen() {
|
|||||||
|
|
||||||
{/* 药品提醒部分 */}
|
{/* 药品提醒部分 */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>药品提醒</Text>
|
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.medicationReminder')}</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<SwitchItem
|
<SwitchItem
|
||||||
title="药品通知提醒"
|
title={t('notificationSettings.items.medicationReminder.title')}
|
||||||
description="在用药时间接收提醒通知"
|
description={t('notificationSettings.items.medicationReminder.description')}
|
||||||
value={medicationReminderEnabled}
|
value={medicationReminderEnabled}
|
||||||
onValueChange={handleMedicationReminderToggle}
|
onValueChange={handleMedicationReminderToggle}
|
||||||
disabled={!notificationEnabled}
|
disabled={!notificationEnabled}
|
||||||
@@ -249,13 +251,10 @@ export default function NotificationSettingsScreen() {
|
|||||||
|
|
||||||
{/* 说明部分 */}
|
{/* 说明部分 */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>说明</Text>
|
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.description')}</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.description}>
|
<Text style={styles.description}>
|
||||||
• 消息推送是所有通知的总开关{'\n'}
|
{t('notificationSettings.description.text')}
|
||||||
• 药品通知提醒需要在消息推送开启后才能使用{'\n'}
|
|
||||||
• 您可以在系统设置中管理通知权限{'\n'}
|
|
||||||
• 关闭消息推送将停止所有应用通知
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
|
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
|
||||||
import { fetchMaximumHeartRate } from '@/utils/health';
|
import { fetchMaximumHeartRate } from '@/utils/health';
|
||||||
@@ -46,6 +47,7 @@ interface UserProfile {
|
|||||||
const STORAGE_KEY = '@user_profile';
|
const STORAGE_KEY = '@user_profile';
|
||||||
|
|
||||||
export default function EditProfileScreen() {
|
export default function EditProfileScreen() {
|
||||||
|
const { t } = useI18n();
|
||||||
const safeAreaTop = useSafeAreaTop()
|
const safeAreaTop = useSafeAreaTop()
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const colors = Colors[colorScheme ?? 'light'];
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
@@ -189,7 +191,7 @@ export default function EditProfileScreen() {
|
|||||||
const handleSaveWithProfile = async (profileData: UserProfile) => {
|
const handleSaveWithProfile = async (profileData: UserProfile) => {
|
||||||
try {
|
try {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
Alert.alert('未登录', '请先登录后再尝试保存');
|
Alert.alert(t('editProfile.alerts.notLoggedIn.title'), t('editProfile.alerts.notLoggedIn.message'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const next: UserProfile = { ...profileData };
|
const next: UserProfile = { ...profileData };
|
||||||
@@ -215,7 +217,7 @@ export default function EditProfileScreen() {
|
|||||||
console.warn('更新用户信息失败', e?.message || e);
|
console.warn('更新用户信息失败', e?.message || e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('保存失败', '请稍后重试');
|
Alert.alert(t('editProfile.alerts.saveFailed.title'), t('editProfile.alerts.saveFailed.message'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,7 +251,7 @@ export default function EditProfileScreen() {
|
|||||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||||
if (!libGranted) {
|
if (!libGranted) {
|
||||||
Alert.alert('权限不足', '需要相册权限以选择头像');
|
Alert.alert(t('editProfile.alerts.avatarPermissions.title'), t('editProfile.alerts.avatarPermissions.message'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
@@ -275,21 +277,21 @@ export default function EditProfileScreen() {
|
|||||||
setProfile((p) => ({ ...p, avatarUri: url }));
|
setProfile((p) => ({ ...p, avatarUri: url }));
|
||||||
// 保存更新后的 profile
|
// 保存更新后的 profile
|
||||||
await handleSaveWithProfile({ ...profile, avatarUri: url });
|
await handleSaveWithProfile({ ...profile, avatarUri: url });
|
||||||
Alert.alert('成功', '头像更新成功');
|
Alert.alert(t('editProfile.alerts.avatarSuccess.title'), t('editProfile.alerts.avatarSuccess.message'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('上传头像失败', e);
|
console.warn('上传头像失败', e);
|
||||||
Alert.alert('上传失败', '头像上传失败,请重试');
|
Alert.alert(t('editProfile.alerts.avatarUploadFailed.title'), t('editProfile.alerts.avatarUploadFailed.message'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('发生错误', '选择头像失败,请重试');
|
Alert.alert(t('editProfile.alerts.avatarError.title'), t('editProfile.alerts.avatarError.message'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
<View style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="编辑资料"
|
title={t('editProfile.title')}
|
||||||
onBack={() => router.back()}
|
onBack={() => router.back()}
|
||||||
withSafeTop={false}
|
withSafeTop={false}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
@@ -321,8 +323,8 @@ export default function EditProfileScreen() {
|
|||||||
{/* 姓名 */}
|
{/* 姓名 */}
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-name.png'
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-name.png'
|
||||||
title="昵称"
|
title={t('editProfile.fields.name')}
|
||||||
value={profile.name || '今晚要吃肉'}
|
value={profile.name || t('editProfile.defaultValues.name')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setTempValue(profile.name || '');
|
setTempValue(profile.name || '');
|
||||||
setEditingField('name');
|
setEditingField('name');
|
||||||
@@ -334,8 +336,8 @@ export default function EditProfileScreen() {
|
|||||||
icon="body"
|
icon="body"
|
||||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png"
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png"
|
||||||
iconColor="#FF6B9D"
|
iconColor="#FF6B9D"
|
||||||
title="性别"
|
title={t('editProfile.fields.gender')}
|
||||||
value={profile.gender === 'male' ? '男' : profile.gender === 'female' ? '女' : '未设置'}
|
value={profile.gender === 'male' ? t('editProfile.gender.male') : profile.gender === 'female' ? t('editProfile.gender.female') : t('editProfile.gender.notSet')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setEditingField('gender');
|
setEditingField('gender');
|
||||||
}}
|
}}
|
||||||
@@ -344,10 +346,10 @@ export default function EditProfileScreen() {
|
|||||||
{/* 身高 */}
|
{/* 身高 */}
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
|
||||||
title="身高"
|
title={t('editProfile.fields.height')}
|
||||||
value={profile.height ? `${Math.round(profile.height)}厘米` : '170厘米'}
|
value={profile.height ? `${Math.round(profile.height)}${t('editProfile.height.unit')}` : t('editProfile.height.placeholder')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setTempValue(profile.height ? String(Math.round(profile.height)) : '170');
|
setTempValue(profile.height ? String(Math.round(profile.height)) : String(t('editProfile.defaultValues.height')));
|
||||||
setEditingField('height');
|
setEditingField('height');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -355,10 +357,10 @@ export default function EditProfileScreen() {
|
|||||||
{/* 体重 */}
|
{/* 体重 */}
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
|
||||||
title="体重"
|
title={t('editProfile.fields.weight')}
|
||||||
value={profile.weight ? `${round(profile.weight, 1)}公斤` : '55公斤'}
|
value={profile.weight ? `${round(profile.weight, 1)}${t('editProfile.weight.unit')}` : t('editProfile.weight.placeholder')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55');
|
setTempValue(profile.weight ? String(round(profile.weight, 1)) : String(t('editProfile.defaultValues.weight')));
|
||||||
setEditingField('weight');
|
setEditingField('weight');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -366,14 +368,14 @@ export default function EditProfileScreen() {
|
|||||||
{/* 活动水平 */}
|
{/* 活动水平 */}
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
|
||||||
title="活动水平"
|
title={t('editProfile.fields.activityLevel')}
|
||||||
value={(() => {
|
value={(() => {
|
||||||
switch (profile.activityLevel) {
|
switch (profile.activityLevel) {
|
||||||
case 1: return '久坐';
|
case 1: return t('editProfile.activityLevels.1');
|
||||||
case 2: return '轻度活跃';
|
case 2: return t('editProfile.activityLevels.2');
|
||||||
case 3: return '中度活跃';
|
case 3: return t('editProfile.activityLevels.3');
|
||||||
case 4: return '非常活跃';
|
case 4: return t('editProfile.activityLevels.4');
|
||||||
default: return '久坐';
|
default: return t('editProfile.activityLevels.1');
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -384,15 +386,20 @@ export default function EditProfileScreen() {
|
|||||||
{/* 出生日期 */}
|
{/* 出生日期 */}
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
|
||||||
title="出生日期"
|
title={t('editProfile.fields.birthDate')}
|
||||||
value={profile.birthDate ? (() => {
|
value={profile.birthDate ? (() => {
|
||||||
try {
|
try {
|
||||||
const d = new Date(profile.birthDate);
|
const d = new Date(profile.birthDate);
|
||||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
|
if (t('editProfile.birthDate.format').includes('{{year}}年')) {
|
||||||
} catch {
|
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() });
|
||||||
return '1995年1月1日';
|
} else {
|
||||||
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: monthNames[d.getMonth()], day: d.getDate() });
|
||||||
}
|
}
|
||||||
})() : '1995年1月1日'}
|
} catch {
|
||||||
|
return t('editProfile.birthDate.placeholder');
|
||||||
|
}
|
||||||
|
})() : t('editProfile.birthDate.placeholder')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
openDatePicker();
|
openDatePicker();
|
||||||
}}
|
}}
|
||||||
@@ -402,11 +409,11 @@ export default function EditProfileScreen() {
|
|||||||
<ProfileCard
|
<ProfileCard
|
||||||
icon="heart"
|
icon="heart"
|
||||||
iconColor="#FF6B9D"
|
iconColor="#FF6B9D"
|
||||||
title="最大心率"
|
title={t('editProfile.fields.maxHeartRate')}
|
||||||
value={profile.maxHeartRate ? `${Math.round(profile.maxHeartRate)}次/分钟` : '未获取'}
|
value={profile.maxHeartRate ? `${Math.round(profile.maxHeartRate)}${t('editProfile.maxHeartRate.unit')}` : t('editProfile.maxHeartRate.notAvailable')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// 最大心率不可编辑,只显示
|
// 最大心率不可编辑,只显示
|
||||||
Alert.alert('提示', '最大心率数据从健康应用自动获取');
|
Alert.alert(t('editProfile.maxHeartRate.alert.title'), t('editProfile.maxHeartRate.alert.message'));
|
||||||
}}
|
}}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
hideArrow={true}
|
hideArrow={true}
|
||||||
@@ -432,6 +439,7 @@ export default function EditProfileScreen() {
|
|||||||
} else if (field === 'gender') {
|
} else if (field === 'gender') {
|
||||||
updatedProfile.gender = value as 'male' | 'female';
|
updatedProfile.gender = value as 'male' | 'female';
|
||||||
setProfile(p => ({ ...p, gender: value as 'male' | 'female' }));
|
setProfile(p => ({ ...p, gender: value as 'male' | 'female' }));
|
||||||
|
|
||||||
} else if (field === 'height') {
|
} else if (field === 'height') {
|
||||||
updatedProfile.height = parseFloat(value) || undefined;
|
updatedProfile.height = parseFloat(value) || undefined;
|
||||||
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
|
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
|
||||||
@@ -455,8 +463,8 @@ export default function EditProfileScreen() {
|
|||||||
colors={colors}
|
colors={colors}
|
||||||
textColor={textColor}
|
textColor={textColor}
|
||||||
placeholderColor={placeholderColor}
|
placeholderColor={placeholderColor}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 出生日期选择器弹窗 */}
|
{/* 出生日期选择器弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={datePickerVisible}
|
visible={datePickerVisible}
|
||||||
@@ -488,12 +496,12 @@ export default function EditProfileScreen() {
|
|||||||
{Platform.OS === 'ios' && (
|
{Platform.OS === 'ios' && (
|
||||||
<View style={styles.modalActions}>
|
<View style={styles.modalActions}>
|
||||||
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
||||||
<Text style={styles.modalBtnText}>取消</Text>
|
<Text style={styles.modalBtnText}>{t('editProfile.modals.cancel')}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={() => {
|
<Pressable onPress={() => {
|
||||||
onConfirmDate(pickerDate);
|
onConfirmDate(pickerDate);
|
||||||
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
||||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>{t('editProfile.modals.confirm')}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -536,7 +544,7 @@ function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor }: {
|
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor, t }: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
field: string | null;
|
field: string | null;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -546,6 +554,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
colors: any;
|
colors: any;
|
||||||
textColor: string;
|
textColor: string;
|
||||||
placeholderColor: string;
|
placeholderColor: string;
|
||||||
|
t: (key: string) => string;
|
||||||
}) {
|
}) {
|
||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
|
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
|
||||||
@@ -563,10 +572,10 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
case 'name':
|
case 'name':
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.modalTitle}>昵称</Text>
|
<Text style={styles.modalTitle}>{t('editProfile.fields.name')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||||
placeholder="输入昵称"
|
placeholder={t('editProfile.modals.input.namePlaceholder')}
|
||||||
placeholderTextColor={placeholderColor}
|
placeholderTextColor={placeholderColor}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChangeText={setInputValue}
|
onChangeText={setInputValue}
|
||||||
@@ -577,21 +586,21 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
case 'gender':
|
case 'gender':
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.modalTitle}>性别</Text>
|
<Text style={styles.modalTitle}>{t('editProfile.fields.gender')}</Text>
|
||||||
<View style={styles.genderSelector}>
|
<View style={styles.genderSelector}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.genderOption, selectedGender === 'female' && { backgroundColor: colors.primary + '20' }]}
|
style={[styles.genderOption, selectedGender === 'female' && { backgroundColor: colors.primary + '20' }]}
|
||||||
onPress={() => setSelectedGender('female')}
|
onPress={() => setSelectedGender('female')}
|
||||||
>
|
>
|
||||||
<Text style={[styles.genderEmoji, selectedGender === 'female' && { color: colors.primary }]}>♀</Text>
|
<Text style={[styles.genderEmoji, selectedGender === 'female' && { color: colors.primary }]}>♀</Text>
|
||||||
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}>女性</Text>
|
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}>{t('editProfile.modals.female')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.genderOption, selectedGender === 'male' && { backgroundColor: colors.primary + '20' }]}
|
style={[styles.genderOption, selectedGender === 'male' && { backgroundColor: colors.primary + '20' }]}
|
||||||
onPress={() => setSelectedGender('male')}
|
onPress={() => setSelectedGender('male')}
|
||||||
>
|
>
|
||||||
<Text style={[styles.genderEmoji, selectedGender === 'male' && { color: colors.primary }]}>♂</Text>
|
<Text style={[styles.genderEmoji, selectedGender === 'male' && { color: colors.primary }]}>♂</Text>
|
||||||
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}>男性</Text>
|
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}>{t('editProfile.modals.male')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -599,7 +608,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
case 'height':
|
case 'height':
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.modalTitle}>身高</Text>
|
<Text style={styles.modalTitle}>{t('editProfile.fields.height')}</Text>
|
||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
<Picker
|
<Picker
|
||||||
selectedValue={inputValue}
|
selectedValue={inputValue}
|
||||||
@@ -607,7 +616,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
style={styles.picker}
|
style={styles.picker}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 101 }, (_, i) => 120 + i).map(height => (
|
{Array.from({ length: 101 }, (_, i) => 120 + i).map(height => (
|
||||||
<Picker.Item key={height} label={`${height}厘米`} value={String(height)} />
|
<Picker.Item key={height} label={`${height}${t('editProfile.height.unit')}`} value={String(height)} />
|
||||||
))}
|
))}
|
||||||
</Picker>
|
</Picker>
|
||||||
</View>
|
</View>
|
||||||
@@ -616,29 +625,29 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
case 'weight':
|
case 'weight':
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.modalTitle}>体重</Text>
|
<Text style={styles.modalTitle}>{t('editProfile.fields.weight')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||||
placeholder="输入体重"
|
placeholder={t('editProfile.modals.input.weightPlaceholder')}
|
||||||
placeholderTextColor={placeholderColor}
|
placeholderTextColor={placeholderColor}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChangeText={setInputValue}
|
onChangeText={setInputValue}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<Text style={styles.unitText}>公斤 (kg)</Text>
|
<Text style={styles.unitText}>{t('editProfile.modals.input.weightUnit')}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
case 'activity':
|
case 'activity':
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.modalTitle}>活动水平</Text>
|
<Text style={styles.modalTitle}>{t('editProfile.fields.activityLevel')}</Text>
|
||||||
<View style={styles.activitySelector}>
|
<View style={styles.activitySelector}>
|
||||||
{[
|
{[
|
||||||
{ key: 1, label: '久坐', desc: '很少运动' },
|
{ key: 1, label: t('editProfile.activityLevels.1'), desc: t('editProfile.activityLevels.descriptions.1') },
|
||||||
{ key: 2, label: '轻度活跃', desc: '每周1-3次运动' },
|
{ key: 2, label: t('editProfile.activityLevels.2'), desc: t('editProfile.activityLevels.descriptions.2') },
|
||||||
{ key: 3, label: '中度活跃', desc: '每周3-5次运动' },
|
{ key: 3, label: t('editProfile.activityLevels.3'), desc: t('editProfile.activityLevels.descriptions.3') },
|
||||||
{ key: 4, label: '非常活跃', desc: '每周6-7次运动' },
|
{ key: 4, label: t('editProfile.activityLevels.4'), desc: t('editProfile.activityLevels.descriptions.4') },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -668,7 +677,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
<View style={styles.modalButtons}>
|
<View style={styles.modalButtons}>
|
||||||
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
|
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
|
||||||
<Text style={styles.modalCancelText}>取消</Text>
|
<Text style={styles.modalCancelText}>{t('editProfile.modals.cancel')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -682,7 +691,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
|||||||
}}
|
}}
|
||||||
style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]}
|
style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}>保存</Text>
|
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}>{t('editProfile.modals.save')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dayjs from 'dayjs';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface BasalMetabolismCardProps {
|
interface BasalMetabolismCardProps {
|
||||||
@@ -14,6 +15,7 @@ interface BasalMetabolismCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCardProps) {
|
export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [basalMetabolism, setBasalMetabolism] = useState<number | null>(null);
|
const [basalMetabolism, setBasalMetabolism] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('BasalMetabolismCard: 获取基础代谢数据失败:', error);
|
console.error('BasalMetabolismCard: Failed to get basal metabolism data:', error);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
// 清理请求记录
|
// 清理请求记录
|
||||||
@@ -134,20 +136,20 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
// 使用 useMemo 优化状态描述计算
|
// 使用 useMemo 优化状态描述计算
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (basalMetabolism === null || basalMetabolism === 0) {
|
if (basalMetabolism === null || basalMetabolism === 0) {
|
||||||
return { text: '未知', color: '#9AA3AE' };
|
return { text: t('statistics.components.metabolism.status.unknown'), color: '#9AA3AE' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基于常见的基础代谢范围来判断状态
|
// 基于常见的基础代谢范围来判断状态
|
||||||
if (basalMetabolism >= 1800) {
|
if (basalMetabolism >= 1800) {
|
||||||
return { text: '高代谢', color: '#10B981' };
|
return { text: t('statistics.components.metabolism.status.high'), color: '#10B981' };
|
||||||
} else if (basalMetabolism >= 1400) {
|
} else if (basalMetabolism >= 1400) {
|
||||||
return { text: '正常', color: '#3B82F6' };
|
return { text: t('statistics.components.metabolism.status.normal'), color: '#3B82F6' };
|
||||||
} else if (basalMetabolism >= 1000) {
|
} else if (basalMetabolism >= 1000) {
|
||||||
return { text: '偏低', color: '#F59E0B' };
|
return { text: t('statistics.components.metabolism.status.low'), color: '#F59E0B' };
|
||||||
} else {
|
} else {
|
||||||
return { text: '较低', color: '#EF4444' };
|
return { text: t('statistics.components.metabolism.status.veryLow'), color: '#EF4444' };
|
||||||
}
|
}
|
||||||
}, [basalMetabolism]);
|
}, [basalMetabolism, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -163,7 +165,7 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
source={require('@/assets/images/icons/icon-fire.png')}
|
source={require('@/assets/images/icons/icon-fire.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.title}>基础代谢</Text>
|
<Text style={styles.title}>{t('statistics.components.metabolism.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.statusBadge, { backgroundColor: status.color + '20' }]}>
|
<View style={[styles.statusBadge, { backgroundColor: status.color + '20' }]}>
|
||||||
<Text style={[styles.statusText, { color: status.color }]}>{status.text}</Text>
|
<Text style={[styles.statusText, { color: status.color }]}>{status.text}</Text>
|
||||||
@@ -173,9 +175,9 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
{/* 数值显示区域 */}
|
{/* 数值显示区域 */}
|
||||||
<View style={styles.valueSection}>
|
<View style={styles.valueSection}>
|
||||||
<Text style={styles.value}>
|
<Text style={styles.value}>
|
||||||
{loading ? '加载中...' : (basalMetabolism != null && basalMetabolism > 0 ? Math.round(basalMetabolism).toString() : '--')}
|
{loading ? t('statistics.components.metabolism.loading') : (basalMetabolism != null && basalMetabolism > 0 ? Math.round(basalMetabolism).toString() : '--')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.unit}>千卡/日</Text>
|
<Text style={styles.unit}>{t('statistics.components.metabolism.unit')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import LottieView from 'lottie-react-native';
|
import LottieView from 'lottie-react-native';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface MoodCardProps {
|
interface MoodCardProps {
|
||||||
@@ -11,6 +12,7 @@ interface MoodCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
|
export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
|
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
|
||||||
const animationRef = useRef<LottieView>(null);
|
const animationRef = useRef<LottieView>(null);
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
|
|||||||
source={require('@/assets/images/icons/icon-mood.png')}
|
source={require('@/assets/images/icons/icon-mood.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.cardTitle}>心情</Text>
|
<Text style={styles.cardTitle}>{t('statistics.components.mood.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<LottieView
|
<LottieView
|
||||||
ref={animationRef}
|
ref={animationRef}
|
||||||
@@ -48,7 +50,7 @@ export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.moodEmptyText}>点击记录心情</Text>
|
<Text style={styles.moodEmptyText}>{t('statistics.components.mood.empty')}</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateRemainingCalories } from '@/utils/nutrition';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import Svg, { Circle } from 'react-native-svg';
|
import Svg, { Circle } from 'react-native-svg';
|
||||||
|
|
||||||
@@ -25,10 +26,12 @@ export type NutritionRadarCardProps = {
|
|||||||
// 简化的圆环进度组件
|
// 简化的圆环进度组件
|
||||||
const SimpleRingProgress = ({
|
const SimpleRingProgress = ({
|
||||||
remainingCalories,
|
remainingCalories,
|
||||||
totalAvailable
|
totalAvailable,
|
||||||
|
t
|
||||||
}: {
|
}: {
|
||||||
remainingCalories: number;
|
remainingCalories: number;
|
||||||
totalAvailable: number;
|
totalAvailable: number;
|
||||||
|
t: any;
|
||||||
}) => {
|
}) => {
|
||||||
const animatedProgress = useRef(new Animated.Value(0)).current;
|
const animatedProgress = useRef(new Animated.Value(0)).current;
|
||||||
const radius = 32;
|
const radius = 32;
|
||||||
@@ -82,7 +85,7 @@ const SimpleRingProgress = ({
|
|||||||
<Text style={{ fontSize: 12, fontWeight: '600', color: '#192126' }}>
|
<Text style={{ fontSize: 12, fontWeight: '600', color: '#192126' }}>
|
||||||
{Math.round(remainingCalories)}
|
{Math.round(remainingCalories)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 8, color: '#9AA3AE' }}>还能吃</Text>
|
<Text style={{ fontSize: 8, color: '#9AA3AE' }}>{t('statistics.components.diet.remaining')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -93,6 +96,7 @@ export function NutritionRadarCard({
|
|||||||
style,
|
style,
|
||||||
resetToken,
|
resetToken,
|
||||||
}: NutritionRadarCardProps) {
|
}: NutritionRadarCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ export function NutritionRadarCard({
|
|||||||
dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(),
|
dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('NutritionRadarCard: 获取营养卡片数据失败:', error);
|
console.error('NutritionRadarCard: Failed to get nutrition card data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -133,14 +137,14 @@ export function NutritionRadarCard({
|
|||||||
|
|
||||||
const nutritionStats = useMemo(() => {
|
const nutritionStats = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' },
|
{ label: t('statistics.components.diet.calories'), value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} ${t('statistics.components.diet.kcal')}` : `0 ${t('statistics.components.diet.kcal')}`, color: '#FF6B6B' },
|
||||||
{ label: '蛋白质', value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' },
|
{ label: t('statistics.components.diet.protein'), value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' },
|
||||||
{ label: '碳水', value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' },
|
{ label: t('statistics.components.diet.carb'), value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' },
|
||||||
{ label: '脂肪', value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' },
|
{ label: t('statistics.components.diet.fat'), value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' },
|
||||||
{ label: '纤维', value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' },
|
{ label: t('statistics.components.diet.fiber'), value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' },
|
||||||
{ label: '钠', value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' },
|
{ label: t('statistics.components.diet.sodium'), value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' },
|
||||||
];
|
];
|
||||||
}, [nutritionSummary]);
|
}, [nutritionSummary, t]);
|
||||||
|
|
||||||
// 计算还能吃的卡路里
|
// 计算还能吃的卡路里
|
||||||
const consumedCalories = nutritionSummary?.totalCalories || 0;
|
const consumedCalories = nutritionSummary?.totalCalories || 0;
|
||||||
@@ -168,10 +172,10 @@ export function NutritionRadarCard({
|
|||||||
source={require('@/assets/images/icons/icon-healthy-diet.png')}
|
source={require('@/assets/images/icons/icon-healthy-diet.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.cardTitle}>饮食分析</Text>
|
<Text style={styles.cardTitle}>{t('statistics.components.diet.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.cardSubtitle}>
|
<Text style={styles.cardSubtitle}>
|
||||||
{loading ? '加载中...' : `更新: ${dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}`}
|
{loading ? t('statistics.components.diet.loading') : t('statistics.components.diet.updated', { time: dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm') })}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -180,6 +184,7 @@ export function NutritionRadarCard({
|
|||||||
<SimpleRingProgress
|
<SimpleRingProgress
|
||||||
remainingCalories={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
remainingCalories={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
||||||
totalAvailable={(loading || activeCaloriesLoading) ? 0 : effectiveBasalMetabolism + effectiveActiveCalories}
|
totalAvailable={(loading || activeCaloriesLoading) ? 0 : effectiveBasalMetabolism + effectiveActiveCalories}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -199,7 +204,7 @@ export function NutritionRadarCard({
|
|||||||
<View style={styles.calorieSection}>
|
<View style={styles.calorieSection}>
|
||||||
<View style={styles.calorieContent}>
|
<View style={styles.calorieContent}>
|
||||||
<View style={styles.calculationRow}>
|
<View style={styles.calculationRow}>
|
||||||
<Text style={styles.calorieSubtitle}>还能吃</Text>
|
<Text style={styles.calorieSubtitle}>{t('statistics.components.diet.remaining')}</Text>
|
||||||
<View style={styles.remainingCaloriesContainer}>
|
<View style={styles.remainingCaloriesContainer}>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
value={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
||||||
@@ -207,11 +212,11 @@ export function NutritionRadarCard({
|
|||||||
style={styles.mainValue}
|
style={styles.mainValue}
|
||||||
format={(v) => (loading || activeCaloriesLoading) ? '--' : Math.round(v).toString()}
|
format={(v) => (loading || activeCaloriesLoading) ? '--' : Math.round(v).toString()}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.calorieUnit}>千卡</Text>
|
<Text style={styles.calorieUnit}>{t('statistics.components.diet.kcal')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.calculationText}> = </Text>
|
<Text style={styles.calculationText}> = </Text>
|
||||||
<View style={styles.calculationItem}>
|
<View style={styles.calculationItem}>
|
||||||
<Text style={styles.calculationLabel}>基代</Text>
|
<Text style={styles.calculationLabel}>{t('statistics.components.diet.basal')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={loading ? 0 : effectiveBasalMetabolism}
|
value={loading ? 0 : effectiveBasalMetabolism}
|
||||||
@@ -221,7 +226,7 @@ export function NutritionRadarCard({
|
|||||||
/>
|
/>
|
||||||
<Text style={styles.calculationText}> + </Text>
|
<Text style={styles.calculationText}> + </Text>
|
||||||
<View style={styles.calculationItem}>
|
<View style={styles.calculationItem}>
|
||||||
<Text style={styles.calculationLabel}>运动</Text>
|
<Text style={styles.calculationLabel}>{t('statistics.components.diet.exercise')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={activeCaloriesLoading ? 0 : effectiveActiveCalories}
|
value={activeCaloriesLoading ? 0 : effectiveActiveCalories}
|
||||||
@@ -231,7 +236,7 @@ export function NutritionRadarCard({
|
|||||||
/>
|
/>
|
||||||
<Text style={styles.calculationText}> - </Text>
|
<Text style={styles.calculationText}> - </Text>
|
||||||
<View style={styles.calculationItem}>
|
<View style={styles.calculationItem}>
|
||||||
<Text style={styles.calculationLabel}>饮食</Text>
|
<Text style={styles.calculationLabel}>{t('statistics.components.diet.diet')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={loading ? 0 : consumedCalories}
|
value={loading ? 0 : consumedCalories}
|
||||||
@@ -260,7 +265,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionImage}
|
style={styles.foodOptionImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.foodOptionText}>AI识别</Text>
|
<Text style={styles.foodOptionText}>{t('statistics.components.diet.aiRecognition')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -277,7 +282,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionImage}
|
style={styles.foodOptionImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.foodOptionText}>食物库</Text>
|
<Text style={styles.foodOptionText}>{t('statistics.components.diet.foodLibrary')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -294,7 +299,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionImage}
|
style={styles.foodOptionImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.foodOptionText}>一句话记录</Text>
|
<Text style={styles.foodOptionText}>{t('statistics.components.diet.voiceRecord')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -311,7 +316,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionImage}
|
style={styles.foodOptionImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.foodOptionText}>成分表分析</Text>
|
<Text style={styles.foodOptionText}>{t('statistics.components.diet.nutritionLabel')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { logger } from '@/utils/logger';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AnimatedNumber } from './AnimatedNumber';
|
import { AnimatedNumber } from './AnimatedNumber';
|
||||||
// 使用原生View来替代SVG,避免导入问题
|
// 使用原生View来替代SVG,避免导入问题
|
||||||
// import Svg, { Rect } from 'react-native-svg';
|
// import Svg, { Rect } from 'react-native-svg';
|
||||||
@@ -28,6 +29,7 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
curDate,
|
curDate,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [stepCount, setStepCount] = useState(0)
|
const [stepCount, setStepCount] = useState(0)
|
||||||
@@ -36,7 +38,7 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
|
|
||||||
const getStepData = useCallback(async (date: Date) => {
|
const getStepData = useCallback(async (date: Date) => {
|
||||||
try {
|
try {
|
||||||
logger.info('获取步数数据...');
|
logger.info('Getting step data...');
|
||||||
|
|
||||||
// 先获取步数,立即更新UI
|
// 先获取步数,立即更新UI
|
||||||
const [steps, hourly] = await Promise.all([
|
const [steps, hourly] = await Promise.all([
|
||||||
@@ -47,7 +49,7 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
setHourSteps(hourly);
|
setHourSteps(hourly);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('获取步数数据失败:', error);
|
logger.error('Failed to get step data:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
source={require('@/assets/images/icons/icon-step.png')}
|
source={require('@/assets/images/icons/icon-step.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.title}>步数</Text>
|
<Text style={styles.title}>{t('statistics.components.steps.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 柱状图 */}
|
{/* 柱状图 */}
|
||||||
@@ -190,7 +192,7 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={stepCount || 0}
|
value={stepCount || 0}
|
||||||
style={styles.stepCount}
|
style={styles.stepCount}
|
||||||
format={(v) => stepCount !== null ? `${Math.round(v)}` : '——'}
|
format={(v) => stepCount !== null ? `${Math.round(v)}` : '--'}
|
||||||
resetToken={stepCount}
|
resetToken={stepCount}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { fetchHRVWithStatus } from '@/utils/health';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { StressAnalysisModal } from './StressAnalysisModal';
|
import { StressAnalysisModal } from './StressAnalysisModal';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ interface StressMeterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StressMeter({ curDate }: StressMeterProps) {
|
export function StressMeter({ curDate }: StressMeterProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 将HRV值转换为压力指数(0-100)
|
// 将HRV值转换为压力指数(0-100)
|
||||||
// HRV值范围:30-110ms,映射到压力指数100-0
|
// HRV值范围:30-110ms,映射到压力指数100-0
|
||||||
@@ -34,23 +36,23 @@ export function StressMeter({ curDate }: StressMeterProps) {
|
|||||||
|
|
||||||
const getHrvData = async () => {
|
const getHrvData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('StressMeter: 开始获取HRV数据...', curDate);
|
console.log('StressMeter: Starting to get HRV data...', curDate);
|
||||||
|
|
||||||
// 使用智能HRV数据获取功能
|
// 使用智能HRV数据获取功能
|
||||||
const result = await fetchHRVWithStatus(curDate);
|
const result = await fetchHRVWithStatus(curDate);
|
||||||
|
|
||||||
console.log('StressMeter: HRV数据获取结果:', result);
|
console.log('StressMeter: HRV data fetch result:', result);
|
||||||
|
|
||||||
if (result.hrvData) {
|
if (result.hrvData) {
|
||||||
setHrvValue(Math.round(result.hrvData.value));
|
setHrvValue(Math.round(result.hrvData.value));
|
||||||
console.log(`StressMeter: 使用${result.message},HRV值: ${result.hrvData.value}ms`);
|
console.log(`StressMeter: Using ${result.message}, HRV value: ${result.hrvData.value}ms`);
|
||||||
} else {
|
} else {
|
||||||
console.log('StressMeter: 未获取到HRV数据');
|
console.log('StressMeter: No HRV data obtained');
|
||||||
// 可以设置一个默认值或者显示无数据状态
|
// 可以设置一个默认值或者显示无数据状态
|
||||||
setHrvValue(0);
|
setHrvValue(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('StressMeter: 获取HRV数据失败:', error);
|
console.error('StressMeter: Failed to get HRV data:', error);
|
||||||
setHrvValue(0);
|
setHrvValue(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
|
|||||||
source={require('@/assets/images/icons/icon-pressure.png')}
|
source={require('@/assets/images/icons/icon-pressure.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.title}>压力</Text>
|
<Text style={styles.title}>{t('statistics.components.stress.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
{/* {updateTime && (
|
{/* {updateTime && (
|
||||||
<Text style={styles.headerUpdateTime}>{formatUpdateTime(updateTime)}</Text>
|
<Text style={styles.headerUpdateTime}>{formatUpdateTime(updateTime)}</Text>
|
||||||
@@ -94,7 +96,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
|
|||||||
{/* 数值显示区域 */}
|
{/* 数值显示区域 */}
|
||||||
<View style={styles.valueSection}>
|
<View style={styles.valueSection}>
|
||||||
<Text style={styles.value}>{hrvValue || '--'}</Text>
|
<Text style={styles.value}>{hrvValue || '--'}</Text>
|
||||||
<Text>ms</Text>
|
<Text>{t('statistics.components.stress.unit')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 进度条区域 */}
|
{/* 进度条区域 */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Image } from 'expo-image';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import LottieView from 'lottie-react-native';
|
import LottieView from 'lottie-react-native';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -26,6 +27,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
style,
|
style,
|
||||||
selectedDate
|
selectedDate
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = useWaterDataByDate(selectedDate);
|
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = useWaterDataByDate(selectedDate);
|
||||||
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
||||||
@@ -89,7 +91,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
const targetDate = selectedDate || dayjs().format('YYYY-MM-DD');
|
const targetDate = selectedDate || dayjs().format('YYYY-MM-DD');
|
||||||
await getWaterRecordsByDate(targetDate);
|
await getWaterRecordsByDate(targetDate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('页面聚焦时加载数据失败:', error);
|
console.error('Failed to load data on focus:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,11 +187,11 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
source={require('@/assets/images/icons/IconGlass.png')}
|
source={require('@/assets/images/icons/IconGlass.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.title}>喝水</Text>
|
<Text style={styles.title}>{t('statistics.components.water.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
{isToday && (
|
{isToday && (
|
||||||
<TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}>
|
<TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}>
|
||||||
<Text style={styles.addButtonText}>+ {quickWaterAmount}ml</Text>
|
<Text style={styles.addButtonText}>{t('statistics.components.water.addButton', { amount: quickWaterAmount })}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -254,14 +256,14 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={currentIntake}
|
value={currentIntake}
|
||||||
style={styles.currentIntake}
|
style={styles.currentIntake}
|
||||||
format={(value) => `${Math.round(value)}ml`}
|
format={(value) => `${Math.round(value)}${t('statistics.components.water.unit')}`}
|
||||||
resetToken={selectedDate}
|
resetToken={selectedDate}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.currentIntake}>——</Text>
|
<Text style={styles.currentIntake}>--</Text>
|
||||||
)}
|
)}
|
||||||
<Text style={styles.targetIntake}>
|
<Text style={styles.targetIntake}>
|
||||||
/ {targetIntake}ml
|
/ {targetIntake}{t('statistics.components.water.unit')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import dayjs from 'dayjs';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||||
@@ -40,6 +41,7 @@ const DEFAULT_SUMMARY: WorkoutSummary = {
|
|||||||
|
|
||||||
|
|
||||||
export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, style }) => {
|
export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, style }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [summary, setSummary] = useState<WorkoutSummary>(DEFAULT_SUMMARY);
|
const [summary, setSummary] = useState<WorkoutSummary>(DEFAULT_SUMMARY);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -145,13 +147,13 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, st
|
|||||||
|
|
||||||
const label = lastWorkout
|
const label = lastWorkout
|
||||||
? getWorkoutTypeDisplayName(lastWorkout.workoutActivityType)
|
? getWorkoutTypeDisplayName(lastWorkout.workoutActivityType)
|
||||||
: '尚无锻炼数据';
|
: t('statistics.components.workout.noData');
|
||||||
|
|
||||||
const time = lastWorkout
|
const time = lastWorkout
|
||||||
? `${dayjs(lastWorkout.endDate || lastWorkout.startDate).format('HH:mm')} 更新`
|
? `${dayjs(lastWorkout.endDate || lastWorkout.startDate).format('HH:mm')} ${t('statistics.components.workout.updated')}`
|
||||||
: '等待同步';
|
: t('statistics.components.workout.syncing');
|
||||||
|
|
||||||
let source = '来源:等待同步';
|
let source = t('statistics.components.workout.sourceWaiting');
|
||||||
if (hasWorkouts) {
|
if (hasWorkouts) {
|
||||||
const sourceNames = summary.workouts
|
const sourceNames = summary.workouts
|
||||||
.map((workout) => workout.source?.name?.trim() || workout.source?.bundleIdentifier?.trim())
|
.map((workout) => workout.source?.name?.trim() || workout.source?.bundleIdentifier?.trim())
|
||||||
@@ -160,9 +162,11 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, st
|
|||||||
if (sourceNames.length) {
|
if (sourceNames.length) {
|
||||||
const uniqueNames = Array.from(new Set(sourceNames));
|
const uniqueNames = Array.from(new Set(sourceNames));
|
||||||
const displayNames = uniqueNames.slice(0, 2).join('、');
|
const displayNames = uniqueNames.slice(0, 2).join('、');
|
||||||
source = uniqueNames.length > 2 ? `来源:${displayNames} 等` : `来源:${displayNames}`;
|
source = uniqueNames.length > 2
|
||||||
|
? t('statistics.components.workout.sourceFormatMultiple', { source: displayNames })
|
||||||
|
: t('statistics.components.workout.sourceFormat', { source: displayNames });
|
||||||
} else {
|
} else {
|
||||||
source = '来源:未知';
|
source = t('statistics.components.workout.sourceUnknown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +188,7 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, st
|
|||||||
source,
|
source,
|
||||||
badges: uniqueBadges,
|
badges: uniqueBadges,
|
||||||
};
|
};
|
||||||
}, [summary]);
|
}, [summary, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -195,18 +199,18 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, st
|
|||||||
<View style={styles.headerRow}>
|
<View style={styles.headerRow}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Image source={require('@/assets/images/icons/icon-fitness.png')} style={styles.titleIcon} />
|
<Image source={require('@/assets/images/icons/icon-fitness.png')} style={styles.titleIcon} />
|
||||||
<Text style={styles.titleText}>近期锻炼</Text>
|
<Text style={styles.titleText}>{t('statistics.components.workout.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.metricsRow}>
|
<View style={styles.metricsRow}>
|
||||||
<View style={styles.metricItem}>
|
<View style={styles.metricItem}>
|
||||||
<AnimatedNumber value={summary.totalMinutes} resetToken={resetToken} style={styles.metricValue} />
|
<AnimatedNumber value={summary.totalMinutes} resetToken={resetToken} style={styles.metricValue} />
|
||||||
<Text style={styles.metricLabel}>分钟</Text>
|
<Text style={styles.metricLabel}>{t('statistics.components.workout.minutes')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.metricItem}>
|
<View style={styles.metricItem}>
|
||||||
<AnimatedNumber value={summary.totalCalories} resetToken={resetToken} style={styles.metricValue} />
|
<AnimatedNumber value={summary.totalCalories} resetToken={resetToken} style={styles.metricValue} />
|
||||||
<Text style={styles.metricLabel}>千卡</Text>
|
<Text style={styles.metricLabel}>{t('statistics.components.workout.kcal')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { takeMedicationAction } from '@/store/medicationsSlice';
|
import { takeMedicationAction } from '@/store/medicationsSlice';
|
||||||
import type { MedicationDisplayItem } from '@/types/medication';
|
import type { MedicationDisplayItem } from '@/types/medication';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
@@ -19,6 +20,7 @@ export type MedicationCardProps = {
|
|||||||
|
|
||||||
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails, onCelebrate }: MedicationCardProps) {
|
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails, onCelebrate }: MedicationCardProps) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useI18n();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
@@ -43,11 +45,11 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
if (timeDiffMinutes > 60) {
|
if (timeDiffMinutes > 60) {
|
||||||
// 显示二次确认弹窗
|
// 显示二次确认弹窗
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'尚未到服药时间',
|
t('medications.card.earlyTakeAlert.title'),
|
||||||
`该用药计划在 ${medication.scheduledTime},现在还早于1小时以上。\n\n是否确认已服用此药物?`,
|
t('medications.card.earlyTakeAlert.message', { time: medication.scheduledTime }),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: '取消',
|
text: t('medications.card.earlyTakeAlert.cancel'),
|
||||||
style: 'cancel',
|
style: 'cancel',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
// 用户取消,不执行任何操作
|
// 用户取消,不执行任何操作
|
||||||
@@ -55,7 +57,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '确认已服用',
|
text: t('medications.card.earlyTakeAlert.confirm'),
|
||||||
style: 'default',
|
style: 'default',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
// 用户确认,执行服药逻辑
|
// 用户确认,执行服药逻辑
|
||||||
@@ -89,9 +91,9 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[MEDICATION_CARD] 服药操作失败', error);
|
console.error('[MEDICATION_CARD] 服药操作失败', error);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'操作失败',
|
t('medications.card.takeError.title'),
|
||||||
error instanceof Error ? error.message : '记录服药时发生错误,请稍后重试',
|
error instanceof Error ? error.message : t('medications.card.takeError.message'),
|
||||||
[{ text: '确定' }]
|
[{ text: t('medications.card.takeError.confirm') }]
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@@ -102,7 +104,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
if (medication.status === 'missed') {
|
if (medication.status === 'missed') {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.statusChip, styles.statusChipMissed]}>
|
<View style={[styles.statusChip, styles.statusChipMissed]}>
|
||||||
<ThemedText style={styles.statusChipText}>已错过</ThemedText>
|
<ThemedText style={styles.statusChipText}>{t('medications.card.status.missed')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,7 +114,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
return (
|
return (
|
||||||
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
|
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
|
||||||
<Ionicons name="time-outline" size={14} color="#fff" />
|
<Ionicons name="time-outline" size={14} color="#fff" />
|
||||||
<ThemedText style={styles.statusChipText}>到服药时间</ThemedText>
|
<ThemedText style={styles.statusChipText}>{t('medications.card.status.timeToTake')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
return (
|
return (
|
||||||
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
|
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
|
||||||
<Ionicons name="time-outline" size={14} color="#fff" />
|
<Ionicons name="time-outline" size={14} color="#fff" />
|
||||||
<ThemedText style={styles.statusChipText}>剩余 {formatted}</ThemedText>
|
<ThemedText style={styles.statusChipText}>{t('medications.card.status.remaining', { time: formatted })}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
return (
|
return (
|
||||||
<View style={[styles.actionButton, styles.actionButtonTaken]}>
|
<View style={[styles.actionButton, styles.actionButtonTaken]}>
|
||||||
<Ionicons name="checkmark-circle" size={18} color="#fff" />
|
<Ionicons name="checkmark-circle" size={18} color="#fff" />
|
||||||
<ThemedText style={styles.actionButtonText}>已服用</ThemedText>
|
<ThemedText style={styles.actionButtonText}>{t('medications.card.action.taken')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -158,13 +160,13 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
isInteractive={!isSubmitting}
|
isInteractive={!isSubmitting}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.actionButtonText}>
|
<ThemedText style={styles.actionButtonText}>
|
||||||
{isSubmitting ? '提交中...' : '立即服用'}
|
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</GlassView>
|
</GlassView>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.actionButton, styles.actionButtonUpcoming, styles.fallbackActionButton]}>
|
<View style={[styles.actionButton, styles.actionButtonUpcoming, styles.fallbackActionButton]}>
|
||||||
<ThemedText style={styles.actionButtonText}>
|
<ThemedText style={styles.actionButtonText}>
|
||||||
{isSubmitting ? '提交中...' : '立即服用'}
|
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
|||||||
import { selectUserProfile, updateUserBodyMeasurements, UserProfile } from '@/store/userSlice';
|
import { selectUserProfile, updateUserBodyMeasurements, UserProfile } from '@/store/userSlice';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface CircumferenceCardProps {
|
interface CircumferenceCardProps {
|
||||||
@@ -11,6 +12,7 @@ interface CircumferenceCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const userProfile = useAppSelector(selectUserProfile);
|
const userProfile = useAppSelector(selectUserProfile);
|
||||||
|
|
||||||
@@ -30,32 +32,32 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
|||||||
const measurements = [
|
const measurements = [
|
||||||
{
|
{
|
||||||
key: 'chestCircumference',
|
key: 'chestCircumference',
|
||||||
label: '胸围',
|
label: t('statistics.components.circumference.measurements.chest'),
|
||||||
value: userProfile?.chestCircumference,
|
value: userProfile?.chestCircumference,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'waistCircumference',
|
key: 'waistCircumference',
|
||||||
label: '腰围',
|
label: t('statistics.components.circumference.measurements.waist'),
|
||||||
value: userProfile?.waistCircumference,
|
value: userProfile?.waistCircumference,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'upperHipCircumference',
|
key: 'upperHipCircumference',
|
||||||
label: '上臀围',
|
label: t('statistics.components.circumference.measurements.hip'),
|
||||||
value: userProfile?.upperHipCircumference,
|
value: userProfile?.upperHipCircumference,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'armCircumference',
|
key: 'armCircumference',
|
||||||
label: '臂围',
|
label: t('statistics.components.circumference.measurements.arm'),
|
||||||
value: userProfile?.armCircumference,
|
value: userProfile?.armCircumference,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'thighCircumference',
|
key: 'thighCircumference',
|
||||||
label: '大腿围',
|
label: t('statistics.components.circumference.measurements.thigh'),
|
||||||
value: userProfile?.thighCircumference,
|
value: userProfile?.thighCircumference,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'calfCircumference',
|
key: 'calfCircumference',
|
||||||
label: '小腿围',
|
label: t('statistics.components.circumference.measurements.calf'),
|
||||||
value: userProfile?.calfCircumference,
|
value: userProfile?.calfCircumference,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -145,7 +147,7 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
|||||||
onPress={handleCardPress}
|
onPress={handleCardPress}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={styles.title}>围度 (cm)</Text>
|
<Text style={styles.title}>{t('statistics.components.circumference.title')}</Text>
|
||||||
|
|
||||||
<View style={styles.measurementsContainer}>
|
<View style={styles.measurementsContainer}>
|
||||||
{measurements.map((measurement, index) => (
|
{measurements.map((measurement, index) => (
|
||||||
@@ -174,12 +176,12 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
|||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setSelectedMeasurement(null);
|
setSelectedMeasurement(null);
|
||||||
}}
|
}}
|
||||||
title={selectedMeasurement ? `设置${selectedMeasurement.label}` : '设置围度'}
|
title={selectedMeasurement ? t('statistics.components.circumference.setTitle', { label: selectedMeasurement.label }) : t('statistics.components.circumference.title')}
|
||||||
items={circumferenceOptions}
|
items={circumferenceOptions}
|
||||||
selectedValue={selectedMeasurement?.currentValue}
|
selectedValue={selectedMeasurement?.currentValue}
|
||||||
onValueChange={() => { }} // Real-time update not needed
|
onValueChange={() => { }} // Real-time update not needed
|
||||||
onConfirm={handleUpdateMeasurement}
|
onConfirm={handleUpdateMeasurement}
|
||||||
confirmButtonText="确认"
|
confirmButtonText={t('statistics.components.circumference.confirm')}
|
||||||
pickerHeight={180}
|
pickerHeight={180}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useCallback, useRef } from 'react';
|
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
|
||||||
import HealthDataCard from './HealthDataCard';
|
|
||||||
import { fetchOxygenSaturation } from '@/utils/health';
|
import { fetchOxygenSaturation } from '@/utils/health';
|
||||||
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import HealthDataCard from './HealthDataCard';
|
||||||
|
|
||||||
interface OxygenSaturationCardProps {
|
interface OxygenSaturationCardProps {
|
||||||
style?: object;
|
style?: object;
|
||||||
@@ -13,6 +14,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
|||||||
style,
|
style,
|
||||||
selectedDate
|
selectedDate
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
@@ -38,7 +40,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
|||||||
const data = await fetchOxygenSaturation(options);
|
const data = await fetchOxygenSaturation(options);
|
||||||
setOxygenSaturation(data);
|
setOxygenSaturation(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OxygenSaturationCard: 获取血氧饱和度数据失败:', error);
|
console.error('OxygenSaturationCard: Failed to get blood oxygen data:', error);
|
||||||
setOxygenSaturation(null);
|
setOxygenSaturation(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -52,7 +54,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HealthDataCard
|
<HealthDataCard
|
||||||
title="血氧饱和度"
|
title={t('statistics.components.oxygen.title')}
|
||||||
value={loading ? '--' : (oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--')}
|
value={loading ? '--' : (oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--')}
|
||||||
unit="%"
|
unit="%"
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
|||||||
selectedDate,
|
selectedDate,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const challenges = useAppSelector(selectChallengeList);
|
const challenges = useAppSelector(selectChallengeList);
|
||||||
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||||||
@@ -39,7 +41,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
|||||||
const data = await fetchCompleteSleepData(selectedDate);
|
const data = await fetchCompleteSleepData(selectedDate);
|
||||||
setSleepDuration(data?.totalSleepTime || null);
|
setSleepDuration(data?.totalSleepTime || null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('SleepCard: 获取睡眠数据失败:', error);
|
console.error('SleepCard: Failed to get sleep data:', error);
|
||||||
setSleepDuration(null);
|
setSleepDuration(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -75,7 +77,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
|||||||
try {
|
try {
|
||||||
await dispatch(reportChallengeProgress({ id: sleepChallenge.id, value: sleepDuration })).unwrap();
|
await dispatch(reportChallengeProgress({ id: sleepChallenge.id, value: sleepDuration })).unwrap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('SleepCard: 挑战进度上报失败', { error, challengeId: sleepChallenge.id });
|
logger.warn('SleepCard: Challenge progress report failed', { error, challengeId: sleepChallenge.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
lastReportedRef.current = { date: dateKey, value: sleepDuration };
|
lastReportedRef.current = { date: dateKey, value: sleepDuration };
|
||||||
@@ -91,10 +93,10 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
|||||||
source={require('@/assets/images/icons/icon-sleep.png')}
|
source={require('@/assets/images/icons/icon-sleep.png')}
|
||||||
style={styles.titleIcon}
|
style={styles.titleIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.cardTitle}>睡眠</Text>
|
<Text style={styles.cardTitle}>{t('statistics.components.sleep.title')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.sleepValue}>
|
<Text style={styles.sleepValue}>
|
||||||
{loading ? '加载中...' : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')}
|
{loading ? t('statistics.components.sleep.loading') : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -22,13 +23,14 @@ import {
|
|||||||
import Svg, { Circle, Path } from 'react-native-svg';
|
import Svg, { Circle, Path } from 'react-native-svg';
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth } = Dimensions.get('window');
|
||||||
const CARD_WIDTH = screenWidth - 40; // 减去左右边距
|
const CARD_WIDTH = screenWidth - 40; // Subtract left and right margins
|
||||||
const CHART_WIDTH = CARD_WIDTH - 36; // 减去卡片内边距
|
const CHART_WIDTH = CARD_WIDTH - 36; // Subtract card padding
|
||||||
const CHART_HEIGHT = 60;
|
const CHART_HEIGHT = 60;
|
||||||
const PADDING = 10;
|
const PADDING = 10;
|
||||||
|
|
||||||
|
|
||||||
export function WeightHistoryCard() {
|
export function WeightHistoryCard() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const userProfile = useAppSelector((s) => s.user.profile);
|
const userProfile = useAppSelector((s) => s.user.profile);
|
||||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||||
@@ -53,7 +55,7 @@ export function WeightHistoryCard() {
|
|||||||
try {
|
try {
|
||||||
await dispatch(fetchWeightHistory() as any).unwrap();
|
await dispatch(fetchWeightHistory() as any).unwrap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载体重历史失败:', error);
|
console.error('Failed to load weight history:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,20 +72,20 @@ export function WeightHistoryCard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 处理体重历史数据
|
// Process weight history data
|
||||||
const sortedHistory = [...weightHistory]
|
const sortedHistory = [...weightHistory]
|
||||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||||
.slice(-7); // 只显示最近7条记录
|
.slice(-7); // Show only the last 7 records
|
||||||
|
|
||||||
// return (
|
// return (
|
||||||
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||||
// <View style={styles.cardHeader}>
|
// <View style={styles.cardHeader}>
|
||||||
// <Text style={styles.cardTitle}>体重记录</Text>
|
// <Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||||
// </View>
|
// </View>
|
||||||
|
|
||||||
// <View style={styles.emptyContent}>
|
// <View style={styles.emptyContent}>
|
||||||
// <Text style={styles.emptyDescription}>
|
// <Text style={styles.emptyDescription}>
|
||||||
// 暂无体重记录,点击下方按钮开始记录
|
// No weight records yet, click the button below to start recording
|
||||||
// </Text>
|
// </Text>
|
||||||
// <TouchableOpacity
|
// <TouchableOpacity
|
||||||
// style={styles.recordButton}
|
// style={styles.recordButton}
|
||||||
@@ -94,14 +96,14 @@ export function WeightHistoryCard() {
|
|||||||
// activeOpacity={0.8}
|
// activeOpacity={0.8}
|
||||||
// >
|
// >
|
||||||
// <Ionicons name="add" size={18} color="#FFFFFF" />
|
// <Ionicons name="add" size={18} color="#FFFFFF" />
|
||||||
// <Text style={styles.recordButtonText}>记录体重</Text>
|
// <Text style={styles.recordButtonText}>{t('statistics.components.weight.addButton')}</Text>
|
||||||
// </TouchableOpacity>
|
// </TouchableOpacity>
|
||||||
// </View>
|
// </View>
|
||||||
// </TouchableOpacity>
|
// </TouchableOpacity>
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 生成图表数据
|
// Generate chart data
|
||||||
const weights = sortedHistory.map(item => parseFloat(item.weight));
|
const weights = sortedHistory.map(item => parseFloat(item.weight));
|
||||||
const minWeight = Math.min(...weights);
|
const minWeight = Math.min(...weights);
|
||||||
const maxWeight = Math.max(...weights);
|
const maxWeight = Math.max(...weights);
|
||||||
@@ -110,18 +112,18 @@ export function WeightHistoryCard() {
|
|||||||
const points = sortedHistory.map((item, index) => {
|
const points = sortedHistory.map((item, index) => {
|
||||||
const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING);
|
const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING);
|
||||||
const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange;
|
const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange;
|
||||||
// 减少顶部边距,压缩留白
|
// Reduce top margin, compress whitespace
|
||||||
const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16);
|
const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16);
|
||||||
return { x, y, weight: item.weight, date: item.createdAt };
|
return { x, y, weight: item.weight, date: item.createdAt };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成路径
|
// Generate path
|
||||||
const pathData = points.map((point, index) => {
|
const pathData = points.map((point, index) => {
|
||||||
if (index === 0) return `M ${point.x} ${point.y}`;
|
if (index === 0) return `M ${point.x} ${point.y}`;
|
||||||
return `L ${point.x} ${point.y}`;
|
return `L ${point.x} ${point.y}`;
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
|
|
||||||
// 如果只有一个数据点,显示为水平线
|
// If there's only one data point, display as a horizontal line
|
||||||
const singlePointPath = points.length === 1 ?
|
const singlePointPath = points.length === 1 ?
|
||||||
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
|
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
|
||||||
pathData;
|
pathData;
|
||||||
@@ -133,7 +135,7 @@ export function WeightHistoryCard() {
|
|||||||
source={require('@/assets/images/icons/icon-weight.png')}
|
source={require('@/assets/images/icons/icon-weight.png')}
|
||||||
style={styles.iconSquare}
|
style={styles.iconSquare}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.cardTitle}>体重记录</Text>
|
<Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||||
{isLgAvaliable ? (
|
{isLgAvaliable ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={(e) => {
|
onPress={(e) => {
|
||||||
@@ -160,13 +162,13 @@ export function WeightHistoryCard() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 默认显示图表 */}
|
{/* Default chart display */}
|
||||||
{sortedHistory.length > 0 && (
|
{sortedHistory.length > 0 && (
|
||||||
<View style={styles.chartContainer}>
|
<View style={styles.chartContainer}>
|
||||||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||||||
{/* 背景网格线 */}
|
{/* Background grid lines */}
|
||||||
|
|
||||||
{/* 更抽象的折线 - 减小线宽和显示的细节 */}
|
{/* More abstract line - reduce line width and display details */}
|
||||||
<Path
|
<Path
|
||||||
d={singlePointPath}
|
d={singlePointPath}
|
||||||
stroke={Colors.light.accentGreen}
|
stroke={Colors.light.accentGreen}
|
||||||
@@ -177,7 +179,7 @@ export function WeightHistoryCard() {
|
|||||||
opacity={0.8}
|
opacity={0.8}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 简化的数据点 - 更小更精致 */}
|
{/* Simplified data points - smaller and more refined */}
|
||||||
{points.map((point, index) => {
|
{points.map((point, index) => {
|
||||||
const isLastPoint = index === points.length - 1;
|
const isLastPoint = index === points.length - 1;
|
||||||
|
|
||||||
@@ -197,13 +199,13 @@ export function WeightHistoryCard() {
|
|||||||
|
|
||||||
</Svg>
|
</Svg>
|
||||||
|
|
||||||
{/* 精简的图表信息 */}
|
{/* Concise chart information */}
|
||||||
<View style={styles.chartInfo}>
|
<View style={styles.chartInfo}>
|
||||||
<View style={styles.infoItem}>
|
<View style={styles.infoItem}>
|
||||||
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
|
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoItem}>
|
<View style={styles.infoItem}>
|
||||||
<Text style={styles.infoLabel}>{sortedHistory.length}天</Text>
|
<Text style={styles.infoLabel}>{sortedHistory.length}{t('statistics.components.weight.days')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoItem}>
|
<View style={styles.infoItem}>
|
||||||
<Text style={styles.infoLabel}>
|
<Text style={styles.infoLabel}>
|
||||||
@@ -214,7 +216,7 @@ export function WeightHistoryCard() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* BMI 信息弹窗 */}
|
{/* BMI information modal */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={showBMIModal}
|
visible={showBMIModal}
|
||||||
animationType="slide"
|
animationType="slide"
|
||||||
@@ -228,31 +230,31 @@ export function WeightHistoryCard() {
|
|||||||
end={{ x: 0, y: 1 }}
|
end={{ x: 0, y: 1 }}
|
||||||
>
|
>
|
||||||
<ScrollView style={styles.bmiModalContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.bmiModalContent} showsVerticalScrollIndicator={false}>
|
||||||
{/* 标题 */}
|
{/* Title */}
|
||||||
<Text style={styles.bmiModalTitle}>BMI 指数说明</Text>
|
<Text style={styles.bmiModalTitle}>{t('statistics.components.weight.bmiModal.title')}</Text>
|
||||||
|
|
||||||
{/* 介绍部分 */}
|
{/* Introduction section */}
|
||||||
<View style={styles.bmiModalIntroSection}>
|
<View style={styles.bmiModalIntroSection}>
|
||||||
<Text style={styles.bmiModalDescription}>
|
<Text style={styles.bmiModalDescription}>
|
||||||
BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标
|
{t('statistics.components.weight.bmiModal.description')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.bmiModalFormulaContainer}>
|
<View style={styles.bmiModalFormulaContainer}>
|
||||||
<Text style={styles.bmiModalFormulaText}>
|
<Text style={styles.bmiModalFormulaText}>
|
||||||
计算公式:体重(kg) ÷ 身高²(m)
|
{t('statistics.components.weight.bmiModal.formula')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* BMI 分类标准 */}
|
{/* BMI classification standards */}
|
||||||
<Text style={styles.bmiModalSectionTitle}>BMI 分类标准</Text>
|
<Text style={styles.bmiModalSectionTitle}>{t('statistics.components.weight.bmiModal.classificationTitle')}</Text>
|
||||||
|
|
||||||
<View style={styles.bmiModalStatsCard}>
|
<View style={styles.bmiModalStatsCard}>
|
||||||
{BMI_CATEGORIES.map((category, index) => {
|
{BMI_CATEGORIES.map((category, index) => {
|
||||||
const colors = [
|
const colors = [
|
||||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦
|
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // Underweight
|
||||||
{ bg: '#E8F5E8', text: Colors.light.accentGreen, border: Colors.light.accentGreen }, // 正常
|
{ bg: '#E8F5E8', text: Colors.light.accentGreen, border: Colors.light.accentGreen }, // Normal
|
||||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重
|
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // Overweight
|
||||||
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
|
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // Obese
|
||||||
][index];
|
][index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -273,41 +275,41 @@ export function WeightHistoryCard() {
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 健康建议 */}
|
{/* Health tips */}
|
||||||
<Text style={styles.bmiModalSectionTitle}>健康建议</Text>
|
<Text style={styles.bmiModalSectionTitle}>{t('statistics.components.weight.bmiModal.healthTipsTitle')}</Text>
|
||||||
<View style={styles.bmiModalHealthTips}>
|
<View style={styles.bmiModalHealthTips}>
|
||||||
<View style={styles.bmiModalTipsItem}>
|
<View style={styles.bmiModalTipsItem}>
|
||||||
<Ionicons name="nutrition-outline" size={20} color="#3B82F6" />
|
<Ionicons name="nutrition-outline" size={20} color="#3B82F6" />
|
||||||
<Text style={styles.bmiModalTipsText}>保持均衡饮食,控制热量摄入</Text>
|
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.nutrition')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.bmiModalTipsItem}>
|
<View style={styles.bmiModalTipsItem}>
|
||||||
<Ionicons name="walk-outline" size={20} color="#3B82F6" />
|
<Ionicons name="walk-outline" size={20} color="#3B82F6" />
|
||||||
<Text style={styles.bmiModalTipsText}>每周至少150分钟中等强度运动</Text>
|
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.exercise')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.bmiModalTipsItem}>
|
<View style={styles.bmiModalTipsItem}>
|
||||||
<Ionicons name="moon-outline" size={20} color="#3B82F6" />
|
<Ionicons name="moon-outline" size={20} color="#3B82F6" />
|
||||||
<Text style={styles.bmiModalTipsText}>保证7-9小时充足睡眠</Text>
|
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.sleep')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.bmiModalTipsItem}>
|
<View style={styles.bmiModalTipsItem}>
|
||||||
<Ionicons name="calendar-outline" size={20} color="#3B82F6" />
|
<Ionicons name="calendar-outline" size={20} color="#3B82F6" />
|
||||||
<Text style={styles.bmiModalTipsText}>定期监测体重变化,及时调整</Text>
|
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.monitoring')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 免责声明 */}
|
{/* Disclaimer */}
|
||||||
<View style={styles.bmiModalDisclaimer}>
|
<View style={styles.bmiModalDisclaimer}>
|
||||||
<Ionicons name="information-circle-outline" size={16} color="#6B7280" />
|
<Ionicons name="information-circle-outline" size={16} color="#6B7280" />
|
||||||
<Text style={styles.bmiModalDisclaimerText}>
|
<Text style={styles.bmiModalDisclaimerText}>
|
||||||
BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。
|
{t('statistics.components.weight.bmiModal.disclaimer')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* 底部继续按钮 */}
|
{/* Bottom continue button */}
|
||||||
<View style={styles.bmiModalBottomContainer}>
|
<View style={styles.bmiModalBottomContainer}>
|
||||||
<TouchableOpacity style={styles.bmiModalContinueButton} onPress={handleHideBMIModal}>
|
<TouchableOpacity style={styles.bmiModalContinueButton} onPress={handleHideBMIModal}>
|
||||||
<View style={styles.bmiModalButtonBackground}>
|
<View style={styles.bmiModalButtonBackground}>
|
||||||
<Text style={styles.bmiModalButtonText}>继续</Text>
|
<Text style={styles.bmiModalButtonText}>{t('statistics.components.weight.bmiModal.continueButton')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.bmiModalHomeIndicator} />
|
<View style={styles.bmiModalHomeIndicator} />
|
||||||
@@ -429,7 +431,7 @@ const styles = StyleSheet.create({
|
|||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
|
|
||||||
// BMI 弹窗样式
|
// BMI modal styles
|
||||||
bmiModalContainer: {
|
bmiModalContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
10
hooks/useI18n.ts
Normal file
10
hooks/useI18n.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const useI18n = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
i18n,
|
||||||
|
};
|
||||||
|
};
|
||||||
1244
i18n/index.ts
1244
i18n/index.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user