- 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.
351 lines
9.1 KiB
TypeScript
351 lines
9.1 KiB
TypeScript
import { ThemedText } from '@/components/ThemedText';
|
|
import { useI18n } from '@/hooks/useI18n';
|
|
import { useThemeColor } from '@/hooks/useThemeColor';
|
|
import { DietRecord } from '@/services/dietRecords';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import dayjs from 'dayjs';
|
|
import { Image } from 'expo-image';
|
|
import React, { useMemo, useRef, useState } from 'react';
|
|
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { RectButton, Swipeable } from 'react-native-gesture-handler';
|
|
|
|
|
|
export type NutritionRecordCardProps = {
|
|
record: DietRecord;
|
|
onPress?: () => void;
|
|
onDelete?: () => void;
|
|
};
|
|
|
|
const MEAL_TYPE_ICONS = {
|
|
breakfast: 'sunny-outline',
|
|
lunch: 'partly-sunny-outline',
|
|
dinner: 'moon-outline',
|
|
snack: 'cafe-outline',
|
|
other: 'restaurant-outline',
|
|
} as const;
|
|
|
|
const MEAL_TYPE_COLORS = {
|
|
breakfast: '#FF6B35',
|
|
lunch: '#4CAF50',
|
|
dinner: '#2196F3',
|
|
snack: '#FF7A85',
|
|
other: '#9AA3AE',
|
|
} as const;
|
|
|
|
export function NutritionRecordCard({
|
|
record,
|
|
onPress,
|
|
onDelete
|
|
}: NutritionRecordCardProps) {
|
|
const { t } = useI18n();
|
|
const textColor = useThemeColor({}, 'text');
|
|
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
|
|
|
// 左滑删除相关
|
|
const swipeableRef = useRef<Swipeable>(null);
|
|
const [isSwiping, setIsSwiping] = useState(false);
|
|
|
|
// 营养数据统计
|
|
const nutritionStats = useMemo(() => {
|
|
return [
|
|
{
|
|
label: t('nutritionRecords.nutrients.protein'),
|
|
value: record.proteinGrams ? `${Math.round(record.proteinGrams)}` : '-',
|
|
unit: t('nutritionRecords.nutrients.unit'),
|
|
color: '#64748B'
|
|
},
|
|
{
|
|
label: t('nutritionRecords.nutrients.fat'),
|
|
value: record.fatGrams ? `${Math.round(record.fatGrams)}` : '-',
|
|
unit: t('nutritionRecords.nutrients.unit'),
|
|
color: '#64748B'
|
|
},
|
|
{
|
|
label: t('nutritionRecords.nutrients.carbs'),
|
|
value: record.carbohydrateGrams ? `${Math.round(record.carbohydrateGrams)}` : '-',
|
|
unit: t('nutritionRecords.nutrients.unit'),
|
|
color: '#64748B'
|
|
},
|
|
];
|
|
}, [record, t]);
|
|
|
|
const mealTypeColor = MEAL_TYPE_COLORS[record.mealType];
|
|
const mealTypeLabel = t(`nutritionRecords.mealTypes.${record.mealType}`);
|
|
|
|
// 处理点击事件,只有在非滑动状态下才触发
|
|
const handlePress = () => {
|
|
if (!isSwiping && onPress) {
|
|
onPress();
|
|
}
|
|
};
|
|
|
|
const handleSwipeableWillOpen = () => setIsSwiping(true);
|
|
const handleSwipeableClose = () => setTimeout(() => setIsSwiping(false), 100);
|
|
|
|
const handleDelete = () => {
|
|
Alert.alert(
|
|
t('nutritionRecords.delete.title'),
|
|
t('nutritionRecords.delete.message'),
|
|
[
|
|
{ text: t('nutritionRecords.delete.cancel'), style: 'cancel' },
|
|
{
|
|
text: t('nutritionRecords.delete.confirm'),
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
onDelete?.();
|
|
swipeableRef.current?.close();
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const renderRightActions = () => {
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.deleteButton}
|
|
onPress={handleDelete}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Swipeable
|
|
ref={swipeableRef}
|
|
renderRightActions={renderRightActions}
|
|
rightThreshold={40}
|
|
overshootRight={false}
|
|
onSwipeableWillOpen={handleSwipeableWillOpen}
|
|
onSwipeableClose={handleSwipeableClose}
|
|
>
|
|
<RectButton
|
|
style={styles.card}
|
|
onPress={handlePress}
|
|
>
|
|
<View style={styles.mainContent}>
|
|
{/* 左侧:时间线和图标 */}
|
|
<View style={styles.leftSection}>
|
|
<View style={styles.mealIconContainer}>
|
|
<Image
|
|
source={require('@/assets/images/icons/icon-food.png')}
|
|
style={styles.mealIcon}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 中间:主要信息 */}
|
|
<View style={styles.centerSection}>
|
|
<View style={styles.titleRow}>
|
|
<ThemedText style={styles.foodName} numberOfLines={1}>
|
|
{record.foodName}
|
|
</ThemedText>
|
|
<View style={[styles.mealTag, { backgroundColor: `${mealTypeColor}15` }]}>
|
|
<Text style={[styles.mealTagText, { color: mealTypeColor }]}>{mealTypeLabel}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.metaRow}>
|
|
<Ionicons name="time-outline" size={12} color="#94A3B8" />
|
|
<Text style={styles.timeText}>
|
|
{record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '--:--'}
|
|
</Text>
|
|
{record.portionDescription && (
|
|
<>
|
|
<Text style={styles.dotSeparator}>·</Text>
|
|
<Text style={styles.portionText} numberOfLines={1}>{record.portionDescription}</Text>
|
|
</>
|
|
)}
|
|
</View>
|
|
|
|
{/* 营养微缩信息 */}
|
|
<View style={styles.nutritionRow}>
|
|
{nutritionStats.map((stat, index) => (
|
|
<View key={index} style={styles.nutritionItem}>
|
|
<Text style={styles.nutritionValue}>{stat.value}<Text style={styles.nutritionUnit}>{stat.unit}</Text></Text>
|
|
<Text style={styles.nutritionLabel}>{stat.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
{/* 右侧:热量 */}
|
|
<View style={styles.rightSection}>
|
|
<Text style={styles.caloriesValue}>
|
|
{record.estimatedCalories ? Math.round(record.estimatedCalories) : '-'}
|
|
</Text>
|
|
<Text style={styles.caloriesUnit}>{t('nutritionRecords.nutrients.caloriesUnit')}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 如果有图片,显示图片缩略图 */}
|
|
{record.imageUrl && (
|
|
<View style={styles.imageSection}>
|
|
<Image
|
|
source={{ uri: record.imageUrl }}
|
|
style={styles.foodImage}
|
|
contentFit="cover"
|
|
transition={200}
|
|
/>
|
|
</View>
|
|
)}
|
|
</RectButton>
|
|
</Swipeable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
marginBottom: 12,
|
|
marginHorizontal: 24,
|
|
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 12,
|
|
elevation: 2,
|
|
},
|
|
card: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 24,
|
|
padding: 16,
|
|
},
|
|
mainContent: {
|
|
flexDirection: 'row',
|
|
},
|
|
leftSection: {
|
|
marginRight: 12,
|
|
alignItems: 'center',
|
|
},
|
|
mealIconContainer: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 14,
|
|
backgroundColor: '#F8FAFC',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
mealIcon: {
|
|
width: 20,
|
|
height: 20,
|
|
opacity: 0.8,
|
|
},
|
|
centerSection: {
|
|
flex: 1,
|
|
marginRight: 12,
|
|
},
|
|
titleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 4,
|
|
gap: 8,
|
|
},
|
|
foodName: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: '#1E293B',
|
|
fontFamily: 'AliBold',
|
|
flexShrink: 1,
|
|
},
|
|
mealTag: {
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
borderRadius: 6,
|
|
},
|
|
mealTagText: {
|
|
fontSize: 10,
|
|
fontWeight: '600',
|
|
fontFamily: 'AliBold',
|
|
},
|
|
metaRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 10,
|
|
},
|
|
timeText: {
|
|
fontSize: 12,
|
|
color: '#94A3B8',
|
|
marginLeft: 4,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
dotSeparator: {
|
|
marginHorizontal: 4,
|
|
color: '#CBD5E1',
|
|
},
|
|
portionText: {
|
|
fontSize: 12,
|
|
color: '#64748B',
|
|
fontFamily: 'AliRegular',
|
|
flex: 1,
|
|
},
|
|
nutritionRow: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
nutritionItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'baseline',
|
|
gap: 2,
|
|
},
|
|
nutritionValue: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: '#475569',
|
|
fontFamily: 'AliBold',
|
|
},
|
|
nutritionUnit: {
|
|
fontSize: 10,
|
|
fontWeight: '500',
|
|
color: '#94A3B8',
|
|
marginLeft: 1,
|
|
},
|
|
nutritionLabel: {
|
|
fontSize: 10,
|
|
color: '#94A3B8',
|
|
marginLeft: 2,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
rightSection: {
|
|
alignItems: 'flex-end',
|
|
justifyContent: 'flex-start',
|
|
paddingTop: 2,
|
|
},
|
|
caloriesValue: {
|
|
fontSize: 18,
|
|
fontWeight: '800',
|
|
color: '#1E293B',
|
|
fontFamily: 'AliBold',
|
|
lineHeight: 22,
|
|
},
|
|
caloriesUnit: {
|
|
fontSize: 10,
|
|
color: '#94A3B8',
|
|
fontWeight: '500',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
imageSection: {
|
|
marginTop: 12,
|
|
height: 120,
|
|
width: '100%',
|
|
borderRadius: 16,
|
|
overflow: 'hidden',
|
|
backgroundColor: '#F1F5F9',
|
|
},
|
|
foodImage: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
deleteButton: {
|
|
backgroundColor: '#FF6B6B',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
width: 70,
|
|
height: '100%',
|
|
borderRadius: 24,
|
|
marginLeft: 12,
|
|
},
|
|
});
|