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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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}
|
||||
>
|
||||
{aiDraftSaving ? (
|
||||
<ActivityIndicator color={colors.onPrimary} />
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={[
|
||||
styles.primaryFooterBtn,
|
||||
{
|
||||
width: '100%',
|
||||
shadowOpacity: 0,
|
||||
backgroundColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]}
|
||||
glassEffectStyle="regular"
|
||||
tintColor={colors.primary}
|
||||
isInteractive={true}
|
||||
>
|
||||
{aiDraftSaving ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={[styles.primaryFooterText, { color: '#fff' }]}>
|
||||
{t('medications.detail.aiDraft.saveAndCreate')}
|
||||
</Text>
|
||||
)}
|
||||
</GlassView>
|
||||
) : (
|
||||
<Text style={[styles.primaryFooterText, { color: colors.onPrimary }]}>保存并创建</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.primaryFooterBtn,
|
||||
{ width: '100%', backgroundColor: colors.primary },
|
||||
]}
|
||||
>
|
||||
{aiDraftSaving ? (
|
||||
<ActivityIndicator color={colors.onPrimary} />
|
||||
) : (
|
||||
<Text style={[styles.primaryFooterText, { color: colors.onPrimary }]}>
|
||||
{t('medications.detail.aiDraft.saveAndCreate')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1726,7 +1771,7 @@ export default function MedicationDetailScreen() {
|
||||
<View style={styles.modalHandle} />
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: colors.text }]}>
|
||||
编辑药物名称
|
||||
{t('medications.detail.name.edit')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleCloseNameModal} hitSlop={12}>
|
||||
<Ionicons name="close" size={20} color={colors.textSecondary} />
|
||||
@@ -1744,7 +1789,7 @@ export default function MedicationDetailScreen() {
|
||||
<TextInput
|
||||
value={nameDraft}
|
||||
onChangeText={handleNameChange}
|
||||
placeholder="请输入药物名称"
|
||||
placeholder={t('medications.detail.name.placeholder')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
style={[styles.nameInput, { color: colors.text }]}
|
||||
autoFocus
|
||||
@@ -1777,7 +1822,7 @@ export default function MedicationDetailScreen() {
|
||||
<ActivityIndicator color={colors.onPrimary} size="small" />
|
||||
) : (
|
||||
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -2599,7 +2644,7 @@ const styles = StyleSheet.create({
|
||||
elevation: 4,
|
||||
},
|
||||
aiScoreBadgeText: {
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
|
||||
@@ -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,68 +349,160 @@ export default function NutritionRecordsScreen() {
|
||||
// 渲染右侧添加按钮
|
||||
const renderRightButton = () => (
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
|
||||
onPress={handleAddFood}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="add" size={20} color={colorTokens.primary} />
|
||||
{isGlassAvailable ? (
|
||||
<GlassView
|
||||
style={styles.glassAddButton}
|
||||
glassEffectStyle="regular"
|
||||
tintColor="rgba(255, 255, 255, 0.4)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="add" size={24} color={colorTokens.primary} />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.fallbackAddButton, { backgroundColor: 'rgba(255,255,255,0.8)' }]}>
|
||||
<Ionicons name="add" size={24} color={colorTokens.primary} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
|
||||
<HeaderBar
|
||||
title="营养记录"
|
||||
onBack={() => router.back()}
|
||||
right={renderRightButton()}
|
||||
/>
|
||||
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}}>
|
||||
|
||||
{/* {renderViewModeToggle()} */}
|
||||
{renderDateSelector()}
|
||||
|
||||
{/* Calorie Ring Chart */}
|
||||
<CalorieRingChart
|
||||
metabolism={basalMetabolism}
|
||||
exercise={healthData?.activeEnergyBurned || 0}
|
||||
consumed={nutritionSummary?.totalCalories || 0}
|
||||
protein={nutritionSummary?.totalProtein || 0}
|
||||
fat={nutritionSummary?.totalFat || 0}
|
||||
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
||||
proteinGoal={nutritionGoals.proteinGoal}
|
||||
fatGoal={nutritionGoals.fatGoal}
|
||||
carbsGoal={nutritionGoals.carbsGoal}
|
||||
const renderEmptyState = () => (
|
||||
<View style={styles.emptySimpleContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/icon-yingyang.png')}
|
||||
style={styles.emptySimpleImage}
|
||||
contentFit="contain"
|
||||
/>
|
||||
<Text style={styles.emptySimpleText}>
|
||||
{t('nutritionRecords.empty.title')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleAddFood}>
|
||||
<Text style={[styles.emptyActionText, { color: colorTokens.primary }]}>
|
||||
{t('nutritionRecords.empty.action')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
{(
|
||||
<FlatList
|
||||
data={displayRecords}
|
||||
renderItem={({ item, index }) => renderRecord({ item, index })}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={[
|
||||
styles.listContainer,
|
||||
{ paddingBottom: 40, paddingTop: 16 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={colorTokens.primary}
|
||||
colors={[colorTokens.primary]}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={viewMode === 'all' ? loadMoreRecords : undefined}
|
||||
onEndReachedThreshold={0.1}
|
||||
const renderRecord = ({ item }: { item: DietRecord }) => (
|
||||
<NutritionRecordCard
|
||||
record={item}
|
||||
onPress={() => handleRecordPress(item)}
|
||||
onDelete={() => handleDeleteRecord(item.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFooter = () => {
|
||||
if (!hasMoreData) {
|
||||
if (displayRecords.length === 0) return null;
|
||||
return (
|
||||
<View style={styles.footerContainer}>
|
||||
<Text style={[styles.footerText, { color: colorTokens.textSecondary }]}>
|
||||
{t('nutritionRecords.footer.end')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (viewMode === 'all' && displayRecords.length > 0) {
|
||||
return (
|
||||
<TouchableOpacity style={styles.loadMoreButton} onPress={loadMoreRecords}>
|
||||
<Text style={[styles.loadMoreText, { color: colorTokens.primary }]}>
|
||||
{t('nutritionRecords.footer.loadMore')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ListHeader = () => (
|
||||
<View>
|
||||
<View style={styles.headerContent}>
|
||||
{viewMode === 'daily' && (
|
||||
<DateSelector
|
||||
selectedIndex={selectedIndex}
|
||||
onDateSelect={(index, date) => {
|
||||
setSelectedIndex(index);
|
||||
setCurrentSelectedDate(date);
|
||||
}}
|
||||
showMonthTitle={true}
|
||||
disableFutureDates={true}
|
||||
showCalendarIcon={true}
|
||||
containerStyle={styles.dateSelectorContainer}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.chartWrapper}>
|
||||
<CalorieRingChart
|
||||
metabolism={basalMetabolism}
|
||||
exercise={healthData?.activeEnergyBurned || 0}
|
||||
consumed={nutritionSummary?.totalCalories || 0}
|
||||
protein={nutritionSummary?.totalProtein || 0}
|
||||
fat={nutritionSummary?.totalFat || 0}
|
||||
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
||||
proteinGoal={nutritionGoals.proteinGoal}
|
||||
fatGoal={nutritionGoals.fatGoal}
|
||||
carbsGoal={nutritionGoals.carbsGoal}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.listTitleContainer}>
|
||||
<Text style={styles.listTitle}>{t('nutritionRecords.listTitle')}</Text>
|
||||
{displayRecords.length > 0 && (
|
||||
<Text style={styles.listSubtitle}>{t('nutritionRecords.recordCount', { count: displayRecords.length })}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: '#f3f4fb' }]}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
|
||||
{/* 顶部柔和渐变背景 */}
|
||||
<LinearGradient
|
||||
colors={['rgba(255, 243, 224, 0.8)', 'rgba(243, 244, 251, 0)']}
|
||||
style={styles.topGradient}
|
||||
start={{ x: 0.5, y: 0 }}
|
||||
end={{ x: 0.5, y: 1 }}
|
||||
/>
|
||||
|
||||
<HeaderBar
|
||||
title={t('nutritionRecords.title')}
|
||||
onBack={() => router.back()}
|
||||
right={renderRightButton()}
|
||||
transparent={true}
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={displayRecords}
|
||||
renderItem={renderRecord}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={[
|
||||
styles.listContainer,
|
||||
{ paddingTop: safeAreaTop }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={colorTokens.primary}
|
||||
colors={[colorTokens.primary]}
|
||||
/>
|
||||
}
|
||||
ListHeaderComponent={ListHeader}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
ListFooterComponent={renderFooter}
|
||||
onEndReached={viewMode === 'all' ? loadMoreRecords : undefined}
|
||||
onEndReachedThreshold={0.1}
|
||||
/>
|
||||
|
||||
{/* 食物添加悬浮窗 */}
|
||||
<FloatingFoodOverlay
|
||||
@@ -492,130 +518,105 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
viewModeContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
monthTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
},
|
||||
toggleContainer: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 20,
|
||||
padding: 2,
|
||||
},
|
||||
toggleButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
minWidth: 80,
|
||||
alignItems: 'center',
|
||||
},
|
||||
toggleText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
daysContainer: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
daysScrollContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 34,
|
||||
marginRight: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
dayNumber: {
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
textAlign: 'center',
|
||||
},
|
||||
addButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
topGradient: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: 320,
|
||||
},
|
||||
listContainer: {
|
||||
paddingBottom: 100, // 留出底部空间防止遮挡
|
||||
},
|
||||
headerContent: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
dateSelectorContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60,
|
||||
paddingHorizontal: 16,
|
||||
chartWrapper: {
|
||||
marginBottom: 24,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
maxWidth: 320,
|
||||
listTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 12,
|
||||
gap: 8,
|
||||
},
|
||||
emptyTitle: {
|
||||
listTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
emptySubtitle: {
|
||||
listSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
glassAddButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
fallbackAddButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.05)',
|
||||
},
|
||||
emptySimpleContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptySimpleImage: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
opacity: 0.4,
|
||||
marginBottom: 12,
|
||||
},
|
||||
emptySimpleText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
color: '#94A3B8',
|
||||
fontFamily: 'AliRegular',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyActionText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
footerContainer: {
|
||||
paddingVertical: 20,
|
||||
paddingVertical: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
opacity: 0.6,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
loadMoreButton: {
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreText: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 }]}>
|
||||
“晚饭吃了蒸蛋羹、紫菜蛋花汤、小米粥一碗”
|
||||
</Text>
|
||||
{[
|
||||
t('voiceRecord.examples.items.0'),
|
||||
t('voiceRecord.examples.items.1'),
|
||||
t('voiceRecord.examples.items.2')
|
||||
].map((example: string, index: number) => (
|
||||
<Text key={index} style={[styles.exampleText, { color: colorTokens.textSecondary }]}>
|
||||
“{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,106 +182,157 @@ export default function WeightRecordsPage() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 背景渐变 */}
|
||||
{/* 背景 */}
|
||||
<LinearGradient
|
||||
colors={['#F0F9FF', '#E0F2FE']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
colors={['#f3f4fb', '#f3f4fb']}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
/>
|
||||
{/* 顶部装饰性渐变 */}
|
||||
<LinearGradient
|
||||
colors={['rgba(229, 252, 254, 0.8)', 'rgba(243, 244, 251, 0)']}
|
||||
style={styles.topGradient}
|
||||
start={{ x: 0.5, y: 0 }}
|
||||
end={{ x: 0.5, y: 1 }}
|
||||
/>
|
||||
|
||||
<HeaderBar
|
||||
title={t('weightRecords.title')}
|
||||
right={<TouchableOpacity onPress={handleAddWeight} style={styles.addButton}>
|
||||
<Ionicons name="add" size={24} color="#192126" />
|
||||
</TouchableOpacity>}
|
||||
right={
|
||||
isLiquidGlassAvailable() ? (
|
||||
<TouchableOpacity
|
||||
onPress={handleAddWeight}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<GlassView
|
||||
style={styles.addButtonGlass}
|
||||
glassEffectStyle="regular"
|
||||
tintColor="rgba(255, 255, 255, 0.4)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="#1c1f3a" />
|
||||
</GlassView>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={styles.addButtonFallback}
|
||||
onPress={handleAddWeight}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="#1c1f3a" />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}} />
|
||||
{/* Weight Statistics */}
|
||||
<View style={[styles.statsContainer]}>
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{totalWeightLoss.toFixed(1)}kg</Text>
|
||||
<Text style={styles.statLabel}>{t('weightRecords.stats.totalLoss')}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{currentWeight.toFixed(1)}kg</Text>
|
||||
<Text style={styles.statLabel}>{t('weightRecords.stats.currentWeight')}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{initialWeight.toFixed(1)}kg</Text>
|
||||
<View style={styles.statLabelContainer}>
|
||||
<Text style={styles.statLabel}>{t('weightRecords.stats.initialWeight')}</Text>
|
||||
<TouchableOpacity onPress={handleEditInitialWeight} style={styles.editIcon}>
|
||||
<Ionicons name="create-outline" size={12} color="#FF9500" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{targetWeight.toFixed(1)}kg</Text>
|
||||
<View style={styles.statLabelContainer}>
|
||||
<Text style={styles.statLabel}>{t('weightRecords.stats.targetWeight')}</Text>
|
||||
<TouchableOpacity onPress={handleEditTargetWeight} style={styles.editIcon}>
|
||||
<Ionicons name="create-outline" size={12} color="#FF9500" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20 }]}
|
||||
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20, paddingTop: safeAreaTop }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.headerBlock}>
|
||||
<Text style={styles.pageTitle}>{t('weightRecords.title')}</Text>
|
||||
<Text style={styles.pageSubtitle}>{t('weightRecords.pageSubtitle')}</Text>
|
||||
</View>
|
||||
|
||||
{/* Weight Statistics Cards */}
|
||||
<View style={styles.statsGrid}>
|
||||
{/* Current Weight - Hero Card */}
|
||||
<View style={styles.mainStatCard}>
|
||||
<View style={styles.mainStatContent}>
|
||||
<Text style={styles.mainStatLabel}>{t('weightRecords.stats.currentWeight')}</Text>
|
||||
<View style={styles.mainStatValueContainer}>
|
||||
<Text style={styles.mainStatValue}>{currentWeight.toFixed(1)}</Text>
|
||||
<Text style={styles.mainStatUnit}>kg</Text>
|
||||
</View>
|
||||
<View style={styles.totalLossTag}>
|
||||
<Ionicons name={totalWeightLoss <= 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
|
||||
<Text style={styles.totalLossText}>
|
||||
{totalWeightLoss > 0 ? '+' : ''}{totalWeightLoss.toFixed(1)} kg
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<LinearGradient
|
||||
colors={['#4F5BD5', '#6B6CFF']}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
// @ts-ignore
|
||||
borderRadius={24}
|
||||
/>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/iconWeight.png')}
|
||||
style={styles.statIconBg}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Secondary Stats Row */}
|
||||
<View style={styles.secondaryStatsRow}>
|
||||
{/* Initial Weight */}
|
||||
<TouchableOpacity
|
||||
style={styles.secondaryStatCard}
|
||||
onPress={handleEditInitialWeight}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.secondaryStatHeader}>
|
||||
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.initialWeight')}</Text>
|
||||
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
|
||||
</View>
|
||||
<Text style={styles.secondaryStatValue}>{initialWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Target Weight */}
|
||||
<TouchableOpacity
|
||||
style={styles.secondaryStatCard}
|
||||
onPress={handleEditTargetWeight}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.secondaryStatHeader}>
|
||||
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.targetWeight')}</Text>
|
||||
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
|
||||
</View>
|
||||
<Text style={styles.secondaryStatValue}>{targetWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Monthly Records */}
|
||||
{Object.keys(groupedHistory).length > 0 ? (
|
||||
Object.entries(groupedHistory).map(([month, records]) => (
|
||||
<View key={month} style={styles.monthContainer}>
|
||||
{/* Month Header Card */}
|
||||
{/* <View style={styles.monthHeaderCard}>
|
||||
<View style={styles.monthTitleRow}>
|
||||
<Text style={styles.monthNumber}>
|
||||
{dayjs(month, 'YYYY年MM月').format('MM')}
|
||||
</Text>
|
||||
<Text style={styles.monthText}>月</Text>
|
||||
<Text style={styles.yearText}>
|
||||
{dayjs(month, 'YYYY年MM月').format('YYYY年')}
|
||||
</Text>
|
||||
<View style={styles.expandIcon}>
|
||||
<Ionicons name="chevron-up" size={16} color="#FF9500" />
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.monthStatsText}>
|
||||
累计减重:<Text style={styles.statsBold}>{totalWeightLoss.toFixed(1)}kg</Text> 日均减重:<Text style={styles.statsBold}>{avgWeightLoss.toFixed(1)}kg</Text>
|
||||
</Text>
|
||||
</View> */}
|
||||
<View style={styles.historySection}>
|
||||
<Text style={styles.sectionTitle}>{t('weightRecords.history')}</Text>
|
||||
{Object.entries(groupedHistory).map(([month, records]) => (
|
||||
<View key={month} style={styles.monthContainer}>
|
||||
<View style={styles.monthHeader}>
|
||||
<Text style={styles.monthTitle}>{month}</Text>
|
||||
</View>
|
||||
|
||||
{/* Individual Record Cards */}
|
||||
{records.map((record, recordIndex) => {
|
||||
// Calculate weight change from previous record
|
||||
const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null;
|
||||
const weightChange = prevRecord ?
|
||||
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
|
||||
{/* Individual Record Cards */}
|
||||
<View style={styles.recordsList}>
|
||||
{records.map((record, recordIndex) => {
|
||||
// Calculate weight change from previous record
|
||||
const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null;
|
||||
const weightChange = prevRecord ?
|
||||
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
|
||||
|
||||
return (
|
||||
<WeightRecordCard
|
||||
key={`${record.createdAt}-${recordIndex}`}
|
||||
record={record}
|
||||
onPress={handleEditWeightRecord}
|
||||
onDelete={handleDeleteWeightRecord}
|
||||
weightChange={weightChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
))
|
||||
return (
|
||||
<WeightRecordCard
|
||||
key={`${record.createdAt}-${recordIndex}`}
|
||||
record={record}
|
||||
onPress={handleEditWeightRecord}
|
||||
onDelete={handleDeleteWeightRecord}
|
||||
weightChange={weightChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/iconWeight.png')}
|
||||
style={{ width: 80, height: 80, opacity: 0.5, marginBottom: 16, tintColor: '#cbd5e1' }}
|
||||
/>
|
||||
<View style={styles.emptyContent}>
|
||||
<Text style={styles.emptyText}>{t('weightRecords.empty.title')}</Text>
|
||||
<Text style={styles.emptySubtext}>{t('weightRecords.empty.subtitle')}</Text>
|
||||
@@ -304,13 +354,16 @@ export default function WeightRecordsPage() {
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowWeightPicker(false)}
|
||||
/>
|
||||
<View style={[styles.modalSheet, { backgroundColor: themeColors.background }]}>
|
||||
<View style={[styles.modalSheet, { backgroundColor: '#ffffff' }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.modalHeader}>
|
||||
<TouchableOpacity onPress={() => setShowWeightPicker(false)}>
|
||||
<Ionicons name="close" size={24} color={themeColors.text} />
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowWeightPicker(false)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="close" size={24} color="#1c1f3a" />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
|
||||
<Text style={styles.modalTitle}>
|
||||
{pickerType === 'current' && t('weightRecords.modal.recordWeight')}
|
||||
{pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')}
|
||||
{pickerType === 'target' && t('weightRecords.modal.editTargetWeight')}
|
||||
@@ -327,25 +380,26 @@ export default function WeightRecordsPage() {
|
||||
<View style={styles.inputSection}>
|
||||
<View style={styles.weightInputContainer}>
|
||||
<View style={styles.weightIcon}>
|
||||
<Ionicons name="scale-outline" size={20} color="#6366F1" />
|
||||
<Image
|
||||
source={require('@/assets/images/icons/iconWeight.png')}
|
||||
style={{ width: 24, height: 24, tintColor: '#4F5BD5' }}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.weightDisplay, { color: inputWeight ? themeColors.text : themeColors.textSecondary }]}>
|
||||
<Text style={[
|
||||
styles.weightDisplay,
|
||||
{ color: inputWeight ? '#1c1f3a' : '#9ba3c7' }
|
||||
]}>
|
||||
{inputWeight || t('weightRecords.modal.inputPlaceholder')}
|
||||
</Text>
|
||||
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>{t('weightRecords.modal.unit')}</Text>
|
||||
<Text style={styles.unitLabel}>{t('weightRecords.modal.unit')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Weight Range Hint */}
|
||||
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
|
||||
{t('weightRecords.modal.inputHint')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Quick Selection */}
|
||||
<View style={styles.quickSelectionSection}>
|
||||
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}>{t('weightRecords.modal.quickSelection')}</Text>
|
||||
<Text style={styles.quickSelectionTitle}>{t('weightRecords.modal.quickSelection')}</Text>
|
||||
<View style={styles.quickButtons}>
|
||||
{[50, 60, 70, 80, 90].map((weight) => (
|
||||
<TouchableOpacity
|
||||
@@ -355,6 +409,7 @@ export default function WeightRecordsPage() {
|
||||
inputWeight === weight.toString() && styles.quickButtonSelected
|
||||
]}
|
||||
onPress={() => setInputWeight(weight.toString())}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[
|
||||
styles.quickButtonText,
|
||||
@@ -387,8 +442,16 @@ export default function WeightRecordsPage() {
|
||||
]}
|
||||
onPress={handleWeightSave}
|
||||
disabled={!inputWeight.trim()}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
|
||||
<LinearGradient
|
||||
colors={['#4F5BD5', '#6B6CFF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.saveButtonGradient}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -402,144 +465,202 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradientBackground: {
|
||||
topGradient: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 60,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
addButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 300,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
contentContainer: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
statsContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
headerBlock: {
|
||||
paddingHorizontal: 24,
|
||||
marginTop: 10,
|
||||
marginBottom: 24,
|
||||
},
|
||||
statsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
statItem: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 14,
|
||||
pageTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
statLabelContainer: {
|
||||
flexDirection: 'row',
|
||||
pageSubtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
// Add Button Styles
|
||||
addButtonGlass: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#687076',
|
||||
marginRight: 4,
|
||||
textAlign: 'center'
|
||||
addButtonFallback: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#ffffff',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.05)',
|
||||
},
|
||||
editIcon: {
|
||||
padding: 2,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||
|
||||
// Stats Grid
|
||||
statsGrid: {
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 32,
|
||||
gap: 16,
|
||||
},
|
||||
monthContainer: {
|
||||
marginBottom: 20,
|
||||
mainStatCard: {
|
||||
backgroundColor: '#4F5BD5',
|
||||
borderRadius: 28,
|
||||
padding: 24,
|
||||
height: 160,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#4F5BD5',
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 16,
|
||||
elevation: 8,
|
||||
},
|
||||
monthHeaderCard: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
mainStatContent: {
|
||||
zIndex: 2,
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
mainStatLabel: {
|
||||
fontSize: 16,
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
mainStatValueContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
mainStatValue: {
|
||||
fontSize: 48,
|
||||
fontWeight: '800',
|
||||
color: '#ffffff',
|
||||
fontFamily: 'AliBold',
|
||||
marginRight: 8,
|
||||
},
|
||||
mainStatUnit: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
totalLossTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
totalLossText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#ffffff',
|
||||
marginLeft: 4,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
statIconBg: {
|
||||
position: 'absolute',
|
||||
right: -20,
|
||||
bottom: -20,
|
||||
width: 140,
|
||||
height: 140,
|
||||
opacity: 0.2,
|
||||
transform: [{ rotate: '-15deg' }],
|
||||
tintColor: '#ffffff'
|
||||
},
|
||||
secondaryStatsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
},
|
||||
secondaryStatCard: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.06)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
monthTitleRow: {
|
||||
secondaryStatHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
monthNumber: {
|
||||
fontSize: 48,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
lineHeight: 48,
|
||||
secondaryStatLabel: {
|
||||
fontSize: 13,
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
monthText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#192126',
|
||||
marginLeft: 4,
|
||||
marginRight: 8,
|
||||
},
|
||||
yearText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#687076',
|
||||
flex: 1,
|
||||
},
|
||||
expandIcon: {
|
||||
padding: 4,
|
||||
},
|
||||
monthStatsText: {
|
||||
fontSize: 14,
|
||||
color: '#687076',
|
||||
lineHeight: 20,
|
||||
},
|
||||
statsBold: {
|
||||
secondaryStatValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
secondaryStatUnit: {
|
||||
fontSize: 14,
|
||||
color: '#6f7ba7',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliRegular',
|
||||
marginLeft: 2,
|
||||
},
|
||||
|
||||
// History Section
|
||||
historySection: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1c1f3a',
|
||||
marginBottom: 16,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
monthContainer: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
monthHeader: {
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
monthTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
recordsList: {
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 300,
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
@@ -547,145 +668,161 @@ const styles = StyleSheet.create({
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
color: '#1c1f3a',
|
||||
marginBottom: 8,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 14,
|
||||
color: '#687076',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
// Modal Styles
|
||||
|
||||
// Modal Styles (Retain but refined)
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
modalBackdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
modalSheet: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 32,
|
||||
borderTopRightRadius: 32,
|
||||
maxHeight: '85%',
|
||||
minHeight: 500,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 10,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 10,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
modalContent: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
inputSection: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#F8F9FC',
|
||||
borderRadius: 24,
|
||||
padding: 24,
|
||||
marginBottom: 24,
|
||||
marginTop: 8,
|
||||
},
|
||||
weightInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
weightIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F0F9FF',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: '#EEF0FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
marginRight: 16,
|
||||
},
|
||||
inputWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignItems: 'baseline',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
paddingBottom: 6,
|
||||
borderBottomColor: '#E2E8F0',
|
||||
paddingBottom: 8,
|
||||
},
|
||||
weightDisplay: {
|
||||
flex: 1,
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
fontSize: 36,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 4,
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
unitLabel: {
|
||||
fontSize: 18,
|
||||
fontWeight: '500',
|
||||
fontWeight: '600',
|
||||
color: '#6f7ba7',
|
||||
marginLeft: 8,
|
||||
},
|
||||
hintText: {
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
marginTop: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
quickSelectionSection: {
|
||||
paddingHorizontal: 4,
|
||||
marginBottom: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
quickSelectionTitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#6f7ba7',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
fontFamily: 'AliRegular',
|
||||
marginLeft: 4,
|
||||
},
|
||||
quickButtons: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
quickButton: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
minWidth: 60,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F1F5F9',
|
||||
minWidth: 64,
|
||||
alignItems: 'center',
|
||||
},
|
||||
quickButtonSelected: {
|
||||
backgroundColor: '#6366F1',
|
||||
borderColor: '#6366F1',
|
||||
backgroundColor: '#4F5BD5',
|
||||
},
|
||||
quickButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#6B7280',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
quickButtonTextSelected: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
},
|
||||
modalFooter: {
|
||||
paddingHorizontal: 20,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 25,
|
||||
paddingBottom: 34,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F1F5F9',
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: '#6366F1',
|
||||
borderRadius: 16,
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#4F5BD5',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
},
|
||||
saveButtonGradient: {
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
saveButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user