Add Chinese translations for medication management and personal settings

- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
This commit is contained in:
richarjiang
2025-11-28 17:29:51 +08:00
parent fbe0c92f0f
commit bca6670390
42 changed files with 7972 additions and 6632 deletions

View File

@@ -602,7 +602,7 @@ const styles = StyleSheet.create({
marginBottom: 26,
},
title: {
fontSize: 28,
fontSize: 24,
fontWeight: '700',
letterSpacing: 1,
fontFamily: 'AliBold'

View File

@@ -392,7 +392,7 @@ export default function CircumferenceDetailScreen() {
styles.legendText,
!isVisible && styles.legendTextHidden
]}>
{t(`circumferenceDetail.measurements.${type.key.replace('Circumference', '').toLowerCase()}`)}
{t(`circumferenceDetail.measurements.${type.key.replace('Circumference', '')}`)}
</Text>
</TouchableOpacity>
);

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes';
import { useAppSelector } from '@/hooks/redux';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { addDietRecord, type CreateDietRecordDto, type MealType } from '@/services/dietRecords';
import { selectFoodRecognitionResult } from '@/store/foodRecognitionSlice';
@@ -65,15 +66,8 @@ const mockFoodItems = [
}
];
// 餐次映射
const MEAL_TYPE_MAP = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐'
};
export default function FoodAnalysisResultScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
const router = useRouter();
const params = useLocalSearchParams<{
@@ -190,6 +184,15 @@ export default function FoodAnalysisResultScreen() {
}
};
// 餐次映射
const MEAL_TYPE_MAP = {
breakfast: t('nutritionRecords.mealTypes.breakfast'),
lunch: t('nutritionRecords.mealTypes.lunch'),
dinner: t('nutritionRecords.mealTypes.dinner'),
snack: t('nutritionRecords.mealTypes.snack'),
other: t('nutritionRecords.mealTypes.other'),
};
// 计算所有食物的总营养数据
const totalCalories = foodItems.reduce((sum, item) => sum + item.calories, 0);
const totalProtein = foodItems.reduce((sum, item) => sum + item.protein, 0);
@@ -253,24 +256,24 @@ export default function FoodAnalysisResultScreen() {
// 餐次选择选项
const mealOptions = [
{ key: 'breakfast' as const, label: '早餐', color: '#FF6B35' },
{ key: 'lunch' as const, label: '午餐', color: '#4CAF50' },
{ key: 'dinner' as const, label: '晚餐', color: '#2196F3' },
{ key: 'snack' as const, label: '加餐', color: '#FF9800' },
{ key: 'breakfast' as const, label: t('nutritionRecords.mealTypes.breakfast'), color: '#FF6B35' },
{ key: 'lunch' as const, label: t('nutritionRecords.mealTypes.lunch'), color: '#4CAF50' },
{ key: 'dinner' as const, label: t('nutritionRecords.mealTypes.dinner'), color: '#2196F3' },
{ key: 'snack' as const, label: t('nutritionRecords.mealTypes.snack'), color: '#FF9800' },
];
if (!imageUri && !recognitionResult) {
return (
<View style={styles.container}>
<HeaderBar
title="分析结果"
title={t('foodAnalysisResult.title')}
onBack={() => router.back()}
/>
<View style={{
paddingTop: safeAreaTop
}} />
<View style={styles.errorContainer}>
<Text style={styles.errorText}></Text>
<Text style={styles.errorText}>{t('foodAnalysisResult.error.notFound')}</Text>
</View>
</View>
);
@@ -287,7 +290,7 @@ export default function FoodAnalysisResultScreen() {
/>
<HeaderBar
title="分析结果"
title={t('foodAnalysisResult.title')}
onBack={() => router.back()}
transparent={true}
/>
@@ -316,7 +319,7 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.placeholderContainer}>
<View style={styles.placeholderContent}>
<Ionicons name="restaurant-outline" size={48} color="#666" />
<Text style={styles.placeholderText}></Text>
<Text style={styles.placeholderText}>{t('foodAnalysisResult.placeholder')}</Text>
</View>
</View>
)}
@@ -325,8 +328,8 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.descriptionBubble}>
<Text style={styles.descriptionText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
dayjs().format('YYYY年M月D日')
t('foodAnalysisResult.confidence', { value: recognitionResult.confidence }) :
dayjs().format(t('foodAnalysisResult.dateFormats.today'))
}
</Text>
</View>
@@ -337,31 +340,31 @@ export default function FoodAnalysisResultScreen() {
{/* 卡路里 */}
<View style={styles.calorieSection}>
<Text style={styles.calorieValue}>{totalCalories}</Text>
<Text style={styles.calorieUnit}></Text>
<Text style={styles.calorieUnit}>{t('foodAnalysisResult.nutrients.caloriesUnit')}</Text>
</View>
{/* 营养圆环图 */}
<View style={styles.nutritionRings}>
<NutritionRing
label="蛋白质"
label={t('foodAnalysisResult.nutrients.protein')}
value={totalProtein.toFixed(1)}
unit="克"
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, proteinPercentage)}
color="#4CAF50"
resetToken={animationTrigger}
/>
<NutritionRing
label="脂肪"
label={t('foodAnalysisResult.nutrients.fat')}
value={totalFat.toFixed(1)}
unit="克"
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, fatPercentage)}
color="#FF9800"
resetToken={animationTrigger}
/>
<NutritionRing
label="碳水"
label={t('foodAnalysisResult.nutrients.carbs')}
value={totalCarbohydrate.toFixed(1)}
unit="克"
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, carbohydratePercentage)}
color="#2196F3"
resetToken={animationTrigger}
@@ -372,7 +375,7 @@ export default function FoodAnalysisResultScreen() {
{/* 食物摄入部分 */}
<View style={styles.foodIntakeSection}>
<Text style={styles.foodIntakeTitle}>
{recognitionResult ? '识别结果' : '食物摄入'}
{recognitionResult ? t('foodAnalysisResult.sections.recognitionResult') : t('foodAnalysisResult.sections.foodIntake')}
</Text>
{recognitionResult && recognitionResult.analysisText && (
<Text style={styles.analysisText}>{recognitionResult.analysisText}</Text>
@@ -384,15 +387,15 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.nonFoodIcon}>
<Ionicons name="alert-circle-outline" size={48} color="#FF9800" />
</View>
<Text style={styles.nonFoodTitle}></Text>
<Text style={styles.nonFoodTitle}>{t('foodAnalysisResult.nonFood.title')}</Text>
<Text style={styles.nonFoodMessage}>
{recognitionResult.nonFoodMessage || recognitionResult.analysisText}
</Text>
<View style={styles.nonFoodSuggestions}>
<Text style={styles.nonFoodSuggestionsTitle}></Text>
<Text style={styles.nonFoodSuggestionItem}> </Text>
<Text style={styles.nonFoodSuggestionItem}> </Text>
<Text style={styles.nonFoodSuggestionItem}> 线</Text>
<Text style={styles.nonFoodSuggestionsTitle}>{t('foodAnalysisResult.nonFood.suggestions.title')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item1')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item2')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item3')}</Text>
</View>
</View>
)}
@@ -411,7 +414,7 @@ export default function FoodAnalysisResultScreen() {
</View>
<View style={styles.foodIntakeCalories}>
<Text style={styles.foodIntakeCaloriesValue}>{item.calories}</Text>
<Text style={styles.foodIntakeCaloriesValue}>{item.calories}{t('foodAnalysisResult.nutrients.caloriesUnit')}</Text>
{shouldHideRecordBar ? null : <TouchableOpacity
style={styles.editButton}
onPress={() => handleEditFood(item)}
@@ -442,7 +445,7 @@ export default function FoodAnalysisResultScreen() {
activeOpacity={0.8}
>
<Ionicons name="camera-outline" size={20} color={Colors.light.onPrimary} style={{ marginRight: 8 }} />
<Text style={styles.retakePhotoButtonText}></Text>
<Text style={styles.retakePhotoButtonText}>{t('foodAnalysisResult.actions.retake')}</Text>
</TouchableOpacity>
</View>
) : (
@@ -471,7 +474,7 @@ export default function FoodAnalysisResultScreen() {
{isRecording ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
<Text style={styles.recordButtonText}></Text>
<Text style={styles.recordButtonText}>{t('foodAnalysisResult.actions.record')}</Text>
)}
</TouchableOpacity>
</View>
@@ -492,7 +495,7 @@ export default function FoodAnalysisResultScreen() {
/>
<View style={styles.mealSelectorModal}>
<View style={styles.mealSelectorHeader}>
<Text style={styles.mealSelectorTitle}></Text>
<Text style={styles.mealSelectorTitle}>{t('foodAnalysisResult.mealSelector.title')}</Text>
<TouchableOpacity onPress={() => setShowMealSelector(false)}>
<Ionicons name="close" size={24} color="#666" />
</TouchableOpacity>
@@ -539,8 +542,8 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
dayjs().format('YYYY年M月D日 HH:mm')
t('foodAnalysisResult.confidence', { value: recognitionResult.confidence }) :
dayjs().format(t('foodAnalysisResult.dateFormats.full'))
}
</Text>
</View>
@@ -551,7 +554,7 @@ export default function FoodAnalysisResultScreen() {
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
<Text style={styles.imageViewerFooterButtonText}>{t('foodAnalysisResult.actions.close')}</Text>
</TouchableOpacity>
</View>
)}
@@ -587,6 +590,8 @@ function FoodEditModal({
onFormDataChange({ ...formData, [field]: value });
};
const { t } = useI18n();
return (
<Modal
visible={visible}
@@ -598,14 +603,14 @@ function FoodEditModal({
<View style={styles.editModalSheet}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}></Text>
<Text style={styles.modalTitle}>{t('foodAnalysisResult.editModal.title')}</Text>
{/* 食物名称 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}></Text>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.name')}</Text>
<TextInput
style={styles.editInput}
placeholder="输入食物名称"
placeholder={t('foodAnalysisResult.editModal.fields.namePlaceholder')}
placeholderTextColor="#999"
value={formData.name}
onChangeText={(value) => handleFieldChange('name', value)}
@@ -615,10 +620,10 @@ function FoodEditModal({
{/* 重量/数量 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}> ()</Text>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.amount')}</Text>
<TextInput
style={styles.editInput}
placeholder="输入重量"
placeholder={t('foodAnalysisResult.editModal.fields.amountPlaceholder')}
placeholderTextColor="#999"
value={formData.amount}
onChangeText={(value) => handleFieldChange('amount', value)}
@@ -628,10 +633,10 @@ function FoodEditModal({
{/* 卡路里 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}> ()</Text>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.calories')}</Text>
<TextInput
style={styles.editInput}
placeholder="输入卡路里"
placeholder={t('foodAnalysisResult.editModal.fields.caloriesPlaceholder')}
placeholderTextColor="#999"
value={formData.calories}
onChangeText={(value) => handleFieldChange('calories', value)}
@@ -645,13 +650,13 @@ function FoodEditModal({
onPress={onClose}
style={styles.modalCancelBtn}
>
<Text style={styles.modalCancelText}></Text>
<Text style={styles.modalCancelText}>{t('foodAnalysisResult.editModal.actions.cancel')}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onSave}
style={[styles.modalSaveBtn, { backgroundColor: Colors.light.primary }]}
>
<Text style={[styles.modalSaveText, { color: Colors.light.onPrimary }]}></Text>
<Text style={[styles.modalSaveText, { color: Colors.light.onPrimary }]}>{t('foodAnalysisResult.editModal.actions.save')}</Text>
</TouchableOpacity>
</View>
</View>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
deleteNutritionAnalysisRecord,
@@ -30,6 +31,7 @@ import {
import ImageViewing from 'react-native-image-viewing';
export default function NutritionAnalysisHistoryScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop();
const router = useRouter();
@@ -95,15 +97,15 @@ export default function NutritionAnalysisHistoryScreen() {
setHasMore(page < response.data.totalPages);
setCurrentPage(page);
} else {
const errorMessage = response.message || '获取历史记录失败';
const errorMessage = response.message || t('nutritionAnalysisHistory.errors.fetchFailed');
setError(errorMessage);
Alert.alert('错误', errorMessage);
Alert.alert(t('nutritionAnalysisHistory.errors.error'), errorMessage);
}
} catch (error) {
console.error('[HISTORY] 获取历史记录失败:', error);
const errorMessage = '获取历史记录失败,请稍后重试';
const errorMessage = t('nutritionAnalysisHistory.errors.fetchFailedRetry');
setError(errorMessage);
Alert.alert('错误', errorMessage);
Alert.alert(t('nutritionAnalysisHistory.errors.error'), errorMessage);
} finally {
setLoading(false);
setRefreshing(false);
@@ -173,13 +175,13 @@ export default function NutritionAnalysisHistoryScreen() {
const getStatusText = (status: string) => {
switch (status) {
case 'success':
return '成功';
return t('nutritionAnalysisHistory.status.success');
case 'failed':
return '失败';
return t('nutritionAnalysisHistory.status.failed');
case 'processing':
return '处理中';
return t('nutritionAnalysisHistory.status.processing');
default:
return '未知';
return t('nutritionAnalysisHistory.status.unknown');
}
};
@@ -208,15 +210,15 @@ export default function NutritionAnalysisHistoryScreen() {
// 处理删除记录
const handleDeleteRecord = useCallback((recordId: number) => {
Alert.alert(
'确认删除',
'确定要删除这条营养分析记录吗?此操作无法撤销。',
t('nutritionAnalysisHistory.delete.confirmTitle'),
t('nutritionAnalysisHistory.delete.confirmMessage'),
[
{
text: '取消',
text: t('nutritionAnalysisHistory.delete.cancel'),
style: 'cancel',
},
{
text: '删除',
text: t('nutritionAnalysisHistory.delete.delete'),
style: 'destructive',
onPress: async () => {
try {
@@ -231,10 +233,10 @@ export default function NutritionAnalysisHistoryScreen() {
triggerLightHaptic();
// 显示成功提示
Alert.alert('成功', '记录已删除');
Alert.alert(t('nutritionAnalysisHistory.delete.successTitle'), t('nutritionAnalysisHistory.delete.successMessage'));
} catch (error) {
console.error('[HISTORY] 删除记录失败:', error);
Alert.alert('错误', '删除失败,请稍后重试');
Alert.alert(t('nutritionAnalysisHistory.errors.error'), t('nutritionAnalysisHistory.errors.deleteFailed'));
} finally {
setDeletingId(null);
}
@@ -256,11 +258,11 @@ export default function NutritionAnalysisHistoryScreen() {
<View style={styles.recordInfo}>
{isSuccess && (
<Text style={styles.recordTitle}>
{item.nutritionCount}
{t('nutritionAnalysisHistory.recognized', { count: item.nutritionCount })}
</Text>
)}
<Text style={styles.recordDate}>
{dayjs(item.createdAt).format('YYYY年M月D日 HH:mm')}
{dayjs(item.createdAt).format(t('nutritionAnalysisHistory.dateFormat'))}
</Text>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>{getStatusText(item.status)}</Text>
@@ -327,25 +329,25 @@ export default function NutritionAnalysisHistoryScreen() {
<>
{mainNutrients.energy && (
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionLabel}>{t('nutritionAnalysisHistory.nutrients.energy')}</Text>
<Text style={styles.nutritionValue}>{mainNutrients.energy}</Text>
</View>
)}
{mainNutrients.protein && (
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionLabel}>{t('nutritionAnalysisHistory.nutrients.protein')}</Text>
<Text style={styles.nutritionValue}>{mainNutrients.protein}</Text>
</View>
)}
{mainNutrients.carbs && (
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionLabel}>{t('nutritionAnalysisHistory.nutrients.carbs')}</Text>
<Text style={styles.nutritionValue}>{mainNutrients.carbs}</Text>
</View>
)}
{mainNutrients.fat && (
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionLabel}>{t('nutritionAnalysisHistory.nutrients.fat')}</Text>
<Text style={styles.nutritionValue}>{mainNutrients.fat}</Text>
</View>
)}
@@ -371,7 +373,7 @@ export default function NutritionAnalysisHistoryScreen() {
activeOpacity={0.7}
>
<Text style={styles.expandButtonText}>
{isExpanded ? '收起详情' : '展开详情'}
{isExpanded ? t('nutritionAnalysisHistory.actions.collapse') : t('nutritionAnalysisHistory.actions.expand')}
</Text>
<Ionicons
name={isExpanded ? 'chevron-up-outline' : 'chevron-down-outline'}
@@ -383,7 +385,7 @@ export default function NutritionAnalysisHistoryScreen() {
{/* 详细信息 */}
{isExpanded && isSuccess && item.analysisResult && item.analysisResult.data && (
<View style={styles.detailsContainer}>
<Text style={styles.detailsTitle}></Text>
<Text style={styles.detailsTitle}>{t('nutritionAnalysisHistory.details.title')}</Text>
{item.analysisResult.data.map((nutritionItem: NutritionItem) => (
<View key={nutritionItem.key} style={styles.detailItem}>
<View style={styles.nutritionInfo}>
@@ -397,8 +399,8 @@ export default function NutritionAnalysisHistoryScreen() {
))}
<View style={styles.metaInfo}>
<Text style={styles.metaText}>AI : {item.aiModel}</Text>
<Text style={styles.metaText}>: {item.aiProvider}</Text>
<Text style={styles.metaText}>{t('nutritionAnalysisHistory.details.aiModel')}: {item.aiModel}</Text>
<Text style={styles.metaText}>{t('nutritionAnalysisHistory.details.provider')}: {item.aiProvider}</Text>
</View>
</View>
)}
@@ -410,8 +412,8 @@ export default function NutritionAnalysisHistoryScreen() {
const renderEmptyState = () => (
<View style={styles.emptyState}>
<Ionicons name="document-text-outline" size={64} color="#CCC" />
<Text style={styles.emptyStateText}></Text>
<Text style={styles.emptyStateSubtext}></Text>
<Text style={styles.emptyStateText}>{t('nutritionAnalysisHistory.empty.title')}</Text>
<Text style={styles.emptyStateSubtext}>{t('nutritionAnalysisHistory.empty.subtitle')}</Text>
</View>
);
@@ -419,8 +421,8 @@ export default function NutritionAnalysisHistoryScreen() {
const renderErrorState = () => (
<View style={styles.errorState}>
<Ionicons name="alert-circle-outline" size={64} color="#F44336" />
<Text style={styles.errorStateText}></Text>
<Text style={styles.errorStateSubtext}>{error || '未知错误'}</Text>
<Text style={styles.errorStateText}>{t('nutritionAnalysisHistory.errors.loadFailed')}</Text>
<Text style={styles.errorStateSubtext}>{error || t('nutritionAnalysisHistory.errors.unknownError')}</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => {
@@ -428,7 +430,7 @@ export default function NutritionAnalysisHistoryScreen() {
fetchRecords(1, true);
}}
>
<Text style={styles.retryButtonText}></Text>
<Text style={styles.retryButtonText}>{t('nutritionAnalysisHistory.actions.retry')}</Text>
</TouchableOpacity>
</View>
);
@@ -440,7 +442,7 @@ export default function NutritionAnalysisHistoryScreen() {
return (
<View style={styles.loadingFooter}>
<ActivityIndicator size="small" color={Colors.light.primary} />
<Text style={styles.loadingFooterText}>...</Text>
<Text style={styles.loadingFooterText}>{t('nutritionAnalysisHistory.loadingMore')}</Text>
</View>
);
};
@@ -456,7 +458,7 @@ export default function NutritionAnalysisHistoryScreen() {
/>
<HeaderBar
title="历史记录"
title={t('nutritionAnalysisHistory.title')}
onBack={() => router.back()}
transparent={true}
/>
@@ -477,7 +479,7 @@ export default function NutritionAnalysisHistoryScreen() {
activeOpacity={0.7}
>
<Text style={[styles.filterButtonText, !statusFilter && styles.filterButtonTextActive]}>
{t('nutritionAnalysisHistory.filter.all')}
</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -494,7 +496,7 @@ export default function NutritionAnalysisHistoryScreen() {
activeOpacity={0.7}
>
<Text style={[styles.filterButtonText, statusFilter === 'success' && styles.filterButtonTextActive]}>
{t('nutritionAnalysisHistory.status.success')}
</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -511,7 +513,7 @@ export default function NutritionAnalysisHistoryScreen() {
activeOpacity={0.7}
>
<Text style={[styles.filterButtonText, statusFilter === 'failed' && styles.filterButtonTextActive]}>
{t('nutritionAnalysisHistory.status.failed')}
</Text>
</TouchableOpacity>
</View>
@@ -520,7 +522,7 @@ export default function NutritionAnalysisHistoryScreen() {
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>...</Text>
<Text style={styles.loadingText}>{t('nutritionAnalysisHistory.loading')}</Text>
</View>
) : (
<FlatList
@@ -555,7 +557,7 @@ export default function NutritionAnalysisHistoryScreen() {
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{dayjs().format('YYYY年M月D日 HH:mm')}
{dayjs().format(t('nutritionAnalysisHistory.dateFormat'))}
</Text>
</View>
)}
@@ -565,7 +567,7 @@ export default function NutritionAnalysisHistoryScreen() {
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
<Text style={styles.imageViewerFooterButtonText}>{t('nutritionLabelAnalysis.actions.close')}</Text>
</TouchableOpacity>
</View>
)}

View File

@@ -2,6 +2,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useCosUpload } from '@/hooks/useCosUpload';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
analyzeNutritionImage,
@@ -29,6 +30,7 @@ import {
import ImageViewing from 'react-native-image-viewing';
export default function NutritionLabelAnalysisScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop();
const router = useRouter();
const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
@@ -77,7 +79,7 @@ export default function NutritionLabelAnalysisScreen() {
const requestCameraPermission = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert('权限不足', '需要相机权限才能拍摄成分表');
Alert.alert(t('nutritionLabelAnalysis.camera.permissionDenied'), t('nutritionLabelAnalysis.camera.permissionMessage'));
return false;
}
return true;
@@ -153,7 +155,7 @@ export default function NutritionLabelAnalysisScreen() {
// 直接使用服务端返回的数据,不做任何转换
setNewAnalysisResult(analysisResponse);
} else {
throw new Error(analysisResponse.message || '分析失败');
throw new Error(analysisResponse.message || t('nutritionLabelAnalysis.errors.analysisFailed.defaultMessage'));
}
} catch (error: any) {
console.error('[NUTRITION_ANALYSIS] 新API分析失败:', error);
@@ -162,8 +164,8 @@ export default function NutritionLabelAnalysisScreen() {
// 显示错误提示
Alert.alert(
'分析失败',
error.message || '无法识别成分表,请尝试拍摄更清晰的照片'
t('nutritionLabelAnalysis.errors.analysisFailed.title'),
error.message || t('nutritionLabelAnalysis.errors.analysisFailed.message')
);
} finally {
setIsUploading(false);
@@ -182,7 +184,7 @@ export default function NutritionLabelAnalysisScreen() {
/>
<HeaderBar
title="成分表分析"
title={t('nutritionLabelAnalysis.title')}
onBack={() => router.back()}
transparent={true}
right={
@@ -253,7 +255,7 @@ export default function NutritionLabelAnalysisScreen() {
activeOpacity={0.8}
>
<Ionicons name="search-outline" size={20} color="#FFF" />
<Text style={styles.analyzeButtonText}></Text>
<Text style={styles.analyzeButtonText}>{t('nutritionLabelAnalysis.actions.startAnalysis')}</Text>
</TouchableOpacity>
)}
@@ -274,7 +276,7 @@ export default function NutritionLabelAnalysisScreen() {
<View style={styles.placeholderContainer}>
<View style={styles.placeholderContent}>
<Ionicons name="document-text-outline" size={48} color="#666" />
<Text style={styles.placeholderText}></Text>
<Text style={styles.placeholderText}>{t('nutritionLabelAnalysis.placeholder.text')}</Text>
</View>
{/* 操作按钮区域 */}
<View style={styles.imageActionButtonsContainer}>
@@ -284,7 +286,7 @@ export default function NutritionLabelAnalysisScreen() {
activeOpacity={0.8}
>
<Ionicons name="camera-outline" size={20} color={Colors.light.onPrimary} />
<Text style={styles.imageActionButtonText}></Text>
<Text style={styles.imageActionButtonText}>{t('nutritionLabelAnalysis.actions.takePhoto')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.imageActionButton, styles.imageActionButtonSecondary]}
@@ -292,7 +294,7 @@ export default function NutritionLabelAnalysisScreen() {
activeOpacity={0.8}
>
<Ionicons name="image-outline" size={20} color={Colors.light.primary} />
<Text style={[styles.imageActionButtonText, { color: Colors.light.primary }]}></Text>
<Text style={[styles.imageActionButtonText, { color: Colors.light.primary }]}>{t('nutritionLabelAnalysis.actions.selectFromAlbum')}</Text>
</TouchableOpacity>
</View>
</View>
@@ -307,7 +309,7 @@ export default function NutritionLabelAnalysisScreen() {
<View style={styles.analysisSectionHeaderIcon}>
<Ionicons name="document-text-outline" size={18} color="#6B6ED6" />
</View>
<Text style={styles.analysisSectionTitle}></Text>
<Text style={styles.analysisSectionTitle}>{t('nutritionLabelAnalysis.results.title')}</Text>
</View>
<View style={styles.analysisCardsWrapper}>
{newAnalysisResult.data.map((item, index) => (
@@ -352,7 +354,7 @@ export default function NutritionLabelAnalysisScreen() {
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>
... {uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ''}
{t('nutritionLabelAnalysis.status.uploading')} {uploadProgress > 0 ? `${Math.round(uploadProgress)}%` : ''}
</Text>
</View>
)}
@@ -361,7 +363,7 @@ export default function NutritionLabelAnalysisScreen() {
{isAnalyzing && !newAnalysisResult && !isUploading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>...</Text>
<Text style={styles.loadingText}>{t('nutritionLabelAnalysis.status.analyzing')}</Text>
</View>
)}
</ScrollView>
@@ -377,7 +379,7 @@ export default function NutritionLabelAnalysisScreen() {
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{dayjs().format('YYYY年M月D日 HH:mm')}
{dayjs().format(t('nutritionLabelAnalysis.imageViewer.dateFormat'))}
</Text>
</View>
)}
@@ -387,7 +389,7 @@ export default function NutritionLabelAnalysisScreen() {
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
<Text style={styles.imageViewerFooterButtonText}>{t('nutritionLabelAnalysis.actions.close')}</Text>
</TouchableOpacity>
</View>
)}
@@ -514,7 +516,7 @@ const styles = StyleSheet.create({
},
imageActionButtonText: {
color: Colors.light.onPrimary,
fontSize: 14,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
},

View File

@@ -559,19 +559,17 @@ export default function MedicationDetailScreen() {
const formLabel = medication ? t(`medications.manage.formLabels.${medication.form}`) : '';
const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--';
const startDateLabel = medication
? dayjs(medication.startDate).format('YYYY年M月D日')
: '--';
// 计算服药周期显示
const medicationPeriodLabel = useMemo(() => {
if (!medication) return '--';
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
const format = t('medications.detail.plan.dateFormat', { defaultValue: 'YYYY年M月D日' });
const startDate = dayjs(medication.startDate).format(format);
if (medication.endDate) {
// 有结束日期,显示开始日期到结束日期
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
const endDate = dayjs(medication.endDate).format(format);
return `${startDate} - ${endDate}`;
} else {
// 没有结束日期,显示长期
@@ -581,22 +579,23 @@ export default function MedicationDetailScreen() {
// 计算有效期显示
const expiryDateLabel = useMemo(() => {
if (!medication?.expiryDate) return '未设置';
if (!medication?.expiryDate) return t('medications.detail.plan.expiryStatus.notSet');
const expiry = dayjs(medication.expiryDate);
const today = dayjs();
const daysUntilExpiry = expiry.diff(today, 'day');
const format = t('medications.detail.plan.dateFormat', { defaultValue: 'YYYY年M月D日' });
if (daysUntilExpiry < 0) {
return `${expiry.format('YYYY年M月D日')} (已过期)`;
return `${expiry.format(format)} (${t('medications.detail.plan.expiryStatus.expired')})`;
} else if (daysUntilExpiry === 0) {
return `${expiry.format('YYYY年M月D日')} (今天到期)`;
return `${expiry.format(format)} (${t('medications.detail.plan.expiryStatus.expiresToday')})`;
} else if (daysUntilExpiry <= 30) {
return `${expiry.format('YYYY年M月D日')} (${daysUntilExpiry}天后到期)`;
return `${expiry.format(format)} (${t('medications.detail.plan.expiryStatus.expiresInDays', { days: daysUntilExpiry })})`;
} else {
return expiry.format('YYYY年M月D日');
return expiry.format(format);
}
}, [medication?.expiryDate]);
}, [medication?.expiryDate, t]);
const reminderTimes = medication?.medicationTimes?.length
? medication.medicationTimes.join('、')
@@ -617,8 +616,8 @@ export default function MedicationDetailScreen() {
const aiActionLabel = aiAnalysisLoading
? t('medications.detail.aiAnalysis.analyzingButton')
: hasAiAnalysis
? '重新分析'
: '获取 AI 分析';
? t('medications.detail.aiAnalysis.reanalyzeButton')
: t('medications.detail.aiAnalysis.getAnalysisButton');
const handleOpenNoteModal = useCallback(() => {
setNoteDraft(medication?.note ?? '');
@@ -645,15 +644,15 @@ export default function MedicationDetailScreen() {
const trimmed = nameDraft.trim();
if (!trimmed) {
Alert.alert(
'提示',
'药物名称不能为空'
t('common.hint'),
t('medications.detail.name.errors.empty')
);
return;
}
if (Array.from(trimmed).length > 10) {
Alert.alert(
'提示',
'药物名称不能超过10个字'
t('common.hint'),
t('medications.detail.name.errors.tooLong')
);
return;
}
@@ -675,8 +674,8 @@ export default function MedicationDetailScreen() {
} catch (err) {
console.error('更新药物名称失败', err);
Alert.alert(
'提示',
'名称更新失败,请稍后再试'
t('common.hint'),
t('medications.detail.name.errors.updateFailed')
);
} finally {
setNameSaving(false);
@@ -908,16 +907,17 @@ export default function MedicationDetailScreen() {
const handleStartDatePress = useCallback(() => {
if (!medication) return;
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
const format = t('medications.detail.plan.dateFormat', { defaultValue: 'YYYY年M月D日' });
const startDate = dayjs(medication.startDate).format(format);
let message;
if (medication.endDate) {
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
message = `${startDate}${endDate}`;
const endDate = dayjs(medication.endDate).format(format);
message = t('medications.detail.plan.periodRange', { startDate, endDate, defaultValue: `${startDate}${endDate}` });
} else {
message = `${startDate} 至长期`;
message = t('medications.detail.plan.periodLongTerm', { startDate, defaultValue: `${startDate} 至长期` });
}
Alert.alert('服药周期', message);
Alert.alert(t('medications.detail.plan.period'), message);
}, [medication, t]);
const handleTimePress = useCallback(() => {
@@ -990,7 +990,7 @@ export default function MedicationDetailScreen() {
} catch (err) {
console.error('更新有效期失败', err);
Alert.alert('更新失败', '有效期更新失败,请稍后重试');
Alert.alert(t('medications.detail.updateErrors.expiryDate'), t('medications.detail.updateErrors.expiryDateMessage'));
} finally {
setUpdatePending(false);
}
@@ -1185,7 +1185,7 @@ export default function MedicationDetailScreen() {
});
} catch (err: any) {
console.error('[MEDICATION_DETAIL] AI 草稿保存失败', err);
Alert.alert('保存失败', err?.message || '请稍后再试');
Alert.alert(t('medications.detail.aiDraft.saveError.title'), err?.message || t('medications.detail.aiDraft.saveError.message'));
} finally {
setAiDraftSaving(false);
}
@@ -1297,7 +1297,7 @@ export default function MedicationDetailScreen() {
<View style={styles.photoUploadingIndicator}>
<ActivityIndicator color={colors.primary} size="small" />
<Text style={[styles.uploadingText, { color: '#FFF' }]}>
...
{t('medications.detail.photo.uploading')}
</Text>
</View>
)}
@@ -1415,12 +1415,12 @@ export default function MedicationDetailScreen() {
</TouchableOpacity>
</Section>
<Section title="AI 分析" color={colors.text}>
<Section title={t('medications.detail.sections.aiAnalysis')} color={colors.text}>
<View style={[styles.aiCardContainer, { backgroundColor: colors.surface }]}>
<View style={styles.aiHeaderRow}>
<View style={styles.aiHeaderLeft}>
<Ionicons name="sparkles-outline" size={18} color={colors.primary} />
<Text style={[styles.aiHeaderTitle, { color: colors.text }]}></Text>
<Text style={[styles.aiHeaderTitle, { color: colors.text }]}>{t('medications.detail.aiAnalysis.title')}</Text>
</View>
<View
style={[
@@ -1439,7 +1439,7 @@ export default function MedicationDetailScreen() {
{ color: hasAiAnalysis ? '#16a34a' : aiAnalysisLocked ? '#ef4444' : colors.primary },
]}
>
{hasAiAnalysis ? '已生成' : aiAnalysisLocked ? '会员专享' : '待生成'}
{hasAiAnalysis ? t('medications.detail.aiAnalysis.status.generated') : aiAnalysisLocked ? t('medications.detail.aiAnalysis.status.memberExclusive') : t('medications.detail.aiAnalysis.status.pending')}
</Text>
</View>
</View>
@@ -1467,7 +1467,7 @@ export default function MedicationDetailScreen() {
style={styles.aiScoreBadge}
>
<Ionicons name="thumbs-up-outline" size={14} color="#fff" />
<Text style={styles.aiScoreBadgeText}>AI </Text>
<Text style={styles.aiScoreBadgeText}>{t('medications.detail.aiAnalysis.recommendation')}</Text>
</LinearGradient>
)}
</View>
@@ -1476,7 +1476,7 @@ export default function MedicationDetailScreen() {
{medication.name}
</Text>
<Text style={[styles.aiHeroSubtitle, { color: colors.textSecondary }]} numberOfLines={3}>
{aiAnalysisResult?.mainUsage || '获取 AI 分析,快速了解适用人群、成分安全与使用建议。'}
{aiAnalysisResult?.mainUsage || t('medications.detail.aiAnalysis.placeholder')}
</Text>
<View style={styles.aiChipRow}>
<View style={styles.aiChip}>
@@ -1527,7 +1527,7 @@ export default function MedicationDetailScreen() {
<View style={styles.aiColumns}>
<View style={[styles.aiBubbleCard, { backgroundColor: '#ECFEFF', borderColor: '#BAF2F4' }]}>
<View style={styles.aiBubbleHeader}>
<Text style={[styles.aiBubbleTitle, { color: '#0284c7' }]}></Text>
<Text style={[styles.aiBubbleTitle, { color: '#0284c7' }]}>{t('medications.detail.aiAnalysis.categories.suitableFor')}</Text>
<Ionicons name="checkmark-circle" size={16} color="#0ea5e9" />
</View>
{aiAnalysisResult.suitableFor.map((item, idx) => (
@@ -1540,7 +1540,7 @@ export default function MedicationDetailScreen() {
<View style={[styles.aiBubbleCard, { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' }]}>
<View style={styles.aiBubbleHeader}>
<Text style={[styles.aiBubbleTitle, { color: '#ef4444' }]}></Text>
<Text style={[styles.aiBubbleTitle, { color: '#ef4444' }]}>{t('medications.detail.aiAnalysis.categories.unsuitableFor')}</Text>
<Ionicons name="alert-circle" size={16} color="#ef4444" />
</View>
{aiAnalysisResult.unsuitableFor.map((item, idx) => (
@@ -1552,9 +1552,9 @@ export default function MedicationDetailScreen() {
</View>
</View>
{renderAdviceCard('可能的副作用', aiAnalysisResult.sideEffects, 'warning-outline', '#f59e0b', '#FFFBEB')}
{renderAdviceCard('储存建议', aiAnalysisResult.storageAdvice, 'cube-outline', '#10b981', '#ECFDF3')}
{renderAdviceCard('健康/使用建议', aiAnalysisResult.healthAdvice, 'sparkles-outline', '#6366f1', '#EEF2FF')}
{renderAdviceCard(t('medications.detail.aiAnalysis.categories.sideEffects'), aiAnalysisResult.sideEffects, 'warning-outline', '#f59e0b', '#FFFBEB')}
{renderAdviceCard(t('medications.detail.aiAnalysis.categories.storageAdvice'), aiAnalysisResult.storageAdvice, 'cube-outline', '#10b981', '#ECFDF3')}
{renderAdviceCard(t('medications.detail.aiAnalysis.categories.healthAdvice'), aiAnalysisResult.healthAdvice, 'sparkles-outline', '#6366f1', '#EEF2FF')}
</>
) : null}
@@ -1580,8 +1580,8 @@ export default function MedicationDetailScreen() {
<View style={styles.aiMembershipLeft}>
<Ionicons name="diamond-outline" size={18} color="#f59e0b" />
<View>
<Text style={styles.aiMembershipTitle}> AI </Text>
<Text style={styles.aiMembershipSub}>使</Text>
<Text style={styles.aiMembershipTitle}>{t('medications.detail.aiAnalysis.membershipCard.title')}</Text>
<Text style={styles.aiMembershipSub}>{t('medications.detail.aiAnalysis.membershipCard.subtitle')}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color="#f59e0b" />
@@ -1622,22 +1622,67 @@ export default function MedicationDetailScreen() {
{isAiDraft ? (
<View style={styles.footerButtonContainer}>
<TouchableOpacity
style={styles.secondaryFooterBtn}
activeOpacity={0.9}
onPress={() => router.replace('/medications/ai-camera')}
>
<Text style={styles.secondaryFooterText}></Text>
{isLiquidGlassAvailable() ? (
<GlassView
style={[styles.secondaryFooterBtn, { borderWidth: 0, overflow: 'hidden', backgroundColor: 'transparent' }]}
glassEffectStyle="regular"
isInteractive={true}
>
<Text style={styles.secondaryFooterText}>{t('medications.detail.aiDraft.reshoot')}</Text>
</GlassView>
) : (
<View style={styles.secondaryFooterBtn}>
<Text style={styles.secondaryFooterText}>{t('medications.detail.aiDraft.reshoot')}</Text>
</View>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.primaryFooterBtn, { backgroundColor: colors.primary }]}
style={{ flex: 1 }}
activeOpacity={0.9}
onPress={handleAiDraftSave}
disabled={aiDraftSaving}
>
{aiDraftSaving ? (
<ActivityIndicator color={colors.onPrimary} />
{isLiquidGlassAvailable() ? (
<GlassView
style={[
styles.primaryFooterBtn,
{
width: '100%',
shadowOpacity: 0,
backgroundColor: 'transparent',
overflow: 'hidden',
},
]}
glassEffectStyle="regular"
tintColor={colors.primary}
isInteractive={true}
>
{aiDraftSaving ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[styles.primaryFooterText, { color: '#fff' }]}>
{t('medications.detail.aiDraft.saveAndCreate')}
</Text>
)}
</GlassView>
) : (
<Text style={[styles.primaryFooterText, { color: colors.onPrimary }]}></Text>
<View
style={[
styles.primaryFooterBtn,
{ width: '100%', backgroundColor: colors.primary },
]}
>
{aiDraftSaving ? (
<ActivityIndicator color={colors.onPrimary} />
) : (
<Text style={[styles.primaryFooterText, { color: colors.onPrimary }]}>
{t('medications.detail.aiDraft.saveAndCreate')}
</Text>
)}
</View>
)}
</TouchableOpacity>
</View>
@@ -1726,7 +1771,7 @@ export default function MedicationDetailScreen() {
<View style={styles.modalHandle} />
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: colors.text }]}>
{t('medications.detail.name.edit')}
</Text>
<TouchableOpacity onPress={handleCloseNameModal} hitSlop={12}>
<Ionicons name="close" size={20} color={colors.textSecondary} />
@@ -1744,7 +1789,7 @@ export default function MedicationDetailScreen() {
<TextInput
value={nameDraft}
onChangeText={handleNameChange}
placeholder="请输入药物名称"
placeholder={t('medications.detail.name.placeholder')}
placeholderTextColor={colors.textMuted}
style={[styles.nameInput, { color: colors.text }]}
autoFocus
@@ -1777,7 +1822,7 @@ export default function MedicationDetailScreen() {
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}>
{t('common.save')}
</Text>
)}
</TouchableOpacity>
@@ -2599,7 +2644,7 @@ const styles = StyleSheet.create({
elevation: 4,
},
aiScoreBadgeText: {
fontSize: 12,
fontSize: 10,
fontWeight: '700',
color: '#fff',
},

View File

@@ -1,5 +1,6 @@
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { Colors, palette } from '@/constants/Colors';
import { useI18n } from '@/hooks/useI18n';
import { getMedicationRecognitionStatus } from '@/services/medications';
import { MedicationRecognitionTask } from '@/types/medication';
import { Ionicons } from '@expo/vector-icons';
@@ -13,14 +14,15 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const STATUS_STEPS: { key: MedicationRecognitionTask['status']; label: string }[] = [
{ key: 'analyzing_product', label: '正在进行产品分析...' },
{ key: 'analyzing_suitability', label: '正在检测适宜人群...' },
{ key: 'analyzing_ingredients', label: '正在评估成分信息...' },
{ key: 'analyzing_effects', label: '正在生成安全建议...' },
const STEP_KEYS: MedicationRecognitionTask['status'][] = [
'analyzing_product',
'analyzing_suitability',
'analyzing_ingredients',
'analyzing_effects',
];
export default function MedicationAiProgressScreen() {
const { t } = useI18n();
const { taskId, cover } = useLocalSearchParams<{ taskId?: string; cover?: string }>();
const insets = useSafeAreaInsets();
const [task, setTask] = useState<MedicationRecognitionTask | null>(null);
@@ -35,11 +37,16 @@ export default function MedicationAiProgressScreen() {
const floatAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(0.3)).current;
const steps = useMemo(() => STEP_KEYS.map(key => ({
key,
label: t(`medications.aiProgress.steps.${key}`)
})), [t]);
const currentStepIndex = useMemo(() => {
if (!task) return 0;
const idx = STATUS_STEPS.findIndex((step) => step.key === task.status);
const idx = STEP_KEYS.indexOf(task.status as any);
if (idx >= 0) return idx;
if (task.status === 'completed') return STATUS_STEPS.length;
if (task.status === 'completed') return STEP_KEYS.length;
return 0;
}, [task]);
@@ -77,12 +84,12 @@ export default function MedicationAiProgressScreen() {
pollingTimerRef.current = null;
}
// 显示错误提示弹窗
setErrorMessage(data.errorMessage || '识别失败,请重新拍摄');
setErrorMessage(data.errorMessage || t('medications.aiProgress.errors.default'));
setShowErrorModal(true);
}
} catch (err: any) {
console.error('[MEDICATION_AI] status failed', err);
setError(err?.message || '查询失败,请稍后再试');
setError(err?.message || t('medications.aiProgress.errors.queryFailed'));
} finally {
setLoading(false);
}
@@ -148,12 +155,12 @@ export default function MedicationAiProgressScreen() {
};
}, []);
const progress = task?.progress ?? Math.min(100, (currentStepIndex / STATUS_STEPS.length) * 100 + 10);
const progress = task?.progress ?? Math.min(100, (currentStepIndex / steps.length) * 100 + 10);
return (
<SafeAreaView style={styles.container}>
<LinearGradient colors={['#fdfdfd', '#f3f6fb']} style={StyleSheet.absoluteFill} />
<HeaderBar title="识别中" onBack={() => router.back()} transparent />
<LinearGradient colors={[palette.gray[25], palette.gray[50]]} style={StyleSheet.absoluteFill} />
<HeaderBar title={t('medications.aiProgress.title')} onBack={() => router.back()} transparent />
<View style={{ height: insets.top }} />
<View style={styles.heroCard}>
@@ -172,7 +179,7 @@ export default function MedicationAiProgressScreen() {
{/* 渐变蒙版边框,增加视觉层次 */}
<LinearGradient
colors={['rgba(14, 165, 233, 0.3)', 'rgba(6, 182, 212, 0.2)', 'transparent']}
colors={[Colors.light.primary + '4D', Colors.light.accentPurple + '33', 'transparent']}
style={styles.gradientBorder}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
@@ -206,7 +213,7 @@ export default function MedicationAiProgressScreen() {
</View>
<View style={styles.stepList}>
{STATUS_STEPS.map((step, index) => {
{steps.map((step, index) => {
const active = index === currentStepIndex;
const done = index < currentStepIndex;
return (
@@ -221,7 +228,7 @@ export default function MedicationAiProgressScreen() {
{task?.status === 'completed' && (
<View style={styles.stepRow}>
<View style={[styles.bullet, styles.bulletDone]} />
<Text style={[styles.stepLabel, styles.stepLabelDone]}>...</Text>
<Text style={[styles.stepLabel, styles.stepLabelDone]}>{t('medications.aiProgress.steps.completed')}</Text>
</View>
)}
</View>
@@ -251,7 +258,7 @@ export default function MedicationAiProgressScreen() {
<View style={styles.errorModalContent}>
{/* 标题 */}
<Text style={styles.errorModalTitle}></Text>
<Text style={styles.errorModalTitle}>{t('medications.aiProgress.modal.title')}</Text>
{/* 提示信息 */}
<View style={styles.errorMessageBox}>
@@ -268,29 +275,29 @@ export default function MedicationAiProgressScreen() {
<GlassView
style={styles.retryButton}
glassEffectStyle="regular"
tintColor="rgba(14, 165, 233, 0.9)"
tintColor={Colors.light.primary}
isInteractive={true}
>
<LinearGradient
colors={['rgba(14, 165, 233, 0.95)', 'rgba(6, 182, 212, 0.95)']}
colors={[Colors.light.primary, Colors.light.accentPurple]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.retryButtonGradient}
>
<Ionicons name="camera" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}></Text>
<Text style={styles.retryButtonText}>{t('medications.aiProgress.modal.retry')}</Text>
</LinearGradient>
</GlassView>
) : (
<View style={styles.retryButton}>
<LinearGradient
colors={['#0ea5e9', '#06b6d4']}
colors={[Colors.light.primary, Colors.light.accentPurple]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.retryButtonGradient}
>
<Ionicons name="camera" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}></Text>
<Text style={styles.retryButtonText}>{t('medications.aiProgress.modal.retry')}</Text>
</LinearGradient>
</View>
)}
@@ -311,9 +318,9 @@ const styles = StyleSheet.create({
marginHorizontal: 20,
marginTop: 24,
borderRadius: 24,
backgroundColor: '#fff',
backgroundColor: Colors.light.card,
padding: 16,
shadowColor: '#0f172a',
shadowColor: Colors.light.text,
shadowOpacity: 0.08,
shadowRadius: 18,
shadowOffset: { width: 0, height: 10 },
@@ -322,7 +329,7 @@ const styles = StyleSheet.create({
height: 230,
borderRadius: 18,
overflow: 'hidden',
backgroundColor: '#e2e8f0',
backgroundColor: palette.gray[50],
},
heroImage: {
width: '100%',
@@ -330,7 +337,7 @@ const styles = StyleSheet.create({
},
heroPlaceholder: {
flex: 1,
backgroundColor: '#e2e8f0',
backgroundColor: palette.gray[50],
},
// 深色蒙版层,让点阵更清晰可见
overlayMask: {
@@ -368,15 +375,15 @@ const styles = StyleSheet.create({
width: 5,
height: 5,
borderRadius: 2.5,
backgroundColor: '#FFFFFF',
shadowColor: '#0ea5e9',
backgroundColor: Colors.light.background,
shadowColor: Colors.light.primary,
shadowOpacity: 0.9,
shadowRadius: 6,
shadowOffset: { width: 0, height: 0 },
},
progressRow: {
height: 8,
backgroundColor: '#f1f5f9',
backgroundColor: palette.gray[50],
borderRadius: 10,
marginTop: 14,
overflow: 'hidden',
@@ -384,13 +391,13 @@ const styles = StyleSheet.create({
progressBar: {
height: '100%',
borderRadius: 10,
backgroundColor: '#0ea5e9',
backgroundColor: Colors.light.primary,
},
progressText: {
marginTop: 8,
fontSize: 14,
fontWeight: '700',
color: '#0f172a',
color: Colors.light.text,
textAlign: 'right',
},
stepList: {
@@ -407,24 +414,24 @@ const styles = StyleSheet.create({
width: 14,
height: 14,
borderRadius: 7,
backgroundColor: '#e2e8f0',
backgroundColor: palette.gray[50],
},
bulletActive: {
backgroundColor: '#0ea5e9',
backgroundColor: Colors.light.primary,
},
bulletDone: {
backgroundColor: '#22c55e',
backgroundColor: Colors.light.success,
},
stepLabel: {
fontSize: 15,
color: '#94a3b8',
color: Colors.light.textMuted,
},
stepLabelActive: {
color: '#0f172a',
color: Colors.light.text,
fontWeight: '700',
},
stepLabelDone: {
color: '#16a34a',
color: Colors.light.successDark,
fontWeight: '700',
},
loadingBox: {
@@ -433,7 +440,7 @@ const styles = StyleSheet.create({
gap: 12,
},
errorText: {
color: '#ef4444',
color: Colors.light.danger,
fontSize: 14,
},
// Modal 样式
@@ -445,10 +452,10 @@ const styles = StyleSheet.create({
},
errorModalContainer: {
width: SCREEN_WIDTH - 48,
backgroundColor: '#FFFFFF',
backgroundColor: Colors.light.card,
borderRadius: 28,
overflow: 'hidden',
shadowColor: '#0ea5e9',
shadowColor: Colors.light.primary,
shadowOpacity: 0.15,
shadowRadius: 24,
shadowOffset: { width: 0, height: 8 },
@@ -465,36 +472,36 @@ const styles = StyleSheet.create({
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: 'rgba(14, 165, 233, 0.08)',
backgroundColor: palette.purple[50],
alignItems: 'center',
justifyContent: 'center',
},
errorModalTitle: {
fontSize: 22,
fontWeight: '700',
color: '#0f172a',
color: Colors.light.text,
marginBottom: 16,
textAlign: 'center',
},
errorMessageBox: {
backgroundColor: '#f0f9ff',
backgroundColor: palette.purple[25],
borderRadius: 16,
padding: 20,
marginBottom: 28,
width: '100%',
borderWidth: 1,
borderColor: 'rgba(14, 165, 233, 0.2)',
borderColor: palette.purple[200],
},
errorMessageText: {
fontSize: 15,
lineHeight: 24,
color: '#475569',
color: Colors.light.textSecondary,
textAlign: 'center',
},
retryButton: {
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#0ea5e9',
shadowColor: Colors.light.primary,
shadowOpacity: 0.25,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
@@ -509,6 +516,6 @@ const styles = StyleSheet.create({
retryButtonText: {
fontSize: 18,
fontWeight: '700',
color: '#FFFFFF',
color: Colors.light.onPrimary,
},
});

View File

@@ -7,6 +7,7 @@ import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { DietRecord } from '@/services/dietRecords';
import { type FoodRecognitionResponse } from '@/services/foodRecognition';
@@ -20,16 +21,20 @@ import {
selectNutritionRecordsByDate,
selectNutritionSummaryByDate
} from '@/store/nutritionSlice';
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
import { getTodayIndexInMonth } from '@/utils/date';
import { fetchBasalEnergyBurned } from '@/utils/health';
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
FlatList,
RefreshControl,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
@@ -39,26 +44,21 @@ import {
type ViewMode = 'daily' | 'all';
export default function NutritionRecordsScreen() {
const safeAreaTop = useSafeAreaTop()
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const dispatch = useAppDispatch();
const isGlassAvailable = isLiquidGlassAvailable();
const { isLoggedIn } = useAuthGuard()
const { isLoggedIn } = useAuthGuard();
// 日期相关状态 - 使用与统计页面相同的日期逻辑
const days = getMonthDaysZh();
// 日期相关状态
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
const monthTitle = getMonthTitleZh();
// 直接使用 state 管理当前选中日期,而不是从 days 数组派生,以支持 DateSelector 内部月份切换
const [currentSelectedDate, setCurrentSelectedDate] = useState<Date>(new Date());
// 获取当前选中日期 - 使用 useMemo 避免每次渲染都创建新对象
const currentSelectedDate = useMemo(() => {
return days[selectedIndex]?.date?.toDate() ?? new Date();
}, [selectedIndex, days]);
const currentSelectedDateString = useMemo(() => {
return dayjs(currentSelectedDate).format('YYYY-MM-DD');
}, [currentSelectedDate]);
const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD');
// 从 Redux 获取数据
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
@@ -89,7 +89,6 @@ export default function NutritionRecordsScreen() {
const displayRecords = viewMode === 'daily' ? nutritionRecords : allRecords;
const loading = viewMode === 'daily' ? nutritionLoading.records : allRecordsLoading;
// 页面聚焦时自动刷新数据
useFocusEffect(
useCallback(() => {
@@ -123,7 +122,7 @@ export default function NutritionRecordsScreen() {
loadAllRecords();
}
}, [viewMode, currentSelectedDateString, dispatch])
}, [viewMode, currentSelectedDateString, dispatch, isLoggedIn])
);
// 当选中日期或视图模式变化时重新加载数据
@@ -327,71 +326,6 @@ export default function NutritionRecordsScreen() {
});
};
// 渲染日期选择器(仅在按天查看模式下显示)
const renderDateSelector = () => {
if (viewMode !== 'daily') return null;
return (
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={(index, date) => setSelectedIndex(index)}
showMonthTitle={true}
disableFutureDates={true}
showCalendarIcon={true}
containerStyle={{
paddingHorizontal: 16
}}
/>
);
};
const renderEmptyState = () => (
<View style={styles.emptyContainer}>
<View style={styles.emptyContent}>
<Ionicons name="restaurant-outline" size={48} color={colorTokens.textSecondary} />
<Text style={[styles.emptyTitle, { color: colorTokens.text }]}>
{viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'}
</Text>
<Text style={[styles.emptySubtitle, { color: colorTokens.textSecondary }]}>
{viewMode === 'daily' ? '开始记录今日营养摄入' : '开始记录你的营养摄入吧'}
</Text>
</View>
</View>
);
const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => (
<NutritionRecordCard
record={item}
onPress={() => handleRecordPress(item)}
onDelete={() => handleDeleteRecord(item.id)}
/>
);
const renderFooter = () => {
if (!hasMoreData) {
return (
<View style={styles.footerContainer}>
<Text style={[styles.footerText, { color: colorTokens.textSecondary }]}>
</Text>
</View>
);
}
if (viewMode === 'all' && displayRecords.length > 0) {
return (
<TouchableOpacity style={styles.loadMoreButton} onPress={loadMoreRecords}>
<Text style={[styles.loadMoreText, { color: colorTokens.primary }]}>
</Text>
</TouchableOpacity>
);
}
return null;
};
// 根据当前时间智能判断餐次类型
const getCurrentMealType = (): 'breakfast' | 'lunch' | 'dinner' | 'snack' => {
const hour = new Date().getHours();
@@ -415,68 +349,160 @@ export default function NutritionRecordsScreen() {
// 渲染右侧添加按钮
const renderRightButton = () => (
<TouchableOpacity
style={[styles.addButton, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
onPress={handleAddFood}
activeOpacity={0.7}
>
<Ionicons name="add" size={20} color={colorTokens.primary} />
{isGlassAvailable ? (
<GlassView
style={styles.glassAddButton}
glassEffectStyle="regular"
tintColor="rgba(255, 255, 255, 0.4)"
isInteractive={true}
>
<Ionicons name="add" size={24} color={colorTokens.primary} />
</GlassView>
) : (
<View style={[styles.fallbackAddButton, { backgroundColor: 'rgba(255,255,255,0.8)' }]}>
<Ionicons name="add" size={24} color={colorTokens.primary} />
</View>
)}
</TouchableOpacity>
);
return (
<View style={[styles.container, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<HeaderBar
title="营养记录"
onBack={() => router.back()}
right={renderRightButton()}
/>
<View style={{
paddingTop: safeAreaTop
}}>
{/* {renderViewModeToggle()} */}
{renderDateSelector()}
{/* Calorie Ring Chart */}
<CalorieRingChart
metabolism={basalMetabolism}
exercise={healthData?.activeEnergyBurned || 0}
consumed={nutritionSummary?.totalCalories || 0}
protein={nutritionSummary?.totalProtein || 0}
fat={nutritionSummary?.totalFat || 0}
carbs={nutritionSummary?.totalCarbohydrate || 0}
proteinGoal={nutritionGoals.proteinGoal}
fatGoal={nutritionGoals.fatGoal}
carbsGoal={nutritionGoals.carbsGoal}
const renderEmptyState = () => (
<View style={styles.emptySimpleContainer}>
<Image
source={require('@/assets/images/icons/icon-yingyang.png')}
style={styles.emptySimpleImage}
contentFit="contain"
/>
<Text style={styles.emptySimpleText}>
{t('nutritionRecords.empty.title')}
</Text>
<TouchableOpacity onPress={handleAddFood}>
<Text style={[styles.emptyActionText, { color: colorTokens.primary }]}>
{t('nutritionRecords.empty.action')}
</Text>
</TouchableOpacity>
</View>
);
{(
<FlatList
data={displayRecords}
renderItem={({ item, index }) => renderRecord({ item, index })}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={[
styles.listContainer,
{ paddingBottom: 40, paddingTop: 16 }
]}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colorTokens.primary}
colors={[colorTokens.primary]}
/>
}
ListEmptyComponent={renderEmptyState}
ListFooterComponent={renderFooter}
onEndReached={viewMode === 'all' ? loadMoreRecords : undefined}
onEndReachedThreshold={0.1}
const renderRecord = ({ item }: { item: DietRecord }) => (
<NutritionRecordCard
record={item}
onPress={() => handleRecordPress(item)}
onDelete={() => handleDeleteRecord(item.id)}
/>
);
const renderFooter = () => {
if (!hasMoreData) {
if (displayRecords.length === 0) return null;
return (
<View style={styles.footerContainer}>
<Text style={[styles.footerText, { color: colorTokens.textSecondary }]}>
{t('nutritionRecords.footer.end')}
</Text>
</View>
);
}
if (viewMode === 'all' && displayRecords.length > 0) {
return (
<TouchableOpacity style={styles.loadMoreButton} onPress={loadMoreRecords}>
<Text style={[styles.loadMoreText, { color: colorTokens.primary }]}>
{t('nutritionRecords.footer.loadMore')}
</Text>
</TouchableOpacity>
);
}
return null;
};
const ListHeader = () => (
<View>
<View style={styles.headerContent}>
{viewMode === 'daily' && (
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={(index, date) => {
setSelectedIndex(index);
setCurrentSelectedDate(date);
}}
showMonthTitle={true}
disableFutureDates={true}
showCalendarIcon={true}
containerStyle={styles.dateSelectorContainer}
/>
)}
<View style={styles.chartWrapper}>
<CalorieRingChart
metabolism={basalMetabolism}
exercise={healthData?.activeEnergyBurned || 0}
consumed={nutritionSummary?.totalCalories || 0}
protein={nutritionSummary?.totalProtein || 0}
fat={nutritionSummary?.totalFat || 0}
carbs={nutritionSummary?.totalCarbohydrate || 0}
proteinGoal={nutritionGoals.proteinGoal}
fatGoal={nutritionGoals.fatGoal}
carbsGoal={nutritionGoals.carbsGoal}
/>
</View>
<View style={styles.listTitleContainer}>
<Text style={styles.listTitle}>{t('nutritionRecords.listTitle')}</Text>
{displayRecords.length > 0 && (
<Text style={styles.listSubtitle}>{t('nutritionRecords.recordCount', { count: displayRecords.length })}</Text>
)}
</View>
</View>
</View>
);
return (
<View style={[styles.container, { backgroundColor: '#f3f4fb' }]}>
<StatusBar barStyle="dark-content" />
{/* 顶部柔和渐变背景 */}
<LinearGradient
colors={['rgba(255, 243, 224, 0.8)', 'rgba(243, 244, 251, 0)']}
style={styles.topGradient}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<HeaderBar
title={t('nutritionRecords.title')}
onBack={() => router.back()}
right={renderRightButton()}
transparent={true}
/>
<FlatList
data={displayRecords}
renderItem={renderRecord}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={[
styles.listContainer,
{ paddingTop: safeAreaTop }
]}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colorTokens.primary}
colors={[colorTokens.primary]}
/>
}
ListHeaderComponent={ListHeader}
ListEmptyComponent={renderEmptyState}
ListFooterComponent={renderFooter}
onEndReached={viewMode === 'all' ? loadMoreRecords : undefined}
onEndReachedThreshold={0.1}
/>
{/* 食物添加悬浮窗 */}
<FloatingFoodOverlay
@@ -492,130 +518,105 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
viewModeContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
marginBottom: 8,
},
monthTitle: {
fontSize: 22,
fontWeight: '800',
},
toggleContainer: {
flexDirection: 'row',
borderRadius: 20,
padding: 2,
},
toggleButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 18,
minWidth: 80,
alignItems: 'center',
},
toggleText: {
fontSize: 14,
fontWeight: '600',
},
daysContainer: {
marginBottom: 12,
},
daysScrollContainer: {
paddingHorizontal: 16,
paddingVertical: 8,
},
dayPill: {
width: 48,
height: 48,
borderRadius: 34,
marginRight: 12,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 3,
},
dayNumber: {
fontSize: 18,
textAlign: 'center',
},
dayLabel: {
fontSize: 12,
marginTop: 2,
textAlign: 'center',
},
addButton: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
fontWeight: '500',
topGradient: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: 320,
},
listContainer: {
paddingBottom: 100, // 留出底部空间防止遮挡
},
headerContent: {
marginBottom: 16,
},
dateSelectorContainer: {
paddingHorizontal: 16,
paddingTop: 8,
marginBottom: 16,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
paddingHorizontal: 16,
chartWrapper: {
marginBottom: 24,
shadowColor: 'rgba(30, 41, 59, 0.05)',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 4,
},
emptyContent: {
alignItems: 'center',
maxWidth: 320,
listTitleContainer: {
flexDirection: 'row',
alignItems: 'baseline',
paddingHorizontal: 24,
marginBottom: 12,
gap: 8,
},
emptyTitle: {
listTitle: {
fontSize: 18,
fontWeight: '700',
marginTop: 16,
marginBottom: 8,
textAlign: 'center',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
emptySubtitle: {
listSubtitle: {
fontSize: 13,
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
glassAddButton: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
overflow: 'hidden',
},
fallbackAddButton: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
},
emptySimpleContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptySimpleImage: {
width: 48,
height: 48,
opacity: 0.4,
marginBottom: 12,
},
emptySimpleText: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
lineHeight: 20,
color: '#94A3B8',
fontFamily: 'AliRegular',
marginBottom: 8,
},
emptyActionText: {
fontSize: 14,
fontWeight: '600',
fontFamily: 'AliBold',
},
footerContainer: {
paddingVertical: 20,
paddingVertical: 24,
alignItems: 'center',
},
footerText: {
fontSize: 14,
fontSize: 12,
fontWeight: '500',
opacity: 0.6,
fontFamily: 'AliRegular',
},
loadMoreButton: {
paddingVertical: 16,
alignItems: 'center',
},
loadMoreText: {
fontSize: 16,
fontSize: 14,
fontWeight: '600',
fontFamily: 'AliBold',
},
});

View File

@@ -2,7 +2,6 @@ import { InfoModal, type SleepDetailData } from '@/components/sleep/InfoModal';
import { SleepStagesInfoModal } from '@/components/sleep/SleepStagesInfoModal';
import { SleepStageTimeline } from '@/components/sleep/SleepStageTimeline';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import {
@@ -38,7 +37,7 @@ export default function SleepDetailScreen() {
const { t } = useI18n();
const insets = useSafeAreaInsets();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [sleepData, setSleepData] = useState<CompleteSleepData | null>(null);
const [loading, setLoading] = useState(true);

View File

@@ -3,6 +3,7 @@ import { Colors } from '@/constants/Colors';
import { useAppDispatch } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { analyzeFoodFromText } from '@/services/foodRecognition';
import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice';
@@ -24,6 +25,7 @@ import {
type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result' | 'analyzing';
export default function VoiceRecordScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme];
@@ -118,7 +120,7 @@ export default function VoiceRecordScreen() {
// 语音识别回调 - 使用 useCallback 避免每次渲染重新创建
const onSpeechStart = useCallback(() => {
console.log('语音开始');
console.log('Voice started');
if (!isMountedRef.current) return;
setIsListening(true);
@@ -128,11 +130,11 @@ export default function VoiceRecordScreen() {
}, []);
const onSpeechRecognized = useCallback(() => {
console.log('语音识别中...');
console.log('Voice recognition in progress...');
}, []);
const onSpeechEnd = useCallback(() => {
console.log('语音结束');
console.log('Voice ended');
if (!isMountedRef.current) return;
setIsListening(false);
@@ -141,7 +143,7 @@ export default function VoiceRecordScreen() {
}, []);
const onSpeechError = useCallback((error: any) => {
console.log('语音识别错误:', error);
console.log('Voice recognition error:', error);
if (!isMountedRef.current) return;
setIsListening(false);
@@ -150,16 +152,16 @@ export default function VoiceRecordScreen() {
// 显示更友好的错误信息
if (error.error?.code === '7') {
Alert.alert('提示', '没有检测到语音输入,请重试');
Alert.alert(t('voiceRecord.alerts.noVoiceInput'), t('voiceRecord.alerts.noVoiceInput'));
} else if (error.error?.code === '2') {
Alert.alert('提示', '网络连接异常,请检查网络后重试');
Alert.alert(t('voiceRecord.alerts.networkError'), t('voiceRecord.alerts.networkError'));
} else {
Alert.alert('提示', '语音识别出现问题,请重试');
Alert.alert(t('voiceRecord.alerts.voiceError'), t('voiceRecord.alerts.voiceError'));
}
}, []);
const onSpeechResults = useCallback((event: any) => {
console.log('语音识别结果:', event);
console.log('Voice recognition result:', event);
if (!isMountedRef.current) return;
const text = event.value?.[0] || '';
@@ -168,7 +170,7 @@ export default function VoiceRecordScreen() {
setRecordState('result');
} else {
setRecordState('idle');
Alert.alert('提示', '未识别到有效内容,请重新录音');
Alert.alert(t('voiceRecord.alerts.noValidContent'), t('voiceRecord.alerts.noValidContent'));
}
stopAnimations();
}, []);
@@ -215,7 +217,7 @@ export default function VoiceRecordScreen() {
await Voice.destroy();
Voice.removeAllListeners();
} catch (error) {
console.log('清理语音识别资源失败:', error);
console.log('Failed to clean up voice recognition resources:', error);
}
};
cleanup();
@@ -246,22 +248,22 @@ export default function VoiceRecordScreen() {
await Voice.start('zh-CN');
} catch (error) {
console.log('启动语音识别失败:', error);
console.log('Failed to start voice recognition:', error);
setRecordState('idle');
setIsListening(false);
Alert.alert('录音失败', '无法启动语音识别,请检查麦克风权限设置');
Alert.alert(t('voiceRecord.alerts.recordingFailed'), t('voiceRecord.alerts.recordingPermissionError'));
}
};
// 停止录音
const stopRecording = async () => {
try {
console.log('停止录音');
console.log('Stop recording');
setIsListening(false);
await Voice.stop();
triggerHapticFeedback('impactLight');
} catch (error) {
console.log('停止语音识别失败:', error);
console.log('Failed to stop voice recognition:', error);
setIsListening(false);
setRecordState('idle');
}
@@ -287,7 +289,7 @@ export default function VoiceRecordScreen() {
startRecording();
}, 200);
} catch (error) {
console.log('重新录音失败:', error);
console.log('Failed to retry recording:', error);
setRecordState('idle');
setIsListening(false);
}
@@ -296,7 +298,7 @@ export default function VoiceRecordScreen() {
// 确认并分析食物文本
const confirmResult = async () => {
if (!recognizedText.trim()) {
Alert.alert('提示', '请先进行语音识别');
Alert.alert(t('voiceRecord.alerts.pleaseRecordFirst'), t('voiceRecord.alerts.pleaseRecordFirst'));
return;
}
@@ -382,7 +384,7 @@ export default function VoiceRecordScreen() {
const errorMessage = error instanceof Error ? error.message : '分析失败,请重试';
dispatch(setError(errorMessage));
Alert.alert('分析失败', errorMessage);
Alert.alert(t('voiceRecord.alerts.analysisFailed'), errorMessage);
}
};
@@ -401,7 +403,7 @@ export default function VoiceRecordScreen() {
router.back();
} catch (error) {
console.log('返回时清理资源失败:', error);
console.log('Failed to clean up resources when returning:', error);
router.back();
}
};
@@ -410,15 +412,15 @@ export default function VoiceRecordScreen() {
const getStatusText = () => {
switch (recordState) {
case 'idle':
return '轻触麦克风开始录音';
return t('voiceRecord.status.idle');
case 'listening':
return '正在聆听中,请开始说话...';
return t('voiceRecord.status.listening');
case 'processing':
return 'AI正在处理语音内容...';
return t('voiceRecord.status.processing');
case 'analyzing':
return 'AI大模型深度分析营养成分中...';
return t('voiceRecord.status.analyzing');
case 'result':
return '语音识别完成,请确认结果';
return t('voiceRecord.status.result');
default:
return '';
}
@@ -470,7 +472,7 @@ export default function VoiceRecordScreen() {
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<HeaderBar
title="一句话记录"
title={t('voiceRecord.title')}
onBack={handleBack}
tone={theme}
variant="elevated"
@@ -485,7 +487,7 @@ export default function VoiceRecordScreen() {
<View style={styles.topSection}>
<View style={styles.introContainer}>
<Text style={[styles.introDescription, { color: colorTokens.textSecondary }]}>
AI将智能分析营养成分和卡路里
{t('voiceRecord.intro.description')}
</Text>
</View>
</View>
@@ -605,7 +607,7 @@ export default function VoiceRecordScreen() {
{recordState === 'listening' && (
<Text style={[styles.hintText, { color: colorTokens.textSecondary }]}>
{t('voiceRecord.hints.listening')}
</Text>
)}
@@ -614,18 +616,18 @@ export default function VoiceRecordScreen() {
<BlurView intensity={20} tint={theme} style={styles.examplesContainer}>
<View style={styles.examplesContent}>
<Text style={[styles.examplesTitle, { color: colorTokens.text }]}>
{t('voiceRecord.examples.title')}
</Text>
<View style={styles.examplesList}>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;&rdquo;
</Text>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;150&rdquo;
</Text>
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;&rdquo;
</Text>
{[
t('voiceRecord.examples.items.0'),
t('voiceRecord.examples.items.1'),
t('voiceRecord.examples.items.2')
].map((example: string, index: number) => (
<Text key={index} style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
&ldquo;{example}&rdquo;
</Text>
))}
</View>
</View>
</BlurView>
@@ -634,7 +636,7 @@ export default function VoiceRecordScreen() {
{recordState === 'analyzing' && (
<View style={styles.analysisProgressContainer}>
<Text style={[styles.progressText, { color: colorTokens.text }]}>
: {Math.round(analysisProgress)}%
{t('voiceRecord.analysis.progress', { progress: Math.round(analysisProgress) })}
</Text>
<View style={styles.progressBarContainer}>
<Animated.View
@@ -650,7 +652,7 @@ export default function VoiceRecordScreen() {
/>
</View>
<Text style={[styles.analysisHint, { color: colorTokens.textSecondary }]}>
AI正在深度分析您的食物描述...
{t('voiceRecord.analysis.hint')}
</Text>
</View>
)}
@@ -662,7 +664,7 @@ export default function VoiceRecordScreen() {
<BlurView intensity={20} tint={theme} style={styles.resultContainer}>
<View style={styles.resultContent}>
<Text style={[styles.resultLabel, { color: colorTokens.textSecondary }]}>
:
{t('voiceRecord.result.label')}
</Text>
<Text style={[styles.resultText, { color: colorTokens.text }]}>
{recognizedText}
@@ -675,7 +677,7 @@ export default function VoiceRecordScreen() {
onPress={retryRecording}
>
<Ionicons name="refresh" size={16} color="#7B68EE" />
<Text style={styles.retryButtonText}></Text>
<Text style={styles.retryButtonText}>{t('voiceRecord.actions.retry')}</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -683,7 +685,7 @@ export default function VoiceRecordScreen() {
onPress={confirmResult}
>
<Ionicons name="checkmark" size={16} color="white" />
<Text style={styles.confirmButtonText}>使</Text>
<Text style={styles.confirmButtonText}>{t('voiceRecord.actions.confirm')}</Text>
</TouchableOpacity>
</View>
)}

View File

@@ -11,11 +11,12 @@ import { appStoreReviewService } from '@/services/appStoreReview';
import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import {
Alert,
Image,
Modal,
ScrollView,
StyleSheet,
@@ -39,8 +40,6 @@ export default function WeightRecordsPage() {
const colorScheme = useColorScheme();
const themeColors = Colors[colorScheme ?? 'light'];
console.log('userProfile:', userProfile);
const loadWeightHistory = useCallback(async () => {
try {
await dispatch(fetchWeightHistory() as any);
@@ -53,10 +52,6 @@ export default function WeightRecordsPage() {
loadWeightHistory();
}, [loadWeightHistory]);
const handleGoBack = () => {
router.back();
};
const initializeInput = (weight: number) => {
setInputWeight(weight.toString());
};
@@ -166,7 +161,11 @@ export default function WeightRecordsPage() {
// Group by month
const groupedHistory = sortedHistory.reduce((acc, item) => {
const monthKey = dayjs(item.createdAt).format('YYYY年MM月');
const date = dayjs(item.createdAt);
const monthKey = t('weightRecords.historyMonthFormat', {
year: date.format('YYYY'),
month: date.format('MM')
});
if (!acc[monthKey]) {
acc[monthKey] = [];
}
@@ -183,106 +182,157 @@ export default function WeightRecordsPage() {
return (
<View style={styles.container}>
{/* 背景渐变 */}
{/* 背景 */}
<LinearGradient
colors={['#F0F9FF', '#E0F2FE']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
colors={['#f3f4fb', '#f3f4fb']}
style={StyleSheet.absoluteFillObject}
/>
{/* 顶部装饰性渐变 */}
<LinearGradient
colors={['rgba(229, 252, 254, 0.8)', 'rgba(243, 244, 251, 0)']}
style={styles.topGradient}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<HeaderBar
title={t('weightRecords.title')}
right={<TouchableOpacity onPress={handleAddWeight} style={styles.addButton}>
<Ionicons name="add" size={24} color="#192126" />
</TouchableOpacity>}
right={
isLiquidGlassAvailable() ? (
<TouchableOpacity
onPress={handleAddWeight}
activeOpacity={0.7}
>
<GlassView
style={styles.addButtonGlass}
glassEffectStyle="regular"
tintColor="rgba(255, 255, 255, 0.4)"
isInteractive={true}
>
<Ionicons name="add" size={24} color="#1c1f3a" />
</GlassView>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.addButtonFallback}
onPress={handleAddWeight}
activeOpacity={0.7}
>
<Ionicons name="add" size={24} color="#1c1f3a" />
</TouchableOpacity>
)
}
/>
<View style={{
paddingTop: safeAreaTop
}} />
{/* Weight Statistics */}
<View style={[styles.statsContainer]}>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{totalWeightLoss.toFixed(1)}kg</Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.totalLoss')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{currentWeight.toFixed(1)}kg</Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.currentWeight')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{initialWeight.toFixed(1)}kg</Text>
<View style={styles.statLabelContainer}>
<Text style={styles.statLabel}>{t('weightRecords.stats.initialWeight')}</Text>
<TouchableOpacity onPress={handleEditInitialWeight} style={styles.editIcon}>
<Ionicons name="create-outline" size={12} color="#FF9500" />
</TouchableOpacity>
</View>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{targetWeight.toFixed(1)}kg</Text>
<View style={styles.statLabelContainer}>
<Text style={styles.statLabel}>{t('weightRecords.stats.targetWeight')}</Text>
<TouchableOpacity onPress={handleEditTargetWeight} style={styles.editIcon}>
<Ionicons name="create-outline" size={12} color="#FF9500" />
</TouchableOpacity>
</View>
</View>
</View>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20 }]}
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20, paddingTop: safeAreaTop }]}
showsVerticalScrollIndicator={false}
>
<View style={styles.headerBlock}>
<Text style={styles.pageTitle}>{t('weightRecords.title')}</Text>
<Text style={styles.pageSubtitle}>{t('weightRecords.pageSubtitle')}</Text>
</View>
{/* Weight Statistics Cards */}
<View style={styles.statsGrid}>
{/* Current Weight - Hero Card */}
<View style={styles.mainStatCard}>
<View style={styles.mainStatContent}>
<Text style={styles.mainStatLabel}>{t('weightRecords.stats.currentWeight')}</Text>
<View style={styles.mainStatValueContainer}>
<Text style={styles.mainStatValue}>{currentWeight.toFixed(1)}</Text>
<Text style={styles.mainStatUnit}>kg</Text>
</View>
<View style={styles.totalLossTag}>
<Ionicons name={totalWeightLoss <= 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
<Text style={styles.totalLossText}>
{totalWeightLoss > 0 ? '+' : ''}{totalWeightLoss.toFixed(1)} kg
</Text>
</View>
</View>
<LinearGradient
colors={['#4F5BD5', '#6B6CFF']}
style={StyleSheet.absoluteFillObject}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
// @ts-ignore
borderRadius={24}
/>
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={styles.statIconBg}
/>
</View>
{/* Secondary Stats Row */}
<View style={styles.secondaryStatsRow}>
{/* Initial Weight */}
<TouchableOpacity
style={styles.secondaryStatCard}
onPress={handleEditInitialWeight}
activeOpacity={0.7}
>
<View style={styles.secondaryStatHeader}>
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.initialWeight')}</Text>
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
</View>
<Text style={styles.secondaryStatValue}>{initialWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
</TouchableOpacity>
{/* Target Weight */}
<TouchableOpacity
style={styles.secondaryStatCard}
onPress={handleEditTargetWeight}
activeOpacity={0.7}
>
<View style={styles.secondaryStatHeader}>
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.targetWeight')}</Text>
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
</View>
<Text style={styles.secondaryStatValue}>{targetWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
</TouchableOpacity>
</View>
</View>
{/* Monthly Records */}
{Object.keys(groupedHistory).length > 0 ? (
Object.entries(groupedHistory).map(([month, records]) => (
<View key={month} style={styles.monthContainer}>
{/* Month Header Card */}
{/* <View style={styles.monthHeaderCard}>
<View style={styles.monthTitleRow}>
<Text style={styles.monthNumber}>
{dayjs(month, 'YYYY年MM月').format('MM')}
</Text>
<Text style={styles.monthText}>月</Text>
<Text style={styles.yearText}>
{dayjs(month, 'YYYY年MM月').format('YYYY年')}
</Text>
<View style={styles.expandIcon}>
<Ionicons name="chevron-up" size={16} color="#FF9500" />
</View>
</View>
<Text style={styles.monthStatsText}>
累计减重:<Text style={styles.statsBold}>{totalWeightLoss.toFixed(1)}kg</Text> 日均减重:<Text style={styles.statsBold}>{avgWeightLoss.toFixed(1)}kg</Text>
</Text>
</View> */}
<View style={styles.historySection}>
<Text style={styles.sectionTitle}>{t('weightRecords.history')}</Text>
{Object.entries(groupedHistory).map(([month, records]) => (
<View key={month} style={styles.monthContainer}>
<View style={styles.monthHeader}>
<Text style={styles.monthTitle}>{month}</Text>
</View>
{/* Individual Record Cards */}
{records.map((record, recordIndex) => {
// Calculate weight change from previous record
const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null;
const weightChange = prevRecord ?
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
{/* Individual Record Cards */}
<View style={styles.recordsList}>
{records.map((record, recordIndex) => {
// Calculate weight change from previous record
const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null;
const weightChange = prevRecord ?
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
return (
<WeightRecordCard
key={`${record.createdAt}-${recordIndex}`}
record={record}
onPress={handleEditWeightRecord}
onDelete={handleDeleteWeightRecord}
weightChange={weightChange}
/>
);
})}
</View>
))
return (
<WeightRecordCard
key={`${record.createdAt}-${recordIndex}`}
record={record}
onPress={handleEditWeightRecord}
onDelete={handleDeleteWeightRecord}
weightChange={weightChange}
/>
);
})}
</View>
</View>
))}
</View>
) : (
<View style={styles.emptyContainer}>
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={{ width: 80, height: 80, opacity: 0.5, marginBottom: 16, tintColor: '#cbd5e1' }}
/>
<View style={styles.emptyContent}>
<Text style={styles.emptyText}>{t('weightRecords.empty.title')}</Text>
<Text style={styles.emptySubtext}>{t('weightRecords.empty.subtitle')}</Text>
@@ -304,13 +354,16 @@ export default function WeightRecordsPage() {
activeOpacity={1}
onPress={() => setShowWeightPicker(false)}
/>
<View style={[styles.modalSheet, { backgroundColor: themeColors.background }]}>
<View style={[styles.modalSheet, { backgroundColor: '#ffffff' }]}>
{/* Header */}
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setShowWeightPicker(false)}>
<Ionicons name="close" size={24} color={themeColors.text} />
<TouchableOpacity
onPress={() => setShowWeightPicker(false)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="close" size={24} color="#1c1f3a" />
</TouchableOpacity>
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
<Text style={styles.modalTitle}>
{pickerType === 'current' && t('weightRecords.modal.recordWeight')}
{pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')}
{pickerType === 'target' && t('weightRecords.modal.editTargetWeight')}
@@ -327,25 +380,26 @@ export default function WeightRecordsPage() {
<View style={styles.inputSection}>
<View style={styles.weightInputContainer}>
<View style={styles.weightIcon}>
<Ionicons name="scale-outline" size={20} color="#6366F1" />
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={{ width: 24, height: 24, tintColor: '#4F5BD5' }}
/>
</View>
<View style={styles.inputWrapper}>
<Text style={[styles.weightDisplay, { color: inputWeight ? themeColors.text : themeColors.textSecondary }]}>
<Text style={[
styles.weightDisplay,
{ color: inputWeight ? '#1c1f3a' : '#9ba3c7' }
]}>
{inputWeight || t('weightRecords.modal.inputPlaceholder')}
</Text>
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>{t('weightRecords.modal.unit')}</Text>
<Text style={styles.unitLabel}>{t('weightRecords.modal.unit')}</Text>
</View>
</View>
{/* Weight Range Hint */}
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
{t('weightRecords.modal.inputHint')}
</Text>
</View>
{/* Quick Selection */}
<View style={styles.quickSelectionSection}>
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}>{t('weightRecords.modal.quickSelection')}</Text>
<Text style={styles.quickSelectionTitle}>{t('weightRecords.modal.quickSelection')}</Text>
<View style={styles.quickButtons}>
{[50, 60, 70, 80, 90].map((weight) => (
<TouchableOpacity
@@ -355,6 +409,7 @@ export default function WeightRecordsPage() {
inputWeight === weight.toString() && styles.quickButtonSelected
]}
onPress={() => setInputWeight(weight.toString())}
activeOpacity={0.7}
>
<Text style={[
styles.quickButtonText,
@@ -387,8 +442,16 @@ export default function WeightRecordsPage() {
]}
onPress={handleWeightSave}
disabled={!inputWeight.trim()}
activeOpacity={0.8}
>
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
<LinearGradient
colors={['#4F5BD5', '#6B6CFF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.saveButtonGradient}
>
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
@@ -402,144 +465,202 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
topGradient: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 60,
paddingHorizontal: 20,
paddingBottom: 10,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
alignItems: 'center',
justifyContent: 'center',
},
addButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
alignItems: 'center',
justifyContent: 'center',
height: 300,
},
content: {
flex: 1,
paddingHorizontal: 20,
},
contentContainer: {
flexGrow: 1,
paddingBottom: 40,
},
statsContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 20,
marginLeft: 20,
marginRight: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
headerBlock: {
paddingHorizontal: 24,
marginTop: 10,
marginBottom: 24,
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
statItem: {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
},
statValue: {
fontSize: 14,
pageTitle: {
fontSize: 28,
fontWeight: '800',
color: '#192126',
color: '#1c1f3a',
fontFamily: 'AliBold',
marginBottom: 4,
},
statLabelContainer: {
flexDirection: 'row',
pageSubtitle: {
fontSize: 16,
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
// Add Button Styles
addButtonGlass: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
overflow: 'hidden',
},
statLabel: {
fontSize: 12,
color: '#687076',
marginRight: 4,
textAlign: 'center'
addButtonFallback: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
},
editIcon: {
padding: 2,
borderRadius: 8,
backgroundColor: 'rgba(255, 149, 0, 0.1)',
// Stats Grid
statsGrid: {
paddingHorizontal: 24,
marginBottom: 32,
gap: 16,
},
monthContainer: {
marginBottom: 20,
mainStatCard: {
backgroundColor: '#4F5BD5',
borderRadius: 28,
padding: 24,
height: 160,
position: 'relative',
overflow: 'hidden',
shadowColor: '#4F5BD5',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 8,
},
monthHeaderCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
mainStatContent: {
zIndex: 2,
height: '100%',
justifyContent: 'space-between',
},
mainStatLabel: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.9)',
fontFamily: 'AliRegular',
},
mainStatValueContainer: {
flexDirection: 'row',
alignItems: 'baseline',
},
mainStatValue: {
fontSize: 48,
fontWeight: '800',
color: '#ffffff',
fontFamily: 'AliBold',
marginRight: 8,
},
mainStatUnit: {
fontSize: 20,
fontWeight: '600',
color: 'rgba(255, 255, 255, 0.9)',
fontFamily: 'AliRegular',
},
totalLossTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.2)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
alignSelf: 'flex-start',
},
totalLossText: {
fontSize: 14,
fontWeight: '600',
color: '#ffffff',
marginLeft: 4,
fontFamily: 'AliBold',
},
statIconBg: {
position: 'absolute',
right: -20,
bottom: -20,
width: 140,
height: 140,
opacity: 0.2,
transform: [{ rotate: '-15deg' }],
tintColor: '#ffffff'
},
secondaryStatsRow: {
flexDirection: 'row',
gap: 16,
},
secondaryStatCard: {
flex: 1,
backgroundColor: '#ffffff',
borderRadius: 24,
padding: 16,
shadowColor: 'rgba(30, 41, 59, 0.06)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 3,
},
monthTitleRow: {
secondaryStatHeader: {
flexDirection: 'row',
alignItems: 'baseline',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
monthNumber: {
fontSize: 48,
fontWeight: '800',
color: '#192126',
lineHeight: 48,
secondaryStatLabel: {
fontSize: 13,
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
monthText: {
fontSize: 16,
fontWeight: '600',
color: '#192126',
marginLeft: 4,
marginRight: 8,
},
yearText: {
fontSize: 16,
fontWeight: '500',
color: '#687076',
flex: 1,
},
expandIcon: {
padding: 4,
},
monthStatsText: {
fontSize: 14,
color: '#687076',
lineHeight: 20,
},
statsBold: {
secondaryStatValue: {
fontSize: 20,
fontWeight: '700',
color: '#192126',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
secondaryStatUnit: {
fontSize: 14,
color: '#6f7ba7',
fontWeight: '500',
fontFamily: 'AliRegular',
marginLeft: 2,
},
// History Section
historySection: {
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: '#1c1f3a',
marginBottom: 16,
fontFamily: 'AliBold',
},
monthContainer: {
marginBottom: 24,
},
monthHeader: {
marginBottom: 12,
paddingHorizontal: 4,
},
monthTitle: {
fontSize: 15,
fontWeight: '600',
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
recordsList: {
gap: 12,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
minHeight: 300,
paddingVertical: 60,
},
emptyContent: {
alignItems: 'center',
@@ -547,145 +668,161 @@ const styles = StyleSheet.create({
emptyText: {
fontSize: 16,
fontWeight: '700',
color: '#192126',
color: '#1c1f3a',
marginBottom: 8,
fontFamily: 'AliBold',
},
emptySubtext: {
fontSize: 14,
color: '#687076',
fontFamily: 'AliRegular',
},
// Modal Styles
// Modal Styles (Retain but refined)
modalContainer: {
flex: 1,
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.35)',
backgroundColor: 'rgba(0,0,0,0.4)',
},
modalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
maxHeight: '85%',
minHeight: 500,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 10,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 10,
paddingHorizontal: 24,
paddingTop: 24,
paddingBottom: 16,
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
fontWeight: '700',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
modalContent: {
flex: 1,
paddingHorizontal: 20,
paddingHorizontal: 24,
},
inputSection: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginBottom: 12,
backgroundColor: '#F8F9FC',
borderRadius: 24,
padding: 24,
marginBottom: 24,
marginTop: 8,
},
weightInputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
weightIcon: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F0F9FF',
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#EEF0FF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
marginRight: 16,
},
inputWrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
alignItems: 'baseline',
borderBottomWidth: 2,
borderBottomColor: '#E5E7EB',
paddingBottom: 6,
borderBottomColor: '#E2E8F0',
paddingBottom: 8,
},
weightDisplay: {
flex: 1,
fontSize: 24,
fontWeight: '600',
fontSize: 36,
fontWeight: '700',
textAlign: 'center',
paddingVertical: 4,
color: '#1c1f3a',
fontFamily: 'AliBold',
},
unitLabel: {
fontSize: 18,
fontWeight: '500',
fontWeight: '600',
color: '#6f7ba7',
marginLeft: 8,
},
hintText: {
fontSize: 12,
textAlign: 'center',
marginTop: 4,
fontFamily: 'AliRegular',
},
quickSelectionSection: {
paddingHorizontal: 4,
marginBottom: 20,
marginBottom: 24,
},
quickSelectionTitle: {
fontSize: 16,
fontSize: 14,
fontWeight: '600',
color: '#6f7ba7',
marginBottom: 12,
textAlign: 'center',
fontFamily: 'AliRegular',
marginLeft: 4,
},
quickButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 8,
},
quickButton: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: '#F3F4F6',
borderWidth: 1,
borderColor: '#E5E7EB',
minWidth: 60,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: '#F1F5F9',
minWidth: 64,
alignItems: 'center',
},
quickButtonSelected: {
backgroundColor: '#6366F1',
borderColor: '#6366F1',
backgroundColor: '#4F5BD5',
},
quickButtonText: {
fontSize: 13,
fontWeight: '500',
color: '#6B7280',
fontSize: 14,
fontWeight: '600',
color: '#64748B',
fontFamily: 'AliRegular',
},
quickButtonTextSelected: {
color: '#FFFFFF',
fontWeight: '600',
fontWeight: '700',
},
modalFooter: {
paddingHorizontal: 20,
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 25,
paddingBottom: 34,
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
},
saveButton: {
backgroundColor: '#6366F1',
borderRadius: 16,
borderRadius: 24,
overflow: 'hidden',
shadowColor: '#4F5BD5',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 8,
},
saveButtonGradient: {
paddingVertical: 16,
alignItems: 'center',
},
saveButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
fontWeight: '700',
fontFamily: 'AliBold',
},
});