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:
@@ -602,7 +602,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 26,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
fontFamily: 'AliBold'
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Dimensions,
|
||||
Modal,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
|
||||
export default function FoodCameraScreen() {
|
||||
const safeAreaTop = useSafeAreaTop()
|
||||
const { t } = useI18n();
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams<{ mealType?: string }>();
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors;
|
||||
const colors = Colors[scheme];
|
||||
|
||||
const [currentMealType, setCurrentMealType] = useState<MealType>(
|
||||
(params.mealType as MealType) || 'dinner'
|
||||
@@ -33,57 +41,73 @@ export default function FoodCameraScreen() {
|
||||
const [facing, setFacing] = useState<CameraType>('back');
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const [showInstructionModal, setShowInstructionModal] = useState(false);
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
|
||||
// 餐次选择选项
|
||||
const mealOptions = [
|
||||
{ key: 'breakfast' as const, label: '早餐', icon: '☀️' },
|
||||
{ key: 'lunch' as const, label: '午餐', icon: '🌤️' },
|
||||
{ key: 'dinner' as const, label: '晚餐', icon: '🌙' },
|
||||
{ key: 'snack' as const, label: '加餐', icon: '🍎' },
|
||||
{ key: 'breakfast' as const, label: t('nutritionRecords.mealTypes.breakfast'), icon: '☀️' },
|
||||
{ key: 'lunch' as const, label: t('nutritionRecords.mealTypes.lunch'), icon: '🌤️' },
|
||||
{ key: 'dinner' as const, label: t('nutritionRecords.mealTypes.dinner'), icon: '🌙' },
|
||||
{ key: 'snack' as const, label: t('nutritionRecords.mealTypes.snack'), icon: '🍎' },
|
||||
];
|
||||
|
||||
// 计算固定的相机高度
|
||||
const cameraHeight = useMemo(() => {
|
||||
const { height: screenHeight } = Dimensions.get('window');
|
||||
|
||||
// 计算固定占用的高度
|
||||
const headerHeight = insets.top + 40; // HeaderBar 高度
|
||||
const topMetaHeight = 12 + 28 + 26 + 16 + 6; // topMeta 区域
|
||||
const shotsRowHeight = 12 + 88; // MealType 区域
|
||||
const bottomBarHeight = 12 + 86 + 10 + Math.max(insets.bottom, 20); // bottomBar 区域
|
||||
const margins = 12 + 12; // cameraCard 的上下边距
|
||||
|
||||
// 可用于相机的高度
|
||||
const availableHeight = screenHeight - headerHeight - topMetaHeight - shotsRowHeight - bottomBarHeight - margins;
|
||||
|
||||
// 确保最小高度为 300,最大不超过屏幕的 55%
|
||||
return Math.max(300, Math.min(availableHeight, screenHeight * 0.55));
|
||||
}, [insets.top, insets.bottom]);
|
||||
|
||||
if (!permission) {
|
||||
// 权限仍在加载中
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient colors={['#fefefe', '#f4f7fb']} style={StyleSheet.absoluteFill} />
|
||||
<HeaderBar
|
||||
title="食物拍摄"
|
||||
title={t('foodCamera.title')}
|
||||
onBack={() => router.back()}
|
||||
transparent={true}
|
||||
/>
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}} />
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>正在加载相机...</Text>
|
||||
<View style={[styles.loadingContainer, { paddingTop: insets.top + 40 }]}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission.granted) {
|
||||
// 没有相机权限
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: '#f8fafc' }]}>
|
||||
<HeaderBar
|
||||
title="食物拍摄"
|
||||
title={t('foodCamera.title')}
|
||||
onBack={() => router.back()}
|
||||
backColor='#ffffff'
|
||||
transparent
|
||||
/>
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}} />
|
||||
<View style={styles.permissionContainer}>
|
||||
<Ionicons name="camera-outline" size={64} color="#999" />
|
||||
<Text style={styles.permissionTitle}>需要相机权限</Text>
|
||||
<Text style={styles.permissionText}>
|
||||
为了拍摄食物,需要访问您的相机
|
||||
<View style={[styles.permissionCard, { marginTop: insets.top + 60 }]}>
|
||||
<Ionicons name="camera-outline" size={64} color="#94a3b8" style={{ marginBottom: 20 }} />
|
||||
<Text style={styles.permissionTitle}>
|
||||
{t('foodCamera.permission.title')}
|
||||
</Text>
|
||||
<Text style={styles.permissionTip}>
|
||||
{t('foodCamera.permission.description')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.permissionButton}
|
||||
style={[styles.permissionBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={requestPermission}
|
||||
>
|
||||
<Text style={styles.permissionButtonText}>授权访问</Text>
|
||||
<Text style={styles.permissionBtnText}>
|
||||
{t('foodCamera.permission.button')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -97,7 +121,8 @@ export default function FoodCameraScreen() {
|
||||
|
||||
// 拍摄照片
|
||||
const takePicture = async () => {
|
||||
if (cameraRef.current) {
|
||||
if (cameraRef.current && !isCapturing) {
|
||||
setIsCapturing(true);
|
||||
try {
|
||||
const photo = await cameraRef.current.takePictureAsync({
|
||||
quality: 0.8,
|
||||
@@ -105,7 +130,6 @@ export default function FoodCameraScreen() {
|
||||
});
|
||||
|
||||
if (photo) {
|
||||
// 先验证登录状态,再跳转到食物识别页面
|
||||
console.log('照片拍摄成功:', photo.uri);
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (isLoggedIn) {
|
||||
@@ -114,7 +138,9 @@ export default function FoodCameraScreen() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('拍照失败:', error);
|
||||
Alert.alert('拍照失败', '请重试');
|
||||
Alert.alert(t('foodCamera.alerts.captureFailed.title'), t('foodCamera.alerts.captureFailed.message'));
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -132,7 +158,6 @@ export default function FoodCameraScreen() {
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
const imageUri = result.assets[0].uri;
|
||||
console.log('从相册选择的照片:', imageUri);
|
||||
// 先验证登录状态,再跳转到食物识别页面
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (isLoggedIn) {
|
||||
router.push(`/food/food-recognition?imageUri=${encodeURIComponent(imageUri)}&mealType=${currentMealType}`);
|
||||
@@ -140,15 +165,10 @@ export default function FoodCameraScreen() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择照片失败:', error);
|
||||
Alert.alert('选择失败', '请重试');
|
||||
Alert.alert(t('foodCamera.alerts.pickFailed.title'), t('foodCamera.alerts.pickFailed.message'));
|
||||
}
|
||||
};
|
||||
|
||||
// AR功能(暂时显示提示)
|
||||
const handleARPress = () => {
|
||||
Alert.alert('AR功能', 'AR食物识别功能即将推出');
|
||||
};
|
||||
|
||||
// 餐次选择
|
||||
const handleMealTypeChange = (mealType: MealType) => {
|
||||
setCurrentMealType(mealType);
|
||||
@@ -156,35 +176,63 @@ export default function FoodCameraScreen() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
<LinearGradient colors={['#fefefe', '#f4f7fb']} style={StyleSheet.absoluteFill} />
|
||||
|
||||
{/* 头部导航 */}
|
||||
<HeaderBar
|
||||
title=""
|
||||
title={t('foodCamera.title')}
|
||||
onBack={() => router.back()}
|
||||
transparent={true}
|
||||
backColor={'#fff'}
|
||||
right={
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowInstructionModal(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.infoButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="help-circle-outline" size={24} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.infoButton, styles.fallbackInfoButton]}>
|
||||
<Ionicons name="help-circle-outline" size={24} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}} />
|
||||
{/* 主要内容区域 */}
|
||||
<View style={styles.contentContainer}>
|
||||
{/* 取景框容器 */}
|
||||
<View style={styles.cameraFrameContainer}>
|
||||
<Text style={styles.hintText}>确保食物在取景框内</Text>
|
||||
|
||||
{/* 相机取景框包装器 */}
|
||||
<View style={styles.cameraWrapper}>
|
||||
{/* 相机取景框 */}
|
||||
<View style={styles.cameraFrame}>
|
||||
<View style={{ height: insets.top + 40 }} />
|
||||
|
||||
{/* Top Meta Info */}
|
||||
<View style={styles.topMeta}>
|
||||
<View style={styles.metaBadge}>
|
||||
<Text style={styles.metaBadgeText}>{t('foodCamera.hint')}</Text>
|
||||
</View>
|
||||
<Text style={styles.metaTitle}>
|
||||
{t('nutritionRecords.listTitle')}
|
||||
</Text>
|
||||
<Text style={styles.metaSubtitle}>
|
||||
{t('foodCamera.guide.description')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Camera Card */}
|
||||
<View style={styles.cameraCard}>
|
||||
<View style={[styles.cameraFrame, { height: cameraHeight }]}>
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={styles.cameraView}
|
||||
facing={facing}
|
||||
/>
|
||||
</View>
|
||||
{/* 取景框装饰 - 放在外层避免被截断 */}
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.1)']}
|
||||
style={styles.cameraOverlay}
|
||||
/>
|
||||
{/* Viewfinder Overlay */}
|
||||
<View style={styles.viewfinderOverlay}>
|
||||
<View style={[styles.corner, styles.topLeft]} />
|
||||
<View style={[styles.corner, styles.topRight]} />
|
||||
@@ -194,50 +242,122 @@ export default function FoodCameraScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 餐次选择器 */}
|
||||
<View style={styles.mealTypeContainer}>
|
||||
{mealOptions.map((option) => (
|
||||
{/* Meal Type Selector (Replacing Shots Row) */}
|
||||
<View style={styles.shotsRow}>
|
||||
{mealOptions.map((option) => {
|
||||
const active = currentMealType === option.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.key}
|
||||
style={[
|
||||
styles.mealTypeButton,
|
||||
currentMealType === option.key && styles.mealTypeButtonActive
|
||||
]}
|
||||
onPress={() => handleMealTypeChange(option.key)}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.shotCard, active && styles.shotCardActive]}
|
||||
>
|
||||
<Text style={styles.mealTypeIcon}>{option.icon}</Text>
|
||||
<Text style={[
|
||||
styles.mealTypeText,
|
||||
currentMealType === option.key && styles.mealTypeTextActive
|
||||
]}>
|
||||
<Text style={[styles.shotLabel, active && styles.shotLabelActive]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 底部控制栏 */}
|
||||
<View style={styles.bottomContainer}>
|
||||
<View style={styles.controlsContainer}>
|
||||
{/* 相册选择按钮 */}
|
||||
<TouchableOpacity style={styles.galleryButton} onPress={pickImageFromGallery}>
|
||||
<Ionicons name="images-outline" size={24} color="#FFF" />
|
||||
{/* Bottom Actions */}
|
||||
<View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 20) }]}>
|
||||
<View style={styles.bottomActions}>
|
||||
{/* Album Button */}
|
||||
<TouchableOpacity
|
||||
onPress={pickImageFromGallery}
|
||||
disabled={isCapturing}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.secondaryBtn}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.6)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="images-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('foodCamera.buttons.album')}
|
||||
</Text>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.secondaryBtn, styles.fallbackSecondaryBtn]}>
|
||||
<Ionicons name="images-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('foodCamera.buttons.album')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 拍照按钮 */}
|
||||
<TouchableOpacity style={styles.captureButton} onPress={takePicture}>
|
||||
<View style={styles.captureButtonInner} />
|
||||
{/* Capture Button */}
|
||||
<TouchableOpacity
|
||||
onPress={takePicture}
|
||||
disabled={isCapturing}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.captureBtn}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.8)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<View style={styles.captureOuterRing}>
|
||||
{isCapturing ? (
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
) : (
|
||||
<View style={styles.captureInner} />
|
||||
)}
|
||||
</View>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.captureBtn, styles.fallbackCaptureBtn]}>
|
||||
<View style={styles.captureOuterRing}>
|
||||
{isCapturing ? (
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
) : (
|
||||
<View style={styles.captureInner} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 帮助按钮 */}
|
||||
<TouchableOpacity style={styles.helpButton} onPress={() => setShowInstructionModal(true)}>
|
||||
<Ionicons name="help-outline" size={24} color="#FFF" />
|
||||
{/* Flip Button */}
|
||||
<TouchableOpacity
|
||||
onPress={toggleCameraFacing}
|
||||
disabled={isCapturing}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.secondaryBtn}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.6)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="camera-reverse-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('foodCamera.buttons.capture')}
|
||||
</Text>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.secondaryBtn, styles.fallbackSecondaryBtn]}>
|
||||
<Ionicons name="camera-reverse-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('foodCamera.buttons.capture')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 拍摄说明弹窗 */}
|
||||
{/* Instruction Modal */}
|
||||
<Modal
|
||||
visible={showInstructionModal}
|
||||
animationType="fade"
|
||||
@@ -246,48 +366,51 @@ export default function FoodCameraScreen() {
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.instructionModal}>
|
||||
<Text style={styles.instructionTitle}>拍摄示例</Text>
|
||||
<Text style={styles.instructionTitle}>{t('foodCamera.guide.title')}</Text>
|
||||
|
||||
<View style={styles.exampleContainer}>
|
||||
{/* 好的示例 */}
|
||||
{/* Good Example */}
|
||||
<View style={styles.exampleItem}>
|
||||
<View style={styles.exampleImagePlaceholder}>
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<Ionicons name="checkmark" size={32} color="#FFF" />
|
||||
<Ionicons name="checkmark" size={20} color="#FFF" />
|
||||
</View>
|
||||
{/* 这里可以放置好的示例图片 */}
|
||||
<Image
|
||||
style={styles.exampleImage}
|
||||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/function/food-right.jpeg' }}
|
||||
contentFit="cover"
|
||||
cachePolicy={'memory-disk'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.exampleText}>{t('foodCamera.guide.good')}</Text>
|
||||
</View>
|
||||
|
||||
{/* 不好的示例 */}
|
||||
{/* Bad Example */}
|
||||
<View style={styles.exampleItem}>
|
||||
<View style={styles.exampleImagePlaceholder}>
|
||||
<View style={styles.crossContainer}>
|
||||
<Ionicons name="close" size={32} color="#FFF" />
|
||||
<Ionicons name="close" size={20} color="#FFF" />
|
||||
</View>
|
||||
<Image
|
||||
style={styles.exampleImage}
|
||||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/function/food-wrong.jpeg' }}
|
||||
contentFit="cover"
|
||||
cachePolicy={'memory-disk'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.exampleText}>{t('foodCamera.guide.bad')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.instructionDescription}>
|
||||
请上传或拍摄如左图所示的食物照片
|
||||
{t('foodCamera.guide.description')}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.knowButton}
|
||||
onPress={() => setShowInstructionModal(false)}
|
||||
>
|
||||
<Text style={styles.knowButtonText}>知道了</Text>
|
||||
<Text style={styles.knowButtonText}>{t('foodCamera.guide.button')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -299,107 +422,77 @@ export default function FoodCameraScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
paddingTop: 100,
|
||||
},
|
||||
cameraFrameContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
cameraWrapper: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
position: 'relative',
|
||||
},
|
||||
cameraFrame: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
cameraView: {
|
||||
flex: 1,
|
||||
},
|
||||
viewfinderOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
topMeta: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
gap: 6,
|
||||
},
|
||||
permissionContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
paddingHorizontal: 40,
|
||||
metaBadge: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#e0f2fe',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 14,
|
||||
},
|
||||
permissionTitle: {
|
||||
color: '#FFF',
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
metaBadgeText: {
|
||||
color: '#0369a1',
|
||||
fontWeight: '700',
|
||||
fontSize: 12,
|
||||
},
|
||||
permissionText: {
|
||||
color: '#CCC',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
metaTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
},
|
||||
permissionButton: {
|
||||
backgroundColor: Colors.light.primary,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
metaSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#475569',
|
||||
},
|
||||
cameraCard: {
|
||||
marginHorizontal: 20,
|
||||
marginTop: 12,
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 18,
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
},
|
||||
permissionButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
cameraFrame: {
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#0b172a',
|
||||
position: 'relative',
|
||||
},
|
||||
header: {
|
||||
cameraView: {
|
||||
flex: 1,
|
||||
},
|
||||
cameraOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
bottom: 0,
|
||||
height: 80,
|
||||
},
|
||||
hintText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
viewfinderOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: 20,
|
||||
},
|
||||
corner: {
|
||||
position: 'absolute',
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
borderWidth: 4,
|
||||
borderRadius: 2,
|
||||
},
|
||||
topLeft: {
|
||||
top: 0,
|
||||
@@ -425,198 +518,241 @@ const styles = StyleSheet.create({
|
||||
borderLeftWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
mealTypeContainer: {
|
||||
shotsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
marginVertical: 20,
|
||||
paddingTop: 12,
|
||||
gap: 8,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
mealTypeButton: {
|
||||
shotCard: {
|
||||
flex: 1,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#f8fafc',
|
||||
paddingVertical: 12,
|
||||
gap: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
minWidth: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mealTypeButtonActive: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
shotCardActive: {
|
||||
borderColor: '#38bdf8',
|
||||
backgroundColor: '#ecfeff',
|
||||
},
|
||||
mealTypeIcon: {
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
mealTypeText: {
|
||||
color: '#FFF',
|
||||
shotLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#475569',
|
||||
fontWeight: '600',
|
||||
},
|
||||
mealTypeTextActive: {
|
||||
color: '#333',
|
||||
shotLabelActive: {
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
bottomContainer: {
|
||||
paddingBottom: 40,
|
||||
bottomBar: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
gap: 10,
|
||||
},
|
||||
controlsContainer: {
|
||||
bottomActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
controlButton: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
controlButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
fontWeight: '500',
|
||||
},
|
||||
albumButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
captureBtn: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#0ea5e9',
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
},
|
||||
fallbackCaptureBtn: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
borderColor: 'rgba(14, 165, 233, 0.2)',
|
||||
},
|
||||
captureButton: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: '#FFF',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 4,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
captureButtonInner: {
|
||||
captureOuterRing: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#FFF',
|
||||
borderWidth: 2,
|
||||
borderColor: '#333',
|
||||
},
|
||||
arButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
},
|
||||
arButtonText: {
|
||||
color: '#FFF',
|
||||
captureInner: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#0ea5e9',
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
},
|
||||
secondaryBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
minWidth: 88,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
fallbackSecondaryBtn: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15, 23, 42, 0.1)',
|
||||
},
|
||||
secondaryBtnText: {
|
||||
color: '#0f172a',
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
galleryButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
justifyContent: 'center',
|
||||
infoButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
helpButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
justifyContent: 'center',
|
||||
fallbackInfoButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
permissionCard: {
|
||||
marginHorizontal: 24,
|
||||
borderRadius: 18,
|
||||
padding: 24,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
gap: 10,
|
||||
},
|
||||
permissionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
marginBottom: 4,
|
||||
},
|
||||
permissionTip: {
|
||||
fontSize: 14,
|
||||
color: '#475569',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
permissionBtn: {
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 14,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
permissionBtnText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
instructionModal: {
|
||||
backgroundColor: '#FFF',
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 32,
|
||||
minHeight: 400,
|
||||
borderRadius: 24,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
instructionTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 32,
|
||||
color: '#333',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
marginBottom: 24,
|
||||
},
|
||||
exampleContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
exampleItem: {
|
||||
flex: 1,
|
||||
marginHorizontal: 8,
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
exampleImagePlaceholder: {
|
||||
width: '100%',
|
||||
aspectRatio: 3 / 4,
|
||||
backgroundColor: '#F0F0F0',
|
||||
aspectRatio: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
exampleImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
exampleText: {
|
||||
fontSize: 13,
|
||||
color: '#64748b',
|
||||
fontWeight: '500',
|
||||
},
|
||||
checkmarkContainer: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#4CAF50',
|
||||
top: 8,
|
||||
right: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#22c55e',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
crossContainer: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F44336',
|
||||
top: 8,
|
||||
right: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#ef4444',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
exampleImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
instructionDescription: {
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
marginBottom: 32,
|
||||
lineHeight: 24,
|
||||
paddingHorizontal: 16,
|
||||
color: '#334155',
|
||||
marginBottom: 24,
|
||||
lineHeight: 22,
|
||||
},
|
||||
knowButton: {
|
||||
backgroundColor: '#000',
|
||||
borderRadius: 25,
|
||||
paddingVertical: 16,
|
||||
marginHorizontal: 16,
|
||||
backgroundColor: '#0f172a',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 32,
|
||||
width: '100%',
|
||||
},
|
||||
knowButtonText: {
|
||||
color: '#FFF',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
{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>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.primaryFooterBtn,
|
||||
{ width: '100%', backgroundColor: colors.primary },
|
||||
]}
|
||||
>
|
||||
{aiDraftSaving ? (
|
||||
<ActivityIndicator color={colors.onPrimary} />
|
||||
) : (
|
||||
<Text style={[styles.primaryFooterText, { color: colors.onPrimary }]}>保存并创建</Text>
|
||||
<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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,30 +349,95 @@ 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()}
|
||||
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>
|
||||
);
|
||||
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}}>
|
||||
const renderRecord = ({ item }: { item: DietRecord }) => (
|
||||
<NutritionRecordCard
|
||||
record={item}
|
||||
onPress={() => handleRecordPress(item)}
|
||||
onDelete={() => handleDeleteRecord(item.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
{/* {renderViewModeToggle()} */}
|
||||
{renderDateSelector()}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Calorie Ring Chart */}
|
||||
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}
|
||||
@@ -450,15 +449,44 @@ export default function NutritionRecordsScreen() {
|
||||
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={({ item, index }) => renderRecord({ item, index })}
|
||||
renderItem={renderRecord}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={[
|
||||
styles.listContainer,
|
||||
{ paddingBottom: 40, paddingTop: 16 }
|
||||
{ paddingTop: safeAreaTop }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
@@ -469,14 +497,12 @@ export default function NutritionRecordsScreen() {
|
||||
colors={[colorTokens.primary]}
|
||||
/>
|
||||
}
|
||||
ListHeaderComponent={ListHeader}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={viewMode === 'all' ? loadMoreRecords : undefined}
|
||||
onEndReachedThreshold={0.1}
|
||||
/>
|
||||
)}
|
||||
|
||||
</View>
|
||||
|
||||
{/* 食物添加悬浮窗 */}
|
||||
<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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 }]}>
|
||||
“今早吃了两个煎蛋、一片全麦面包和一杯牛奶”
|
||||
</Text>
|
||||
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
|
||||
“午饭吃了红烧肉约150克、米饭一小碗、青菜一份”
|
||||
</Text>
|
||||
<Text style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
|
||||
“晚饭吃了蒸蛋羹、紫菜蛋花汤、小米粥一碗”
|
||||
{[
|
||||
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 }]}>
|
||||
“{example}”
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -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,86 +182,131 @@ 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 style={styles.historySection}>
|
||||
<Text style={styles.sectionTitle}>{t('weightRecords.history')}</Text>
|
||||
{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 style={styles.monthHeader}>
|
||||
<Text style={styles.monthTitle}>{month}</Text>
|
||||
</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> */}
|
||||
|
||||
{/* 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;
|
||||
@@ -280,9 +324,15 @@ export default function WeightRecordsPage() {
|
||||
);
|
||||
})}
|
||||
</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}
|
||||
>
|
||||
<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',
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, StyleSheet, View } from 'react-native';
|
||||
import Svg, { Circle } from 'react-native-svg';
|
||||
import Svg, { Circle, Defs, Stop, LinearGradient as SvgLinearGradient } from 'react-native-svg';
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||
|
||||
@@ -26,12 +26,8 @@ export function CalorieRingChart({
|
||||
protein,
|
||||
fat,
|
||||
carbs,
|
||||
proteinGoal,
|
||||
fatGoal,
|
||||
carbsGoal,
|
||||
|
||||
}: CalorieRingChartProps) {
|
||||
const surfaceColor = useThemeColor({}, 'surface');
|
||||
const { t } = useI18n();
|
||||
const textColor = useThemeColor({}, 'text');
|
||||
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
||||
|
||||
@@ -46,9 +42,9 @@ export function CalorieRingChart({
|
||||
const totalAvailable = metabolism + exercise;
|
||||
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
||||
|
||||
// 圆环参数 - 减小尺寸以优化空间占用
|
||||
const radius = 48;
|
||||
const strokeWidth = 8; // 增加圆环厚度
|
||||
// 圆环参数 - 缩小尺寸
|
||||
const radius = 42;
|
||||
const strokeWidth = 8;
|
||||
const center = radius + strokeWidth;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const strokeDasharray = circumference;
|
||||
@@ -70,34 +66,32 @@ export function CalorieRingChart({
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: surfaceColor }]}>
|
||||
{/* 左上角公式展示 */}
|
||||
<View style={styles.formulaContainer}>
|
||||
<ThemedText style={[styles.formulaText, { color: textSecondaryColor }]}>
|
||||
还能吃 = 代谢 + 运动 - 饮食
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<View style={styles.container}>
|
||||
<View style={styles.mainContent}>
|
||||
{/* 左侧圆环图 */}
|
||||
<View style={styles.chartContainer}>
|
||||
<Svg width={center * 2} height={center * 2}>
|
||||
<Defs>
|
||||
<SvgLinearGradient id="progressGradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<Stop offset="0" stopColor={progressPercentage > 80 ? "#FF9966" : "#4facfe"} stopOpacity="1" />
|
||||
<Stop offset="1" stopColor={progressPercentage > 80 ? "#FF5E62" : "#00f2fe"} stopOpacity="1" />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
{/* 背景圆环 */}
|
||||
<Circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke="#F0F0F0"
|
||||
stroke="#F5F7FA"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
{/* 进度圆环 - 保持固定颜色 */}
|
||||
{/* 进度圆环 */}
|
||||
<AnimatedCircle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke={progressPercentage > 80 ? "#FF6B6B" : "#4ECDC4"}
|
||||
stroke="url(#progressGradient)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={`${strokeDasharray}`}
|
||||
@@ -109,67 +103,67 @@ export function CalorieRingChart({
|
||||
|
||||
{/* 中心内容 */}
|
||||
<View style={styles.centerContent}>
|
||||
<ThemedText style={[styles.centerLabel, { color: textSecondaryColor }]}>
|
||||
还能吃
|
||||
<ThemedText style={styles.centerLabel}>
|
||||
{t('nutritionRecords.chart.remaining')}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.centerValue, { color: textColor }]}>
|
||||
{Math.round(canEat)}千卡
|
||||
<ThemedText style={styles.centerValue}>
|
||||
{Math.round(canEat)}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.centerUnit}>
|
||||
{t('nutritionRecords.nutrients.caloriesUnit')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧数据展示 */}
|
||||
{/* 右侧数据展示 - 优化布局 */}
|
||||
<View style={styles.dataContainer}>
|
||||
<View style={styles.dataBackground}>
|
||||
{/* 左右两列布局 */}
|
||||
<View style={styles.dataColumns}>
|
||||
{/* 左列:卡路里数据 */}
|
||||
<View style={styles.dataColumn}>
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(metabolism)}千卡
|
||||
{/* 公式 */}
|
||||
<View style={styles.formulaContainer}>
|
||||
<ThemedText style={styles.formulaText}>
|
||||
{t('nutritionRecords.chart.formula')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>运动</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(exercise)}千卡
|
||||
</ThemedText>
|
||||
{/* 代谢 & 运动 & 饮食 */}
|
||||
<View style={styles.statsGroup}>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotMetabolism} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.metabolism')}</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>饮食</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(consumed)}千卡
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.statValue}>{Math.round(metabolism)}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotExercise} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.exercise')}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.statValue}>{Math.round(exercise)}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotConsumed} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.diet')}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.statValue}>{Math.round(consumed)}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右列:营养数据 */}
|
||||
<View style={styles.dataColumn}>
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>蛋白质</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(protein)}g
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>脂肪</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(fat)}g
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>碳水</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(carbs)}g
|
||||
</ThemedText>
|
||||
{/* 营养素 - 水平排布 */}
|
||||
<View style={styles.nutritionRow}>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(protein)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.protein')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(fat)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.fat')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(carbs)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.carbs')}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -181,40 +175,35 @@ export function CalorieRingChart({
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
marginHorizontal: 20,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 16,
|
||||
elevation: 6,
|
||||
},
|
||||
formulaContainer: {
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
formulaText: {
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
lineHeight: 16,
|
||||
color: '#94A3B8',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
mainContent: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 0, // 移除底部间距,因为不再有底部营养容器
|
||||
paddingHorizontal: 8,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
chartContainer: {
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 112, // 减少宽度以匹配更小的圆环 (48*2 + 8*2)
|
||||
flexShrink: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginTop: 8,
|
||||
},
|
||||
centerContent: {
|
||||
position: 'absolute',
|
||||
@@ -222,71 +211,95 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
centerLabel: {
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
marginBottom: 2,
|
||||
color: '#94A3B8',
|
||||
marginBottom: 1,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
centerValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333333',
|
||||
marginBottom: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
lineHeight: 24,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
centerPercentage: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
centerUnit: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
dataContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
marginLeft: 20,
|
||||
},
|
||||
dataBackground: {
|
||||
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 1,
|
||||
statsGroup: {
|
||||
gap: 6,
|
||||
},
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 3,
|
||||
elevation: 1,
|
||||
// 添加边框增强毛玻璃效果
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255, 255, 255, 0.8)',
|
||||
gap: 4,
|
||||
},
|
||||
dataItem: {
|
||||
statRowCompact: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
dataIcon: {
|
||||
labelWithDot: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dotMetabolism: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#94A3B8',
|
||||
marginRight: 6,
|
||||
},
|
||||
dataLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
minWidth: 28,
|
||||
dotExercise: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#4facfe',
|
||||
marginRight: 6,
|
||||
},
|
||||
dataValue: {
|
||||
fontSize: 11,
|
||||
dotConsumed: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#FF9966',
|
||||
marginRight: 6,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#333333',
|
||||
color: '#334155',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dataColumns: {
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
marginVertical: 10,
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
},
|
||||
dataColumn: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
nutritionItem: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
statLabelSmall: {
|
||||
fontSize: 10,
|
||||
color: '#94A3B8',
|
||||
marginTop: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
statValueSmall: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#475569',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
});
|
||||
@@ -463,16 +463,16 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
monthTitle: {
|
||||
fontSize: 22,
|
||||
fontSize: 26,
|
||||
fontWeight: '800',
|
||||
color: '#1a1a1a',
|
||||
letterSpacing: -0.5,
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
marginLeft: 8,
|
||||
},
|
||||
calendarIconButton: {
|
||||
padding: 4,
|
||||
borderRadius: 6,
|
||||
marginLeft: 4,
|
||||
padding: 6,
|
||||
borderRadius: 12,
|
||||
marginLeft: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
calendarIconFallback: {
|
||||
@@ -481,22 +481,19 @@ const styles = StyleSheet.create({
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
todayButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
marginRight: 8,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 16,
|
||||
marginRight: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
todayButtonFallback: {
|
||||
backgroundColor: '#EEF2FF',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(124, 58, 237, 0.2)',
|
||||
},
|
||||
todayButtonText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#7c3aed',
|
||||
letterSpacing: 0.2,
|
||||
color: '#5F6BF0',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
daysContainer: {
|
||||
@@ -508,8 +505,8 @@ const styles = StyleSheet.create({
|
||||
marginRight: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 40,
|
||||
height: 60,
|
||||
width: 48,
|
||||
height: 68,
|
||||
borderRadius: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -523,14 +520,12 @@ const styles = StyleSheet.create({
|
||||
transform: [{ scale: 0.96 }],
|
||||
},
|
||||
dayPillSelectedFallback: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: '#5F6BF0',
|
||||
shadowColor: 'rgba(95, 107, 240, 0.3)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
dayPillDisabled: {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -538,30 +533,30 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
color: '#8e8e93',
|
||||
marginBottom: 2,
|
||||
letterSpacing: 0.1,
|
||||
fontFamily: 'AliBold',
|
||||
fontWeight: '600',
|
||||
color: '#94A3B8',
|
||||
marginBottom: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
dayLabelSelected: {
|
||||
color: '#1a1a1a',
|
||||
fontWeight: '800',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
opacity: 0.9,
|
||||
},
|
||||
dayLabelDisabled: {
|
||||
color: '#c7c7cc',
|
||||
},
|
||||
dayDate: {
|
||||
fontSize: 13,
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: '#8e8e93',
|
||||
letterSpacing: -0.2,
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dayDateSelected: {
|
||||
color: '#1a1a1a',
|
||||
fontWeight: '800',
|
||||
fontSize: 16,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dayDateDisabled: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { useRouter } from 'expo-router';
|
||||
@@ -20,6 +21,7 @@ interface FloatingFoodOverlayProps {
|
||||
|
||||
export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { pushIfAuthedElseLogin } = useAuthGuard()
|
||||
|
||||
@@ -41,21 +43,21 @@ export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: F
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'scan',
|
||||
title: 'AI识别',
|
||||
title: t('nutritionRecords.overlay.scan'),
|
||||
icon: '📷',
|
||||
backgroundColor: '#4FC3F7',
|
||||
onPress: handlePhotoRecognition,
|
||||
},
|
||||
{
|
||||
id: 'food-library',
|
||||
title: '食物库',
|
||||
title: t('nutritionRecords.overlay.foodLibrary'),
|
||||
icon: '🍎',
|
||||
backgroundColor: '#FF9500',
|
||||
onPress: handleFoodLibrary,
|
||||
},
|
||||
{
|
||||
id: 'voice-record',
|
||||
title: '一句话记录',
|
||||
title: t('nutritionRecords.overlay.voiceRecord'),
|
||||
icon: '🎤',
|
||||
backgroundColor: '#7B68EE',
|
||||
onPress: handleVoiceRecord,
|
||||
@@ -81,7 +83,7 @@ export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: F
|
||||
<View style={styles.container}>
|
||||
<BlurView intensity={80} tint="light" style={styles.blurContainer}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>记录方式</Text>
|
||||
<Text style={styles.title}>{t('nutritionRecords.overlay.title')}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.menuGrid}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
import { DietRecord } from '@/services/dietRecords';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -15,14 +16,6 @@ export type NutritionRecordCardProps = {
|
||||
onDelete?: () => void;
|
||||
};
|
||||
|
||||
const MEAL_TYPE_LABELS = {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
other: '其他',
|
||||
} as const;
|
||||
|
||||
const MEAL_TYPE_ICONS = {
|
||||
breakfast: 'sunny-outline',
|
||||
lunch: 'partly-sunny-outline',
|
||||
@@ -44,46 +37,40 @@ export function NutritionRecordCard({
|
||||
onPress,
|
||||
onDelete
|
||||
}: NutritionRecordCardProps) {
|
||||
const surfaceColor = useThemeColor({}, 'surface');
|
||||
const { t } = useI18n();
|
||||
const textColor = useThemeColor({}, 'text');
|
||||
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
||||
|
||||
// Popover 状态管理
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const popoverRef = useRef<any>(null);
|
||||
|
||||
// 左滑删除相关
|
||||
const swipeableRef = useRef<Swipeable>(null);
|
||||
|
||||
// 添加滑动状态管理,防止滑动时触发点击事件
|
||||
const [isSwiping, setIsSwiping] = useState(false);
|
||||
|
||||
// 营养数据统计
|
||||
const nutritionStats = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: '蛋白质',
|
||||
value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)}g` : '-',
|
||||
icon: '🥩',
|
||||
color: '#FF6B6B'
|
||||
label: t('nutritionRecords.nutrients.protein'),
|
||||
value: record.proteinGrams ? `${Math.round(record.proteinGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
{
|
||||
label: '脂肪',
|
||||
value: record.fatGrams ? `${record.fatGrams.toFixed(1)}g` : '-',
|
||||
icon: '🥑',
|
||||
color: '#FFB366'
|
||||
label: t('nutritionRecords.nutrients.fat'),
|
||||
value: record.fatGrams ? `${Math.round(record.fatGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
{
|
||||
label: '碳水',
|
||||
value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)}g` : '-',
|
||||
icon: '🍞',
|
||||
color: '#4ECDC4'
|
||||
label: t('nutritionRecords.nutrients.carbs'),
|
||||
value: record.carbohydrateGrams ? `${Math.round(record.carbohydrateGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
];
|
||||
}, [record]);
|
||||
}, [record, t]);
|
||||
|
||||
const mealTypeColor = MEAL_TYPE_COLORS[record.mealType];
|
||||
const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType];
|
||||
const mealTypeLabel = t(`nutritionRecords.mealTypes.${record.mealType}`);
|
||||
|
||||
// 处理点击事件,只有在非滑动状态下才触发
|
||||
const handlePress = () => {
|
||||
@@ -92,31 +79,17 @@ export function NutritionRecordCard({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理滑动开始
|
||||
const handleSwipeableWillOpen = () => {
|
||||
setIsSwiping(true);
|
||||
};
|
||||
const handleSwipeableWillOpen = () => setIsSwiping(true);
|
||||
const handleSwipeableClose = () => setTimeout(() => setIsSwiping(false), 100);
|
||||
|
||||
// 处理滑动结束
|
||||
const handleSwipeableClose = () => {
|
||||
// 延迟重置滑动状态,防止滑动结束时立即触发点击
|
||||
setTimeout(() => {
|
||||
setIsSwiping(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Alert.alert(
|
||||
'确认删除',
|
||||
`确定要删除这条营养记录吗?此操作无法撤销。`,
|
||||
t('nutritionRecords.delete.title'),
|
||||
t('nutritionRecords.delete.message'),
|
||||
[
|
||||
{ text: t('nutritionRecords.delete.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: '取消',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
text: t('nutritionRecords.delete.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
onDelete?.();
|
||||
@@ -127,7 +100,6 @@ export function NutritionRecordCard({
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染删除按钮
|
||||
const renderRightActions = () => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -136,7 +108,6 @@ export function NutritionRecordCard({
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
||||
<Text style={styles.deleteButtonText}>删除</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -152,239 +123,228 @@ export function NutritionRecordCard({
|
||||
onSwipeableClose={handleSwipeableClose}
|
||||
>
|
||||
<RectButton
|
||||
style={[
|
||||
styles.card,
|
||||
|
||||
]}
|
||||
style={styles.card}
|
||||
onPress={handlePress}
|
||||
// activeOpacity={0.7}
|
||||
>
|
||||
{/* 主要内容区域 - 水平布局 */}
|
||||
<View style={styles.mainContent}>
|
||||
{/* 左侧:食物图片 */}
|
||||
<View style={[styles.foodImageContainer, !record.imageUrl && styles.foodImagePlaceholder]}>
|
||||
{record.imageUrl ? (
|
||||
{/* 左侧:时间线和图标 */}
|
||||
<View style={styles.leftSection}>
|
||||
<View style={styles.mealIconContainer}>
|
||||
<Image
|
||||
source={{ uri: record.imageUrl }}
|
||||
style={styles.foodImage}
|
||||
cachePolicy={'memory-disk'}
|
||||
source={require('@/assets/images/icons/icon-food.png')}
|
||||
style={styles.mealIcon}
|
||||
/>
|
||||
) : (
|
||||
<Ionicons name="restaurant" size={28} color={textSecondaryColor} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 中间:主要信息 */}
|
||||
<View style={styles.centerSection}>
|
||||
<View style={styles.titleRow}>
|
||||
<ThemedText style={styles.foodName} numberOfLines={1}>
|
||||
{record.foodName}
|
||||
</ThemedText>
|
||||
<View style={[styles.mealTag, { backgroundColor: `${mealTypeColor}15` }]}>
|
||||
<Text style={[styles.mealTagText, { color: mealTypeColor }]}>{mealTypeLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.metaRow}>
|
||||
<Ionicons name="time-outline" size={12} color="#94A3B8" />
|
||||
<Text style={styles.timeText}>
|
||||
{record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '--:--'}
|
||||
</Text>
|
||||
{record.portionDescription && (
|
||||
<>
|
||||
<Text style={styles.dotSeparator}>·</Text>
|
||||
<Text style={styles.portionText} numberOfLines={1}>{record.portionDescription}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 中间:食物信息 */}
|
||||
<View style={styles.foodInfoContainer}>
|
||||
{/* 食物名称 */}
|
||||
<ThemedText style={[styles.foodName, { color: textColor }]}>
|
||||
{record.foodName}
|
||||
</ThemedText>
|
||||
|
||||
{/* 时间 */}
|
||||
<ThemedText style={[styles.mealTime, { color: textSecondaryColor }]}>
|
||||
{record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '--:--'}
|
||||
</ThemedText>
|
||||
|
||||
{/* 营养信息 - 水平排列 */}
|
||||
<View style={styles.nutritionContainer}>
|
||||
{/* 营养微缩信息 */}
|
||||
<View style={styles.nutritionRow}>
|
||||
{nutritionStats.map((stat, index) => (
|
||||
<View key={stat.label} style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.nutritionIcon}>{stat.icon}</ThemedText>
|
||||
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
||||
{stat.value}
|
||||
</ThemedText>
|
||||
<View key={index} style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{stat.value}<Text style={styles.nutritionUnit}>{stat.unit}</Text></Text>
|
||||
<Text style={styles.nutritionLabel}>{stat.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧:热量和餐次标签 */}
|
||||
{/* 右侧:热量 */}
|
||||
<View style={styles.rightSection}>
|
||||
{/* 热量显示 */}
|
||||
<View style={styles.caloriesContainer}>
|
||||
<ThemedText style={[styles.caloriesText]}>
|
||||
{record.estimatedCalories ? `${Math.round(record.estimatedCalories)} kcal` : '- kcal'}
|
||||
</ThemedText>
|
||||
<Text style={styles.caloriesValue}>
|
||||
{record.estimatedCalories ? Math.round(record.estimatedCalories) : '-'}
|
||||
</Text>
|
||||
<Text style={styles.caloriesUnit}>{t('nutritionRecords.nutrients.caloriesUnit')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 餐次标签 */}
|
||||
<View style={[styles.mealTypeBadge]}>
|
||||
<ThemedText style={[styles.mealTypeText, { color: mealTypeColor }]}>
|
||||
{mealTypeLabel}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
{/* 如果有图片,显示图片缩略图 */}
|
||||
{record.imageUrl && (
|
||||
<View style={styles.imageSection}>
|
||||
<Image
|
||||
source={{ uri: record.imageUrl }}
|
||||
style={styles.foodImage}
|
||||
contentFit="cover"
|
||||
transition={200}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</RectButton>
|
||||
</Swipeable>
|
||||
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
// iOS 阴影效果 - 更自然的阴影
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
marginBottom: 12,
|
||||
marginHorizontal: 24,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
// Android 阴影效果
|
||||
elevation: 3,
|
||||
shadowRadius: 12,
|
||||
elevation: 2,
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
minHeight: 100,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
leftSection: {
|
||||
marginRight: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
foodImageContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
marginRight: 16,
|
||||
mealIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#F8FAFC',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mealIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
opacity: 0.8,
|
||||
},
|
||||
centerSection: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
gap: 8,
|
||||
},
|
||||
foodName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#1E293B',
|
||||
fontFamily: 'AliBold',
|
||||
flexShrink: 1,
|
||||
},
|
||||
mealTag: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
mealTagText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
color: '#94A3B8',
|
||||
marginLeft: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
dotSeparator: {
|
||||
marginHorizontal: 4,
|
||||
color: '#CBD5E1',
|
||||
},
|
||||
portionText: {
|
||||
fontSize: 12,
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
flex: 1,
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
nutritionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
gap: 2,
|
||||
},
|
||||
nutritionValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#475569',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
nutritionUnit: {
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
color: '#94A3B8',
|
||||
marginLeft: 1,
|
||||
},
|
||||
nutritionLabel: {
|
||||
fontSize: 10,
|
||||
color: '#94A3B8',
|
||||
marginLeft: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
rightSection: {
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 2,
|
||||
},
|
||||
caloriesValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
fontFamily: 'AliBold',
|
||||
lineHeight: 22,
|
||||
},
|
||||
caloriesUnit: {
|
||||
fontSize: 10,
|
||||
color: '#94A3B8',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
imageSection: {
|
||||
marginTop: 12,
|
||||
height: 120,
|
||||
width: '100%',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
foodImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
},
|
||||
foodImagePlaceholder: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
foodInfoContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
foodName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333333',
|
||||
lineHeight: 20,
|
||||
},
|
||||
mealTime: {
|
||||
fontSize: 12,
|
||||
fontWeight: '400',
|
||||
color: '#999999',
|
||||
lineHeight: 16,
|
||||
},
|
||||
nutritionContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
marginTop: 2,
|
||||
},
|
||||
nutritionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
nutritionIcon: {
|
||||
fontSize: 14,
|
||||
},
|
||||
nutritionValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#666666',
|
||||
},
|
||||
rightSection: {
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
minHeight: 60,
|
||||
},
|
||||
caloriesContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
caloriesText: {
|
||||
fontSize: 14,
|
||||
color: '#333333',
|
||||
fontWeight: '600',
|
||||
},
|
||||
mealTypeBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
},
|
||||
mealTypeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
moreButton: {
|
||||
padding: 2,
|
||||
},
|
||||
notesSection: {
|
||||
marginTop: 8,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(0,0,0,0.06)',
|
||||
},
|
||||
notesText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
popoverContainer: {
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#FFFFFF',
|
||||
// iOS 阴影效果
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
// Android 阴影效果
|
||||
elevation: 8,
|
||||
// 添加边框
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
popoverBackground: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
popoverContent: {
|
||||
minWidth: 140,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
popoverItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
gap: 12,
|
||||
},
|
||||
popoverText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#EF4444',
|
||||
backgroundColor: '#FF6B6B',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
borderRadius: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
width: 70,
|
||||
height: '100%',
|
||||
borderRadius: 24,
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import CustomCheckBox from '@/components/ui/CheckBox';
|
||||
import { USER_AGREEMENT_URL } from '@/constants/Agree';
|
||||
import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import {
|
||||
MEMBERSHIP_PLAN_META,
|
||||
extractMembershipProductsFromOfferings,
|
||||
@@ -65,51 +66,6 @@ interface BenefitItem {
|
||||
regular: PermissionConfig;
|
||||
}
|
||||
|
||||
// 权益对比配置
|
||||
const BENEFIT_COMPARISON: BenefitItem[] = [
|
||||
{
|
||||
title: 'AI拍照记录热量',
|
||||
description: '通过拍照识别食物并自动记录热量',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '无限次使用',
|
||||
vipText: '无限次使用'
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: '有限次使用',
|
||||
vipText: '每日3次'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'AI拍照识别包装',
|
||||
description: '识别食品包装上的营养成分信息',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '无限次使用',
|
||||
vipText: '无限次使用'
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: '有限次使用',
|
||||
vipText: '每日5次'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '每日健康提醒',
|
||||
description: '根据个人目标提供个性化健康提醒',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '完全支持',
|
||||
vipText: '智能提醒'
|
||||
},
|
||||
regular: {
|
||||
type: 'unlimited',
|
||||
text: '基础提醒',
|
||||
vipText: '基础提醒'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlanType, { gradient: readonly [string, string]; accent: string }> = {
|
||||
lifetime: {
|
||||
@@ -151,6 +107,7 @@ const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
|
||||
};
|
||||
|
||||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -165,6 +122,80 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 保存监听器引用,用于移除监听器
|
||||
const purchaseListenerRef = useRef<((customerInfo: CustomerInfo) => void) | null>(null);
|
||||
|
||||
// 权益对比配置 - Move inside component to use t function
|
||||
const benefitComparison: BenefitItem[] = [
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiCalories.title'),
|
||||
description: t('membershipModal.benefits.items.aiCalories.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 3 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiNutrition.title'),
|
||||
description: t('membershipModal.benefits.items.aiNutrition.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 5 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.healthReminder.title'),
|
||||
description: t('membershipModal.benefits.items.healthReminder.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.fullSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.smartReminder')
|
||||
},
|
||||
regular: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.basicSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.basicSupport')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiMedication.title'),
|
||||
description: t('membershipModal.benefits.items.aiMedication.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.fullAnalysis'),
|
||||
vipText: t('membershipModal.benefits.permissions.fullAnalysis')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.customChallenge.title'),
|
||||
description: t('membershipModal.benefits.items.customChallenge.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.createUnlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.createUnlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// 根据选中的产品生成tips内容
|
||||
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
|
||||
if (!product) return '';
|
||||
@@ -176,11 +207,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
switch (plan.type) {
|
||||
case 'lifetime':
|
||||
return '终身陪伴,见证您的每一次健康蜕变';
|
||||
return t('membershipModal.plans.lifetime.subtitle');
|
||||
case 'quarterly':
|
||||
return '3个月科学计划,让健康成为生活习惯';
|
||||
return t('membershipModal.plans.quarterly.subtitle');
|
||||
case 'weekly':
|
||||
return '7天体验期,感受专业健康指导的力量';
|
||||
return t('membershipModal.plans.weekly.subtitle');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -326,7 +357,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
// 显示成功提示
|
||||
GlobalToast.show({
|
||||
message: '会员开通成功',
|
||||
message: t('membershipModal.success.purchase'),
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
@@ -492,11 +523,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否已同意协议
|
||||
if (!agreementAccepted) {
|
||||
Alert.alert(
|
||||
'请阅读并同意相关协议',
|
||||
'购买前需要同意用户协议、会员协议和自动续费协议',
|
||||
t('membershipModal.agreements.alert.title'),
|
||||
t('membershipModal.agreements.alert.message'),
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -517,11 +548,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否选择了产品
|
||||
if (!selectedProduct) {
|
||||
Alert.alert(
|
||||
'请选择会员套餐',
|
||||
t('membershipModal.errors.selectPlan'),
|
||||
'',
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -579,32 +610,32 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
// 用户取消购买
|
||||
GlobalToast.show({
|
||||
message: '购买已取消',
|
||||
message: t('membershipModal.errors.purchaseCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR) {
|
||||
// 商品已拥有
|
||||
GlobalToast.show({
|
||||
message: '您已拥有此商品',
|
||||
message: t('membershipModal.errors.alreadyPurchased'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
// 网络错误
|
||||
GlobalToast.show({
|
||||
message: '网络连接失败',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
||||
// 支付待处理
|
||||
GlobalToast.show({
|
||||
message: '支付正在处理中',
|
||||
message: t('membershipModal.errors.paymentPending'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
// 凭据无效
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
// 其他错误
|
||||
GlobalToast.show({
|
||||
message: '购买失败',
|
||||
message: t('membershipModal.errors.purchaseFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -701,7 +732,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onClose?.();
|
||||
|
||||
GlobalToast.show({
|
||||
message: '恢复购买成功',
|
||||
message: t('membershipModal.errors.restoreSuccess'),
|
||||
});
|
||||
|
||||
} catch (apiError: any) {
|
||||
@@ -720,7 +751,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
|
||||
// 但不关闭弹窗,让用户知道可能需要重试
|
||||
GlobalToast.show({
|
||||
message: '恢复购买部分失败',
|
||||
message: t('membershipModal.errors.restorePartialFailed'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -734,7 +765,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeSubscriptionsCount: activeSubscriptionIds.length
|
||||
});
|
||||
GlobalToast.show({
|
||||
message: '没有找到购买记录',
|
||||
message: t('membershipModal.errors.noPurchasesFound'),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -754,19 +785,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 处理特定的恢复购买错误
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买已取消',
|
||||
message: t('membershipModal.errors.restoreCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '网络错误',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买失败',
|
||||
message: t('membershipModal.errors.restoreFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -780,7 +811,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||||
const planMeta = getPlanMetaById(product.identifier);
|
||||
const isSelected = selectedProduct === product;
|
||||
const displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
|
||||
// 优先使用翻译的标题,如果找不到 meta 则回退到产品标题
|
||||
let displayTitle = product.title;
|
||||
let displaySubtitle = planMeta?.subtitle ?? '';
|
||||
|
||||
if (planMeta) {
|
||||
displayTitle = t(`membershipModal.plans.${planMeta.type}.title`);
|
||||
displaySubtitle = t(`membershipModal.plans.${planMeta.type}.subtitle`);
|
||||
} else {
|
||||
// 如果没有 meta,尝试使用 resolvePlanDisplayName (虽然这里主要依赖 meta)
|
||||
displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
}
|
||||
|
||||
const priceLabel = product.priceString || '';
|
||||
const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined;
|
||||
|
||||
@@ -797,7 +840,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeOpacity={loading ? 1 : 0.8}
|
||||
accessible={true}
|
||||
accessibilityLabel={`${displayTitle} ${priceLabel}`}
|
||||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||||
accessibilityHint={loading ? t('membershipModal.loading.purchase') : t('membershipModal.actions.selectPlan', { plan: displayTitle })}
|
||||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||||
>
|
||||
<LinearGradient
|
||||
@@ -809,7 +852,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.planCardTopSection}>
|
||||
{planMeta?.tag && (
|
||||
<View style={styles.planTag}>
|
||||
<Text style={styles.planTagText}>{planMeta.tag}</Text>
|
||||
<Text style={styles.planTagText}>{t('membershipModal.plans.tag')}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||||
@@ -825,7 +868,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.planCardBottomSection}>
|
||||
<Text style={styles.planCardDescription}>{planMeta?.subtitle ?? ''}</Text>
|
||||
<Text style={styles.planCardDescription}>{displaySubtitle}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
@@ -854,8 +897,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
accessible={true}
|
||||
accessibilityLabel="返回"
|
||||
accessibilityHint="关闭会员购买弹窗"
|
||||
accessibilityLabel={t('membershipModal.actions.back')}
|
||||
accessibilityHint={t('membershipModal.actions.close')}
|
||||
style={styles.floatingBackButtonContainer}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
@@ -887,14 +930,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="star" size={16} color="#7B2CBF" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>会员套餐</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.sectionTitle.plans')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>灵活选择,跟随节奏稳步提升</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.sectionTitle.plansSubtitle')}</Text>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
{t('membershipModal.errors.noProducts')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -917,17 +960,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="checkbox" size={16} color="#FF9F0A" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>权益对比</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.benefits.title')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>核心权益一目了然,选择更安心</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.benefits.subtitle')}</Text>
|
||||
|
||||
<View style={styles.comparisonTable}>
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>权益</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>VIP</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>普通用户</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>{t('membershipModal.benefits.table.benefit')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>{t('membershipModal.benefits.table.vip')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>{t('membershipModal.benefits.table.regular')}</Text>
|
||||
</View>
|
||||
{BENEFIT_COMPARISON.map((row, index) => (
|
||||
{benefitComparison.map((row, index) => (
|
||||
<View
|
||||
key={row.title}
|
||||
style={[
|
||||
@@ -963,7 +1006,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.bottomSection}>
|
||||
<View style={styles.agreementRow}>
|
||||
<View style={styles.agreementContainer}>
|
||||
<View style={styles.checkboxWrapper}>
|
||||
<CustomCheckBox
|
||||
checked={agreementAccepted}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
@@ -971,31 +1015,37 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
<Text style={styles.agreementPrefix}>开通即视为同意</Text>
|
||||
<TouchableOpacity
|
||||
</View>
|
||||
<Text style={styles.agreementText}>
|
||||
{t('membershipModal.agreements.prefix')}
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《用户协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
{t('membershipModal.agreements.userAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click membership agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《会员协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
{t('membershipModal.agreements.membershipAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《自动续费协议》</Text>
|
||||
</TouchableOpacity>
|
||||
{t('membershipModal.agreements.autoRenewalAgreement')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -1006,10 +1056,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{restoring ? (
|
||||
<View style={styles.restoreButtonContent}>
|
||||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restoring')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restore')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1031,15 +1081,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1047,10 +1097,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</GlassView>
|
||||
@@ -1066,15 +1116,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1082,10 +1132,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1168,12 +1218,14 @@ const styles = StyleSheet.create({
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2B2B2E',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#6B6B73',
|
||||
marginTop: 6,
|
||||
marginBottom: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
configurationNotice: {
|
||||
borderRadius: 16,
|
||||
@@ -1185,6 +1237,7 @@ const styles = StyleSheet.create({
|
||||
color: '#B86A04',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
plansContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1217,35 +1270,40 @@ const styles = StyleSheet.create({
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#2F2F36',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 6,
|
||||
marginBottom: 12,
|
||||
},
|
||||
planTagText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardOriginalPrice: {
|
||||
fontSize: 13,
|
||||
color: '#8E8EA1',
|
||||
textDecorationLine: 'line-through',
|
||||
marginTop: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardDescription: {
|
||||
fontSize: 12,
|
||||
color: '#6C6C77',
|
||||
lineHeight: 17,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardTopSection: {
|
||||
flex: 1,
|
||||
@@ -1275,6 +1333,7 @@ const styles = StyleSheet.create({
|
||||
color: '#9B6200',
|
||||
marginLeft: 6,
|
||||
lineHeight: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
comparisonTable: {
|
||||
borderRadius: 16,
|
||||
@@ -1298,10 +1357,12 @@ const styles = StyleSheet.create({
|
||||
color: '#575764',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.4,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
tableCellText: {
|
||||
fontSize: 13,
|
||||
color: '#3E3E44',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
tableTitleCell: {
|
||||
flex: 1.5,
|
||||
@@ -1361,6 +1422,7 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1369,29 +1431,34 @@ const styles = StyleSheet.create({
|
||||
loadingSpinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementRow: {
|
||||
agreementContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
marginBottom: 16,
|
||||
marginBottom: 20,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
agreementPrefix: {
|
||||
fontSize: 10,
|
||||
checkboxWrapper: {
|
||||
marginTop: 2, // Align with text line-height
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementText: {
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
color: '#666672',
|
||||
marginRight: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
agreementLink: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#E91E63',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
marginHorizontal: 2,
|
||||
textDecorationLine: 'underline',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
agreementSeparator: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#A0A0B0',
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
restoreButton: {
|
||||
alignSelf: 'center',
|
||||
@@ -1401,6 +1468,7 @@ const styles = StyleSheet.create({
|
||||
color: '#6F6F7A',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
disabledRestoreButton: {
|
||||
opacity: 0.5,
|
||||
@@ -1422,6 +1490,7 @@ const styles = StyleSheet.create({
|
||||
color: '#8E8E93',
|
||||
marginTop: 2,
|
||||
lineHeight: 14,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
permissionContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -1435,5 +1504,6 @@ const styles = StyleSheet.create({
|
||||
marginTop: 4,
|
||||
textAlign: 'center',
|
||||
lineHeight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -15,11 +20,11 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
|
||||
const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF'];
|
||||
const CTA_DISABLED_GRADIENT: [string, string] = ['#d3d7e8', '#c1c6da'];
|
||||
|
||||
export interface CreateCustomFoodModalProps {
|
||||
visible: boolean;
|
||||
@@ -43,9 +48,10 @@ export function CreateCustomFoodModal({
|
||||
onClose,
|
||||
onSave
|
||||
}: CreateCustomFoodModalProps) {
|
||||
const { t } = useI18n();
|
||||
const [foodName, setFoodName] = useState('');
|
||||
const [defaultAmount, setDefaultAmount] = useState('100');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState('千卡');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState(t('createCustomFood.units.kcal'));
|
||||
const [calories, setCalories] = useState('100');
|
||||
const [imageUrl, setImageUrl] = useState<string>('');
|
||||
const [protein, setProtein] = useState('0');
|
||||
@@ -93,7 +99,7 @@ export function CreateCustomFoodModal({
|
||||
if (visible) {
|
||||
setFoodName('');
|
||||
setDefaultAmount('100');
|
||||
setCaloriesUnit('千卡');
|
||||
setCaloriesUnit(t('createCustomFood.units.kcal'));
|
||||
setCalories('100');
|
||||
setImageUrl('');
|
||||
setProtein('0');
|
||||
@@ -102,16 +108,16 @@ export function CreateCustomFoodModal({
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// 选择热量单位
|
||||
|
||||
|
||||
// 选择图片
|
||||
const handleSelectImage = async () => {
|
||||
try {
|
||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||
if (!libGranted) {
|
||||
Alert.alert('权限不足', '需要相册权限以选择照片');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.permissionDenied.title'),
|
||||
t('createCustomFood.alerts.permissionDenied.message')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,11 +143,17 @@ export function CreateCustomFoodModal({
|
||||
setImageUrl(url);
|
||||
} catch (e) {
|
||||
console.warn('上传照片失败', e);
|
||||
Alert.alert('上传失败', '照片上传失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.uploadFailed.title'),
|
||||
t('createCustomFood.alerts.uploadFailed.message')
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('发生错误', '选择照片失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.error.title'),
|
||||
t('createCustomFood.alerts.error.message')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,12 +163,18 @@ export function CreateCustomFoodModal({
|
||||
// 保存自定义食物
|
||||
const handleSave = () => {
|
||||
if (!foodName.trim()) {
|
||||
Alert.alert('提示', '请输入食物名称');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.nameRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calories.trim() || parseFloat(calories) <= 0) {
|
||||
Alert.alert('提示', '请输入有效的热量值');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.caloriesRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,98 +193,122 @@ export function CreateCustomFoodModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isSaveDisabled = !foodName.trim() || !calories.trim();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="fade"
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={onClose}
|
||||
presentationStyle="overFullScreen"
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<BlurView intensity={20} tint="dark" style={styles.overlay}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<View style={[
|
||||
<TouchableOpacity activeOpacity={1} onPress={onClose} style={styles.dismissArea} />
|
||||
<View
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
keyboardHeight > 0 && {
|
||||
height: screenHeight - keyboardHeight,
|
||||
maxHeight: screenHeight - keyboardHeight,
|
||||
}
|
||||
]}>
|
||||
height: screenHeight - keyboardHeight - 60,
|
||||
maxHeight: screenHeight - keyboardHeight - 60,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.modalHeaderBar}>
|
||||
<View style={styles.dragIndicator} />
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 0
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 40,
|
||||
}}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton}>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton} activeOpacity={0.7}>
|
||||
<Ionicons name="close-circle" size={32} color="#E2E8F0" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>创建自定义食物</Text>
|
||||
<Text style={styles.headerTitle}>{t('createCustomFood.title')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.saveButton,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonDisabled
|
||||
]}
|
||||
style={[styles.saveButton, isSaveDisabled && styles.saveButtonDisabled]}
|
||||
onPress={handleSave}
|
||||
disabled={!foodName.trim() || !calories.trim()}
|
||||
disabled={isSaveDisabled}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[
|
||||
styles.saveButtonText,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonTextDisabled
|
||||
]}>保存</Text>
|
||||
<LinearGradient
|
||||
colors={isSaveDisabled ? CTA_DISABLED_GRADIENT : CTA_GRADIENT}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.saveButtonGradient}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>{t('createCustomFood.save')}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 效果预览区域 */}
|
||||
<View style={styles.previewSection}>
|
||||
<Text style={styles.sectionTitle}>效果预览</Text>
|
||||
<View style={styles.previewCard}>
|
||||
<LinearGradient
|
||||
colors={['#ffffff', '#F8F9FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<View style={styles.previewHeader}>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.preview.title')}</Text>
|
||||
</View>
|
||||
<View style={styles.previewContent}>
|
||||
<View style={styles.imageWrapper}>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.previewImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.previewImagePlaceholder}>
|
||||
<Ionicons name="restaurant" size={20} color="#999" />
|
||||
<Ionicons name="restaurant" size={24} color="#94A3B8" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.previewInfo}>
|
||||
<Text style={styles.previewName}>
|
||||
{foodName || '食物名称'}
|
||||
<Text style={styles.previewName} numberOfLines={1}>
|
||||
{foodName || t('createCustomFood.preview.defaultName')}
|
||||
</Text>
|
||||
<View style={styles.previewBadge}>
|
||||
<Ionicons name="flame" size={14} color="#F59E0B" />
|
||||
<Text style={styles.previewCalories}>
|
||||
{actualCalories}{caloriesUnit}/{defaultAmount}g
|
||||
{actualCalories} {caloriesUnit} / {defaultAmount}
|
||||
{t('createCustomFood.units.g')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>基本信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.basicInfo.title')}</Text>
|
||||
<Text style={styles.requiredIndicator}>*</Text>
|
||||
</View>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 食物名称和单位 */}
|
||||
{/* 食物名称 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>食物名称</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.name')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={foodName}
|
||||
onChangeText={setFoodName}
|
||||
placeholder="例如,汉堡"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder={t('createCustomFood.basicInfo.namePlaceholder')}
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -274,36 +316,36 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 默认数量 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>默认数量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.defaultAmount')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={defaultAmount}
|
||||
onChangeText={setDefaultAmount}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>g</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.g')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 食物热量 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>食物热量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.calories')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={calories}
|
||||
onChangeText={setCalories}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>千卡</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.kcal')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -312,23 +354,26 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 可选信息 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>可选信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.optionalInfo.title')}</Text>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 照片 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>照片</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.photo')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<TouchableOpacity
|
||||
style={styles.modernImageSelector}
|
||||
onPress={handleSelectImage}
|
||||
disabled={uploading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.selectedImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.modernImagePlaceholder}>
|
||||
<Ionicons name="camera" size={28} color="#A0A0A0" />
|
||||
<Text style={styles.imagePlaceholderText}>添加照片</Text>
|
||||
<Ionicons name="camera-outline" size={28} color="#94A3B8" />
|
||||
<Text style={styles.imagePlaceholderText}>
|
||||
{t('createCustomFood.optionalInfo.addPhoto')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{uploading && (
|
||||
@@ -342,54 +387,56 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 蛋白质 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>蛋白质</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.protein')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={protein}
|
||||
onChangeText={setProtein}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 脂肪 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>脂肪</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.fat')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={fat}
|
||||
onChangeText={setFat}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 碳水化合物 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>碳水化合物</Text>
|
||||
<Text style={styles.inputRowLabel}>
|
||||
{t('createCustomFood.optionalInfo.carbohydrate')}
|
||||
</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={carbohydrate}
|
||||
onChangeText={setCarbohydrate}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -398,7 +445,7 @@ export function CreateCustomFoodModal({
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</BlurView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -408,331 +455,272 @@ const { height: screenHeight } = Dimensions.get('window');
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
dismissArea: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
marginTop: 50,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
backgroundColor: '#F1F5F9', // Slate 100
|
||||
borderTopLeftRadius: 32,
|
||||
borderTopRightRadius: 32,
|
||||
height: '90%',
|
||||
maxHeight: '90%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalHeaderBar: {
|
||||
width: '100%',
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
dragIndicator: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: '#CBD5E1',
|
||||
borderRadius: 2,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
backButton: {
|
||||
padding: 4,
|
||||
marginLeft: -8,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
flex: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
textAlign: 'center',
|
||||
marginHorizontal: 20,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
saveButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
opacity: 0.6,
|
||||
},
|
||||
saveButtonGradient: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
saveButtonText: {
|
||||
fontSize: 16,
|
||||
color: Colors.light.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
saveButtonTextDisabled: {
|
||||
color: Colors.light.textMuted,
|
||||
fontSize: 14,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewSection: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
previewCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.6)',
|
||||
},
|
||||
previewHeader: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
previewContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageWrapper: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
previewImage: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
previewImagePlaceholder: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#E5E5E5',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F1F5F9',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
},
|
||||
previewInfo: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
marginLeft: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
previewName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
marginBottom: 2,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1E293B',
|
||||
marginBottom: 6,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFBEB',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
alignSelf: 'flex-start',
|
||||
gap: 4,
|
||||
},
|
||||
previewCalories: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontSize: 13,
|
||||
color: '#D97706',
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
marginLeft: 8
|
||||
fontWeight: '700',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliBold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
requiredIndicator: {
|
||||
fontSize: 16,
|
||||
color: '#FF4444',
|
||||
fontSize: 14,
|
||||
color: '#EF4444',
|
||||
marginLeft: 4,
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowItem: {
|
||||
flex: 1,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernTextInput: {
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
marginLeft: 20,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
numberInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
textAlign: 'right',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modernSelectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
selectButtonText: {
|
||||
fontSize: 14,
|
||||
color: 'gray',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
selectedImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8F8F8',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 12,
|
||||
color: '#A0A0A0',
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
nutritionItem: {
|
||||
flex: 1,
|
||||
},
|
||||
// 保留旧样式以防兼容性问题
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
numberInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
textAlign: 'right',
|
||||
},
|
||||
inputWithUnit: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
inputUnit: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
minWidth: 30,
|
||||
},
|
||||
selectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
imageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imagePlaceholder: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F0F0F0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
},
|
||||
disclaimer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
lineHeight: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
sectionCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
},
|
||||
// 新增行布局样式
|
||||
inputRowContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputRowLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
width: 80,
|
||||
fontSize: 15,
|
||||
color: '#475569',
|
||||
fontWeight: '600',
|
||||
width: 90,
|
||||
marginRight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
inputRowContent: {
|
||||
flex: 1,
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
modernInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#1E293B',
|
||||
textAlign: 'right',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#94A3B8',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
selectedImage: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F8FAFC',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 11,
|
||||
color: '#94A3B8',
|
||||
marginTop: 4,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { formatTime, getSleepStageColor, SleepStage, type SleepSample } from '@/utils/sleepHealthKit';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -26,6 +27,7 @@ export const SleepStageTimeline = ({
|
||||
hideHeader = false,
|
||||
style
|
||||
}: SleepStageTimelineProps) => {
|
||||
const { t } = useI18n();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
@@ -139,7 +141,9 @@ export const SleepStageTimeline = ({
|
||||
<View style={[styles.container, { backgroundColor: colorTokens.background }, style]}>
|
||||
{!hideHeader && (
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>睡眠阶段图</Text>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
||||
{t('sleepDetail.sleepStages')}
|
||||
</Text>
|
||||
{onInfoPress && (
|
||||
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
|
||||
@@ -149,7 +153,7 @@ export const SleepStageTimeline = ({
|
||||
)}
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: colorTokens.textSecondary }]}>
|
||||
暂无睡眠阶段数据
|
||||
{t('sleepDetail.noData')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -161,7 +165,9 @@ export const SleepStageTimeline = ({
|
||||
{/* 标题栏 */}
|
||||
{!hideHeader && (
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>睡眠阶段图</Text>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
||||
{t('sleepDetail.sleepStages')}
|
||||
</Text>
|
||||
{onInfoPress && (
|
||||
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
|
||||
@@ -173,13 +179,17 @@ export const SleepStageTimeline = ({
|
||||
{/* 睡眠时间范围 */}
|
||||
<View style={styles.timeRange}>
|
||||
<View style={styles.timePoint}>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>入睡</Text>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.infoModalTitles.sleepTime')}
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colorTokens.text }]}>
|
||||
{formatTime(bedtime)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.timePoint}>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>起床</Text>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.sleepDuration')}
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colorTokens.text }]}>
|
||||
{formatTime(wakeupTime)}
|
||||
</Text>
|
||||
@@ -233,21 +243,29 @@ export const SleepStageTimeline = ({
|
||||
<View style={styles.legendRow}>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Deep) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>深度睡眠</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.deep')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Core) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>核心睡眠</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.core')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.legendRow}>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.REM) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>快速眼动</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.rem')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Awake) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>清醒时间</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.awake')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WeightHistoryItem } from '@/store/userSlice';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef } from 'react';
|
||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
|
||||
interface WeightRecordCardProps {
|
||||
@@ -58,124 +58,174 @@ export const WeightRecordCard: React.FC<WeightRecordCardProps> = ({
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
||||
<Text style={styles.deleteButtonText}>{t('weightRecords.card.deleteButton')}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.cardContainer}>
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
overshootRight={false}
|
||||
>
|
||||
<View
|
||||
style={[styles.recordCard]}
|
||||
>
|
||||
<View style={styles.recordHeader}>
|
||||
<Text style={[styles.recordDateTime, { color: themeColors.textSecondary }]}>
|
||||
{dayjs(record.createdAt).format('MM[月]DD[日] HH:mm')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.recordEditButton}
|
||||
onPress={() => onPress?.(record)}
|
||||
>
|
||||
<Ionicons name="create-outline" size={16} color="#FF9500" />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.recordCard}>
|
||||
<View style={styles.leftContent}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/iconWeight.png')}
|
||||
style={styles.icon}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.recordContent}>
|
||||
<Text style={[styles.recordWeightLabel, { color: themeColors.textSecondary }]}>{t('weightRecords.card.weightLabel')}:</Text>
|
||||
<Text style={[styles.recordWeightValue, { color: themeColors.text }]}>{record.weight}{t('weightRecords.modal.unit')}</Text>
|
||||
<View style={styles.textContent}>
|
||||
<View style={styles.dateTimeContainer}>
|
||||
<Text style={styles.dateText}>
|
||||
{dayjs(record.createdAt).format('MM-DD')}
|
||||
</Text>
|
||||
<Text style={styles.timeText}>
|
||||
{dayjs(record.createdAt).format('HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.weightInfo}>
|
||||
<Text style={styles.weightValue}>{record.weight}<Text style={styles.unitText}>{t('weightRecords.modal.unit')}</Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.rightContent}>
|
||||
{Math.abs(weightChange) > 0 && (
|
||||
<View style={[
|
||||
styles.weightChangeTag,
|
||||
styles.changeTag,
|
||||
{ backgroundColor: weightChange < 0 ? '#E8F5E8' : '#FFF2E8' }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={weightChange < 0 ? "arrow-down" : "arrow-up"}
|
||||
size={12}
|
||||
color={weightChange < 0 ? Colors.light.accentGreen : '#FF9500'}
|
||||
size={10}
|
||||
color={weightChange < 0 ? '#22C55E' : '#FF9500'}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.weightChangeText,
|
||||
{ color: weightChange < 0 ? Colors.light.accentGreen : '#FF9500' }
|
||||
styles.changeText,
|
||||
{ color: weightChange < 0 ? '#22C55E' : '#FF9500' }
|
||||
]}>
|
||||
{Math.abs(weightChange).toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.editButton}
|
||||
onPress={() => onPress?.(record)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="ellipsis-vertical" size={16} color="#9ba3c7" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Swipeable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardContainer: {
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
recordCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
recordHeader: {
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
recordDateTime: {
|
||||
fontSize: 14,
|
||||
color: '#687076',
|
||||
fontWeight: '500',
|
||||
},
|
||||
recordEditButton: {
|
||||
padding: 6,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||
},
|
||||
recordContent: {
|
||||
leftContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
recordWeightLabel: {
|
||||
fontSize: 16,
|
||||
color: '#687076',
|
||||
fontWeight: '500',
|
||||
},
|
||||
recordWeightValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#192126',
|
||||
marginLeft: 4,
|
||||
flex: 1,
|
||||
},
|
||||
weightChangeTag: {
|
||||
iconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: '#F0F2F5',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: '#4F5BD5',
|
||||
},
|
||||
textContent: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dateTimeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
marginLeft: 12,
|
||||
marginBottom: 4,
|
||||
},
|
||||
weightChangeText: {
|
||||
fontSize: 12,
|
||||
dateText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#1c1f3a',
|
||||
marginRight: 8,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
weightInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
weightValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#6f7ba7',
|
||||
marginLeft: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
rightContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
changeTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 8,
|
||||
},
|
||||
changeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
marginLeft: 2,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
editButton: {
|
||||
padding: 4,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#EF4444',
|
||||
backgroundColor: '#FF6B6B',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
width: 70,
|
||||
borderRadius: 24,
|
||||
marginLeft: 12,
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
303
i18n/en/challenge.ts
Normal file
303
i18n/en/challenge.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
export const challengeDetail = {
|
||||
title: 'Challenge Details',
|
||||
notFound: 'Challenge not found, please try again later.',
|
||||
loading: 'Loading challenge details…',
|
||||
retry: 'Reload',
|
||||
share: {
|
||||
generating: 'Generating share card...',
|
||||
failed: 'Share failed, please try again later',
|
||||
messageJoined: 'I\'m participating in "{{title}}" challenge, completed {{completed}}/{{target}} days! Join me!',
|
||||
messageNotJoined: 'Found an amazing challenge "{{title}}", let\'s join together!',
|
||||
},
|
||||
dateRange: {
|
||||
format: '{{start}} - {{end}}',
|
||||
monthDay: 'Month {{month}} Day {{day}}',
|
||||
ongoing: 'Ongoing updates',
|
||||
},
|
||||
participants: {
|
||||
count: '{{count}} participants',
|
||||
ongoing: 'Ongoing updates',
|
||||
more: 'More',
|
||||
},
|
||||
detail: {
|
||||
requirement: 'Daily check-in auto accumulates',
|
||||
viewAllRanking: 'View All',
|
||||
},
|
||||
checkIn: {
|
||||
title: 'Challenge Check-in',
|
||||
todayChecked: 'Checked in today',
|
||||
subtitle: 'Daily check-ins accumulate progress towards goal',
|
||||
subtitleChecked: 'Today\'s progress recorded, keep it up tomorrow',
|
||||
button: {
|
||||
checkIn: 'Check In Now',
|
||||
checking: 'Checking in…',
|
||||
checked: 'Checked in today',
|
||||
notJoined: 'Join to check in',
|
||||
upcoming: 'Not started yet',
|
||||
expired: 'Challenge ended',
|
||||
},
|
||||
toast: {
|
||||
alreadyChecked: 'Already checked in today',
|
||||
notStarted: 'Challenge not started yet, check in after it begins',
|
||||
expired: 'Challenge has ended, cannot check in',
|
||||
mustJoin: 'Join the challenge to check in',
|
||||
success: 'Check-in successful, keep going!',
|
||||
failed: 'Check-in failed, please try again',
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
join: 'Join Challenge',
|
||||
joining: 'Joining…',
|
||||
leave: 'Leave Challenge',
|
||||
leaving: 'Leaving…',
|
||||
delete: 'Delete Challenge',
|
||||
deleting: 'Deleting…',
|
||||
upcoming: 'Starting Soon',
|
||||
expired: 'Challenge Ended',
|
||||
},
|
||||
highlight: {
|
||||
join: {
|
||||
title: 'Join Challenge Now',
|
||||
subtitle: 'Invite friends to persist together, achieve more easily',
|
||||
},
|
||||
leave: {
|
||||
title: 'Don\'t leave just yet',
|
||||
subtitle: 'Keep going, the next milestone is around the corner',
|
||||
},
|
||||
upcoming: {
|
||||
title: 'Challenge Starting Soon',
|
||||
subtitle: 'Starts on {{date}}, stay tuned',
|
||||
subtitleFallback: 'Challenge coming soon, stay tuned',
|
||||
},
|
||||
expired: {
|
||||
title: 'Challenge Ended',
|
||||
subtitle: 'Ended on {{date}}, look forward to the next one',
|
||||
subtitleFallback: 'This round has ended, look forward to the next challenge',
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
leaveConfirm: {
|
||||
title: 'Confirm leaving challenge?',
|
||||
message: 'You will need to rejoin to continue.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Leave Challenge',
|
||||
},
|
||||
joinFailed: 'Failed to join challenge',
|
||||
leaveFailed: 'Failed to leave challenge',
|
||||
archiveConfirm: {
|
||||
title: 'Delete this challenge?',
|
||||
message: 'This cannot be undone and participants will lose access.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Delete Challenge',
|
||||
},
|
||||
archiveFailed: 'Failed to delete challenge',
|
||||
archiveSuccess: 'Challenge deleted',
|
||||
},
|
||||
ranking: {
|
||||
title: 'Leaderboard',
|
||||
description: '',
|
||||
empty: 'Leaderboard opening soon, grab your spot.',
|
||||
today: 'Today',
|
||||
todayGoal: 'Today\'s Goal',
|
||||
hour: 'hrs',
|
||||
},
|
||||
leaderboard: {
|
||||
title: 'Leaderboard',
|
||||
loading: 'Loading leaderboard…',
|
||||
notFound: 'Challenge not found.',
|
||||
loadFailed: 'Unable to load leaderboard, please try again later.',
|
||||
empty: 'Leaderboard opening soon, grab your spot.',
|
||||
loadMore: 'Loading more…',
|
||||
loadMoreFailed: 'Failed to load more, pull to refresh and retry',
|
||||
},
|
||||
shareCard: {
|
||||
footer: 'Out Live · Beyond Life',
|
||||
progress: {
|
||||
label: 'My Progress',
|
||||
days: '{{completed}} / {{target}} days',
|
||||
completed: '🎉 Challenge Completed!',
|
||||
remaining: '{{remaining}} days to complete',
|
||||
},
|
||||
info: {
|
||||
checkInDaily: 'Daily check-in',
|
||||
joinUs: 'Join us!',
|
||||
},
|
||||
shareCode: {
|
||||
copied: 'Share code copied',
|
||||
},
|
||||
},
|
||||
shareCode: {
|
||||
copied: 'Share code copied',
|
||||
},
|
||||
};
|
||||
|
||||
export const badges = {
|
||||
title: 'Badge Gallery',
|
||||
subtitle: 'Celebrate every effort',
|
||||
hero: {
|
||||
highlight: 'Keep checking in to unlock rarer badges.',
|
||||
earnedLabel: 'Earned',
|
||||
totalLabel: 'Total',
|
||||
progressLabel: 'Progress',
|
||||
},
|
||||
categories: {
|
||||
all: 'All',
|
||||
sleep: 'Sleep',
|
||||
exercise: 'Exercise',
|
||||
diet: 'Nutrition',
|
||||
challenge: 'Challenge',
|
||||
social: 'Social',
|
||||
special: 'Special',
|
||||
},
|
||||
rarities: {
|
||||
common: 'Common',
|
||||
uncommon: 'Uncommon',
|
||||
rare: 'Rare',
|
||||
epic: 'Epic',
|
||||
legendary: 'Legendary',
|
||||
},
|
||||
status: {
|
||||
earned: 'Unlocked',
|
||||
locked: 'Locked',
|
||||
earnedAt: 'Unlocked on {{date}}',
|
||||
},
|
||||
legend: 'Rarity legend',
|
||||
filterLabel: 'Badge categories',
|
||||
empty: {
|
||||
title: 'No badges yet',
|
||||
description: 'Complete sleep, workout, or challenge tasks to earn your first badge.',
|
||||
action: 'Explore plans',
|
||||
},
|
||||
};
|
||||
|
||||
export const challenges = {
|
||||
title: 'Challenges',
|
||||
subtitle: 'Join challenges to stay consistent',
|
||||
loading: 'Loading challenges…',
|
||||
loadFailed: 'Failed to load challenges, please try again later.',
|
||||
retry: 'Retry',
|
||||
empty: 'No challenges yet. Join one to get started.',
|
||||
customChallenges: 'Custom Challenges',
|
||||
officialChallengesTitle: 'Official Challenges',
|
||||
officialChallenges: 'Official challenges launching soon.',
|
||||
join: 'Join',
|
||||
joined: 'Joined',
|
||||
invalidInviteCode: 'Please enter a valid invite code',
|
||||
joinSuccess: 'Joined challenge successfully',
|
||||
joinFailed: 'Failed to join challenge',
|
||||
joinModal: {
|
||||
title: 'Join via invite code',
|
||||
description: 'Enter the invite code to join a challenge',
|
||||
confirm: 'Join',
|
||||
joining: 'Joining…',
|
||||
cancel: 'Cancel',
|
||||
placeholder: 'Enter invite code',
|
||||
},
|
||||
statusLabels: {
|
||||
upcoming: 'Upcoming',
|
||||
ongoing: 'Ongoing',
|
||||
expired: 'Ended',
|
||||
},
|
||||
createCustom: {
|
||||
title: 'Create Challenge',
|
||||
editTitle: 'Edit Challenge',
|
||||
yourChallenge: 'Your challenge',
|
||||
basicInfo: 'Basic Info',
|
||||
challengeSettings: 'Challenge Settings',
|
||||
displayInteraction: 'Display & Interaction',
|
||||
durationDays: '{{days}} days',
|
||||
durationDaysChallenge: '{{days}}-day challenge',
|
||||
dayUnit: 'days',
|
||||
defaultTitle: 'Custom Challenge',
|
||||
rankingDescription: 'Leaderboard updates daily',
|
||||
typeLabels: {
|
||||
water: 'Hydration',
|
||||
exercise: 'Exercise',
|
||||
diet: 'Diet',
|
||||
sleep: 'Sleep',
|
||||
mood: 'Mood',
|
||||
weight: 'Weight',
|
||||
custom: 'Custom',
|
||||
},
|
||||
fields: {
|
||||
title: 'Challenge title',
|
||||
titlePlaceholder: 'e.g., 21-day hydration',
|
||||
coverImage: 'Cover image',
|
||||
uploadCover: 'Upload cover',
|
||||
challengeDescription: 'Challenge description',
|
||||
descriptionPlaceholder: 'Describe the goal and check-in rules',
|
||||
challengeType: 'Challenge type',
|
||||
challengeTypeHelper: 'Pick the category closest to your goal',
|
||||
timeRange: 'Time range',
|
||||
start: 'Start date',
|
||||
end: 'End date',
|
||||
duration: 'Duration',
|
||||
periodLabel: 'Period label',
|
||||
periodLabelPlaceholder: 'e.g., 21-day sprint',
|
||||
dailyTargetAndUnit: 'Daily target & unit',
|
||||
dailyTargetPlaceholder: 'Daily target value',
|
||||
unitPlaceholder: 'Unit (cups / mins / steps...)',
|
||||
unitHelper: 'Optional, shown after the daily target',
|
||||
minimumCheckInDays: 'Minimum check-in days',
|
||||
minimumCheckInDaysPlaceholder: 'Cannot exceed total duration',
|
||||
maxParticipants: 'Max participants',
|
||||
noLimit: 'No limit',
|
||||
isPublic: 'Allow public join',
|
||||
publicDescription: 'Others can join with the invite code when enabled.',
|
||||
},
|
||||
floatingCTA: {
|
||||
title: 'Generate invite code',
|
||||
subtitle: 'Create a challenge and share it with friends',
|
||||
editTitle: 'Save changes',
|
||||
editSubtitle: 'Update the challenge for all participants',
|
||||
},
|
||||
buttons: {
|
||||
createAndGenerateCode: 'Create & generate code',
|
||||
creating: 'Creating…',
|
||||
updateAndSave: 'Save changes',
|
||||
updating: 'Saving…',
|
||||
},
|
||||
datePicker: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
alerts: {
|
||||
titleRequired: 'Please enter a challenge title',
|
||||
endTimeError: 'End date must be after start date',
|
||||
targetValueError: 'Daily target must be between 1 and 1000',
|
||||
minimumDaysError: 'Minimum check-in days must be between 1 and 365',
|
||||
minimumDaysExceedError: 'Minimum check-in days cannot exceed total duration',
|
||||
participantsError: 'Participants must be between 2 and 10000 or leave empty',
|
||||
createFailed: 'Failed to create challenge',
|
||||
createSuccess: 'Challenge created',
|
||||
updateSuccess: 'Challenge updated',
|
||||
},
|
||||
imageUpload: {
|
||||
selectSource: 'Choose cover',
|
||||
selectMessage: 'Take a photo or pick from album',
|
||||
camera: 'Camera',
|
||||
album: 'Album',
|
||||
cancel: 'Cancel',
|
||||
cameraPermission: 'Camera permission required',
|
||||
cameraPermissionMessage: 'Enable camera access to take a photo.',
|
||||
albumPermissionMessage: 'Enable photo access to choose from library.',
|
||||
cameraFailed: 'Failed to open camera',
|
||||
cameraFailedMessage: 'Please try again or choose from album.',
|
||||
selectFailed: 'Selection failed',
|
||||
selectFailedMessage: 'Could not select an image, please try again.',
|
||||
uploadFailed: 'Upload failed',
|
||||
uploadFailedMessage: 'Cover upload failed, please retry.',
|
||||
uploading: 'Uploading…',
|
||||
clear: 'Remove cover',
|
||||
helper: 'Use a 16:9 cover under 2MB for better results.',
|
||||
},
|
||||
shareModal: {
|
||||
title: 'Invite code generated',
|
||||
subtitle: 'Share this code so others can join your challenge',
|
||||
generatingCode: 'Generating…',
|
||||
copyCode: 'Copy code',
|
||||
viewChallenge: 'View challenge',
|
||||
later: 'Share later',
|
||||
},
|
||||
},
|
||||
};
|
||||
5
i18n/en/common.ts
Normal file
5
i18n/en/common.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const dateSelector = {
|
||||
backToToday: 'Back to Today',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
};
|
||||
551
i18n/en/diet.ts
Normal file
551
i18n/en/diet.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
export const nutritionRecords = {
|
||||
title: 'Nutrition Records',
|
||||
listTitle: 'Today\'s Meals',
|
||||
recordCount: '{{count}} records',
|
||||
empty: {
|
||||
title: 'No records today',
|
||||
action: 'Add Record',
|
||||
},
|
||||
footer: {
|
||||
end: '- No more records -',
|
||||
loadMore: 'Load More',
|
||||
},
|
||||
delete: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete this nutrition record? This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Delete',
|
||||
},
|
||||
mealTypes: {
|
||||
breakfast: 'Breakfast',
|
||||
lunch: 'Lunch',
|
||||
dinner: 'Dinner',
|
||||
snack: 'Snack',
|
||||
other: 'Other',
|
||||
},
|
||||
nutrients: {
|
||||
protein: 'Protein',
|
||||
fat: 'Fat',
|
||||
carbs: 'Carbs',
|
||||
unit: 'g',
|
||||
caloriesUnit: 'kcal',
|
||||
},
|
||||
overlay: {
|
||||
title: 'Record Method',
|
||||
scan: 'AI Scan',
|
||||
foodLibrary: 'Food Library',
|
||||
voiceRecord: 'Voice Log',
|
||||
},
|
||||
chart: {
|
||||
remaining: 'Remaining',
|
||||
formula: 'Remaining = Metabolism + Exercise - Diet',
|
||||
metabolism: 'Metabolism',
|
||||
exercise: 'Exercise',
|
||||
diet: 'Diet',
|
||||
},
|
||||
};
|
||||
|
||||
export const foodCamera = {
|
||||
title: 'Food Camera',
|
||||
hint: 'Keep food within the frame',
|
||||
permission: {
|
||||
title: 'Camera Permission Required',
|
||||
description: 'Camera access is needed to capture food for AI recognition',
|
||||
button: 'Allow Access',
|
||||
},
|
||||
guide: {
|
||||
title: 'Shooting Guide',
|
||||
description: 'Please upload or take clear photos of food to improve recognition accuracy',
|
||||
button: 'Got it',
|
||||
good: 'Good lighting, clear subject',
|
||||
bad: 'Blurry, poor lighting',
|
||||
},
|
||||
buttons: {
|
||||
album: 'Album',
|
||||
capture: 'Capture',
|
||||
help: 'Help',
|
||||
},
|
||||
alerts: {
|
||||
captureFailed: {
|
||||
title: 'Capture Failed',
|
||||
message: 'Please try again',
|
||||
},
|
||||
pickFailed: {
|
||||
title: 'Selection Failed',
|
||||
message: 'Please try again',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const foodRecognition = {
|
||||
title: 'Food Recognition',
|
||||
header: {
|
||||
confirm: 'Confirm Food',
|
||||
recognizing: 'AI Recognizing',
|
||||
},
|
||||
errors: {
|
||||
noImage: 'Image not found',
|
||||
generic: 'Food recognition failed, please try again',
|
||||
unknown: 'Unknown error',
|
||||
noFoodDetected: 'Recognition failed: No food detected',
|
||||
processError: 'Error during recognition process',
|
||||
},
|
||||
logs: {
|
||||
uploading: '📤 Uploading image to cloud...',
|
||||
uploadSuccess: '✅ Image upload completed',
|
||||
analyzing: '🤖 AI model analyzing...',
|
||||
analysisSuccess: '✅ AI analysis completed',
|
||||
confidence: '🎯 Confidence: {{value}}%',
|
||||
itemsFound: '🍽️ Detected {{count}} food items',
|
||||
failed: '❌ Recognition failed: No food detected',
|
||||
error: '❌ Error during recognition process',
|
||||
},
|
||||
status: {
|
||||
idle: {
|
||||
title: 'Ready',
|
||||
subtitle: 'Please wait...',
|
||||
},
|
||||
uploading: {
|
||||
title: 'Uploading Image',
|
||||
subtitle: 'Uploading image to cloud server...',
|
||||
},
|
||||
recognizing: {
|
||||
title: 'AI Analyzing',
|
||||
subtitle: 'AI model is analyzing food ingredients...',
|
||||
},
|
||||
completed: {
|
||||
title: 'Success',
|
||||
subtitle: 'Redirecting to analysis results...',
|
||||
},
|
||||
failed: {
|
||||
title: 'Failed',
|
||||
subtitle: 'Please check network or try again later',
|
||||
},
|
||||
processing: {
|
||||
title: 'Processing...',
|
||||
subtitle: 'Please wait...',
|
||||
},
|
||||
},
|
||||
mealTypes: {
|
||||
breakfast: 'Breakfast',
|
||||
lunch: 'Lunch',
|
||||
dinner: 'Dinner',
|
||||
snack: 'Snack',
|
||||
unknown: 'Unknown',
|
||||
},
|
||||
info: {
|
||||
title: 'Smart Food Recognition',
|
||||
description: 'AI will analyze the photo, identify food types, estimate nutrition, and generate a detailed report.',
|
||||
},
|
||||
actions: {
|
||||
start: 'Start Recognition',
|
||||
retry: 'Retry',
|
||||
logs: 'Process Logs',
|
||||
logsPlaceholder: 'Ready to start...',
|
||||
},
|
||||
alerts: {
|
||||
recognizing: {
|
||||
title: 'Recognition in progress',
|
||||
message: 'Recognition is not complete. Are you sure you want to go back?',
|
||||
continue: 'Continue',
|
||||
back: 'Go Back',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const foodAnalysisResult = {
|
||||
title: 'Analysis Result',
|
||||
error: {
|
||||
notFound: 'Image or recognition result not found',
|
||||
},
|
||||
placeholder: 'Nutrition Record',
|
||||
nutrients: {
|
||||
caloriesUnit: 'kcal',
|
||||
protein: 'Protein',
|
||||
fat: 'Fat',
|
||||
carbs: 'Carbs',
|
||||
unit: 'g',
|
||||
},
|
||||
sections: {
|
||||
recognitionResult: 'Recognition Result',
|
||||
foodIntake: 'Food Intake',
|
||||
},
|
||||
nonFood: {
|
||||
title: 'No Food Detected',
|
||||
suggestions: {
|
||||
title: 'Suggestions:',
|
||||
item1: '• Ensure food is in the frame',
|
||||
item2: '• Try a clearer angle',
|
||||
item3: '• Avoid blur or poor lighting',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
retake: 'Retake',
|
||||
record: 'Record',
|
||||
close: 'Close',
|
||||
},
|
||||
mealSelector: {
|
||||
title: 'Select Meal',
|
||||
},
|
||||
editModal: {
|
||||
title: 'Edit Food Info',
|
||||
fields: {
|
||||
name: 'Food Name',
|
||||
namePlaceholder: 'Enter food name',
|
||||
amount: 'Weight (g)',
|
||||
amountPlaceholder: 'Enter weight',
|
||||
calories: 'Calories (kcal)',
|
||||
caloriesPlaceholder: 'Enter calories',
|
||||
},
|
||||
actions: {
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
},
|
||||
},
|
||||
confidence: 'Confidence: {{value}}%',
|
||||
dateFormats: {
|
||||
today: 'MMM D, YYYY',
|
||||
full: 'MMM D, YYYY HH:mm',
|
||||
},
|
||||
};
|
||||
|
||||
export const foodLibrary = {
|
||||
title: 'Food Library',
|
||||
custom: 'Custom',
|
||||
search: {
|
||||
placeholder: 'Search food...',
|
||||
loading: 'Searching...',
|
||||
empty: 'No relevant food found',
|
||||
noData: 'No food data',
|
||||
},
|
||||
loading: 'Loading food library...',
|
||||
retry: 'Retry',
|
||||
mealTypes: {
|
||||
breakfast: 'Breakfast',
|
||||
lunch: 'Lunch',
|
||||
dinner: 'Dinner',
|
||||
snack: 'Snack',
|
||||
},
|
||||
actions: {
|
||||
record: 'Record',
|
||||
selectMeal: 'Select Meal',
|
||||
},
|
||||
alerts: {
|
||||
deleteFailed: {
|
||||
title: 'Delete Failed',
|
||||
message: 'Error occurred while deleting food, please try again later',
|
||||
},
|
||||
createFailed: {
|
||||
title: 'Create Failed',
|
||||
message: 'Error occurred while creating custom food, please try again later',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createCustomFood = {
|
||||
title: 'Create Custom Food',
|
||||
save: 'Save',
|
||||
preview: {
|
||||
title: 'Preview',
|
||||
defaultName: 'Food Name',
|
||||
},
|
||||
basicInfo: {
|
||||
title: 'Basic Info',
|
||||
name: 'Food Name',
|
||||
namePlaceholder: 'e.g. Hamburger',
|
||||
defaultAmount: 'Default Amount',
|
||||
calories: 'Calories',
|
||||
},
|
||||
optionalInfo: {
|
||||
title: 'Optional Info',
|
||||
photo: 'Photo',
|
||||
addPhoto: 'Add Photo',
|
||||
protein: 'Protein',
|
||||
fat: 'Fat',
|
||||
carbohydrate: 'Carbs',
|
||||
},
|
||||
units: {
|
||||
kcal: 'kcal',
|
||||
g: 'g',
|
||||
gram: 'g',
|
||||
},
|
||||
alerts: {
|
||||
permissionDenied: {
|
||||
title: 'Permission Denied',
|
||||
message: 'Photo library permission is required to select photos',
|
||||
},
|
||||
uploadFailed: {
|
||||
title: 'Upload Failed',
|
||||
message: 'Photo upload failed, please try again',
|
||||
},
|
||||
error: {
|
||||
title: 'Error',
|
||||
message: 'Failed to select photo, please try again',
|
||||
},
|
||||
validation: {
|
||||
title: 'Notice',
|
||||
nameRequired: 'Please enter food name',
|
||||
caloriesRequired: 'Please enter valid calories',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const voiceRecord = {
|
||||
title: 'Voice Log',
|
||||
intro: {
|
||||
description: 'Describe your meal with voice, AI will intelligently analyze nutrition and calories',
|
||||
},
|
||||
status: {
|
||||
idle: 'Tap microphone to start recording',
|
||||
listening: 'Listening... Please start speaking...',
|
||||
processing: 'AI is processing voice content...',
|
||||
analyzing: 'AI model is deeply analyzing nutritional components...',
|
||||
result: 'Voice recognition completed, please confirm the result',
|
||||
},
|
||||
hints: {
|
||||
listening: 'Tell us about the food you want to record',
|
||||
},
|
||||
examples: {
|
||||
title: 'Recording Examples:',
|
||||
items: [
|
||||
'This morning I had two fried eggs, a slice of whole wheat bread and a glass of milk',
|
||||
'For lunch I had about 150g of braised pork, a small bowl of rice and a serving of vegetables',
|
||||
'For dinner I had steamed egg custard, seaweed egg drop soup and a bowl of millet porridge',
|
||||
],
|
||||
},
|
||||
analysis: {
|
||||
progress: 'Analysis Progress: {{progress}}%',
|
||||
hint: 'AI is deeply analyzing your food description...',
|
||||
},
|
||||
result: {
|
||||
label: 'Recognition Result:',
|
||||
},
|
||||
actions: {
|
||||
retry: 'Retry Recording',
|
||||
confirm: 'Confirm & Use',
|
||||
},
|
||||
alerts: {
|
||||
noVoiceInput: 'No voice input detected, please try again',
|
||||
networkError: 'Network connection error, please check network and try again',
|
||||
voiceError: 'Voice recognition problem occurred, please try again',
|
||||
noValidContent: 'No valid content recognized, please record again',
|
||||
pleaseRecordFirst: 'Please perform voice recognition first',
|
||||
recordingFailed: 'Recording Failed',
|
||||
recordingPermissionError: 'Unable to start voice recognition, please check microphone permission settings',
|
||||
analysisFailed: 'Analysis Failed',
|
||||
},
|
||||
};
|
||||
|
||||
export const nutritionLabelAnalysis = {
|
||||
title: 'Nutrition Label Analysis',
|
||||
camera: {
|
||||
permissionDenied: 'Permission Denied',
|
||||
permissionMessage: 'Camera permission is required to take nutrition label photos',
|
||||
},
|
||||
actions: {
|
||||
takePhoto: 'Take Photo',
|
||||
selectFromAlbum: 'Select from Album',
|
||||
startAnalysis: 'Start Analysis',
|
||||
close: 'Close',
|
||||
},
|
||||
placeholder: {
|
||||
text: 'Take or select a nutrition label photo',
|
||||
},
|
||||
status: {
|
||||
uploading: 'Uploading image...',
|
||||
analyzing: 'Analyzing nutrition label...',
|
||||
},
|
||||
errors: {
|
||||
analysisFailed: {
|
||||
title: 'Analysis Failed',
|
||||
message: 'Error occurred while analyzing the image, please try again',
|
||||
defaultMessage: 'Analysis service is temporarily unavailable',
|
||||
},
|
||||
cannotRecognize: 'Unable to recognize nutrition label, please try taking a clearer photo',
|
||||
cameraPermissionDenied: 'Camera permission is required to take nutrition label photos',
|
||||
},
|
||||
results: {
|
||||
title: 'Detailed Nutrition Analysis',
|
||||
detailedAnalysis: 'Detailed Nutrition Analysis',
|
||||
},
|
||||
imageViewer: {
|
||||
close: 'Close',
|
||||
dateFormat: 'MMM D, YYYY HH:mm',
|
||||
},
|
||||
};
|
||||
|
||||
export const nutritionAnalysisHistory = {
|
||||
title: 'History',
|
||||
dateFormat: 'MMM D, YYYY HH:mm',
|
||||
recognized: 'Recognized {{count}} nutrients',
|
||||
loadingMore: 'Loading more...',
|
||||
loading: 'Loading history...',
|
||||
filter: {
|
||||
all: 'All',
|
||||
},
|
||||
filters: {
|
||||
all: 'All',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
},
|
||||
status: {
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
processing: 'Processing',
|
||||
unknown: 'Unknown',
|
||||
},
|
||||
nutrients: {
|
||||
energy: 'Energy',
|
||||
protein: 'Protein',
|
||||
carbs: 'Carbs',
|
||||
fat: 'Fat',
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: 'Confirm Delete',
|
||||
confirmMessage: 'Are you sure you want to delete this record?',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
successTitle: 'Deleted Successfully',
|
||||
successMessage: 'Record has been deleted successfully',
|
||||
},
|
||||
actions: {
|
||||
expand: 'Expand Details',
|
||||
collapse: 'Collapse Details',
|
||||
expandDetails: 'Expand Details',
|
||||
collapseDetails: 'Collapse Details',
|
||||
confirmDelete: 'Confirm Delete',
|
||||
delete: 'Delete',
|
||||
cancel: 'Cancel',
|
||||
retry: 'Retry',
|
||||
},
|
||||
empty: {
|
||||
title: 'No History Records',
|
||||
subtitle: 'Start recognizing nutrition labels',
|
||||
},
|
||||
errors: {
|
||||
error: 'Error',
|
||||
loadFailed: 'Load Failed',
|
||||
unknownError: 'Unknown Error',
|
||||
fetchFailed: 'Failed to fetch history records',
|
||||
fetchFailedRetry: 'Failed to fetch history records, please retry',
|
||||
deleteFailed: 'Delete failed, please try again later',
|
||||
},
|
||||
loadingState: {
|
||||
records: 'Loading history...',
|
||||
more: 'Loading more...',
|
||||
},
|
||||
details: {
|
||||
title: 'Detailed Nutrition Information',
|
||||
nutritionDetails: 'Detailed Nutrition Information',
|
||||
aiModel: 'AI Model',
|
||||
provider: 'Service Provider',
|
||||
serviceProvider: 'Service Provider',
|
||||
},
|
||||
records: {
|
||||
nutritionCount: 'Recognized {{count}} nutrients',
|
||||
},
|
||||
imageViewer: {
|
||||
close: 'Close',
|
||||
},
|
||||
};
|
||||
|
||||
export const waterDetail = {
|
||||
title: 'Water Details',
|
||||
waterRecord: 'Water Records',
|
||||
today: 'Today',
|
||||
total: 'Total: ',
|
||||
goal: 'Goal: ',
|
||||
noRecords: 'No water records',
|
||||
noRecordsSubtitle: 'Tap "Add Record" to start tracking water intake',
|
||||
deleteConfirm: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete this water record? This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Delete',
|
||||
},
|
||||
deleteButton: 'Delete',
|
||||
water: 'Water',
|
||||
loadingUserPreferences: 'Failed to load user preferences',
|
||||
};
|
||||
|
||||
export const waterSettings = {
|
||||
title: 'Water Settings',
|
||||
sections: {
|
||||
dailyGoal: 'Daily Water Goal',
|
||||
quickAdd: 'Quick Add Default',
|
||||
reminder: 'Water Reminder',
|
||||
},
|
||||
descriptions: {
|
||||
quickAdd: 'Set the default water amount when clicking the "+" button',
|
||||
reminder: 'Set periodic reminders to replenish water',
|
||||
},
|
||||
labels: {
|
||||
ml: 'ml',
|
||||
disabled: 'Disabled',
|
||||
},
|
||||
alerts: {
|
||||
goalSuccess: {
|
||||
title: 'Settings Saved',
|
||||
message: 'Daily water goal has been set to {{amount}}ml',
|
||||
},
|
||||
quickAddSuccess: {
|
||||
title: 'Settings Saved',
|
||||
message: 'Quick add default has been set to {{amount}}ml',
|
||||
},
|
||||
quickAddFailed: {
|
||||
title: 'Save Failed',
|
||||
message: 'Unable to save quick add default, please try again',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
status: {
|
||||
reminderEnabled: '{{startTime}}-{{endTime}}, every {{interval}} minutes',
|
||||
},
|
||||
};
|
||||
|
||||
export const waterReminderSettings = {
|
||||
title: 'Water Reminder',
|
||||
sections: {
|
||||
notifications: 'Push Notifications',
|
||||
timeRange: 'Reminder Time Range',
|
||||
interval: 'Reminder Interval',
|
||||
},
|
||||
descriptions: {
|
||||
notifications: 'Enable to receive periodic water reminders during specified time periods',
|
||||
timeRange: 'Only send reminders during specified time periods to avoid disturbing your rest',
|
||||
interval: 'Choose the reminder frequency, recommended 30-120 minutes',
|
||||
},
|
||||
labels: {
|
||||
startTime: 'Start Time',
|
||||
endTime: 'End Time',
|
||||
interval: 'Reminder Interval',
|
||||
saveSettings: 'Save Settings',
|
||||
hours: 'Hours',
|
||||
timeRangePreview: 'Time Range Preview',
|
||||
minutes: 'Minutes',
|
||||
},
|
||||
alerts: {
|
||||
timeValidation: {
|
||||
title: 'Time Setting Tip',
|
||||
startTimeInvalid: 'Start time cannot be later than or equal to end time, please select again',
|
||||
endTimeInvalid: 'End time cannot be earlier than or equal to start time, please select again',
|
||||
},
|
||||
success: {
|
||||
enabled: 'Settings Saved',
|
||||
enabledMessage: 'Water reminder has been enabled\n\nTime range: {{timeRange}}\nReminder interval: {{interval}}\n\nWe will periodically remind you to drink water during the specified time period',
|
||||
disabled: 'Settings Saved',
|
||||
disabledMessage: 'Water reminder has been disabled',
|
||||
},
|
||||
error: {
|
||||
title: 'Save Failed',
|
||||
message: 'Unable to save water reminder settings, please try again',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
};
|
||||
507
i18n/en/health.ts
Normal file
507
i18n/en/health.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
export const healthPermissions = {
|
||||
title: 'Health data disclosure',
|
||||
subtitle: 'We integrate with Apple Health through HealthKit and CareKit to deliver precise training, recovery, and reminder experiences.',
|
||||
cards: {
|
||||
usage: {
|
||||
title: 'Data we read or write',
|
||||
items: [
|
||||
'Activity: steps, active energy, and workouts fuel performance charts and rings.',
|
||||
'Body metrics: height, weight, and body fat keep plans and nutrition tips personalized.',
|
||||
'Sleep & recovery: duration and stages unlock recovery advice and reminders.',
|
||||
'Hydration: we read and write water intake so Health and the app stay in sync.',
|
||||
],
|
||||
},
|
||||
purpose: {
|
||||
title: 'Why we need it',
|
||||
items: [
|
||||
'Generate adaptive training plans, challenges, and recovery nudges.',
|
||||
'Display long-term trends so you can understand progress at a glance.',
|
||||
'Reduce manual input by syncing reminders and challenge progress automatically.',
|
||||
],
|
||||
},
|
||||
control: {
|
||||
title: 'Your control',
|
||||
items: [
|
||||
'Permissions are granted inside Apple Health; change them anytime under iOS Settings > Health > Data Access & Devices.',
|
||||
'We never access data you do not authorize, and cached values are removed if you revoke access.',
|
||||
'Core functionality keeps working and offers manual input alternatives.',
|
||||
],
|
||||
},
|
||||
privacy: {
|
||||
title: 'Storage & privacy',
|
||||
items: [
|
||||
'Health data stays on your device — we do not upload it or share it with third parties.',
|
||||
'Only aggregated, anonymized stats are synced when absolutely necessary.',
|
||||
"We follow Apple's review requirements and will notify you before any changes.",
|
||||
],
|
||||
},
|
||||
},
|
||||
callout: {
|
||||
title: 'What if I skip authorization?',
|
||||
items: [
|
||||
'The related modules will ask for permission and provide manual logging options.',
|
||||
'Declining does not break other areas of the app that do not rely on Health data.',
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: 'Need help?',
|
||||
description: 'Questions about HealthKit or CareKit? Reach out via email or the in-app feedback form:',
|
||||
email: 'richardwei1995@gmail.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const statistics = {
|
||||
title: 'Out Live',
|
||||
sections: {
|
||||
bodyMetrics: 'Body Metrics',
|
||||
},
|
||||
components: {
|
||||
diet: {
|
||||
title: 'Diet Analysis',
|
||||
loading: 'Loading...',
|
||||
updated: 'Updated: {{time}}',
|
||||
remaining: 'Can Still Eat',
|
||||
calories: 'Calories',
|
||||
protein: 'Protein',
|
||||
carb: 'Carbs',
|
||||
fat: 'Fat',
|
||||
fiber: 'Fiber',
|
||||
sodium: 'Sodium',
|
||||
basal: 'Basal',
|
||||
exercise: 'Exercise',
|
||||
diet: 'Diet',
|
||||
kcal: 'kcal',
|
||||
aiRecognition: 'AI Scan',
|
||||
foodLibrary: 'Food Library',
|
||||
voiceRecord: 'Voice Log',
|
||||
nutritionLabel: 'Nutrition Label',
|
||||
},
|
||||
fitness: {
|
||||
kcal: 'kcal',
|
||||
minutes: 'min',
|
||||
hours: 'hrs',
|
||||
},
|
||||
steps: {
|
||||
title: 'Steps',
|
||||
},
|
||||
mood: {
|
||||
title: 'Mood',
|
||||
empty: 'Tap to record mood',
|
||||
},
|
||||
stress: {
|
||||
title: 'Stress',
|
||||
unit: 'ms',
|
||||
},
|
||||
water: {
|
||||
title: 'Water',
|
||||
unit: 'ml',
|
||||
addButton: '+ {{amount}}ml',
|
||||
},
|
||||
metabolism: {
|
||||
title: 'Basal Metabolism',
|
||||
loading: 'Loading...',
|
||||
unit: 'kcal/day',
|
||||
status: {
|
||||
high: 'High',
|
||||
normal: 'Normal',
|
||||
low: 'Low',
|
||||
veryLow: 'Very Low',
|
||||
unknown: 'Unknown',
|
||||
},
|
||||
},
|
||||
sleep: {
|
||||
title: 'Sleep',
|
||||
loading: 'Loading...',
|
||||
},
|
||||
oxygen: {
|
||||
title: 'Blood Oxygen',
|
||||
},
|
||||
circumference: {
|
||||
title: 'Circumference (cm)',
|
||||
setTitle: 'Set {{label}}',
|
||||
confirm: 'Confirm',
|
||||
measurements: {
|
||||
chest: 'Chest',
|
||||
waist: 'Waist',
|
||||
hip: 'Hip',
|
||||
arm: 'Arm',
|
||||
thigh: 'Thigh',
|
||||
calf: 'Calf',
|
||||
},
|
||||
},
|
||||
workout: {
|
||||
title: 'Recent Workout',
|
||||
minutes: 'min',
|
||||
kcal: 'kcal',
|
||||
noData: 'No workout data',
|
||||
syncing: 'Syncing...',
|
||||
sourceWaiting: 'Source: Syncing...',
|
||||
sourceUnknown: 'Source: Unknown',
|
||||
sourceFormat: 'Source: {{source}}',
|
||||
sourceFormatMultiple: 'Source: {{source}} et al.',
|
||||
lastWorkout: 'Latest Workout',
|
||||
updated: 'Updated',
|
||||
},
|
||||
weight: {
|
||||
title: 'Weight Records',
|
||||
addButton: 'Record Weight',
|
||||
bmi: 'BMI',
|
||||
weight: 'Weight',
|
||||
days: 'days',
|
||||
range: 'Range',
|
||||
unit: 'kg',
|
||||
bmiModal: {
|
||||
title: 'BMI Index Explanation',
|
||||
description: 'BMI (Body Mass Index) is an internationally recognized health indicator for assessing weight relative to height',
|
||||
formula: 'Formula: weight(kg) ÷ height²(m)',
|
||||
classificationTitle: 'BMI Classification Standards',
|
||||
healthTipsTitle: 'Health Tips',
|
||||
tips: {
|
||||
nutrition: 'Maintain a balanced diet and control calorie intake',
|
||||
exercise: 'At least 150 minutes of moderate-intensity exercise per week',
|
||||
sleep: 'Ensure 7-9 hours of adequate sleep',
|
||||
monitoring: 'Regularly monitor weight changes and adjust promptly',
|
||||
},
|
||||
disclaimer: 'BMI is for reference only and cannot reflect muscle mass, bone density, etc. If you have health concerns, please consult a professional doctor.',
|
||||
continueButton: 'Continue',
|
||||
},
|
||||
},
|
||||
fitnessRings: {
|
||||
title: 'Fitness Rings',
|
||||
activeCalories: 'Active Calories',
|
||||
exerciseMinutes: 'Exercise Minutes',
|
||||
standHours: 'Stand Hours',
|
||||
goal: '/{{goal}}',
|
||||
ringLabels: {
|
||||
active: 'Active',
|
||||
exercise: 'Exercise',
|
||||
stand: 'Stand',
|
||||
},
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
health: 'Health',
|
||||
medications: 'Meds',
|
||||
fasting: 'Fasting',
|
||||
challenges: 'Challenges',
|
||||
personal: 'Me',
|
||||
},
|
||||
activityHeatMap: {
|
||||
subtitle: 'Active {{days}} days in the last 6 months',
|
||||
activeRate: '{{rate}}%',
|
||||
popover: {
|
||||
title: 'Accumulated energy can be redeemed for AI-related benefits',
|
||||
subtitle: 'How to earn',
|
||||
rules: {
|
||||
login: '1. Daily login earns energy +1',
|
||||
mood: '2. Daily mood record earns energy +1',
|
||||
diet: '3. Diet record earns energy +1',
|
||||
goal: '4. Complete a goal earns energy +1',
|
||||
},
|
||||
},
|
||||
months: {
|
||||
1: 'Jan',
|
||||
2: 'Feb',
|
||||
3: 'Mar',
|
||||
4: 'Apr',
|
||||
5: 'May',
|
||||
6: 'Jun',
|
||||
7: 'Jul',
|
||||
8: 'Aug',
|
||||
9: 'Sep',
|
||||
10: 'Oct',
|
||||
11: 'Nov',
|
||||
12: 'Dec',
|
||||
},
|
||||
legend: {
|
||||
less: 'Less',
|
||||
more: 'More',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sleepDetail = {
|
||||
title: 'Sleep Details',
|
||||
loading: 'Loading sleep data...',
|
||||
today: 'Today',
|
||||
sleepScore: 'Sleep Score',
|
||||
noData: 'No sleep data available',
|
||||
noDataRecommendation: 'Please ensure you are running on a real iOS device with authorized health data access, or wait until you have sleep data to view.',
|
||||
sleepDuration: 'Sleep Duration',
|
||||
sleepQuality: 'Sleep Quality',
|
||||
sleepStages: 'Sleep Stages',
|
||||
learnMore: 'Learn More',
|
||||
awake: 'Awake',
|
||||
rem: 'REM',
|
||||
core: 'Core Sleep',
|
||||
deep: 'Deep Sleep',
|
||||
unknown: 'Unknown',
|
||||
rawData: 'Raw Data',
|
||||
rawDataDescription: 'Contains {{count}} HealthKit sleep sample records',
|
||||
infoModalTitles: {
|
||||
sleepTime: 'Sleep Time',
|
||||
sleepQuality: 'Sleep Quality',
|
||||
},
|
||||
sleepGrades: {
|
||||
low: 'Low',
|
||||
normal: 'Normal',
|
||||
good: 'Good',
|
||||
excellent: 'Excellent',
|
||||
poor: 'Poor',
|
||||
fair: 'Fair',
|
||||
},
|
||||
sleepTimeDescription: 'Sleep is most important - it accounts for more than half of your sleep score. Longer sleep can reduce sleep debt, but regular sleep times are crucial for quality rest.',
|
||||
sleepQualityDescription: 'Sleep quality comprehensively evaluates multiple indicators such as your sleep efficiency, deep sleep duration, REM sleep ratio, etc. High-quality sleep depends not only on duration but also on sleep continuity and balance of sleep stages.',
|
||||
sleepStagesInfo: {
|
||||
title: 'Understand Your Sleep Stages',
|
||||
description: 'People have many misconceptions about sleep stages and sleep quality. Some people may need more deep sleep, while others may not. Scientists and doctors are still exploring the role of different sleep stages and their effects on the body. By tracking sleep stages and paying attention to how you feel each morning, you may gain deeper insights into your own sleep.',
|
||||
awake: {
|
||||
title: 'Awake Time',
|
||||
description: 'During a sleep period, you may wake up several times. Occasional waking is normal. You may fall back asleep immediately and not remember waking up during the night.',
|
||||
},
|
||||
rem: {
|
||||
title: 'REM Sleep',
|
||||
description: 'This sleep stage may have some impact on learning and memory. During this stage, your muscles are most relaxed and your eyes move rapidly left and right. This is also the stage where most of your dreams occur.',
|
||||
},
|
||||
core: {
|
||||
title: 'Core Sleep',
|
||||
description: 'This stage is sometimes called light sleep and is as important as other stages. This stage usually occupies most of your sleep time each night. Brain waves that are crucial for cognition are generated during this stage.',
|
||||
},
|
||||
deep: {
|
||||
title: 'Deep Sleep',
|
||||
description: 'Due to the characteristics of brain waves, this stage is also called slow-wave sleep. During this stage, body tissues are repaired and important hormones are released. It usually occurs in the first half of sleep and lasts longer. During deep sleep, the body is very relaxed, so you may find it harder to wake up during this stage compared to other stages.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sleepQuality = {
|
||||
excellent: {
|
||||
description: 'You feel refreshed and energized',
|
||||
recommendation: 'Congratulations on getting quality sleep! If you feel energized, consider moderate exercise to maintain a healthy lifestyle and further reduce stress for optimal sleep.'
|
||||
},
|
||||
good: {
|
||||
description: 'Good sleep quality, decent mental state',
|
||||
recommendation: 'Your sleep quality is decent but has room for improvement.建议 maintaining regular sleep schedules, avoiding electronic devices before bed, and creating a quiet, comfortable sleep environment.'
|
||||
},
|
||||
fair: {
|
||||
description: 'Fair sleep quality, may affect daytime performance',
|
||||
recommendation: 'Your sleep needs improvement.建议 establishing a fixed bedtime routine, limiting caffeine intake, ensuring appropriate bedroom temperature, and considering light exercise to improve sleep quality.'
|
||||
},
|
||||
poor: {
|
||||
description: 'Poor sleep quality, attention to sleep health recommended',
|
||||
recommendation: 'Your sleep quality needs serious attention.建议 consulting a doctor or sleep specialist to check for sleep disorders, while improving sleep environment and habits, avoiding stimulating activities before bed.'
|
||||
}
|
||||
};
|
||||
|
||||
export const stepsDetail = {
|
||||
title: 'Steps Details',
|
||||
loading: 'Loading...',
|
||||
stats: {
|
||||
totalSteps: 'Total Steps',
|
||||
averagePerHour: 'Average Per Hour',
|
||||
mostActiveTime: 'Most Active Time',
|
||||
},
|
||||
chart: {
|
||||
title: 'Hourly Steps Distribution',
|
||||
averageLabel: 'Average {{steps}} steps',
|
||||
},
|
||||
activityLevel: {
|
||||
currentActivity: 'Your activity level today is',
|
||||
levels: {
|
||||
inactive: 'Inactive',
|
||||
light: 'Lightly Active',
|
||||
moderate: 'Moderately Active',
|
||||
very_active: 'Very Active',
|
||||
},
|
||||
progress: {
|
||||
current: 'Current',
|
||||
nextLevel: 'Next: {{level}}',
|
||||
highestLevel: 'Highest Level',
|
||||
},
|
||||
},
|
||||
timeLabels: {
|
||||
midnight: '0:00',
|
||||
noon: '12:00',
|
||||
nextDay: '24:00',
|
||||
},
|
||||
};
|
||||
|
||||
export const fitnessRingsDetail = {
|
||||
title: 'Fitness Rings Detail',
|
||||
loading: 'Loading...',
|
||||
weekDays: {
|
||||
monday: 'Mon',
|
||||
tuesday: 'Tue',
|
||||
wednesday: 'Wed',
|
||||
thursday: 'Thu',
|
||||
friday: 'Fri',
|
||||
saturday: 'Sat',
|
||||
sunday: 'Sun',
|
||||
},
|
||||
cards: {
|
||||
activeCalories: {
|
||||
title: 'Active Calories',
|
||||
unit: 'kcal',
|
||||
},
|
||||
exerciseMinutes: {
|
||||
title: 'Exercise Minutes',
|
||||
unit: 'minutes',
|
||||
info: {
|
||||
title: 'Exercise Minutes:',
|
||||
description: 'Exercise at an intensity of at least "brisk walking" will accumulate corresponding exercise minutes.',
|
||||
recommendation: 'WHO recommends adults to maintain at least 30 minutes of moderate to high-intensity exercise daily.',
|
||||
knowButton: 'Got it',
|
||||
},
|
||||
},
|
||||
standHours: {
|
||||
title: 'Stand Hours',
|
||||
unit: 'hours',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
weeklyClosedRings: 'Weekly Closed Rings',
|
||||
daysUnit: 'days',
|
||||
},
|
||||
datePicker: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
errors: {
|
||||
loadExerciseInfoPreference: 'Failed to load exercise minutes info preference',
|
||||
saveExerciseInfoPreference: 'Failed to save exercise minutes info preference',
|
||||
},
|
||||
};
|
||||
|
||||
export const circumferenceDetail = {
|
||||
title: 'Circumference Statistics',
|
||||
loading: 'Loading...',
|
||||
error: 'Loading failed',
|
||||
retry: 'Retry',
|
||||
noData: 'No data available',
|
||||
noDataSelected: 'Please select circumference data to display',
|
||||
tabs: {
|
||||
week: 'By Week',
|
||||
month: 'By Month',
|
||||
year: 'By Year',
|
||||
},
|
||||
measurements: {
|
||||
chest: 'Chest',
|
||||
waist: 'Waist',
|
||||
upperHip: 'Upper Hip',
|
||||
arm: 'Arm',
|
||||
thigh: 'Thigh',
|
||||
calf: 'Calf',
|
||||
},
|
||||
modal: {
|
||||
title: 'Set {{label}}',
|
||||
defaultTitle: 'Set Circumference',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
chart: {
|
||||
weekLabel: 'Week {{week}}',
|
||||
monthLabel: '{{month}}',
|
||||
empty: 'No data available',
|
||||
noSelection: 'Please select circumference data to display',
|
||||
},
|
||||
};
|
||||
|
||||
export const basalMetabolismDetail = {
|
||||
title: 'Basal Metabolism',
|
||||
currentData: {
|
||||
title: '{{date}} Basal Metabolism',
|
||||
unit: 'kcal',
|
||||
normalRange: 'Normal range: {{min}}-{{max}} kcal',
|
||||
noData: '--',
|
||||
},
|
||||
stats: {
|
||||
title: 'Basal Metabolism Statistics',
|
||||
tabs: {
|
||||
week: 'By Week',
|
||||
month: 'By Month',
|
||||
},
|
||||
},
|
||||
chart: {
|
||||
loading: 'Loading...',
|
||||
loadingText: 'Loading...',
|
||||
error: {
|
||||
text: 'Loading failed: {{error}}',
|
||||
retry: 'Retry',
|
||||
fetchFailed: 'Failed to fetch data',
|
||||
},
|
||||
empty: 'No data available',
|
||||
yAxisSuffix: 'kcal',
|
||||
weekLabel: 'Week {{week}}',
|
||||
},
|
||||
modal: {
|
||||
title: 'Basal Metabolism',
|
||||
closeButton: '×',
|
||||
description: 'Basal metabolism, also known as Basal Metabolic Rate (BMR), refers to the minimum energy consumption required for the human body to maintain basic life functions (heartbeat, breathing, body temperature regulation, etc.) in a completely resting state, usually measured in calories.',
|
||||
sections: {
|
||||
importance: {
|
||||
title: 'Why is it important?',
|
||||
content: 'Basal metabolism accounts for 60-75% of total energy consumption and is the foundation of energy balance. Understanding your basal metabolism helps develop scientific nutrition plans, optimize weight management strategies, and assess metabolic health status.',
|
||||
},
|
||||
normalRange: {
|
||||
title: 'Normal Range',
|
||||
formulas: {
|
||||
male: 'Male: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age + 5',
|
||||
female: 'Female: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age - 161',
|
||||
},
|
||||
userRange: 'Your normal range: {{min}}-{{max}} kcal/day',
|
||||
rangeNote: '(Within 15% above or below the calculated value is considered normal)',
|
||||
userInfo: 'Based on your information: {{gender}}, {{age}} years old, {{height}}cm, {{weight}}kg',
|
||||
incompleteInfo: 'Please complete basic information to calculate your metabolic rate',
|
||||
},
|
||||
strategies: {
|
||||
title: 'Strategies to Boost Metabolism',
|
||||
subtitle: 'Scientific research supports the following methods:',
|
||||
items: [
|
||||
'1. Increase muscle mass (2-3 strength training sessions per week)',
|
||||
'2. High-intensity interval training (HIIT)',
|
||||
'3. Adequate protein intake (1.6-2.2g per kg of body weight)',
|
||||
'4. Ensure adequate sleep (7-9 hours per night)',
|
||||
'5. Avoid excessive calorie restriction (not less than 80% of BMR)',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
gender: {
|
||||
male: 'Male',
|
||||
female: 'Female',
|
||||
},
|
||||
comments: {
|
||||
reloadData: 'Reload data',
|
||||
},
|
||||
};
|
||||
|
||||
export const workoutHistory = {
|
||||
title: 'Workout Summary',
|
||||
loading: 'Loading workout records...',
|
||||
error: {
|
||||
permissionDenied: 'Health data permission not granted',
|
||||
loadFailed: 'Failed to load workout records, please try again later',
|
||||
detailLoadFailed: 'Failed to load workout details, please try again later',
|
||||
},
|
||||
retry: 'Retry',
|
||||
monthlyStats: {
|
||||
title: 'Workout Time',
|
||||
periodText: 'Statistics period: 1st - {{day}} (This month)',
|
||||
overviewWithStats: 'As of {{date}}, you have completed {{count}} workouts, totaling {{duration}}.',
|
||||
overviewEmpty: 'No workout records this month yet, start moving to collect your first one!',
|
||||
emptyData: 'No workout data this month',
|
||||
},
|
||||
intensity: {
|
||||
low: 'Low Intensity',
|
||||
medium: 'Medium Intensity',
|
||||
high: 'High Intensity',
|
||||
},
|
||||
historyCard: {
|
||||
calories: '{{calories}} kcal · {{minutes}} min',
|
||||
activityTime: '{{activity}}, {{time}}',
|
||||
},
|
||||
empty: {
|
||||
title: 'No Workout Records',
|
||||
subtitle: 'Complete a workout to view detailed history here',
|
||||
},
|
||||
monthOccurrence: 'This is your {{index}} {{activity}} in {{month}}.',
|
||||
};
|
||||
17
i18n/en/index.ts
Normal file
17
i18n/en/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as Challenge from './challenge';
|
||||
import * as Common from './common';
|
||||
import * as Diet from './diet';
|
||||
import * as Health from './health';
|
||||
import * as Medication from './medication';
|
||||
import * as Personal from './personal';
|
||||
import * as Weight from './weight';
|
||||
|
||||
export default {
|
||||
...Personal,
|
||||
...Health,
|
||||
...Diet,
|
||||
...Medication,
|
||||
...Weight,
|
||||
...Challenge,
|
||||
...Common,
|
||||
};
|
||||
472
i18n/en/medication.ts
Normal file
472
i18n/en/medication.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
export const medications = {
|
||||
greeting: 'Hello, {{name}}',
|
||||
welcome: 'Welcome to Medication Assistant!',
|
||||
todayMedications: 'Today\'s Medications',
|
||||
filters: {
|
||||
all: 'All',
|
||||
taken: 'Taken',
|
||||
missed: 'Missed',
|
||||
},
|
||||
emptyState: {
|
||||
title: 'No medications scheduled for today',
|
||||
subtitle: 'No medication plans added yet. Let\'s add some.',
|
||||
},
|
||||
stack: {
|
||||
completed: 'Completed ({{count}})',
|
||||
},
|
||||
dateFormats: {
|
||||
today: 'Today, {{date}}',
|
||||
other: '{{date}}',
|
||||
},
|
||||
// MedicationCard
|
||||
card: {
|
||||
status: {
|
||||
missed: 'Missed',
|
||||
timeToTake: 'Time to take',
|
||||
remaining: '{{time}} remaining',
|
||||
},
|
||||
action: {
|
||||
takeNow: 'Take Now',
|
||||
taken: 'Taken',
|
||||
skipped: 'Skipped',
|
||||
skip: 'Skip',
|
||||
submitting: 'Submitting...',
|
||||
},
|
||||
skipAlert: {
|
||||
title: 'Confirm Skip',
|
||||
message: 'Are you sure you want to skip this medication?\n\nIt will not be recorded as taken.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm Skip',
|
||||
},
|
||||
earlyTakeAlert: {
|
||||
title: 'Not yet time to take medication',
|
||||
message: 'This medication is scheduled for {{time}}, which is more than 1 hour from now.\n\nHave you already taken this medication?',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm Taken',
|
||||
},
|
||||
takeError: {
|
||||
title: 'Operation Failed',
|
||||
message: 'An error occurred while recording medication, please try again later',
|
||||
confirm: 'OK',
|
||||
},
|
||||
skipError: {
|
||||
title: 'Operation Failed',
|
||||
message: 'Skip operation failed, please try again later',
|
||||
confirm: 'OK',
|
||||
},
|
||||
},
|
||||
// Add Medication Page
|
||||
add: {
|
||||
title: 'Add Medication',
|
||||
steps: {
|
||||
name: 'Medication Name',
|
||||
dosage: 'Dosage & Form',
|
||||
frequency: 'Frequency',
|
||||
time: 'Reminder Time',
|
||||
note: 'Notes',
|
||||
},
|
||||
descriptions: {
|
||||
name: 'Name the medication and upload package photo for easy identification',
|
||||
dosage: 'Select tablet type and fill in dosage per administration',
|
||||
frequency: 'Set medication frequency and daily times',
|
||||
time: 'Add and manage daily reminder times',
|
||||
note: 'Fill in notes or doctor instructions (optional)',
|
||||
},
|
||||
name: {
|
||||
placeholder: 'Enter or search medication name',
|
||||
},
|
||||
photo: {
|
||||
title: 'Upload Medication Photo',
|
||||
subtitle: 'Take a photo or select from album to help identify medication packaging',
|
||||
selectTitle: 'Select Image',
|
||||
selectMessage: 'Please select image source',
|
||||
camera: 'Camera',
|
||||
album: 'From Album',
|
||||
cancel: 'Cancel',
|
||||
retake: 'Retake',
|
||||
uploading: 'Uploading...',
|
||||
uploadingText: 'Uploading',
|
||||
remove: 'Remove',
|
||||
cameraPermission: 'Camera permission is required to take medication photos',
|
||||
albumPermission: 'Album permission is required to select medication photos',
|
||||
uploadFailed: 'Upload Failed',
|
||||
uploadFailedMessage: 'Image upload failed, please try again later',
|
||||
cameraFailed: 'Camera Failed',
|
||||
cameraFailedMessage: 'Unable to open camera, please try again later',
|
||||
selectFailed: 'Selection Failed',
|
||||
selectFailedMessage: 'Unable to open album, please try again later',
|
||||
},
|
||||
dosage: {
|
||||
label: 'Dosage per administration',
|
||||
placeholder: '0.5',
|
||||
type: 'Type',
|
||||
unitSelector: 'Select dosage unit',
|
||||
},
|
||||
frequency: {
|
||||
label: 'Times per day',
|
||||
value: '{{count}} times/day',
|
||||
period: 'Medication period',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
longTerm: 'Long-term',
|
||||
startDateInvalid: 'Invalid date',
|
||||
startDateInvalidMessage: 'Start date cannot be earlier than today',
|
||||
endDateInvalid: 'Invalid date',
|
||||
endDateInvalidMessage: 'End date cannot be earlier than start date',
|
||||
},
|
||||
time: {
|
||||
label: 'Daily reminder times',
|
||||
addTime: 'Add Time',
|
||||
editTime: 'Edit Reminder Time',
|
||||
addTimeButton: 'Add Time',
|
||||
},
|
||||
note: {
|
||||
label: 'Notes',
|
||||
placeholder: 'Record precautions, doctor instructions or custom reminders',
|
||||
voiceNotSupported: 'Voice-to-text is not supported on this device, you can type notes directly',
|
||||
voiceError: 'Voice recognition unavailable',
|
||||
voiceErrorMessage: 'Unable to use voice input, please check permission settings and try again',
|
||||
voiceStartError: 'Unable to start voice input',
|
||||
voiceStartErrorMessage: 'Please check microphone and voice recognition permissions and try again',
|
||||
},
|
||||
actions: {
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
complete: 'Complete',
|
||||
},
|
||||
success: {
|
||||
title: 'Added Successfully',
|
||||
message: 'Successfully added medication "{{name}}"',
|
||||
confirm: 'OK',
|
||||
},
|
||||
error: {
|
||||
title: 'Add Failed',
|
||||
message: 'An error occurred while creating medication, please try again later',
|
||||
confirm: 'OK',
|
||||
},
|
||||
datePickers: {
|
||||
startDate: 'Select Start Date',
|
||||
endDate: 'Select End Date',
|
||||
time: 'Select Time',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
pickers: {
|
||||
timesPerDay: 'Select Times Per Day',
|
||||
dosageUnit: 'Select Dosage Unit',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
},
|
||||
// Medication Management Page
|
||||
manage: {
|
||||
title: 'Medication Management',
|
||||
subtitle: 'Manage status and reminders for all medications',
|
||||
filters: {
|
||||
all: 'All',
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
},
|
||||
loading: 'Loading medication information...',
|
||||
empty: {
|
||||
title: 'No Medications',
|
||||
subtitle: 'No medication records yet, click the top right to add',
|
||||
},
|
||||
deactivate: {
|
||||
title: 'Deactivate {{name}}?',
|
||||
description: 'After deactivation, medication plans generated for the day will be deleted and cannot be recovered.',
|
||||
confirm: 'Confirm Deactivation',
|
||||
cancel: 'Cancel',
|
||||
error: {
|
||||
title: 'Operation Failed',
|
||||
message: 'An error occurred while deactivating medication, please try again later.',
|
||||
},
|
||||
},
|
||||
toggleError: {
|
||||
title: 'Operation Failed',
|
||||
message: 'An error occurred while toggling medication status, please try again later.',
|
||||
},
|
||||
formLabels: {
|
||||
capsule: 'Capsule',
|
||||
pill: 'Tablet',
|
||||
tablet: 'Tablet',
|
||||
injection: 'Injection',
|
||||
spray: 'Spray',
|
||||
drop: 'Drops',
|
||||
syrup: 'Syrup',
|
||||
other: 'Other',
|
||||
ointment: 'Ointment',
|
||||
},
|
||||
frequency: {
|
||||
daily: 'Daily',
|
||||
weekly: 'Weekly',
|
||||
custom: 'Custom',
|
||||
},
|
||||
cardMeta: 'Started {{date}} | Reminder: {{reminder}}',
|
||||
reminderNotSet: 'Not set',
|
||||
unknownDate: 'Unknown date',
|
||||
},
|
||||
// Medication Detail Page
|
||||
detail: {
|
||||
title: 'Medication Details',
|
||||
notFound: {
|
||||
title: 'Medication information not found',
|
||||
subtitle: 'Please re-enter this page from the medication list.',
|
||||
},
|
||||
loading: 'Loading...',
|
||||
error: {
|
||||
title: 'Unable to retrieve medication information at this time, please try again later.',
|
||||
subtitle: 'Please check your network and try again, or return to the previous page.',
|
||||
},
|
||||
sections: {
|
||||
plan: 'Medication Plan',
|
||||
dosage: 'Dosage & Form',
|
||||
note: 'Notes',
|
||||
overview: 'Medication Overview',
|
||||
aiAnalysis: 'AI Medication Analysis',
|
||||
},
|
||||
plan: {
|
||||
period: 'Medication Period',
|
||||
time: 'Medication Time',
|
||||
frequency: 'Frequency',
|
||||
expiryDate: 'Expiry Date',
|
||||
longTerm: 'Long-term',
|
||||
periodMessage: 'Start date: {{startDate}}\n{{endDateInfo}}',
|
||||
longTermPlan: 'Medication plan: Long-term medication',
|
||||
timeMessage: 'Set times: {{times}}',
|
||||
dateFormat: 'MMM D, YYYY',
|
||||
periodRange: 'From {{startDate}} to {{endDate}}',
|
||||
periodLongTerm: 'From {{startDate}} until indefinitely',
|
||||
expiryStatus: {
|
||||
notSet: 'Not set',
|
||||
expired: 'Expired',
|
||||
expiresToday: 'Expires today',
|
||||
expiresInDays: 'Expires in {{days}} days',
|
||||
},
|
||||
},
|
||||
dosage: {
|
||||
label: 'Dosage per administration',
|
||||
form: 'Form',
|
||||
selectDosage: 'Select Dosage',
|
||||
selectForm: 'Select Form',
|
||||
dosageValue: 'Dosage Value',
|
||||
unit: 'Unit',
|
||||
},
|
||||
note: {
|
||||
label: 'Medication Notes',
|
||||
placeholder: 'Record precautions, doctor instructions or custom reminders',
|
||||
edit: 'Edit Notes',
|
||||
noNote: 'No notes',
|
||||
voiceNotSupported: 'Voice-to-text is not supported on this device, you can type notes directly',
|
||||
save: 'Save',
|
||||
saveError: {
|
||||
title: 'Save Failed',
|
||||
message: 'An error occurred while submitting notes, please try again later.',
|
||||
},
|
||||
},
|
||||
overview: {
|
||||
calculating: 'Calculating...',
|
||||
takenCount: 'Taken {{count}} times in total',
|
||||
calculatingDays: 'Calculating adherence days',
|
||||
startedDays: 'Adhered for {{days}} days',
|
||||
startDate: 'Started {{date}}',
|
||||
noStartDate: 'No start date',
|
||||
},
|
||||
aiAnalysis: {
|
||||
analyzing: 'Analyzing medication information...',
|
||||
analyzingButton: 'Analyzing...',
|
||||
reanalyzeButton: 'Reanalyze',
|
||||
getAnalysisButton: 'Get AI Analysis',
|
||||
button: 'AI Analysis',
|
||||
status: {
|
||||
generated: 'Generated',
|
||||
memberExclusive: 'Member Exclusive',
|
||||
pending: 'Pending',
|
||||
},
|
||||
title: 'Analysis Results',
|
||||
recommendation: 'AI Recommended',
|
||||
placeholder: 'Get AI analysis to quickly understand suitable populations, ingredient safety, and usage recommendations.',
|
||||
categories: {
|
||||
suitableFor: 'Suitable For',
|
||||
unsuitableFor: 'Unsuitable For',
|
||||
sideEffects: 'Possible Side Effects',
|
||||
storageAdvice: 'Storage Advice',
|
||||
healthAdvice: 'Health/Usage Advice',
|
||||
},
|
||||
membershipCard: {
|
||||
title: 'Member Exclusive AI In-depth Analysis',
|
||||
subtitle: 'Unlock complete medication analysis and unlimited usage',
|
||||
},
|
||||
error: {
|
||||
title: 'Analysis Failed',
|
||||
message: 'AI analysis failed, please try again later',
|
||||
networkError: 'Failed to initiate analysis request, please check network connection',
|
||||
unauthorized: 'Please log in first',
|
||||
forbidden: 'No access to this medication',
|
||||
notFound: 'Medication not found',
|
||||
},
|
||||
},
|
||||
aiDraft: {
|
||||
reshoot: 'Reshoot',
|
||||
saveAndCreate: 'Save & Create',
|
||||
saveError: {
|
||||
title: 'Save Failed',
|
||||
message: 'An error occurred while creating medication, please try again later',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
enabled: 'Reminders Enabled',
|
||||
disabled: 'Reminders Disabled',
|
||||
},
|
||||
delete: {
|
||||
title: 'Delete {{name}}?',
|
||||
description: 'After deletion, reminders and history related to this medication will be cleared and cannot be recovered.',
|
||||
confirm: 'Delete',
|
||||
cancel: 'Cancel',
|
||||
error: {
|
||||
title: 'Delete Failed',
|
||||
message: 'An error occurred while removing this medication, please try again later.',
|
||||
},
|
||||
},
|
||||
deactivate: {
|
||||
title: 'Deactivate {{name}}?',
|
||||
description: 'After deactivation, medication plans generated for the day will be deleted and cannot be recovered.',
|
||||
confirm: 'Confirm Deactivation',
|
||||
cancel: 'Cancel',
|
||||
error: {
|
||||
title: 'Operation Failed',
|
||||
message: 'An error occurred while deactivating medication, please try again later.',
|
||||
},
|
||||
},
|
||||
toggleError: {
|
||||
title: 'Operation Failed',
|
||||
message: 'An error occurred while toggling reminder status, please try again later.',
|
||||
},
|
||||
updateErrors: {
|
||||
dosage: 'Update Failed',
|
||||
dosageMessage: 'An error occurred while updating dosage, please try again later.',
|
||||
form: 'Update Failed',
|
||||
formMessage: 'An error occurred while updating form, please try again later.',
|
||||
expiryDate: 'Update Failed',
|
||||
expiryDateMessage: 'Failed to update expiry date, please try again later.',
|
||||
},
|
||||
imageViewer: {
|
||||
close: 'Close',
|
||||
},
|
||||
pickers: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
},
|
||||
// Edit Frequency Page
|
||||
editFrequency: {
|
||||
title: 'Edit Medication Frequency',
|
||||
missingParams: 'Missing required parameters',
|
||||
medicationName: 'Editing: {{name}}',
|
||||
sections: {
|
||||
frequency: 'Medication Frequency',
|
||||
frequencyDescription: 'Set daily medication frequency',
|
||||
time: 'Daily Reminder Times',
|
||||
timeDescription: 'Add and manage daily reminder times',
|
||||
},
|
||||
frequency: {
|
||||
repeatPattern: 'Repeat Pattern',
|
||||
timesPerDay: 'Times Per Day',
|
||||
daily: 'Daily',
|
||||
weekly: 'Weekly',
|
||||
custom: 'Custom',
|
||||
timesLabel: '{{count}} times',
|
||||
summary: '{{pattern}} {{count}} times',
|
||||
},
|
||||
time: {
|
||||
addTime: 'Add Time',
|
||||
editTime: 'Edit Reminder Time',
|
||||
addTimeButton: 'Add Time',
|
||||
},
|
||||
actions: {
|
||||
save: 'Save Changes',
|
||||
},
|
||||
error: {
|
||||
title: 'Update Failed',
|
||||
message: 'An error occurred while updating medication frequency, please try again later.',
|
||||
},
|
||||
pickers: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
},
|
||||
aiProgress: {
|
||||
title: 'Analyzing',
|
||||
steps: {
|
||||
analyzing_product: 'Analyzing product...',
|
||||
analyzing_suitability: 'Checking suitability...',
|
||||
analyzing_ingredients: 'Evaluating ingredients...',
|
||||
analyzing_effects: 'Generating safety advice...',
|
||||
completed: 'Completed, loading details...',
|
||||
},
|
||||
errors: {
|
||||
default: 'Recognition failed, please retake photo',
|
||||
queryFailed: 'Query failed, please try again later',
|
||||
},
|
||||
modal: {
|
||||
title: 'Retake Required',
|
||||
retry: 'Retake Photo',
|
||||
},
|
||||
},
|
||||
aiCamera: {
|
||||
title: 'AI Scan',
|
||||
steps: {
|
||||
front: {
|
||||
title: 'Front',
|
||||
subtitle: 'Ensure medication name is clearly visible',
|
||||
},
|
||||
side: {
|
||||
title: 'Back',
|
||||
subtitle: 'Include specs and ingredients info',
|
||||
},
|
||||
aux: {
|
||||
title: 'Side',
|
||||
subtitle: 'Add more details to improve accuracy',
|
||||
},
|
||||
stepProgress: 'Step {{current}} / {{total}}',
|
||||
optional: '(Optional)',
|
||||
notTaken: 'Empty',
|
||||
},
|
||||
buttons: {
|
||||
flip: 'Flip',
|
||||
capture: 'Snap',
|
||||
complete: 'Done',
|
||||
album: 'Album',
|
||||
},
|
||||
permission: {
|
||||
title: 'Camera Permission Required',
|
||||
description: 'Allow access to capture medication packaging for automatic recognition',
|
||||
button: 'Allow Camera Access',
|
||||
},
|
||||
alerts: {
|
||||
pickFailed: {
|
||||
title: 'Selection Failed',
|
||||
message: 'Please try again or choose another image',
|
||||
},
|
||||
captureFailed: {
|
||||
title: 'Capture Failed',
|
||||
message: 'Please try again',
|
||||
},
|
||||
insufficientPhotos: {
|
||||
title: 'Photos Missing',
|
||||
message: 'Please capture at least front and back sides',
|
||||
},
|
||||
taskFailed: {
|
||||
title: 'Task Creation Failed',
|
||||
defaultMessage: 'Please check network and try again',
|
||||
},
|
||||
},
|
||||
guideModal: {
|
||||
badge: 'Guide',
|
||||
title: 'Keep Photos Clear',
|
||||
description1: 'Please capture the product name and description on the front/back of the medication.',
|
||||
description2: 'Ensure good lighting, avoid glare, and keep text legible. Photo clarity affects recognition accuracy.',
|
||||
button: 'Got it!',
|
||||
},
|
||||
},
|
||||
};
|
||||
408
i18n/en/personal.ts
Normal file
408
i18n/en/personal.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
export const personal = {
|
||||
edit: 'Edit',
|
||||
login: 'Log in',
|
||||
memberNumber: 'Member ID: {{number}}',
|
||||
aiUsage: 'Free AI credits: {{value}}',
|
||||
aiUsageUnlimited: 'Unlimited',
|
||||
fishRecord: 'Energy log',
|
||||
badgesPreview: {
|
||||
title: 'My badges',
|
||||
subtitle: 'Celebrate every milestone',
|
||||
cta: 'View all',
|
||||
loading: 'Syncing your badges…',
|
||||
empty: 'Complete sleep or challenge tasks to unlock your first badge.',
|
||||
lockedHint: 'Keep building the habit to unlock more.',
|
||||
},
|
||||
stats: {
|
||||
height: 'Height',
|
||||
weight: 'Weight',
|
||||
age: 'Age',
|
||||
ageSuffix: ' yrs',
|
||||
},
|
||||
membership: {
|
||||
badge: 'Premium member',
|
||||
planFallback: 'VIP Membership',
|
||||
expiryLabel: 'Valid until',
|
||||
changeButton: 'Change plan',
|
||||
validForever: 'No expiry',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
},
|
||||
sections: {
|
||||
notifications: 'Notifications',
|
||||
developer: 'Developer',
|
||||
other: 'Other',
|
||||
account: 'Account & Security',
|
||||
language: 'Language',
|
||||
healthData: 'Health data permissions',
|
||||
medicalSources: 'Medical Advice Sources',
|
||||
customization: 'Customization',
|
||||
},
|
||||
menu: {
|
||||
notificationSettings: 'Notification settings',
|
||||
developerOptions: 'Developer options',
|
||||
pushSettings: 'Push notification settings',
|
||||
privacyPolicy: 'Privacy policy',
|
||||
feedback: 'Feedback',
|
||||
userAgreement: 'User agreement',
|
||||
logout: 'Log out',
|
||||
deleteAccount: 'Delete account',
|
||||
healthDataPermissions: 'Health data disclosure',
|
||||
whoSource: 'World Health Organization (WHO)',
|
||||
tabBarConfig: 'Tab Bar Settings',
|
||||
},
|
||||
language: {
|
||||
title: 'Language',
|
||||
menuTitle: 'Display language',
|
||||
modalTitle: 'Choose language',
|
||||
modalSubtitle: 'Your selection applies immediately',
|
||||
cancel: 'Cancel',
|
||||
options: {
|
||||
zh: {
|
||||
label: 'Chinese',
|
||||
description: 'Use the Chinese interface',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
description: 'Use the app in English',
|
||||
},
|
||||
},
|
||||
},
|
||||
tabBarConfig: {
|
||||
title: 'Tab Bar Settings',
|
||||
subtitle: 'Customize your bottom navigation',
|
||||
description: 'Use toggles to show or hide tabs',
|
||||
resetButton: 'Reset',
|
||||
cannotDisable: 'Cannot be disabled',
|
||||
resetConfirm: {
|
||||
title: 'Reset to Default?',
|
||||
message: 'This will reset all tab bar settings and visibility',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
resetSuccess: 'Settings reset to default',
|
||||
},
|
||||
};
|
||||
|
||||
export const editProfile = {
|
||||
title: 'Edit Profile',
|
||||
fields: {
|
||||
name: 'Nickname',
|
||||
gender: 'Gender',
|
||||
height: 'Height',
|
||||
weight: 'Weight',
|
||||
activityLevel: 'Activity Level',
|
||||
birthDate: 'Birth Date',
|
||||
maxHeartRate: 'Max Heart Rate',
|
||||
},
|
||||
gender: {
|
||||
male: 'Male',
|
||||
female: 'Female',
|
||||
notSet: 'Not set',
|
||||
},
|
||||
height: {
|
||||
unit: 'cm',
|
||||
placeholder: '170cm',
|
||||
},
|
||||
weight: {
|
||||
unit: 'kg',
|
||||
placeholder: '55kg',
|
||||
},
|
||||
activityLevels: {
|
||||
1: 'Sedentary',
|
||||
2: 'Lightly active',
|
||||
3: 'Moderately active',
|
||||
4: 'Very active',
|
||||
descriptions: {
|
||||
1: 'Rarely exercise',
|
||||
2: 'Exercise 1-3 times per week',
|
||||
3: 'Exercise 3-5 times per week',
|
||||
4: 'Exercise 6-7 times per week',
|
||||
},
|
||||
},
|
||||
birthDate: {
|
||||
placeholder: 'January 1, 1995',
|
||||
format: '{{month}} {{day}}, {{year}}',
|
||||
},
|
||||
maxHeartRate: {
|
||||
unit: 'bpm',
|
||||
notAvailable: 'Not available',
|
||||
alert: {
|
||||
title: 'Notice',
|
||||
message: 'Max heart rate data is automatically retrieved from Health app',
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
notLoggedIn: {
|
||||
title: 'Not logged in',
|
||||
message: 'Please log in before trying to save',
|
||||
},
|
||||
saveFailed: {
|
||||
title: 'Save failed',
|
||||
message: 'Please try again later',
|
||||
},
|
||||
avatarPermissions: {
|
||||
title: 'Insufficient permissions',
|
||||
message: 'Photo album permission is required to select avatar',
|
||||
},
|
||||
avatarUploadFailed: {
|
||||
title: 'Upload failed',
|
||||
message: 'Avatar upload failed, please try again',
|
||||
},
|
||||
avatarError: {
|
||||
title: 'Error occurred',
|
||||
message: 'Failed to select avatar, please try again',
|
||||
},
|
||||
avatarSuccess: {
|
||||
title: 'Success',
|
||||
message: 'Avatar updated successfully',
|
||||
},
|
||||
},
|
||||
modals: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
save: 'Save',
|
||||
input: {
|
||||
namePlaceholder: 'Enter nickname',
|
||||
weightPlaceholder: 'Enter weight',
|
||||
weightUnit: 'kg',
|
||||
},
|
||||
selectHeight: 'Select Height',
|
||||
selectGender: 'Select Gender',
|
||||
selectActivityLevel: 'Select Activity Level',
|
||||
female: 'Female',
|
||||
male: 'Male',
|
||||
},
|
||||
defaultValues: {
|
||||
name: 'TonightEatMeat',
|
||||
height: 170,
|
||||
weight: 55,
|
||||
birthDate: '1995-01-01',
|
||||
activityLevel: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const login = {
|
||||
title: 'Log In',
|
||||
subtitle: 'Healthy living, freedom through self-discipline',
|
||||
appleLogin: 'Sign in with Apple',
|
||||
loggingIn: 'Logging in...',
|
||||
agreement: {
|
||||
readAndAgree: 'I have read and agree to ',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
and: ' and ',
|
||||
userAgreement: 'User Agreement',
|
||||
alert: {
|
||||
title: 'Please read and agree',
|
||||
message: 'Please read and check the "Privacy Policy" and "User Agreement" before continuing. Clicking "Agree and Continue" will automatically check the box and proceed.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Agree and Continue',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
appleIdentityTokenMissing: 'Failed to get Apple identity token',
|
||||
loginFailed: 'Login failed, please try again later',
|
||||
loginFailedTitle: 'Login Failed',
|
||||
},
|
||||
success: {
|
||||
loginSuccess: 'Login Successful',
|
||||
},
|
||||
};
|
||||
|
||||
export const authGuard = {
|
||||
logout: {
|
||||
error: 'Logout Failed',
|
||||
errorMessage: 'Failed to logout, please try again later',
|
||||
},
|
||||
confirmLogout: {
|
||||
title: 'Confirm Logout',
|
||||
message: 'Are you sure you want to logout of your current account?',
|
||||
cancelButton: 'Cancel',
|
||||
confirmButton: 'Confirm',
|
||||
},
|
||||
deleteAccount: {
|
||||
successTitle: 'Account Deleted',
|
||||
successMessage: 'Your account has been successfully deleted',
|
||||
confirmButton: 'OK',
|
||||
errorTitle: 'Deletion Failed',
|
||||
errorMessage: 'Failed to delete account, please try again later',
|
||||
},
|
||||
confirmDeleteAccount: {
|
||||
title: 'Confirm Account Deletion',
|
||||
message: 'This action cannot be undone. Your account and all related data will be permanently deleted. Are you sure you want to continue?',
|
||||
cancelButton: 'Cancel',
|
||||
confirmButton: 'Confirm Deletion',
|
||||
},
|
||||
};
|
||||
|
||||
export const membershipModal = {
|
||||
plans: {
|
||||
lifetime: {
|
||||
title: 'Lifetime',
|
||||
subtitle: 'Lifetime companion, witnessing every health transformation',
|
||||
},
|
||||
quarterly: {
|
||||
title: 'Quarterly',
|
||||
subtitle: '3-month scientific plan, making health a habit',
|
||||
},
|
||||
weekly: {
|
||||
title: 'Weekly',
|
||||
subtitle: '7-day trial, experience the power of professional guidance',
|
||||
},
|
||||
unknown: 'Unknown Plan',
|
||||
tag: 'Best Value',
|
||||
},
|
||||
benefits: {
|
||||
title: 'Benefits Comparison',
|
||||
subtitle: 'Core benefits at a glance, choose with confidence',
|
||||
table: {
|
||||
benefit: 'Benefit',
|
||||
vip: 'VIP',
|
||||
regular: 'Regular',
|
||||
},
|
||||
items: {
|
||||
aiCalories: {
|
||||
title: 'AI Calorie Tracking',
|
||||
description: 'Photo recognition for automatic calorie tracking',
|
||||
},
|
||||
aiNutrition: {
|
||||
title: 'AI Nutrition Label',
|
||||
description: 'Identify nutrition facts from food packaging',
|
||||
},
|
||||
healthReminder: {
|
||||
title: 'Daily Health Reminder',
|
||||
description: 'Personalized health reminders based on goals',
|
||||
},
|
||||
aiMedication: {
|
||||
title: 'AI Medication Manager',
|
||||
description: 'Deep analysis of interactions & personalized schedules',
|
||||
},
|
||||
customChallenge: {
|
||||
title: 'Unlimited Custom Challenges',
|
||||
description: 'Create exclusive challenges & invite friends to join the journey',
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
unlimited: 'Unlimited',
|
||||
limited: 'Limited',
|
||||
dailyLimit: '{{count}} times/day',
|
||||
fullSupport: 'Full Support',
|
||||
basicSupport: 'Basic',
|
||||
smartReminder: 'Smart',
|
||||
fullAnalysis: 'Deep Analysis',
|
||||
createUnlimited: 'Unlimited',
|
||||
notSupported: 'Not Supported',
|
||||
},
|
||||
},
|
||||
sectionTitle: {
|
||||
plans: 'Membership Plans',
|
||||
plansSubtitle: 'Flexible choices, improve at your own pace',
|
||||
},
|
||||
actions: {
|
||||
subscribe: 'Subscribe Now',
|
||||
processing: 'Processing...',
|
||||
restore: 'Restore Purchase',
|
||||
restoring: 'Restoring...',
|
||||
back: 'Back',
|
||||
close: 'Close membership modal',
|
||||
selectPlan: 'Select {{plan}} plan',
|
||||
purchaseHint: 'Click to purchase {{plan}} membership',
|
||||
},
|
||||
agreements: {
|
||||
prefix: 'By subscribing, you agree to',
|
||||
userAgreement: 'User Agreement',
|
||||
membershipAgreement: 'Membership Agreement',
|
||||
autoRenewalAgreement: 'Auto-Renewal Agreement',
|
||||
alert: {
|
||||
title: 'Please read and agree',
|
||||
message: 'Please agree to User Agreement, Membership Agreement and Auto-Renewal Agreement before purchasing',
|
||||
confirm: 'OK',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noProducts: 'No membership products found. Please configure iOS products in RevenueCat and sync to current Offering.',
|
||||
purchaseCancelled: 'Purchase cancelled',
|
||||
alreadyPurchased: 'You already own this item',
|
||||
networkError: 'Network connection failed',
|
||||
paymentPending: 'Payment is processing',
|
||||
invalidCredentials: 'Account verification failed',
|
||||
purchaseFailed: 'Purchase failed',
|
||||
restoreSuccess: 'Restore successful',
|
||||
restoreFailed: 'Restore failed',
|
||||
restoreCancelled: 'Restore cancelled',
|
||||
restorePartialFailed: 'Restore partially failed',
|
||||
noPurchasesFound: 'No purchase records found',
|
||||
selectPlan: 'Please select a plan',
|
||||
},
|
||||
loading: {
|
||||
products: 'Loading membership plans, please wait',
|
||||
purchase: 'Purchase in progress, please wait',
|
||||
},
|
||||
success: {
|
||||
purchase: 'Membership activated successfully',
|
||||
},
|
||||
};
|
||||
|
||||
export const notificationSettings = {
|
||||
title: 'Notification Settings',
|
||||
loading: 'Loading...',
|
||||
sections: {
|
||||
notifications: 'Notification Settings',
|
||||
medicationReminder: 'Medication Reminder',
|
||||
nutritionReminder: 'Nutrition Reminder',
|
||||
moodReminder: 'Mood Reminder',
|
||||
description: 'Description',
|
||||
},
|
||||
items: {
|
||||
pushNotifications: {
|
||||
title: 'Push Notifications',
|
||||
description: 'Receive app notifications when enabled',
|
||||
},
|
||||
medicationReminder: {
|
||||
title: 'Medication Reminder',
|
||||
description: 'Receive reminder notifications at medication time',
|
||||
},
|
||||
nutritionReminder: {
|
||||
title: 'Nutrition Record Reminder',
|
||||
description: 'Receive nutrition record reminders at meal times',
|
||||
},
|
||||
moodReminder: {
|
||||
title: 'Mood Record Reminder',
|
||||
description: 'Receive mood record reminders in the evening',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
text: '• Push notifications is the master switch for all notifications\n• Various reminders require push notifications to be enabled\n• You can manage notification permissions in system settings\n• Disabling push notifications will stop all app notifications',
|
||||
},
|
||||
alerts: {
|
||||
permissionDenied: {
|
||||
title: 'Permission Denied',
|
||||
message: 'Please enable notification permission in system settings, then try to enable push notifications',
|
||||
cancel: 'Cancel',
|
||||
goToSettings: 'Go to Settings',
|
||||
},
|
||||
error: {
|
||||
title: 'Error',
|
||||
message: 'Failed to request notification permission',
|
||||
saveFailed: 'Failed to save settings',
|
||||
medicationReminderFailed: 'Failed to set medication reminder',
|
||||
nutritionReminderFailed: 'Failed to set nutrition reminder',
|
||||
moodReminderFailed: 'Failed to set mood reminder',
|
||||
},
|
||||
notificationsEnabled: {
|
||||
title: 'Notifications Enabled',
|
||||
body: 'You will receive app notifications and reminders',
|
||||
},
|
||||
medicationReminderEnabled: {
|
||||
title: 'Medication Reminder Enabled',
|
||||
body: 'You will receive reminder notifications at medication time',
|
||||
},
|
||||
nutritionReminderEnabled: {
|
||||
title: 'Nutrition Reminder Enabled',
|
||||
body: 'You will receive nutrition record reminders at meal times',
|
||||
},
|
||||
moodReminderEnabled: {
|
||||
title: 'Mood Reminder Enabled',
|
||||
body: 'You will receive mood record reminders in the evening',
|
||||
},
|
||||
},
|
||||
};
|
||||
31
i18n/en/weight.ts
Normal file
31
i18n/en/weight.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const weightRecords = {
|
||||
title: 'Weight Records',
|
||||
pageSubtitle: 'Track your weight progress and trends',
|
||||
loadingHistory: 'Failed to load weight history',
|
||||
history: 'Records',
|
||||
historyMonthFormat: '{{year}}.{{month}}',
|
||||
stats: {
|
||||
currentWeight: 'Current Weight',
|
||||
initialWeight: 'Starting Weight',
|
||||
targetWeight: 'Target Weight',
|
||||
},
|
||||
empty: {
|
||||
title: 'No weight records',
|
||||
subtitle: 'Tap the + button to add your first record.',
|
||||
},
|
||||
modal: {
|
||||
recordWeight: 'Record Weight',
|
||||
editInitialWeight: 'Edit Starting Weight',
|
||||
editTargetWeight: 'Edit Target Weight',
|
||||
editRecord: 'Edit Record',
|
||||
inputPlaceholder: 'Enter weight',
|
||||
unit: 'kg',
|
||||
quickSelection: 'Quick pick',
|
||||
confirm: 'Save',
|
||||
},
|
||||
alerts: {
|
||||
deleteFailed: 'Failed to delete record',
|
||||
invalidWeight: 'Please enter a valid weight between 0 and 500 kg',
|
||||
saveFailed: 'Failed to save weight, please try again',
|
||||
},
|
||||
};
|
||||
3442
i18n/index.ts
3442
i18n/index.ts
File diff suppressed because it is too large
Load Diff
303
i18n/zh/challenge.ts
Normal file
303
i18n/zh/challenge.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
export const challengeDetail = {
|
||||
title: '挑战详情',
|
||||
notFound: '未找到该挑战,稍后再试试吧。',
|
||||
loading: '加载挑战详情中…',
|
||||
retry: '重新加载',
|
||||
share: {
|
||||
generating: '正在生成分享卡片...',
|
||||
failed: '分享失败,请稍后重试',
|
||||
messageJoined: '我正在参与「{{title}}」挑战,已完成 {{completed}}/{{target}} 天!一起加入吧!',
|
||||
messageNotJoined: '发现一个很棒的挑战「{{title}}」,一起来参与吧!',
|
||||
},
|
||||
dateRange: {
|
||||
format: '{{start}} - {{end}}',
|
||||
monthDay: '{{month}}月{{day}}日',
|
||||
ongoing: '持续更新中',
|
||||
},
|
||||
participants: {
|
||||
count: '{{count}} 人正在参与',
|
||||
ongoing: '持续更新中',
|
||||
more: '更多',
|
||||
},
|
||||
detail: {
|
||||
requirement: '按日打卡自动累计',
|
||||
viewAllRanking: '查看全部',
|
||||
},
|
||||
checkIn: {
|
||||
title: '挑战打卡',
|
||||
todayChecked: '今日已打卡',
|
||||
subtitle: '每日打卡会累计进度,达成目标天数',
|
||||
subtitleChecked: '已记录今日进度,明天继续保持',
|
||||
button: {
|
||||
checkIn: '立即打卡',
|
||||
checking: '打卡中…',
|
||||
checked: '今日已打卡',
|
||||
notJoined: '加入后打卡',
|
||||
upcoming: '挑战未开始',
|
||||
expired: '挑战已结束',
|
||||
},
|
||||
toast: {
|
||||
alreadyChecked: '今日已打卡',
|
||||
notStarted: '挑战未开始,开始后再来打卡',
|
||||
expired: '挑战已结束,无法打卡',
|
||||
mustJoin: '加入挑战后才能打卡',
|
||||
success: '打卡成功,继续坚持!',
|
||||
failed: '打卡失败,请稍后再试',
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
join: '立即加入挑战',
|
||||
joining: '加入中…',
|
||||
leave: '退出挑战',
|
||||
leaving: '退出中…',
|
||||
delete: '删除挑战',
|
||||
deleting: '删除中…',
|
||||
upcoming: '挑战即将开始',
|
||||
expired: '挑战已结束',
|
||||
},
|
||||
highlight: {
|
||||
join: {
|
||||
title: '立即加入挑战',
|
||||
subtitle: '邀请好友一起坚持,更容易收获成果',
|
||||
},
|
||||
leave: {
|
||||
title: '先别急着离开',
|
||||
subtitle: '再坚持一下,下一个里程碑就要出现了',
|
||||
},
|
||||
upcoming: {
|
||||
title: '挑战即将开始',
|
||||
subtitle: '{{date}} 开始,敬请期待',
|
||||
subtitleFallback: '挑战即将开启,敬请期待',
|
||||
},
|
||||
expired: {
|
||||
title: '挑战已结束',
|
||||
subtitle: '{{date}} 已截止,期待下一次挑战',
|
||||
subtitleFallback: '本轮挑战已结束,期待下一次挑战',
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
leaveConfirm: {
|
||||
title: '确认退出挑战?',
|
||||
message: '退出后需要重新加入才能继续坚持。',
|
||||
cancel: '取消',
|
||||
confirm: '退出挑战',
|
||||
},
|
||||
joinFailed: '加入挑战失败',
|
||||
leaveFailed: '退出挑战失败',
|
||||
archiveConfirm: {
|
||||
title: '确认删除该挑战?',
|
||||
message: '删除后将无法恢复,参与者也将无法再访问此挑战。',
|
||||
cancel: '取消',
|
||||
confirm: '删除挑战',
|
||||
},
|
||||
archiveFailed: '删除挑战失败',
|
||||
archiveSuccess: '挑战已删除',
|
||||
},
|
||||
ranking: {
|
||||
title: '排行榜',
|
||||
description: '',
|
||||
empty: '榜单即将开启,快来抢占席位。',
|
||||
today: '今日',
|
||||
todayGoal: '今日目标',
|
||||
hour: '小时',
|
||||
},
|
||||
leaderboard: {
|
||||
title: '排行榜',
|
||||
loading: '加载榜单中…',
|
||||
notFound: '未找到该挑战。',
|
||||
loadFailed: '暂时无法加载榜单,请稍后再试。',
|
||||
empty: '榜单即将开启,快来抢占席位。',
|
||||
loadMore: '加载更多…',
|
||||
loadMoreFailed: '加载更多失败,请下拉刷新重试',
|
||||
},
|
||||
shareCard: {
|
||||
footer: 'Out Live · 超越生命',
|
||||
progress: {
|
||||
label: '我的坚持进度',
|
||||
days: '{{completed}} / {{target}} 天',
|
||||
completed: '🎉 已完成挑战!',
|
||||
remaining: '还差 {{remaining}} 天完成挑战',
|
||||
},
|
||||
info: {
|
||||
checkInDaily: '按日打卡',
|
||||
joinUs: '快来一起坚持吧',
|
||||
},
|
||||
shareCode: {
|
||||
copied: '分享码已复制',
|
||||
},
|
||||
},
|
||||
shareCode: {
|
||||
copied: '分享码已复制',
|
||||
},
|
||||
};
|
||||
|
||||
export const badges = {
|
||||
title: '勋章馆',
|
||||
subtitle: '点亮每一次坚持',
|
||||
hero: {
|
||||
highlight: '保持连续打卡即可解锁更多稀有勋章',
|
||||
earnedLabel: '已获得',
|
||||
totalLabel: '总数',
|
||||
progressLabel: '解锁进度',
|
||||
},
|
||||
categories: {
|
||||
all: '全部',
|
||||
sleep: '睡眠',
|
||||
exercise: '运动',
|
||||
diet: '饮食',
|
||||
challenge: '挑战',
|
||||
social: '社交',
|
||||
special: '特别',
|
||||
},
|
||||
rarities: {
|
||||
common: '普通',
|
||||
uncommon: '少见',
|
||||
rare: '稀有',
|
||||
epic: '史诗',
|
||||
legendary: '传说',
|
||||
},
|
||||
status: {
|
||||
earned: '已获得',
|
||||
locked: '待解锁',
|
||||
earnedAt: '{{date}} 获得',
|
||||
},
|
||||
legend: '稀有度说明',
|
||||
filterLabel: '勋章分类',
|
||||
empty: {
|
||||
title: '还没有勋章',
|
||||
description: '完成睡眠、运动、挑战等任务即可点亮你的第一枚勋章。',
|
||||
action: '去探索计划',
|
||||
},
|
||||
};
|
||||
|
||||
export const challenges = {
|
||||
title: '挑战',
|
||||
subtitle: '加入官方或自定义挑战,一起坚持',
|
||||
loading: '加载挑战中…',
|
||||
loadFailed: '暂时无法获取挑战,请稍后再试。',
|
||||
retry: '重新加载',
|
||||
empty: '暂时没有挑战,先去创建或加入一个吧。',
|
||||
customChallenges: '自定义挑战',
|
||||
officialChallengesTitle: '官方挑战',
|
||||
officialChallenges: '官方挑战即将上线。',
|
||||
join: '加入',
|
||||
joined: '已加入',
|
||||
invalidInviteCode: '请输入有效的分享码',
|
||||
joinSuccess: '加入挑战成功',
|
||||
joinFailed: '加入失败,请稍后再试',
|
||||
joinModal: {
|
||||
title: '输入分享码加入',
|
||||
description: '输入好友分享码即可加入挑战',
|
||||
confirm: '加入挑战',
|
||||
joining: '加入中…',
|
||||
cancel: '取消',
|
||||
placeholder: '请输入分享码',
|
||||
},
|
||||
statusLabels: {
|
||||
upcoming: '即将开始',
|
||||
ongoing: '进行中',
|
||||
expired: '已结束',
|
||||
},
|
||||
createCustom: {
|
||||
title: '创建挑战',
|
||||
editTitle: '编辑挑战',
|
||||
yourChallenge: '你的挑战',
|
||||
basicInfo: '基础信息',
|
||||
challengeSettings: '挑战设置',
|
||||
displayInteraction: '展示与互动',
|
||||
durationDays: '{{days}} 天',
|
||||
durationDaysChallenge: '{{days}} 天挑战',
|
||||
dayUnit: '天',
|
||||
defaultTitle: '自定义挑战',
|
||||
rankingDescription: '榜单每日更新',
|
||||
typeLabels: {
|
||||
water: '饮水',
|
||||
exercise: '运动',
|
||||
diet: '饮食',
|
||||
sleep: '睡眠',
|
||||
mood: '心情',
|
||||
weight: '体重',
|
||||
custom: '自定义',
|
||||
},
|
||||
fields: {
|
||||
title: '挑战名称',
|
||||
titlePlaceholder: '例如 21 天早睡',
|
||||
coverImage: '封面图',
|
||||
uploadCover: '上传封面',
|
||||
challengeDescription: '挑战简介',
|
||||
descriptionPlaceholder: '写下挑战目标和打卡方式',
|
||||
challengeType: '挑战类型',
|
||||
challengeTypeHelper: '选择最贴近目标的类型',
|
||||
timeRange: '时间范围',
|
||||
start: '开始日期',
|
||||
end: '结束日期',
|
||||
duration: '持续时间',
|
||||
periodLabel: '周期标签',
|
||||
periodLabelPlaceholder: '例如 21 天养成计划',
|
||||
dailyTargetAndUnit: '每日目标与单位',
|
||||
dailyTargetPlaceholder: '每日目标数值',
|
||||
unitPlaceholder: '单位(杯/分钟/步数等)',
|
||||
unitHelper: '选填,展示在每日目标后',
|
||||
minimumCheckInDays: '最少打卡天数',
|
||||
minimumCheckInDaysPlaceholder: '不能超过总天数',
|
||||
maxParticipants: '参与人数上限',
|
||||
noLimit: '不限制',
|
||||
isPublic: '允许他人通过分享码加入',
|
||||
publicDescription: '开启后他人可凭分享码加入;关闭则仅自己可见',
|
||||
},
|
||||
floatingCTA: {
|
||||
title: '生成分享码',
|
||||
subtitle: '创建挑战并分享给好友一起打卡',
|
||||
editTitle: '保存更改',
|
||||
editSubtitle: '更新挑战信息并同步给参与者',
|
||||
},
|
||||
buttons: {
|
||||
createAndGenerateCode: '创建并生成分享码',
|
||||
creating: '创建中…',
|
||||
updateAndSave: '保存修改',
|
||||
updating: '保存中…',
|
||||
},
|
||||
datePicker: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
},
|
||||
alerts: {
|
||||
titleRequired: '请输入挑战名称',
|
||||
endTimeError: '结束时间需要晚于开始时间',
|
||||
targetValueError: '每日目标需在 1-1000 之间',
|
||||
minimumDaysError: '最少打卡天数需在 1-365 之间',
|
||||
minimumDaysExceedError: '最少打卡天数不能超过挑战总天数',
|
||||
participantsError: '人数需在 2-10000 之间或留空',
|
||||
createFailed: '创建挑战失败',
|
||||
createSuccess: '挑战已创建',
|
||||
updateSuccess: '挑战已更新',
|
||||
},
|
||||
imageUpload: {
|
||||
selectSource: '选择封面',
|
||||
selectMessage: '拍照或从相册选择封面',
|
||||
camera: '拍照',
|
||||
album: '相册',
|
||||
cancel: '取消',
|
||||
cameraPermission: '需要相机权限',
|
||||
cameraPermissionMessage: '请开启相机权限以拍摄封面',
|
||||
albumPermissionMessage: '请开启相册权限以选择图片',
|
||||
cameraFailed: '打开相机失败',
|
||||
cameraFailedMessage: '请重试或从相册选择',
|
||||
selectFailed: '选择失败',
|
||||
selectFailedMessage: '暂时无法选择图片,请重试',
|
||||
uploadFailed: '上传失败',
|
||||
uploadFailedMessage: '封面上传失败,请稍后重试',
|
||||
uploading: '上传中…',
|
||||
clear: '移除封面',
|
||||
helper: '推荐使用 16:9 的高清图片,大小 2MB 内',
|
||||
},
|
||||
shareModal: {
|
||||
title: '分享码已生成',
|
||||
subtitle: '分享给好友即可一起参与挑战',
|
||||
generatingCode: '生成中…',
|
||||
copyCode: '复制分享码',
|
||||
viewChallenge: '查看挑战',
|
||||
later: '稍后再说',
|
||||
},
|
||||
},
|
||||
};
|
||||
5
i18n/zh/common.ts
Normal file
5
i18n/zh/common.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const dateSelector = {
|
||||
backToToday: '回到今天',
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
};
|
||||
551
i18n/zh/diet.ts
Normal file
551
i18n/zh/diet.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
export const nutritionRecords = {
|
||||
title: '营养记录',
|
||||
listTitle: '今日餐食',
|
||||
recordCount: '{{count}} 条记录',
|
||||
empty: {
|
||||
title: '今天还没有记录',
|
||||
action: '记一笔',
|
||||
},
|
||||
footer: {
|
||||
end: '- 已经到底啦 -',
|
||||
loadMore: '加载更多',
|
||||
},
|
||||
delete: {
|
||||
title: '确认删除',
|
||||
message: '确定要删除这条营养记录吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
confirm: '删除',
|
||||
},
|
||||
mealTypes: {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
other: '其他',
|
||||
},
|
||||
nutrients: {
|
||||
protein: '蛋白质',
|
||||
fat: '脂肪',
|
||||
carbs: '碳水',
|
||||
unit: 'g',
|
||||
caloriesUnit: '千卡',
|
||||
},
|
||||
overlay: {
|
||||
title: '记录方式',
|
||||
scan: 'AI识别',
|
||||
foodLibrary: '食物库',
|
||||
voiceRecord: '一句话记录',
|
||||
},
|
||||
chart: {
|
||||
remaining: '还能吃',
|
||||
formula: '还能吃 = 代谢 + 运动 - 饮食',
|
||||
metabolism: '代谢',
|
||||
exercise: '运动',
|
||||
diet: '饮食',
|
||||
},
|
||||
};
|
||||
|
||||
export const foodCamera = {
|
||||
title: '食物拍摄',
|
||||
hint: '确保食物在取景框内',
|
||||
permission: {
|
||||
title: '需要相机权限',
|
||||
description: '为了拍摄食物并进行AI识别,需要访问您的相机',
|
||||
button: '授权访问',
|
||||
},
|
||||
guide: {
|
||||
title: '拍摄示例',
|
||||
description: '请上传或拍摄清晰的食物照片,有助于提高识别准确率',
|
||||
button: '知道了',
|
||||
good: '光线充足,主体清晰',
|
||||
bad: '模糊不清,光线昏暗',
|
||||
},
|
||||
buttons: {
|
||||
album: '相册',
|
||||
capture: '拍照',
|
||||
help: '帮助',
|
||||
},
|
||||
alerts: {
|
||||
captureFailed: {
|
||||
title: '拍照失败',
|
||||
message: '请重试',
|
||||
},
|
||||
pickFailed: {
|
||||
title: '选择失败',
|
||||
message: '请重试',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const foodRecognition = {
|
||||
title: '食物识别',
|
||||
header: {
|
||||
confirm: '确认食物',
|
||||
recognizing: 'AI 识别中',
|
||||
},
|
||||
errors: {
|
||||
noImage: '未找到图片',
|
||||
generic: '食物识别失败,请重试',
|
||||
unknown: '未知错误',
|
||||
noFoodDetected: '识别失败:未检测到食物',
|
||||
processError: '识别过程出错',
|
||||
},
|
||||
logs: {
|
||||
uploading: '📤 正在上传图片到云端...',
|
||||
uploadSuccess: '✅ 图片上传完成',
|
||||
analyzing: '🤖 AI大模型分析中...',
|
||||
analysisSuccess: '✅ AI分析完成',
|
||||
confidence: '🎯 识别置信度: {{value}}%',
|
||||
itemsFound: '🍽️ 识别到 {{count}} 种食物',
|
||||
failed: '❌ 识别失败:未检测到食物',
|
||||
error: '❌ 识别过程出错',
|
||||
},
|
||||
status: {
|
||||
idle: {
|
||||
title: '准备就绪',
|
||||
subtitle: '请稍候...',
|
||||
},
|
||||
uploading: {
|
||||
title: '上传图片',
|
||||
subtitle: '正在上传图片到云端服务器...',
|
||||
},
|
||||
recognizing: {
|
||||
title: 'AI 分析中',
|
||||
subtitle: '智能模型正在分析食物成分...',
|
||||
},
|
||||
completed: {
|
||||
title: '识别成功',
|
||||
subtitle: '即将跳转到分析结果页面',
|
||||
},
|
||||
failed: {
|
||||
title: '识别失败',
|
||||
subtitle: '请检查网络或稍后重试',
|
||||
},
|
||||
processing: {
|
||||
title: '处理中...',
|
||||
subtitle: '请稍候...',
|
||||
},
|
||||
},
|
||||
mealTypes: {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
unknown: '未知',
|
||||
},
|
||||
info: {
|
||||
title: '智能食物识别',
|
||||
description: 'AI 将分析照片,自动识别食物种类并估算营养成分,生成详细报告。',
|
||||
},
|
||||
actions: {
|
||||
start: '开始识别',
|
||||
retry: '返回重试',
|
||||
logs: '处理日志',
|
||||
logsPlaceholder: '准备开始...',
|
||||
},
|
||||
alerts: {
|
||||
recognizing: {
|
||||
title: '正在识别中',
|
||||
message: '识别过程尚未完成,确定要返回吗?',
|
||||
continue: '继续识别',
|
||||
back: '返回',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const foodAnalysisResult = {
|
||||
title: '分析结果',
|
||||
error: {
|
||||
notFound: '未找到图片或识别结果',
|
||||
},
|
||||
placeholder: '营养记录',
|
||||
nutrients: {
|
||||
caloriesUnit: '千卡',
|
||||
protein: '蛋白质',
|
||||
fat: '脂肪',
|
||||
carbs: '碳水',
|
||||
unit: '克',
|
||||
},
|
||||
sections: {
|
||||
recognitionResult: '识别结果',
|
||||
foodIntake: '食物摄入',
|
||||
},
|
||||
nonFood: {
|
||||
title: '未识别到食物',
|
||||
suggestions: {
|
||||
title: '建议:',
|
||||
item1: '• 确保图片中包含食物',
|
||||
item2: '• 尝试更清晰的照片角度',
|
||||
item3: '• 避免过度模糊或光线不足',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
retake: '重新拍照',
|
||||
record: '记录',
|
||||
close: '关闭',
|
||||
},
|
||||
mealSelector: {
|
||||
title: '选择餐次',
|
||||
},
|
||||
editModal: {
|
||||
title: '编辑食物信息',
|
||||
fields: {
|
||||
name: '食物名称',
|
||||
namePlaceholder: '输入食物名称',
|
||||
amount: '重量 (克)',
|
||||
amountPlaceholder: '输入重量',
|
||||
calories: '卡路里 (千卡)',
|
||||
caloriesPlaceholder: '输入卡路里',
|
||||
},
|
||||
actions: {
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
},
|
||||
},
|
||||
confidence: '置信度: {{value}}%',
|
||||
dateFormats: {
|
||||
today: 'YYYY年M月D日',
|
||||
full: 'YYYY年M月D日 HH:mm',
|
||||
},
|
||||
};
|
||||
|
||||
export const foodLibrary = {
|
||||
title: '食物库',
|
||||
custom: '自定义',
|
||||
search: {
|
||||
placeholder: '搜索食物...',
|
||||
loading: '搜索中...',
|
||||
empty: '未找到相关食物',
|
||||
noData: '暂无食物数据',
|
||||
},
|
||||
loading: '加载食物库中...',
|
||||
retry: '重试',
|
||||
mealTypes: {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
},
|
||||
actions: {
|
||||
record: '记录',
|
||||
selectMeal: '选择餐次',
|
||||
},
|
||||
alerts: {
|
||||
deleteFailed: {
|
||||
title: '删除失败',
|
||||
message: '删除食物时发生错误,请稍后重试',
|
||||
},
|
||||
createFailed: {
|
||||
title: '创建失败',
|
||||
message: '创建自定义食物时发生错误,请稍后重试',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createCustomFood = {
|
||||
title: '创建自定义食物',
|
||||
save: '保存',
|
||||
preview: {
|
||||
title: '效果预览',
|
||||
defaultName: '食物名称',
|
||||
},
|
||||
basicInfo: {
|
||||
title: '基本信息',
|
||||
name: '食物名称',
|
||||
namePlaceholder: '例如,汉堡',
|
||||
defaultAmount: '默认数量',
|
||||
calories: '食物热量',
|
||||
},
|
||||
optionalInfo: {
|
||||
title: '可选信息',
|
||||
photo: '照片',
|
||||
addPhoto: '添加照片',
|
||||
protein: '蛋白质',
|
||||
fat: '脂肪',
|
||||
carbohydrate: '碳水化合物',
|
||||
},
|
||||
units: {
|
||||
kcal: '千卡',
|
||||
g: 'g',
|
||||
gram: '克',
|
||||
},
|
||||
alerts: {
|
||||
permissionDenied: {
|
||||
title: '权限不足',
|
||||
message: '需要相册权限以选择照片',
|
||||
},
|
||||
uploadFailed: {
|
||||
title: '上传失败',
|
||||
message: '照片上传失败,请重试',
|
||||
},
|
||||
error: {
|
||||
title: '发生错误',
|
||||
message: '选择照片失败,请重试',
|
||||
},
|
||||
validation: {
|
||||
title: '提示',
|
||||
nameRequired: '请输入食物名称',
|
||||
caloriesRequired: '请输入有效的热量值',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const voiceRecord = {
|
||||
title: '一句话记录',
|
||||
intro: {
|
||||
description: '通过语音描述您的饮食内容,AI将智能分析营养成分和卡路里',
|
||||
},
|
||||
status: {
|
||||
idle: '轻触麦克风开始录音',
|
||||
listening: '正在聆听中,请开始说话...',
|
||||
processing: 'AI正在处理语音内容...',
|
||||
analyzing: 'AI大模型深度分析营养成分中...',
|
||||
result: '语音识别完成,请确认结果',
|
||||
},
|
||||
hints: {
|
||||
listening: '说出您想记录的食物内容',
|
||||
},
|
||||
examples: {
|
||||
title: '记录示例:',
|
||||
items: [
|
||||
'今早吃了两个煎蛋、一片全麦面包和一杯牛奶',
|
||||
'午饭吃了红烧肉约150克、米饭一小碗、青菜一份',
|
||||
'晚饭吃了蒸蛋羹、紫菜蛋花汤、小米粥一碗',
|
||||
],
|
||||
},
|
||||
analysis: {
|
||||
progress: '分析进度: {{progress}}%',
|
||||
hint: 'AI正在深度分析您的食物描述...',
|
||||
},
|
||||
result: {
|
||||
label: '识别结果:',
|
||||
},
|
||||
actions: {
|
||||
retry: '重新录音',
|
||||
confirm: '确认使用',
|
||||
},
|
||||
alerts: {
|
||||
noVoiceInput: '没有检测到语音输入,请重试',
|
||||
networkError: '网络连接异常,请检查网络后重试',
|
||||
voiceError: '语音识别出现问题,请重试',
|
||||
noValidContent: '未识别到有效内容,请重新录音',
|
||||
pleaseRecordFirst: '请先进行语音识别',
|
||||
recordingFailed: '录音失败',
|
||||
recordingPermissionError: '无法启动语音识别,请检查麦克风权限设置',
|
||||
analysisFailed: '分析失败',
|
||||
},
|
||||
};
|
||||
|
||||
export const nutritionLabelAnalysis = {
|
||||
title: '成分表分析',
|
||||
camera: {
|
||||
permissionDenied: '权限不足',
|
||||
permissionMessage: '需要相机权限才能拍摄成分表',
|
||||
},
|
||||
actions: {
|
||||
takePhoto: '拍摄',
|
||||
selectFromAlbum: '相册',
|
||||
startAnalysis: '开始分析',
|
||||
close: '关闭',
|
||||
},
|
||||
placeholder: {
|
||||
text: '拍摄或选择成分表照片',
|
||||
},
|
||||
status: {
|
||||
uploading: '正在上传图片...',
|
||||
analyzing: '正在分析成分表...',
|
||||
},
|
||||
errors: {
|
||||
analysisFailed: {
|
||||
title: '分析失败',
|
||||
message: '分析图片时发生错误,请重试',
|
||||
defaultMessage: '分析服务暂时不可用',
|
||||
},
|
||||
cannotRecognize: '无法识别成分表,请尝试拍摄更清晰的照片',
|
||||
cameraPermissionDenied: '需要相机权限才能拍摄成分表',
|
||||
},
|
||||
results: {
|
||||
title: '营养成分详细分析',
|
||||
detailedAnalysis: '营养成分详细分析',
|
||||
},
|
||||
imageViewer: {
|
||||
close: '关闭',
|
||||
dateFormat: 'YYYY年M月D日 HH:mm',
|
||||
},
|
||||
};
|
||||
|
||||
export const nutritionAnalysisHistory = {
|
||||
title: '历史记录',
|
||||
dateFormat: 'YYYY年M月D日 HH:mm',
|
||||
recognized: '识别 {{count}} 项营养素',
|
||||
loadingMore: '加载更多...',
|
||||
loading: '加载历史记录...',
|
||||
filter: {
|
||||
all: '全部',
|
||||
},
|
||||
filters: {
|
||||
all: '全部',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
},
|
||||
status: {
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
processing: '处理中',
|
||||
unknown: '未知',
|
||||
},
|
||||
nutrients: {
|
||||
energy: '热量',
|
||||
protein: '蛋白质',
|
||||
carbs: '碳水',
|
||||
fat: '脂肪',
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: '确认删除',
|
||||
confirmMessage: '确定要删除这条记录吗?',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
successTitle: '删除成功',
|
||||
successMessage: '记录已成功删除',
|
||||
},
|
||||
actions: {
|
||||
expand: '展开详情',
|
||||
collapse: '收起详情',
|
||||
expandDetails: '展开详情',
|
||||
collapseDetails: '收起详情',
|
||||
confirmDelete: '确认删除',
|
||||
delete: '删除',
|
||||
cancel: '取消',
|
||||
retry: '重试',
|
||||
},
|
||||
empty: {
|
||||
title: '暂无历史记录',
|
||||
subtitle: '开始识别营养成分表吧',
|
||||
},
|
||||
errors: {
|
||||
error: '错误',
|
||||
loadFailed: '加载失败',
|
||||
unknownError: '未知错误',
|
||||
fetchFailed: '获取历史记录失败',
|
||||
fetchFailedRetry: '获取历史记录失败,请重试',
|
||||
deleteFailed: '删除失败,请稍后重试',
|
||||
},
|
||||
loadingState: {
|
||||
records: '加载历史记录...',
|
||||
more: '加载更多...',
|
||||
},
|
||||
details: {
|
||||
title: '详细营养成分',
|
||||
nutritionDetails: '详细营养成分',
|
||||
aiModel: 'AI 模型',
|
||||
provider: '服务提供商',
|
||||
serviceProvider: '服务提供商',
|
||||
},
|
||||
records: {
|
||||
nutritionCount: '识别 {{count}} 项营养素',
|
||||
},
|
||||
imageViewer: {
|
||||
close: '关闭',
|
||||
},
|
||||
};
|
||||
|
||||
export const waterDetail = {
|
||||
title: '饮水详情',
|
||||
waterRecord: '饮水记录',
|
||||
today: '今日',
|
||||
total: '总计:',
|
||||
goal: '目标:',
|
||||
noRecords: '暂无饮水记录',
|
||||
noRecordsSubtitle: '点击"添加记录"开始记录饮水量',
|
||||
deleteConfirm: {
|
||||
title: '确认删除',
|
||||
message: '确定要删除这条饮水记录吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
confirm: '删除',
|
||||
},
|
||||
deleteButton: '删除',
|
||||
water: '水',
|
||||
loadingUserPreferences: '加载用户偏好设置失败',
|
||||
};
|
||||
|
||||
export const waterSettings = {
|
||||
title: '饮水设置',
|
||||
sections: {
|
||||
dailyGoal: '每日饮水目标',
|
||||
quickAdd: '快速添加默认值',
|
||||
reminder: '喝水提醒',
|
||||
},
|
||||
descriptions: {
|
||||
quickAdd: '设置点击"+"按钮时添加的默认饮水量',
|
||||
reminder: '设置定时提醒您补充水分',
|
||||
},
|
||||
labels: {
|
||||
ml: 'ml',
|
||||
disabled: '已关闭',
|
||||
},
|
||||
alerts: {
|
||||
goalSuccess: {
|
||||
title: '设置成功',
|
||||
message: '每日饮水目标已设置为 {{amount}}ml',
|
||||
},
|
||||
quickAddSuccess: {
|
||||
title: '设置成功',
|
||||
message: '快速添加默认值已设置为 {{amount}}ml',
|
||||
},
|
||||
quickAddFailed: {
|
||||
title: '设置失败',
|
||||
message: '无法保存快速添加默认值,请重试',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
status: {
|
||||
reminderEnabled: '{{startTime}}-{{endTime}}, 每{{interval}}分钟',
|
||||
},
|
||||
};
|
||||
|
||||
export const waterReminderSettings = {
|
||||
title: '喝水提醒',
|
||||
sections: {
|
||||
notifications: '推送提醒',
|
||||
timeRange: '提醒时间段',
|
||||
interval: '提醒间隔',
|
||||
},
|
||||
descriptions: {
|
||||
notifications: '开启后将在指定时间段内定期推送喝水提醒',
|
||||
timeRange: '只在指定时间段内发送提醒,避免打扰您的休息',
|
||||
interval: '选择提醒的频率,建议30-120分钟为宜',
|
||||
},
|
||||
labels: {
|
||||
startTime: '开始时间',
|
||||
endTime: '结束时间',
|
||||
interval: '提醒间隔',
|
||||
saveSettings: '保存设置',
|
||||
hours: '小时',
|
||||
timeRangePreview: '时间段预览',
|
||||
minutes: '分钟',
|
||||
},
|
||||
alerts: {
|
||||
timeValidation: {
|
||||
title: '时间设置提示',
|
||||
startTimeInvalid: '开始时间不能晚于或等于结束时间,请重新选择',
|
||||
endTimeInvalid: '结束时间不能早于或等于开始时间,请重新选择',
|
||||
},
|
||||
success: {
|
||||
enabled: '设置成功',
|
||||
enabledMessage: '喝水提醒已开启\n\n时间段:{{timeRange}}\n提醒间隔:{{interval}}\n\n我们将在指定时间段内定期提醒您喝水',
|
||||
disabled: '设置成功',
|
||||
disabledMessage: '喝水提醒已关闭',
|
||||
},
|
||||
error: {
|
||||
title: '保存失败',
|
||||
message: '无法保存喝水提醒设置,请重试',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
},
|
||||
};
|
||||
507
i18n/zh/health.ts
Normal file
507
i18n/zh/health.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
export const healthPermissions = {
|
||||
title: '健康数据授权说明',
|
||||
subtitle: '我们通过 Apple Health 的 HealthKit/CareKit 接口同步必要的数据,让训练、恢复和提醒更贴合你的身体状态。',
|
||||
cards: {
|
||||
usage: {
|
||||
title: '我们会读取 / 写入的数据',
|
||||
items: [
|
||||
'运动与活动:步数、活动能量、锻炼记录用于生成训练表现和热力图。',
|
||||
'身体指标:身高、体重、体脂率帮助制定个性化训练与营养建议。',
|
||||
'睡眠与恢复:睡眠时长与阶段用于智能提醒与恢复建议。',
|
||||
'水分摄入:读取与写入饮水记录,保持与「健康」App 一致。',
|
||||
],
|
||||
},
|
||||
purpose: {
|
||||
title: '使用这些数据的目的',
|
||||
items: [
|
||||
'提供个性化训练计划、挑战与恢复建议。',
|
||||
'在统计页展示长期趋势,帮助你理解身体变化。',
|
||||
'减少重复输入,在提醒与挑战中自动同步进度。',
|
||||
],
|
||||
},
|
||||
control: {
|
||||
title: '你的控制权',
|
||||
items: [
|
||||
'授权流程完全由 Apple Health 控制,你可随时在 iOS 设置 > 健康 > 数据访问与设备 中更改权限。',
|
||||
'未授权的数据不会被访问,撤销授权后我们会清理相关缓存。',
|
||||
'核心功能依旧可用,并提供手动输入等替代方案。',
|
||||
],
|
||||
},
|
||||
privacy: {
|
||||
title: '数据存储与隐私',
|
||||
items: [
|
||||
'健康数据仅存储在你的设备上,我们不会上传服务器或共享给第三方。',
|
||||
'只有在需要同步的功能中才会保存聚合后的匿名统计值。',
|
||||
'我们遵循 Apple 的审核要求,任何变更都会提前告知。',
|
||||
],
|
||||
},
|
||||
},
|
||||
callout: {
|
||||
title: '未授权会怎样?',
|
||||
items: [
|
||||
'相关模块会提示你授权,并提供手动记录入口。',
|
||||
'拒绝授权不会影响其它与健康数据无关的功能。',
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: '需要更多帮助?',
|
||||
description: '如果你对 HealthKit / CareKit 的使用方式有疑问,可通过以下邮箱或在个人中心提交反馈:',
|
||||
email: 'richardwei1995@gmail.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const statistics = {
|
||||
title: 'Out Live',
|
||||
sections: {
|
||||
bodyMetrics: '身体指标',
|
||||
},
|
||||
components: {
|
||||
diet: {
|
||||
title: '饮食分析',
|
||||
loading: '加载中...',
|
||||
updated: '更新: {{time}}',
|
||||
remaining: '还能吃',
|
||||
calories: '热量',
|
||||
protein: '蛋白质',
|
||||
carb: '碳水',
|
||||
fat: '脂肪',
|
||||
fiber: '纤维',
|
||||
sodium: '钠',
|
||||
basal: '基代',
|
||||
exercise: '运动',
|
||||
diet: '饮食',
|
||||
kcal: '千卡',
|
||||
aiRecognition: 'AI识别',
|
||||
foodLibrary: '食物库',
|
||||
voiceRecord: '一句话记录',
|
||||
nutritionLabel: '成分表分析',
|
||||
},
|
||||
fitness: {
|
||||
kcal: '千卡',
|
||||
minutes: '分钟',
|
||||
hours: '小时',
|
||||
},
|
||||
steps: {
|
||||
title: '步数',
|
||||
},
|
||||
mood: {
|
||||
title: '心情',
|
||||
empty: '点击记录心情',
|
||||
},
|
||||
stress: {
|
||||
title: '压力',
|
||||
unit: 'ms',
|
||||
},
|
||||
water: {
|
||||
title: '喝水',
|
||||
unit: 'ml',
|
||||
addButton: '+ {{amount}}ml',
|
||||
},
|
||||
metabolism: {
|
||||
title: '基础代谢',
|
||||
loading: '加载中...',
|
||||
unit: '千卡/日',
|
||||
status: {
|
||||
high: '高代谢',
|
||||
normal: '正常',
|
||||
low: '偏低',
|
||||
veryLow: '较低',
|
||||
unknown: '未知',
|
||||
},
|
||||
},
|
||||
sleep: {
|
||||
title: '睡眠',
|
||||
loading: '加载中...',
|
||||
},
|
||||
oxygen: {
|
||||
title: '血氧饱和度',
|
||||
},
|
||||
circumference: {
|
||||
title: '围度 (cm)',
|
||||
setTitle: '设置{{label}}',
|
||||
confirm: '确认',
|
||||
measurements: {
|
||||
chest: '胸围',
|
||||
waist: '腰围',
|
||||
hip: '上臀围',
|
||||
arm: '臂围',
|
||||
thigh: '大腿围',
|
||||
calf: '小腿围',
|
||||
},
|
||||
},
|
||||
workout: {
|
||||
title: '近期锻炼',
|
||||
minutes: '分钟',
|
||||
kcal: '千卡',
|
||||
noData: '尚无锻炼数据',
|
||||
syncing: '等待同步',
|
||||
sourceWaiting: '来源:等待同步',
|
||||
sourceUnknown: '来源:未知',
|
||||
sourceFormat: '来源:{{source}}',
|
||||
sourceFormatMultiple: '来源:{{source}} 等',
|
||||
lastWorkout: '最近锻炼',
|
||||
updated: '更新',
|
||||
},
|
||||
weight: {
|
||||
title: '体重记录',
|
||||
addButton: '记录体重',
|
||||
bmi: 'BMI',
|
||||
weight: '体重',
|
||||
days: '天',
|
||||
range: '范围',
|
||||
unit: 'kg',
|
||||
bmiModal: {
|
||||
title: 'BMI 指数说明',
|
||||
description: 'BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标',
|
||||
formula: '计算公式:体重(kg) ÷ 身高²(m)',
|
||||
classificationTitle: 'BMI 分类标准',
|
||||
healthTipsTitle: '健康建议',
|
||||
tips: {
|
||||
nutrition: '保持均衡饮食,控制热量摄入',
|
||||
exercise: '每周至少150分钟中等强度运动',
|
||||
sleep: '保证7-9小时充足睡眠',
|
||||
monitoring: '定期监测体重变化,及时调整',
|
||||
},
|
||||
disclaimer: 'BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。',
|
||||
continueButton: '继续',
|
||||
},
|
||||
},
|
||||
fitnessRings: {
|
||||
title: '健身圆环',
|
||||
activeCalories: '活动卡路里',
|
||||
exerciseMinutes: '锻炼分钟',
|
||||
standHours: '站立小时',
|
||||
goal: '/{{goal}}',
|
||||
ringLabels: {
|
||||
active: '活动',
|
||||
exercise: '锻炼',
|
||||
stand: '站立',
|
||||
},
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
health: '健康',
|
||||
medications: '用药',
|
||||
fasting: '断食',
|
||||
challenges: '挑战',
|
||||
personal: '个人',
|
||||
},
|
||||
activityHeatMap: {
|
||||
subtitle: '最近6个月活跃 {{days}} 天',
|
||||
activeRate: '{{rate}}%',
|
||||
popover: {
|
||||
title: '能量值的积攒后续可以用来兑换 AI 相关权益',
|
||||
subtitle: '获取说明',
|
||||
rules: {
|
||||
login: '1. 每日登录获得能量值+1',
|
||||
mood: '2. 每日记录心情获得能量值+1',
|
||||
diet: '3. 记饮食获得能量值+1',
|
||||
goal: '4. 完成一次目标获得能量值+1',
|
||||
},
|
||||
},
|
||||
months: {
|
||||
1: '1月',
|
||||
2: '2月',
|
||||
3: '3月',
|
||||
4: '4月',
|
||||
5: '5月',
|
||||
6: '6月',
|
||||
7: '7月',
|
||||
8: '8月',
|
||||
9: '9月',
|
||||
10: '10月',
|
||||
11: '11月',
|
||||
12: '12月',
|
||||
},
|
||||
legend: {
|
||||
less: '少',
|
||||
more: '多',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sleepDetail = {
|
||||
title: '睡眠详情',
|
||||
loading: '加载睡眠数据中...',
|
||||
today: '今天',
|
||||
sleepScore: '睡眠评分',
|
||||
noData: '暂无睡眠数据',
|
||||
noDataRecommendation: '请确保在真实iOS设备上运行并授权访问健康数据,或等待有睡眠数据后再查看。',
|
||||
sleepDuration: '睡眠时长',
|
||||
sleepQuality: '睡眠质量',
|
||||
sleepStages: '睡眠阶段',
|
||||
learnMore: '了解更多',
|
||||
awake: '清醒',
|
||||
rem: '快速眼动',
|
||||
core: '核心睡眠',
|
||||
deep: '深度睡眠',
|
||||
unknown: '未知',
|
||||
rawData: '原始数据',
|
||||
rawDataDescription: '包含 {{count}} 条 HealthKit 睡眠样本记录',
|
||||
infoModalTitles: {
|
||||
sleepTime: '睡眠时间',
|
||||
sleepQuality: '睡眠质量',
|
||||
},
|
||||
sleepGrades: {
|
||||
low: '低',
|
||||
normal: '正常',
|
||||
good: '良好',
|
||||
excellent: '优秀',
|
||||
poor: '较差',
|
||||
fair: '一般',
|
||||
},
|
||||
sleepTimeDescription: '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。',
|
||||
sleepQualityDescription: '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长,还包括睡眠的连续性和各睡眠阶段的平衡。',
|
||||
sleepStagesInfo: {
|
||||
title: '了解你的睡眠阶段',
|
||||
description: '人们对睡眠阶段和睡眠质量有许多误解。有些人可能需要更多深度睡眠,其他人则不然。科学家和医生仍在探索不同睡眠阶段的作用及其对身体的影响。通过跟踪睡眠阶段并留意每天清晨的感受,你或许能深入了解自己的睡眠。',
|
||||
awake: {
|
||||
title: '清醒时间',
|
||||
description: '一次睡眠期间,你可能会醒来几次。偶尔醒来很正常。可能你会立刻再次入睡,并不记得曾在夜间醒来。',
|
||||
},
|
||||
rem: {
|
||||
title: '快速动眼睡眠',
|
||||
description: '这一睡眠阶段可能对学习和记忆产生一定影响。在此阶段,你的肌肉最为放松,眼球也会快速左右移动。这也是你大多数梦境出现的阶段。',
|
||||
},
|
||||
core: {
|
||||
title: '核心睡眠',
|
||||
description: '这一阶段有时也称为浅睡期,与其他阶段一样重要。此阶段通常占据你每晚大部分的睡眠时间。对于认知至关重要的脑电波会在这一阶段产生。',
|
||||
},
|
||||
deep: {
|
||||
title: '深度睡眠',
|
||||
description: '因为脑电波的特征,这一阶段也称为慢波睡眠。在此阶段,身体组织得到修复,并释放重要荷尔蒙。它通常出现在睡眠的前半段,且持续时间较长。深度睡眠期间,身体非常放松,因此相较于其他阶段,你可能更难在此阶段醒来。',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sleepQuality = {
|
||||
excellent: {
|
||||
description: '你身心愉悦并且精力充沛',
|
||||
recommendation: '恭喜你获得优质的睡眠!如果你感到精力充沛,可以考虑中等强度的运动,以维持健康的生活方式,并进一步减轻压力,以获得最佳睡眠。'
|
||||
},
|
||||
good: {
|
||||
description: '睡眠质量良好,精神状态不错',
|
||||
recommendation: '你的睡眠质量还不错,但还有改善空间。建议保持规律的睡眠时间,睡前避免使用电子设备,营造安静舒适的睡眠环境。'
|
||||
},
|
||||
fair: {
|
||||
description: '睡眠质量一般,可能影响日间表现',
|
||||
recommendation: '你的睡眠需要改善。建议制定固定的睡前例行程序,限制咖啡因摄入,确保卧室温度适宜,考虑进行轻度运动来改善睡眠质量。'
|
||||
},
|
||||
poor: {
|
||||
description: '睡眠质量较差,建议重视睡眠健康',
|
||||
recommendation: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。'
|
||||
}
|
||||
};
|
||||
|
||||
export const stepsDetail = {
|
||||
title: '步数详情',
|
||||
loading: '加载中...',
|
||||
stats: {
|
||||
totalSteps: '总步数',
|
||||
averagePerHour: '平均每小时',
|
||||
mostActiveTime: '最活跃时段',
|
||||
},
|
||||
chart: {
|
||||
title: '每小时步数分布',
|
||||
averageLabel: '平均 {{steps}}步',
|
||||
},
|
||||
activityLevel: {
|
||||
currentActivity: '你今天的活动量处于',
|
||||
levels: {
|
||||
inactive: '不怎么动',
|
||||
light: '轻度活跃',
|
||||
moderate: '中等活跃',
|
||||
very_active: '非常活跃',
|
||||
},
|
||||
progress: {
|
||||
current: '当前',
|
||||
nextLevel: '下一级: {{level}}',
|
||||
highestLevel: '已达最高级',
|
||||
},
|
||||
},
|
||||
timeLabels: {
|
||||
midnight: '0:00',
|
||||
noon: '12:00',
|
||||
nextDay: '24:00',
|
||||
},
|
||||
};
|
||||
|
||||
export const fitnessRingsDetail = {
|
||||
title: '健身圆环详情',
|
||||
loading: '加载中...',
|
||||
weekDays: {
|
||||
monday: '周一',
|
||||
tuesday: '周二',
|
||||
wednesday: '周三',
|
||||
thursday: '周四',
|
||||
friday: '周五',
|
||||
saturday: '周六',
|
||||
sunday: '周日',
|
||||
},
|
||||
cards: {
|
||||
activeCalories: {
|
||||
title: '活动热量',
|
||||
unit: '千卡',
|
||||
},
|
||||
exerciseMinutes: {
|
||||
title: '锻炼分钟数',
|
||||
unit: '分钟',
|
||||
info: {
|
||||
title: '锻炼分钟数:',
|
||||
description: '进行强度不低于"快走"的运动锻炼,就会积累对应时长的锻炼分钟数。',
|
||||
recommendation: '世卫组织推荐的成年人每天至少保持30分钟以上的中高强度运动。',
|
||||
knowButton: '知道了',
|
||||
},
|
||||
},
|
||||
standHours: {
|
||||
title: '活动小时数',
|
||||
unit: '小时',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
weeklyClosedRings: '周闭环天数',
|
||||
daysUnit: '天',
|
||||
},
|
||||
datePicker: {
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
errors: {
|
||||
loadExerciseInfoPreference: '加载锻炼分钟说明偏好失败',
|
||||
saveExerciseInfoPreference: '保存锻炼分钟说明偏好失败',
|
||||
},
|
||||
};
|
||||
|
||||
export const circumferenceDetail = {
|
||||
title: '围度统计',
|
||||
loading: '加载中...',
|
||||
error: '加载失败',
|
||||
retry: '重试',
|
||||
noData: '暂无数据',
|
||||
noDataSelected: '请选择要显示的围度数据',
|
||||
tabs: {
|
||||
week: '按周',
|
||||
month: '按月',
|
||||
year: '按年',
|
||||
},
|
||||
measurements: {
|
||||
chest: '胸围',
|
||||
waist: '腰围',
|
||||
upperHip: '上臀围',
|
||||
arm: '臂围',
|
||||
thigh: '大腿围',
|
||||
calf: '小腿围',
|
||||
},
|
||||
modal: {
|
||||
title: '设置{{label}}',
|
||||
defaultTitle: '设置围度',
|
||||
confirm: '确认',
|
||||
},
|
||||
chart: {
|
||||
weekLabel: '第{{week}}周',
|
||||
monthLabel: '{{month}}月',
|
||||
empty: '暂无数据',
|
||||
noSelection: '请选择要显示的围度数据',
|
||||
},
|
||||
};
|
||||
|
||||
export const basalMetabolismDetail = {
|
||||
title: '基础代谢',
|
||||
currentData: {
|
||||
title: '{{date}} 基础代谢',
|
||||
unit: '千卡',
|
||||
normalRange: '正常范围: {{min}}-{{max}} 千卡',
|
||||
noData: '--',
|
||||
},
|
||||
stats: {
|
||||
title: '基础代谢统计',
|
||||
tabs: {
|
||||
week: '按周',
|
||||
month: '按月',
|
||||
},
|
||||
},
|
||||
chart: {
|
||||
loading: '加载中...',
|
||||
loadingText: '加载中...',
|
||||
error: {
|
||||
text: '加载失败: {{error}}',
|
||||
retry: '重试',
|
||||
fetchFailed: '获取数据失败',
|
||||
},
|
||||
empty: '暂无数据',
|
||||
yAxisSuffix: '千卡',
|
||||
weekLabel: '第{{week}}周',
|
||||
},
|
||||
modal: {
|
||||
title: '基础代谢',
|
||||
closeButton: '×',
|
||||
description: '基础代谢,也称基础代谢率(BMR),是指人体在完全静息状态下维持基本生命功能(心跳、呼吸、体温调节等)所需的最低能量消耗,通常以卡路里为单位。',
|
||||
sections: {
|
||||
importance: {
|
||||
title: '为什么重要?',
|
||||
content: '基础代谢占总能量消耗的60-75%,是能量平衡的基础。了解您的基础代谢有助于制定科学的营养计划、优化体重管理策略,以及评估代谢健康状态。',
|
||||
},
|
||||
normalRange: {
|
||||
title: '正常范围',
|
||||
formulas: {
|
||||
male: '男性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5',
|
||||
female: '女性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161',
|
||||
},
|
||||
userRange: '您的正常区间:{{min}}-{{max}}千卡/天',
|
||||
rangeNote: '(在公式基础计算值上下浮动15%都属于正常范围)',
|
||||
userInfo: '基于您的信息:{{gender}},{{age}}岁,{{height}}cm,{{weight}}kg',
|
||||
incompleteInfo: '请完善基本信息以计算您的代谢率',
|
||||
},
|
||||
strategies: {
|
||||
title: '提高代谢率的策略',
|
||||
subtitle: '科学研究支持以下方法:',
|
||||
items: [
|
||||
'1.增加肌肉量 (每周2-3次力量训练)',
|
||||
'2.高强度间歇训练 (HIIT)',
|
||||
'3.充分蛋白质摄入 (体重每公斤1.6-2.2g)',
|
||||
'4.保证充足睡眠 (7-9小时/晚)',
|
||||
'5.避免过度热量限制 (不低于BMR的80%)',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
gender: {
|
||||
male: '男性',
|
||||
female: '女性',
|
||||
},
|
||||
comments: {
|
||||
reloadData: '重新加载数据',
|
||||
},
|
||||
};
|
||||
|
||||
export const workoutHistory = {
|
||||
title: '锻炼总结',
|
||||
loading: '正在加载锻炼记录...',
|
||||
error: {
|
||||
permissionDenied: '尚未授予健康数据权限',
|
||||
loadFailed: '加载锻炼记录失败,请稍后再试',
|
||||
detailLoadFailed: '加载锻炼详情失败,请稍后再试',
|
||||
},
|
||||
retry: '重试',
|
||||
monthlyStats: {
|
||||
title: '锻炼时间',
|
||||
periodText: '统计周期:1日 - {{day}}日(本月)',
|
||||
overviewWithStats: '截至{{date}},你已完成{{count}}次锻炼,累计{{duration}}。',
|
||||
overviewEmpty: '本月还没有锻炼记录,动起来收集第一条吧!',
|
||||
emptyData: '本月还没有锻炼数据',
|
||||
},
|
||||
intensity: {
|
||||
low: '低强度',
|
||||
medium: '中强度',
|
||||
high: '高强度',
|
||||
},
|
||||
historyCard: {
|
||||
calories: '{{calories}}千卡 · {{minutes}}分钟',
|
||||
activityTime: '{{activity}},{{time}}',
|
||||
},
|
||||
empty: {
|
||||
title: '暂无锻炼记录',
|
||||
subtitle: '完成一次锻炼后即可在此查看详细历史',
|
||||
},
|
||||
monthOccurrence: '这是你{{month}}的第 {{index}} 次{{activity}}。',
|
||||
};
|
||||
17
i18n/zh/index.ts
Normal file
17
i18n/zh/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as Challenge from './challenge';
|
||||
import * as Common from './common';
|
||||
import * as Diet from './diet';
|
||||
import * as Health from './health';
|
||||
import * as Medication from './medication';
|
||||
import * as Personal from './personal';
|
||||
import * as Weight from './weight';
|
||||
|
||||
export default {
|
||||
...Personal,
|
||||
...Health,
|
||||
...Diet,
|
||||
...Medication,
|
||||
...Weight,
|
||||
...Challenge,
|
||||
...Common,
|
||||
};
|
||||
472
i18n/zh/medication.ts
Normal file
472
i18n/zh/medication.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
export const medications = {
|
||||
greeting: '你好,{{name}}',
|
||||
welcome: '欢迎来到用药助手!',
|
||||
todayMedications: '今日用药',
|
||||
filters: {
|
||||
all: '全部',
|
||||
taken: '已服用',
|
||||
missed: '未服用',
|
||||
},
|
||||
emptyState: {
|
||||
title: '今日暂无用药安排',
|
||||
subtitle: '还未添加任何用药计划,快来补充吧。',
|
||||
},
|
||||
stack: {
|
||||
completed: '已完成 ({{count}})',
|
||||
},
|
||||
dateFormats: {
|
||||
today: '今天,{{date}}',
|
||||
other: '{{date}}',
|
||||
},
|
||||
// MedicationCard 组件翻译
|
||||
card: {
|
||||
status: {
|
||||
missed: '已错过',
|
||||
timeToTake: '到服药时间',
|
||||
remaining: '剩余 {{time}}',
|
||||
},
|
||||
action: {
|
||||
takeNow: '立即服用',
|
||||
taken: '已服用',
|
||||
skipped: '已跳过',
|
||||
skip: '跳过',
|
||||
submitting: '提交中...',
|
||||
},
|
||||
skipAlert: {
|
||||
title: '确认跳过',
|
||||
message: '确定要跳过本次用药吗?\n\n跳过后将不会记录为已服用。',
|
||||
cancel: '取消',
|
||||
confirm: '确认跳过',
|
||||
},
|
||||
earlyTakeAlert: {
|
||||
title: '尚未到服药时间',
|
||||
message: '该用药计划在 {{time}},现在还早于1小时以上。\n\n是否确认已服用此药物?',
|
||||
cancel: '取消',
|
||||
confirm: '确认已服用',
|
||||
},
|
||||
takeError: {
|
||||
title: '操作失败',
|
||||
message: '记录服药时发生错误,请稍后重试',
|
||||
confirm: '确定',
|
||||
},
|
||||
skipError: {
|
||||
title: '操作失败',
|
||||
message: '跳过操作失败,请稍后重试',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
// 添加药物页面翻译
|
||||
add: {
|
||||
title: '添加药物',
|
||||
steps: {
|
||||
name: '药品名称',
|
||||
dosage: '剂型与剂量',
|
||||
frequency: '服药频率',
|
||||
time: '服药时间',
|
||||
note: '备注',
|
||||
},
|
||||
descriptions: {
|
||||
name: '为药物命名并上传包装照片,方便识别',
|
||||
dosage: '选择药片类型并填写每次的用药剂量',
|
||||
frequency: '设置用药频率以及每日次数',
|
||||
time: '添加并管理每天的提醒时间',
|
||||
note: '填写备注或医生叮嘱(可选)',
|
||||
},
|
||||
name: {
|
||||
placeholder: '输入或搜索药品名称',
|
||||
},
|
||||
photo: {
|
||||
title: '上传药品图片',
|
||||
subtitle: '拍照或从相册选择,辅助识别药品包装',
|
||||
selectTitle: '选择图片',
|
||||
selectMessage: '请选择图片来源',
|
||||
camera: '拍照',
|
||||
album: '从相册选择',
|
||||
cancel: '取消',
|
||||
retake: '重新选择',
|
||||
uploading: '上传中…',
|
||||
uploadingText: '正在上传',
|
||||
remove: '删除',
|
||||
cameraPermission: '需要相机权限以拍摄药品照片',
|
||||
albumPermission: '需要相册权限以选择药品照片',
|
||||
uploadFailed: '上传失败',
|
||||
uploadFailedMessage: '图片上传失败,请稍后重试',
|
||||
cameraFailed: '拍照失败',
|
||||
cameraFailedMessage: '无法打开相机,请稍后再试',
|
||||
selectFailed: '选择失败',
|
||||
selectFailedMessage: '无法打开相册,请稍后再试',
|
||||
},
|
||||
dosage: {
|
||||
label: '每次剂量',
|
||||
placeholder: '0.5',
|
||||
type: '类型',
|
||||
unitSelector: '选择剂量单位',
|
||||
},
|
||||
frequency: {
|
||||
label: '每日次数',
|
||||
value: '{{count}} 次/日',
|
||||
period: '用药周期',
|
||||
start: '开始',
|
||||
end: '结束',
|
||||
longTerm: '长期',
|
||||
startDateInvalid: '日期无效',
|
||||
startDateInvalidMessage: '开始日期不能早于今天',
|
||||
endDateInvalid: '日期无效',
|
||||
endDateInvalidMessage: '结束日期不能早于开始日期',
|
||||
},
|
||||
time: {
|
||||
label: '每日提醒时间',
|
||||
addTime: '添加时间',
|
||||
editTime: '修改提醒时间',
|
||||
addTimeButton: '添加时间',
|
||||
},
|
||||
note: {
|
||||
label: '备注',
|
||||
placeholder: '记录注意事项、医生叮嘱或自定义提醒',
|
||||
voiceNotSupported: '当前设备暂不支持语音转文字,可直接输入备注',
|
||||
voiceError: '语音识别不可用',
|
||||
voiceErrorMessage: '无法使用语音输入,请检查权限设置后重试',
|
||||
voiceStartError: '无法启动语音输入',
|
||||
voiceStartErrorMessage: '请检查麦克风与语音识别权限后重试',
|
||||
},
|
||||
actions: {
|
||||
previous: '上一步',
|
||||
next: '下一步',
|
||||
complete: '完成',
|
||||
},
|
||||
success: {
|
||||
title: '添加成功',
|
||||
message: '已成功添加药物"{{name}}"',
|
||||
confirm: '确定',
|
||||
},
|
||||
error: {
|
||||
title: '添加失败',
|
||||
message: '创建药物时发生错误,请稍后重试',
|
||||
confirm: '确定',
|
||||
},
|
||||
datePickers: {
|
||||
startDate: '选择开始日期',
|
||||
endDate: '选择结束日期',
|
||||
time: '选择时间',
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
pickers: {
|
||||
timesPerDay: '选择每日次数',
|
||||
dosageUnit: '选择剂量单位',
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
// 药物管理页面翻译
|
||||
manage: {
|
||||
title: '药品管理',
|
||||
subtitle: '管理所有药品的状态与提醒',
|
||||
filters: {
|
||||
all: '全部',
|
||||
active: '进行中',
|
||||
inactive: '已停用',
|
||||
},
|
||||
loading: '正在载入药品信息...',
|
||||
empty: {
|
||||
title: '暂无药品',
|
||||
subtitle: '还没有相关药品记录,点击右上角添加',
|
||||
},
|
||||
deactivate: {
|
||||
title: '停用 {{name}}?',
|
||||
description: '停用后,当天已生成的用药计划会一并删除,且无法恢复。',
|
||||
confirm: '确认停用',
|
||||
cancel: '取消',
|
||||
error: {
|
||||
title: '操作失败',
|
||||
message: '停用药物时发生问题,请稍后重试。',
|
||||
},
|
||||
},
|
||||
toggleError: {
|
||||
title: '操作失败',
|
||||
message: '切换药物状态时发生问题,请稍后重试。',
|
||||
},
|
||||
formLabels: {
|
||||
capsule: '胶囊',
|
||||
pill: '药片',
|
||||
tablet: '药片',
|
||||
injection: '注射',
|
||||
spray: '喷雾',
|
||||
drop: '滴剂',
|
||||
syrup: '糖浆',
|
||||
other: '其他',
|
||||
ointment: '软膏',
|
||||
},
|
||||
frequency: {
|
||||
daily: '每日',
|
||||
weekly: '每周',
|
||||
custom: '自定义',
|
||||
},
|
||||
cardMeta: '开始于 {{date}} | 提醒:{{reminder}}',
|
||||
reminderNotSet: '尚未设置',
|
||||
unknownDate: '未知日期',
|
||||
},
|
||||
// 药物详情页面翻译
|
||||
detail: {
|
||||
title: '药品详情',
|
||||
notFound: {
|
||||
title: '未找到药品信息',
|
||||
subtitle: '请从用药列表重新进入此页面。',
|
||||
},
|
||||
loading: '正在载入...',
|
||||
error: {
|
||||
title: '暂时无法获取该药品的信息,请稍后重试。',
|
||||
subtitle: '请检查网络后重试,或返回上一页。',
|
||||
},
|
||||
sections: {
|
||||
plan: '服药计划',
|
||||
dosage: '剂量与形式',
|
||||
note: '备注',
|
||||
overview: '服药概览',
|
||||
aiAnalysis: 'AI 用药分析',
|
||||
},
|
||||
plan: {
|
||||
period: '服药周期',
|
||||
time: '用药时间',
|
||||
frequency: '频率',
|
||||
expiryDate: '药品有效期',
|
||||
longTerm: '长期',
|
||||
periodMessage: '开始服药日期:{{startDate}}\n{{endDateInfo}}',
|
||||
longTermPlan: '服药计划:长期服药',
|
||||
timeMessage: '设置的时间:{{times}}',
|
||||
dateFormat: 'YYYY年M月D日',
|
||||
periodRange: '从 {{startDate}} 至 {{endDate}}',
|
||||
periodLongTerm: '从 {{startDate}} 至长期',
|
||||
expiryStatus: {
|
||||
notSet: '未设置',
|
||||
expired: '已过期',
|
||||
expiresToday: '今天到期',
|
||||
expiresInDays: '{{days}}天后到期',
|
||||
},
|
||||
},
|
||||
dosage: {
|
||||
label: '每次剂量',
|
||||
form: '剂型',
|
||||
selectDosage: '选择剂量',
|
||||
selectForm: '选择剂型',
|
||||
dosageValue: '剂量值',
|
||||
unit: '单位',
|
||||
},
|
||||
note: {
|
||||
label: '药品备注',
|
||||
placeholder: '记录注意事项、医生叮嘱或自定义提醒',
|
||||
edit: '编辑备注',
|
||||
noNote: '暂无备注信息',
|
||||
voiceNotSupported: '当前设备暂不支持语音转文字,可直接输入备注',
|
||||
save: '保存',
|
||||
saveError: {
|
||||
title: '保存失败',
|
||||
message: '提交备注时出现问题,请稍后重试。',
|
||||
},
|
||||
},
|
||||
overview: {
|
||||
calculating: '统计中...',
|
||||
takenCount: '累计服药 {{count}} 次',
|
||||
calculatingDays: '正在计算坚持天数',
|
||||
startedDays: '已坚持 {{days}} 天',
|
||||
startDate: '开始于 {{date}}',
|
||||
noStartDate: '暂无开始日期',
|
||||
},
|
||||
aiAnalysis: {
|
||||
analyzing: '正在分析用药信息...',
|
||||
analyzingButton: '分析中...',
|
||||
reanalyzeButton: '重新分析',
|
||||
getAnalysisButton: '获取 AI 分析',
|
||||
button: 'AI 分析',
|
||||
status: {
|
||||
generated: '已生成',
|
||||
memberExclusive: '会员专享',
|
||||
pending: '待生成',
|
||||
},
|
||||
title: '分析结果',
|
||||
recommendation: 'AI 推荐',
|
||||
placeholder: '获取 AI 分析,快速了解适用人群、成分安全与使用建议。',
|
||||
categories: {
|
||||
suitableFor: '适合人群',
|
||||
unsuitableFor: '不适合人群',
|
||||
sideEffects: '可能的副作用',
|
||||
storageAdvice: '储存建议',
|
||||
healthAdvice: '健康/使用建议',
|
||||
},
|
||||
membershipCard: {
|
||||
title: '会员专享 AI 深度解读',
|
||||
subtitle: '解锁完整药品分析与无限次使用',
|
||||
},
|
||||
error: {
|
||||
title: '分析失败',
|
||||
message: 'AI 分析失败,请稍后重试',
|
||||
networkError: '发起分析请求失败,请检查网络连接',
|
||||
unauthorized: '请先登录',
|
||||
forbidden: '无权访问此药物',
|
||||
notFound: '药物不存在',
|
||||
},
|
||||
},
|
||||
aiDraft: {
|
||||
reshoot: '重新拍摄',
|
||||
saveAndCreate: '保存并创建',
|
||||
saveError: {
|
||||
title: '保存失败',
|
||||
message: '创建药物时发生错误,请稍后重试',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
enabled: '提醒已开启',
|
||||
disabled: '提醒已关闭',
|
||||
},
|
||||
delete: {
|
||||
title: '删除 {{name}}?',
|
||||
description: '删除后将清除与该药品相关的提醒与历史记录,且无法恢复。',
|
||||
confirm: '删除',
|
||||
cancel: '取消',
|
||||
error: {
|
||||
title: '删除失败',
|
||||
message: '移除该药品时出现问题,请稍后再试。',
|
||||
},
|
||||
},
|
||||
deactivate: {
|
||||
title: '停用 {{name}}?',
|
||||
description: '停用后,当天已生成的用药计划会一并删除,且无法恢复。',
|
||||
confirm: '确认停用',
|
||||
cancel: '取消',
|
||||
error: {
|
||||
title: '操作失败',
|
||||
message: '停用药物时发生问题,请稍后重试。',
|
||||
},
|
||||
},
|
||||
toggleError: {
|
||||
title: '操作失败',
|
||||
message: '切换提醒状态时出现问题,请稍后重试。',
|
||||
},
|
||||
updateErrors: {
|
||||
dosage: '更新失败',
|
||||
dosageMessage: '更新剂量时出现问题,请稍后重试。',
|
||||
form: '更新失败',
|
||||
formMessage: '更新剂型时出现问题,请稍后重试。',
|
||||
expiryDate: '更新失败',
|
||||
expiryDateMessage: '有效期更新失败,请稍后重试',
|
||||
},
|
||||
imageViewer: {
|
||||
close: '关闭',
|
||||
},
|
||||
pickers: {
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
// 编辑频率页面翻译
|
||||
editFrequency: {
|
||||
title: '编辑服药频率',
|
||||
missingParams: '缺少必要参数',
|
||||
medicationName: '正在编辑:{{name}}',
|
||||
sections: {
|
||||
frequency: '服药频率',
|
||||
frequencyDescription: '设置每日服药次数',
|
||||
time: '每日提醒时间',
|
||||
timeDescription: '添加并管理每天的提醒时间',
|
||||
},
|
||||
frequency: {
|
||||
repeatPattern: '重复模式',
|
||||
timesPerDay: '每日次数',
|
||||
daily: '每日',
|
||||
weekly: '每周',
|
||||
custom: '自定义',
|
||||
timesLabel: '{{count}} 次',
|
||||
summary: '{{pattern}} {{count}} 次',
|
||||
},
|
||||
time: {
|
||||
addTime: '添加时间',
|
||||
editTime: '修改提醒时间',
|
||||
addTimeButton: '添加时间',
|
||||
},
|
||||
actions: {
|
||||
save: '保存修改',
|
||||
},
|
||||
error: {
|
||||
title: '更新失败',
|
||||
message: '更新服药频率时出现问题,请稍后重试。',
|
||||
},
|
||||
pickers: {
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
aiProgress: {
|
||||
title: '识别中',
|
||||
steps: {
|
||||
analyzing_product: '正在进行产品分析...',
|
||||
analyzing_suitability: '正在检测适宜人群...',
|
||||
analyzing_ingredients: '正在评估成分信息...',
|
||||
analyzing_effects: '正在生成安全建议...',
|
||||
completed: '识别完成,正在载入详情...',
|
||||
},
|
||||
errors: {
|
||||
default: '识别失败,请重新拍摄',
|
||||
queryFailed: '查询失败,请稍后再试',
|
||||
},
|
||||
modal: {
|
||||
title: '需要重新拍摄',
|
||||
retry: '重新拍摄',
|
||||
},
|
||||
},
|
||||
aiCamera: {
|
||||
title: 'AI 用药识别',
|
||||
steps: {
|
||||
front: {
|
||||
title: '正面',
|
||||
subtitle: '保证药品名称清晰可见',
|
||||
},
|
||||
side: {
|
||||
title: '背面',
|
||||
subtitle: '包含规格、成分等信息',
|
||||
},
|
||||
aux: {
|
||||
title: '侧面',
|
||||
subtitle: '补充更多细节提升准确率',
|
||||
},
|
||||
stepProgress: '步骤 {{current}} / {{total}}',
|
||||
optional: '(可选)',
|
||||
notTaken: '未拍摄',
|
||||
},
|
||||
buttons: {
|
||||
flip: '翻转',
|
||||
capture: '拍照',
|
||||
complete: '完成',
|
||||
album: '从相册',
|
||||
},
|
||||
permission: {
|
||||
title: '需要相机权限',
|
||||
description: '授权后即可快速拍摄药品包装,自动识别信息',
|
||||
button: '授权访问相机',
|
||||
},
|
||||
alerts: {
|
||||
pickFailed: {
|
||||
title: '选择失败',
|
||||
message: '请重试或更换图片',
|
||||
},
|
||||
captureFailed: {
|
||||
title: '拍摄失败',
|
||||
message: '请重试',
|
||||
},
|
||||
insufficientPhotos: {
|
||||
title: '照片不足',
|
||||
message: '请至少完成正面和背面拍摄',
|
||||
},
|
||||
taskFailed: {
|
||||
title: '创建任务失败',
|
||||
defaultMessage: '请检查网络后重试',
|
||||
},
|
||||
},
|
||||
guideModal: {
|
||||
badge: '规范',
|
||||
title: '拍摄图片清晰',
|
||||
description1: '请拍摄药品正面\\背面的产品名称\\说明部分。',
|
||||
description2: '注意拍摄时光线充分,没有反光,文字部分清晰可见。照片的清晰度会影响识别的准确率。',
|
||||
button: '知道了!',
|
||||
},
|
||||
},
|
||||
};
|
||||
408
i18n/zh/personal.ts
Normal file
408
i18n/zh/personal.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
export const personal = {
|
||||
edit: '编辑',
|
||||
login: '登录',
|
||||
memberNumber: '会员编号: {{number}}',
|
||||
aiUsage: '免费AI次数: {{value}}',
|
||||
aiUsageUnlimited: '无限',
|
||||
fishRecord: '能量记录',
|
||||
badgesPreview: {
|
||||
title: '我的勋章',
|
||||
subtitle: '记录你的荣耀时刻',
|
||||
cta: '查看全部',
|
||||
loading: '正在同步勋章...',
|
||||
empty: '完成睡眠或挑战任务即可解锁首枚勋章',
|
||||
lockedHint: '坚持训练即可点亮更多勋章',
|
||||
},
|
||||
stats: {
|
||||
height: '身高',
|
||||
weight: '体重',
|
||||
age: '年龄',
|
||||
ageSuffix: '岁',
|
||||
},
|
||||
membership: {
|
||||
badge: '尊享会员',
|
||||
planFallback: 'VIP 会员',
|
||||
expiryLabel: '会员有效期',
|
||||
changeButton: '更改会员套餐',
|
||||
validForever: '长期有效',
|
||||
dateFormat: 'YYYY年MM月DD日',
|
||||
},
|
||||
sections: {
|
||||
notifications: '通知',
|
||||
developer: '开发者',
|
||||
other: '其他',
|
||||
account: '账号与安全',
|
||||
language: '语言',
|
||||
healthData: '健康数据授权',
|
||||
medicalSources: '医学建议来源',
|
||||
customization: '个性化',
|
||||
},
|
||||
menu: {
|
||||
notificationSettings: '通知设置',
|
||||
developerOptions: '开发者选项',
|
||||
pushSettings: '推送通知设置',
|
||||
privacyPolicy: '隐私政策',
|
||||
feedback: '意见反馈',
|
||||
userAgreement: '用户协议',
|
||||
logout: '退出登录',
|
||||
deleteAccount: '注销帐号',
|
||||
healthDataPermissions: '健康数据授权说明',
|
||||
whoSource: '世界卫生组织 (WHO)',
|
||||
tabBarConfig: '底部栏配置',
|
||||
},
|
||||
language: {
|
||||
title: '语言',
|
||||
menuTitle: '界面语言',
|
||||
modalTitle: '选择语言',
|
||||
modalSubtitle: '选择后界面会立即更新',
|
||||
cancel: '取消',
|
||||
options: {
|
||||
zh: {
|
||||
label: '中文',
|
||||
description: '推荐中文用户使用',
|
||||
},
|
||||
en: {
|
||||
label: '英文',
|
||||
description: '使用英文界面',
|
||||
},
|
||||
},
|
||||
},
|
||||
tabBarConfig: {
|
||||
title: '底部栏配置',
|
||||
subtitle: '自定义你的底部导航栏',
|
||||
description: '使用开关控制标签的显示和隐藏',
|
||||
resetButton: '恢复默认',
|
||||
cannotDisable: '此标签不可关闭',
|
||||
resetConfirm: {
|
||||
title: '恢复默认设置?',
|
||||
message: '将重置所有底部栏配置和显示状态',
|
||||
cancel: '取消',
|
||||
confirm: '确认恢复',
|
||||
},
|
||||
resetSuccess: '已恢复默认设置',
|
||||
},
|
||||
};
|
||||
|
||||
export const editProfile = {
|
||||
title: '编辑资料',
|
||||
fields: {
|
||||
name: '昵称',
|
||||
gender: '性别',
|
||||
height: '身高',
|
||||
weight: '体重',
|
||||
activityLevel: '活动水平',
|
||||
birthDate: '出生日期',
|
||||
maxHeartRate: '最大心率',
|
||||
},
|
||||
gender: {
|
||||
male: '男',
|
||||
female: '女',
|
||||
notSet: '未设置',
|
||||
},
|
||||
height: {
|
||||
unit: '厘米',
|
||||
placeholder: '170厘米',
|
||||
},
|
||||
weight: {
|
||||
unit: '公斤',
|
||||
placeholder: '55公斤',
|
||||
},
|
||||
activityLevels: {
|
||||
1: '久坐',
|
||||
2: '轻度活跃',
|
||||
3: '中度活跃',
|
||||
4: '非常活跃',
|
||||
descriptions: {
|
||||
1: '很少运动',
|
||||
2: '每周1-3次运动',
|
||||
3: '每周3-5次运动',
|
||||
4: '每周6-7次运动',
|
||||
},
|
||||
},
|
||||
birthDate: {
|
||||
placeholder: '1995年1月1日',
|
||||
format: '{{year}}年{{month}}月{{day}}日',
|
||||
},
|
||||
maxHeartRate: {
|
||||
unit: '次/分钟',
|
||||
notAvailable: '未获取',
|
||||
alert: {
|
||||
title: '提示',
|
||||
message: '最大心率数据从健康应用自动获取',
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
notLoggedIn: {
|
||||
title: '未登录',
|
||||
message: '请先登录后再尝试保存',
|
||||
},
|
||||
saveFailed: {
|
||||
title: '保存失败',
|
||||
message: '请稍后重试',
|
||||
},
|
||||
avatarPermissions: {
|
||||
title: '权限不足',
|
||||
message: '需要相册权限以选择头像',
|
||||
},
|
||||
avatarUploadFailed: {
|
||||
title: '上传失败',
|
||||
message: '头像上传失败,请重试',
|
||||
},
|
||||
avatarError: {
|
||||
title: '发生错误',
|
||||
message: '选择头像失败,请重试',
|
||||
},
|
||||
avatarSuccess: {
|
||||
title: '成功',
|
||||
message: '头像更新成功',
|
||||
},
|
||||
},
|
||||
modals: {
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
save: '保存',
|
||||
input: {
|
||||
namePlaceholder: '输入昵称',
|
||||
weightPlaceholder: '输入体重',
|
||||
weightUnit: '公斤 (kg)',
|
||||
},
|
||||
selectHeight: '选择身高',
|
||||
selectGender: '选择性别',
|
||||
selectActivityLevel: '选择活动水平',
|
||||
female: '女性',
|
||||
male: '男性',
|
||||
},
|
||||
defaultValues: {
|
||||
name: '今晚要吃肉',
|
||||
height: 170,
|
||||
weight: 55,
|
||||
birthDate: '1995-01-01',
|
||||
activityLevel: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const login = {
|
||||
title: '登录',
|
||||
subtitle: '健康生活,自律让我更自由',
|
||||
appleLogin: '使用 Apple 登录',
|
||||
loggingIn: '登录中...',
|
||||
agreement: {
|
||||
readAndAgree: '我已阅读并同意',
|
||||
privacyPolicy: '《隐私政策》',
|
||||
and: '和',
|
||||
userAgreement: '《用户协议》',
|
||||
alert: {
|
||||
title: '请先阅读并同意',
|
||||
message: '继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。',
|
||||
cancel: '取消',
|
||||
confirm: '同意并继续',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
appleIdentityTokenMissing: '未获取到 Apple 身份令牌',
|
||||
loginFailed: '登录失败,请稍后再试',
|
||||
loginFailedTitle: '登录失败',
|
||||
},
|
||||
success: {
|
||||
loginSuccess: '登录成功',
|
||||
},
|
||||
};
|
||||
|
||||
export const authGuard = {
|
||||
logout: {
|
||||
error: '退出登录失败',
|
||||
errorMessage: '退出登录失败,请稍后重试',
|
||||
},
|
||||
confirmLogout: {
|
||||
title: '确认退出',
|
||||
message: '确定要退出当前账号吗?',
|
||||
cancelButton: '取消',
|
||||
confirmButton: '确定',
|
||||
},
|
||||
deleteAccount: {
|
||||
successTitle: '账号已注销',
|
||||
successMessage: '您的账号已成功注销',
|
||||
confirmButton: '确定',
|
||||
errorTitle: '注销失败',
|
||||
errorMessage: '注销失败,请稍后重试',
|
||||
},
|
||||
confirmDeleteAccount: {
|
||||
title: '确认注销账号',
|
||||
message: '此操作不可恢复,将删除您的账号及相关数据。确定继续吗?',
|
||||
cancelButton: '取消',
|
||||
confirmButton: '确认注销',
|
||||
},
|
||||
};
|
||||
|
||||
export const membershipModal = {
|
||||
plans: {
|
||||
lifetime: {
|
||||
title: '终身会员',
|
||||
subtitle: '终身陪伴,见证您的每一次健康蜕变',
|
||||
},
|
||||
quarterly: {
|
||||
title: '季度会员',
|
||||
subtitle: '3个月科学计划,让健康成为生活习惯',
|
||||
},
|
||||
weekly: {
|
||||
title: '周会员',
|
||||
subtitle: '7天体验期,感受专业健康指导的力量',
|
||||
},
|
||||
unknown: '未知套餐',
|
||||
tag: '超值推荐',
|
||||
},
|
||||
benefits: {
|
||||
title: '权益对比',
|
||||
subtitle: '核心权益一目了然,选择更安心',
|
||||
table: {
|
||||
benefit: '权益',
|
||||
vip: 'VIP',
|
||||
regular: '普通用户',
|
||||
},
|
||||
items: {
|
||||
aiCalories: {
|
||||
title: 'AI拍照记录热量',
|
||||
description: '通过拍照识别食物并自动记录热量',
|
||||
},
|
||||
aiNutrition: {
|
||||
title: 'AI拍照识别包装',
|
||||
description: '识别食品包装上的营养成分信息',
|
||||
},
|
||||
healthReminder: {
|
||||
title: '每日健康提醒',
|
||||
description: '根据个人目标提供个性化健康提醒',
|
||||
},
|
||||
aiMedication: {
|
||||
title: 'AI 智能用药管家',
|
||||
description: '深度解析用药禁忌,生成专属服药计划,科学守护健康每一刻',
|
||||
},
|
||||
customChallenge: {
|
||||
title: '解锁无限自定义挑战',
|
||||
description: '突破限制,邀请挚友同行,让坚持不再孤单,共同见证蜕变',
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
unlimited: '无限次使用',
|
||||
limited: '有限次使用',
|
||||
dailyLimit: '每日{{count}}次',
|
||||
fullSupport: '完全支持',
|
||||
basicSupport: '基础提醒',
|
||||
smartReminder: '智能提醒',
|
||||
fullAnalysis: '深度分析',
|
||||
createUnlimited: '无限创建',
|
||||
notSupported: '不支持',
|
||||
},
|
||||
},
|
||||
sectionTitle: {
|
||||
plans: '会员套餐',
|
||||
plansSubtitle: '灵活选择,跟随节奏稳步提升',
|
||||
},
|
||||
actions: {
|
||||
subscribe: '立即订阅',
|
||||
processing: '正在处理购买...',
|
||||
restore: '恢复购买',
|
||||
restoring: '恢复中...',
|
||||
back: '返回',
|
||||
close: '关闭会员购买弹窗',
|
||||
selectPlan: '选择{{plan}}套餐',
|
||||
purchaseHint: '点击购买{{plan}}会员套餐',
|
||||
},
|
||||
agreements: {
|
||||
prefix: '开通即视为同意',
|
||||
userAgreement: '《用户协议》',
|
||||
membershipAgreement: '《会员协议》',
|
||||
autoRenewalAgreement: '《自动续费协议》',
|
||||
alert: {
|
||||
title: '请阅读并同意相关协议',
|
||||
message: '购买前需要同意用户协议、会员协议和自动续费协议',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noProducts: '暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。',
|
||||
purchaseCancelled: '购买已取消',
|
||||
alreadyPurchased: '您已拥有此商品',
|
||||
networkError: '网络连接失败',
|
||||
paymentPending: '支付正在处理中',
|
||||
invalidCredentials: '账户验证失败',
|
||||
purchaseFailed: '购买失败',
|
||||
restoreSuccess: '恢复购买成功',
|
||||
restoreFailed: '恢复购买失败',
|
||||
restoreCancelled: '恢复购买已取消',
|
||||
restorePartialFailed: '恢复购买部分失败',
|
||||
noPurchasesFound: '没有找到购买记录',
|
||||
selectPlan: '请选择会员套餐',
|
||||
},
|
||||
loading: {
|
||||
products: '正在加载会员套餐,请稍候',
|
||||
purchase: '购买正在进行中,请稍候',
|
||||
},
|
||||
success: {
|
||||
purchase: '会员开通成功',
|
||||
},
|
||||
};
|
||||
|
||||
export const notificationSettings = {
|
||||
title: '通知设置',
|
||||
loading: '加载中...',
|
||||
sections: {
|
||||
notifications: '通知设置',
|
||||
medicationReminder: '药品提醒',
|
||||
nutritionReminder: '营养提醒',
|
||||
moodReminder: '心情提醒',
|
||||
description: '说明',
|
||||
},
|
||||
items: {
|
||||
pushNotifications: {
|
||||
title: '消息推送',
|
||||
description: '开启后将接收应用通知',
|
||||
},
|
||||
medicationReminder: {
|
||||
title: '药品通知提醒',
|
||||
description: '在用药时间接收提醒通知',
|
||||
},
|
||||
nutritionReminder: {
|
||||
title: '营养记录提醒',
|
||||
description: '在用餐时间接收营养记录提醒',
|
||||
},
|
||||
moodReminder: {
|
||||
title: '心情记录提醒',
|
||||
description: '在晚间接收心情记录提醒',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
text: '• 消息推送是所有通知的总开关\n• 各类提醒需要在消息推送开启后才能使用\n• 您可以在系统设置中管理通知权限\n• 关闭消息推送将停止所有应用通知',
|
||||
},
|
||||
alerts: {
|
||||
permissionDenied: {
|
||||
title: '权限被拒绝',
|
||||
message: '请在系统设置中开启通知权限,然后再尝试开启推送功能',
|
||||
cancel: '取消',
|
||||
goToSettings: '去设置',
|
||||
},
|
||||
error: {
|
||||
title: '错误',
|
||||
message: '请求通知权限失败',
|
||||
saveFailed: '保存设置失败',
|
||||
medicationReminderFailed: '设置药品提醒失败',
|
||||
nutritionReminderFailed: '设置营养提醒失败',
|
||||
moodReminderFailed: '设置心情提醒失败',
|
||||
},
|
||||
notificationsEnabled: {
|
||||
title: '通知已开启',
|
||||
body: '您将收到应用通知和提醒',
|
||||
},
|
||||
medicationReminderEnabled: {
|
||||
title: '药品提醒已开启',
|
||||
body: '您将在用药时间收到提醒通知',
|
||||
},
|
||||
nutritionReminderEnabled: {
|
||||
title: '营养提醒已开启',
|
||||
body: '您将在用餐时间收到营养记录提醒',
|
||||
},
|
||||
moodReminderEnabled: {
|
||||
title: '心情提醒已开启',
|
||||
body: '您将在晚间收到心情记录提醒',
|
||||
},
|
||||
},
|
||||
};
|
||||
31
i18n/zh/weight.ts
Normal file
31
i18n/zh/weight.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const weightRecords = {
|
||||
title: '体重记录',
|
||||
pageSubtitle: '追踪体重变化与趋势',
|
||||
loadingHistory: '体重记录加载失败',
|
||||
history: '体重记录',
|
||||
historyMonthFormat: '{{year}}年{{month}}月',
|
||||
stats: {
|
||||
currentWeight: '当前体重',
|
||||
initialWeight: '初始体重',
|
||||
targetWeight: '目标体重',
|
||||
},
|
||||
empty: {
|
||||
title: '暂无体重记录',
|
||||
subtitle: '点击右上角 + 按钮添加第一条记录',
|
||||
},
|
||||
modal: {
|
||||
recordWeight: '记录体重',
|
||||
editInitialWeight: '编辑初始体重',
|
||||
editTargetWeight: '编辑目标体重',
|
||||
editRecord: '编辑记录',
|
||||
inputPlaceholder: '输入体重',
|
||||
unit: 'kg',
|
||||
quickSelection: '快速选择',
|
||||
confirm: '保存',
|
||||
},
|
||||
alerts: {
|
||||
deleteFailed: '删除记录失败',
|
||||
invalidWeight: '请输入 0-500kg 之间的有效体重',
|
||||
saveFailed: '保存体重失败,请稍后再试',
|
||||
},
|
||||
};
|
||||
@@ -10,7 +10,8 @@ export type MedicationForm =
|
||||
| 'spray' // 喷雾
|
||||
| 'drop' // 滴剂
|
||||
| 'syrup' // 糖浆
|
||||
| 'other'; // 其他
|
||||
| 'other' // 其他
|
||||
| 'ointment' // 软膏
|
||||
|
||||
// 服药状态
|
||||
export type MedicationStatus =
|
||||
|
||||
182
utils/health.ts
182
utils/health.ts
@@ -1,5 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { AppState, AppStateStatus, NativeModules } from 'react-native';
|
||||
import i18n from '../i18n';
|
||||
import { SimpleEventEmitter } from './SimpleEventEmitter';
|
||||
|
||||
type HealthDataOptions = {
|
||||
@@ -1901,102 +1902,9 @@ export function formatWorkoutDistance(distanceInMeters: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
const WORKOUT_TYPE_LABELS: Record<string, string> = {
|
||||
running: '跑步',
|
||||
walking: '步行',
|
||||
cycling: '骑行',
|
||||
swimming: '游泳',
|
||||
yoga: '瑜伽',
|
||||
pilates: '普拉提',
|
||||
functionalstrengthtraining: '功能性力量训练',
|
||||
traditionalstrengthtraining: '传统力量训练',
|
||||
crosstraining: '交叉训练',
|
||||
mixedcardio: '混合有氧',
|
||||
highintensityintervaltraining: '高强度间歇训练',
|
||||
flexibility: '柔韧性训练',
|
||||
cooldown: '放松运动',
|
||||
dance: '舞蹈',
|
||||
danceinspiredtraining: '舞蹈训练',
|
||||
cardiodance: '有氧舞蹈',
|
||||
socialdance: '社交舞',
|
||||
swimbikerun: '铁人三项',
|
||||
transition: '项目转换',
|
||||
underwaterdiving: '水下潜水',
|
||||
pickleball: '匹克球',
|
||||
americanfootball: '美式橄榄球',
|
||||
australianfootball: '澳式橄榄球',
|
||||
archery: '射箭',
|
||||
badminton: '羽毛球',
|
||||
baseball: '棒球',
|
||||
basketball: '篮球',
|
||||
bowling: '保龄球',
|
||||
boxing: '拳击',
|
||||
climbing: '攀岩',
|
||||
cricket: '板球',
|
||||
curling: '冰壶',
|
||||
elliptical: '椭圆机',
|
||||
equestriansports: '马术',
|
||||
fencing: '击剑',
|
||||
fishing: '钓鱼',
|
||||
golf: '高尔夫',
|
||||
gymnastics: '体操',
|
||||
handball: '手球',
|
||||
hiking: '徒步',
|
||||
hockey: '曲棍球',
|
||||
hunting: '狩猎',
|
||||
lacrosse: '长曲棍球',
|
||||
martialarts: '武术',
|
||||
mindandbody: '身心运动',
|
||||
mixedmetaboliccardiotraining: '混合代谢有氧训练',
|
||||
paddlesports: '桨类运动',
|
||||
play: '自由活动',
|
||||
preparationandrecovery: '准备与恢复',
|
||||
racquetball: '壁球',
|
||||
rowing: '划船',
|
||||
rugby: '橄榄球',
|
||||
sailing: '帆船',
|
||||
skatingsports: '滑冰运动',
|
||||
snowsports: '雪上运动',
|
||||
soccer: '足球',
|
||||
softball: '垒球',
|
||||
squash: '壁球',
|
||||
stairclimbing: '爬楼梯',
|
||||
surfing: '冲浪',
|
||||
surfingsports: '冲浪运动',
|
||||
tabletennis: '乒乓球',
|
||||
tennis: '网球',
|
||||
trackandfield: '田径',
|
||||
volleyball: '排球',
|
||||
waterfitness: '水中健身',
|
||||
watersports: '水上运动',
|
||||
weighttraining: '重量训练',
|
||||
wrestling: '摔跤',
|
||||
barre: '芭蕾杆训练',
|
||||
corebTraining: '核心训练',
|
||||
jumprope: '跳绳',
|
||||
kickboxing: '踢拳',
|
||||
taichi: '太极',
|
||||
taichichuan: '太极拳',
|
||||
nordicwalking: '北欧式行走',
|
||||
frisbee: '飞盘',
|
||||
ultimatefrisbee: '极限飞盘',
|
||||
mountainbiking: '山地自行车',
|
||||
roadcycling: '公路骑行',
|
||||
virtualrunning: '虚拟跑步',
|
||||
virtualcycling: '虚拟骑行',
|
||||
trailrunning: '越野跑',
|
||||
treadmillrunning: '跑步机跑步',
|
||||
trackrunning: '场地跑',
|
||||
openwaterswimming: '公开水域游泳',
|
||||
poolswimming: '游泳池游泳',
|
||||
apneadiving: '自由潜',
|
||||
functionalStrengthTraining: '功能性力量训练',
|
||||
other: '其他运动',
|
||||
};
|
||||
|
||||
function humanizeWorkoutTypeKey(raw: string | undefined): string {
|
||||
if (!raw) {
|
||||
return '其他运动';
|
||||
return i18n.t('workoutTypes.other');
|
||||
}
|
||||
|
||||
const cleaned = raw
|
||||
@@ -2005,7 +1913,7 @@ function humanizeWorkoutTypeKey(raw: string | undefined): string {
|
||||
.trim();
|
||||
|
||||
if (!cleaned) {
|
||||
return '其他运动';
|
||||
return i18n.t('workoutTypes.other');
|
||||
}
|
||||
|
||||
const withSpaces = cleaned.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
|
||||
@@ -2021,77 +1929,25 @@ function humanizeWorkoutTypeKey(raw: string | undefined): string {
|
||||
export function getWorkoutTypeDisplayName(workoutType: WorkoutActivityType | string): string {
|
||||
if (typeof workoutType === 'string') {
|
||||
const normalized = workoutType.replace(/\s+/g, '').toLowerCase();
|
||||
return WORKOUT_TYPE_LABELS[normalized] || humanizeWorkoutTypeKey(workoutType);
|
||||
// 尝试从翻译中获取
|
||||
const translationKey = `workoutTypes.${normalized}`;
|
||||
if (i18n.exists(translationKey)) {
|
||||
return i18n.t(translationKey);
|
||||
}
|
||||
return humanizeWorkoutTypeKey(workoutType);
|
||||
}
|
||||
|
||||
switch (workoutType) {
|
||||
case WorkoutActivityType.Running:
|
||||
return '跑步';
|
||||
case WorkoutActivityType.Cycling:
|
||||
return '骑行';
|
||||
case WorkoutActivityType.Walking:
|
||||
return '步行';
|
||||
case WorkoutActivityType.Swimming:
|
||||
return '游泳';
|
||||
case WorkoutActivityType.Yoga:
|
||||
return '瑜伽';
|
||||
case WorkoutActivityType.FunctionalStrengthTraining:
|
||||
return '功能性力量训练';
|
||||
case WorkoutActivityType.TraditionalStrengthTraining:
|
||||
return '传统力量训练';
|
||||
case WorkoutActivityType.CrossTraining:
|
||||
return '交叉训练';
|
||||
case WorkoutActivityType.MixedCardio:
|
||||
return '混合有氧';
|
||||
case WorkoutActivityType.HighIntensityIntervalTraining:
|
||||
return '高强度间歇训练';
|
||||
case WorkoutActivityType.Flexibility:
|
||||
return '柔韧性训练';
|
||||
case WorkoutActivityType.Cooldown:
|
||||
return '放松运动';
|
||||
case WorkoutActivityType.Tennis:
|
||||
return '网球';
|
||||
case WorkoutActivityType.Basketball:
|
||||
return '篮球';
|
||||
case WorkoutActivityType.Soccer:
|
||||
return '足球';
|
||||
case WorkoutActivityType.Baseball:
|
||||
return '棒球';
|
||||
case WorkoutActivityType.Volleyball:
|
||||
return '排球';
|
||||
case WorkoutActivityType.Dance:
|
||||
return '舞蹈';
|
||||
case WorkoutActivityType.DanceInspiredTraining:
|
||||
return '舞蹈训练';
|
||||
case WorkoutActivityType.Elliptical:
|
||||
return '椭圆机';
|
||||
case WorkoutActivityType.Rowing:
|
||||
return '划船';
|
||||
case WorkoutActivityType.StairClimbing:
|
||||
return '爬楼梯';
|
||||
case WorkoutActivityType.Hiking:
|
||||
return '徒步';
|
||||
case WorkoutActivityType.Climbing:
|
||||
return '攀岩';
|
||||
case WorkoutActivityType.MindAndBody:
|
||||
return '身心运动';
|
||||
case WorkoutActivityType.MartialArts:
|
||||
return '武术';
|
||||
case WorkoutActivityType.Golf:
|
||||
return '高尔夫';
|
||||
case WorkoutActivityType.Boxing:
|
||||
return '拳击';
|
||||
case WorkoutActivityType.SnowSports:
|
||||
return '雪上运动';
|
||||
case WorkoutActivityType.SurfingSports:
|
||||
return '冲浪运动';
|
||||
case WorkoutActivityType.WaterFitness:
|
||||
return '水中健身';
|
||||
case WorkoutActivityType.Other:
|
||||
return '其他运动';
|
||||
default:
|
||||
return humanizeWorkoutTypeKey(WorkoutActivityType[workoutType]);
|
||||
// 使用枚举值查找对应的字符串键
|
||||
const enumKey = WorkoutActivityType[workoutType];
|
||||
if (enumKey) {
|
||||
const normalized = enumKey.toLowerCase();
|
||||
const translationKey = `workoutTypes.${normalized}`;
|
||||
if (i18n.exists(translationKey)) {
|
||||
return i18n.t(translationKey);
|
||||
}
|
||||
}
|
||||
|
||||
return humanizeWorkoutTypeKey(WorkoutActivityType[workoutType]);
|
||||
}
|
||||
|
||||
// 测试锻炼记录获取功能
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import dayjs from 'dayjs';
|
||||
import HealthKitManager, { HealthKitUtils } from './healthKit';
|
||||
|
||||
@@ -340,25 +341,27 @@ export const calculateSleepScore = (
|
||||
* 获取睡眠质量描述和建议
|
||||
*/
|
||||
export const getSleepQualityInfo = (sleepScore: number): { description: string; recommendation: string } => {
|
||||
const { t } = useI18n();
|
||||
|
||||
if (sleepScore >= 85) {
|
||||
return {
|
||||
description: '你身心愉悦并且精力充沛',
|
||||
recommendation: '恭喜你获得优质的睡眠!如果你感到精力充沛,可以考虑中等强度的运动,以维持健康的生活方式,并进一步减轻压力,以获得最佳睡眠。'
|
||||
description: t('sleepQuality.excellent.description'),
|
||||
recommendation: t('sleepQuality.excellent.recommendation')
|
||||
};
|
||||
} else if (sleepScore >= 70) {
|
||||
return {
|
||||
description: '睡眠质量良好,精神状态不错',
|
||||
recommendation: '你的睡眠质量还不错,但还有改善空间。建议保持规律的睡眠时间,睡前避免使用电子设备,营造安静舒适的睡眠环境。'
|
||||
description: t('sleepQuality.good.description'),
|
||||
recommendation: t('sleepQuality.good.recommendation')
|
||||
};
|
||||
} else if (sleepScore >= 50) {
|
||||
return {
|
||||
description: '睡眠质量一般,可能影响日间表现',
|
||||
recommendation: '你的睡眠需要改善。建议制定固定的睡前例行程序,限制咖啡因摄入,确保卧室温度适宜,考虑进行轻度运动来改善睡眠质量。'
|
||||
description: t('sleepQuality.fair.description'),
|
||||
recommendation: t('sleepQuality.fair.recommendation')
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
description: '睡眠质量较差,建议重视睡眠健康',
|
||||
recommendation: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。'
|
||||
description: t('sleepQuality.poor.description'),
|
||||
recommendation: t('sleepQuality.poor.recommendation')
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user