Add Chinese translations for medication management and personal settings

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

View File

@@ -1,4 +1,5 @@
import { ThemedText } from '@/components/ThemedText';
import { useI18n } from '@/hooks/useI18n';
import { useThemeColor } from '@/hooks/useThemeColor';
import { DietRecord } from '@/services/dietRecords';
import { Ionicons } from '@expo/vector-icons';
@@ -15,14 +16,6 @@ export type NutritionRecordCardProps = {
onDelete?: () => void;
};
const MEAL_TYPE_LABELS = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐',
other: '其他',
} as const;
const MEAL_TYPE_ICONS = {
breakfast: 'sunny-outline',
lunch: 'partly-sunny-outline',
@@ -44,46 +37,40 @@ export function NutritionRecordCard({
onPress,
onDelete
}: NutritionRecordCardProps) {
const surfaceColor = useThemeColor({}, 'surface');
const { t } = useI18n();
const textColor = useThemeColor({}, 'text');
const textSecondaryColor = useThemeColor({}, 'textSecondary');
// Popover 状态管理
const [showPopover, setShowPopover] = useState(false);
const popoverRef = useRef<any>(null);
// 左滑删除相关
const swipeableRef = useRef<Swipeable>(null);
// 添加滑动状态管理,防止滑动时触发点击事件
const [isSwiping, setIsSwiping] = useState(false);
// 营养数据统计
const nutritionStats = useMemo(() => {
return [
{
label: '蛋白质',
value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)}g` : '-',
icon: '🥩',
color: '#FF6B6B'
label: t('nutritionRecords.nutrients.protein'),
value: record.proteinGrams ? `${Math.round(record.proteinGrams)}` : '-',
unit: t('nutritionRecords.nutrients.unit'),
color: '#64748B'
},
{
label: '脂肪',
value: record.fatGrams ? `${record.fatGrams.toFixed(1)}g` : '-',
icon: '🥑',
color: '#FFB366'
label: t('nutritionRecords.nutrients.fat'),
value: record.fatGrams ? `${Math.round(record.fatGrams)}` : '-',
unit: t('nutritionRecords.nutrients.unit'),
color: '#64748B'
},
{
label: '碳水',
value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)}g` : '-',
icon: '🍞',
color: '#4ECDC4'
label: t('nutritionRecords.nutrients.carbs'),
value: record.carbohydrateGrams ? `${Math.round(record.carbohydrateGrams)}` : '-',
unit: t('nutritionRecords.nutrients.unit'),
color: '#64748B'
},
];
}, [record]);
}, [record, t]);
const mealTypeColor = MEAL_TYPE_COLORS[record.mealType];
const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType];
const mealTypeLabel = t(`nutritionRecords.mealTypes.${record.mealType}`);
// 处理点击事件,只有在非滑动状态下才触发
const handlePress = () => {
@@ -92,31 +79,17 @@ export function NutritionRecordCard({
}
};
// 处理滑动开始
const handleSwipeableWillOpen = () => {
setIsSwiping(true);
};
const handleSwipeableWillOpen = () => setIsSwiping(true);
const handleSwipeableClose = () => setTimeout(() => setIsSwiping(false), 100);
// 处理滑动结束
const handleSwipeableClose = () => {
// 延迟重置滑动状态,防止滑动结束时立即触发点击
setTimeout(() => {
setIsSwiping(false);
}, 100);
};
// 处理删除操作
const handleDelete = () => {
Alert.alert(
'确认删除',
`确定要删除这条营养记录吗?此操作无法撤销。`,
t('nutritionRecords.delete.title'),
t('nutritionRecords.delete.message'),
[
{ text: t('nutritionRecords.delete.cancel'), style: 'cancel' },
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
text: t('nutritionRecords.delete.confirm'),
style: 'destructive',
onPress: () => {
onDelete?.();
@@ -127,7 +100,6 @@ export function NutritionRecordCard({
);
};
// 渲染删除按钮
const renderRightActions = () => {
return (
<TouchableOpacity
@@ -136,7 +108,6 @@ export function NutritionRecordCard({
activeOpacity={0.8}
>
<Ionicons name="trash" size={20} color="#FFFFFF" />
<Text style={styles.deleteButtonText}></Text>
</TouchableOpacity>
);
};
@@ -152,239 +123,228 @@ export function NutritionRecordCard({
onSwipeableClose={handleSwipeableClose}
>
<RectButton
style={[
styles.card,
]}
style={styles.card}
onPress={handlePress}
// activeOpacity={0.7}
>
{/* 主要内容区域 - 水平布局 */}
<View style={styles.mainContent}>
{/* 左侧:食物图片 */}
<View style={[styles.foodImageContainer, !record.imageUrl && styles.foodImagePlaceholder]}>
{record.imageUrl ? (
<Image
source={{ uri: record.imageUrl }}
style={styles.foodImage}
cachePolicy={'memory-disk'}
/>
) : (
<Ionicons name="restaurant" size={28} color={textSecondaryColor} />
)}
{/* 左侧:时间线和图标 */}
<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.foodInfoContainer}>
{/* 食物名称 */}
<ThemedText style={[styles.foodName, { color: textColor }]}>
{record.foodName}
</ThemedText>
{/* 时间 */}
<ThemedText style={[styles.mealTime, { color: textSecondaryColor }]}>
{record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '--:--'}
</ThemedText>
{/* 营养信息 - 水平排列 */}
<View style={styles.nutritionContainer}>
{/* 中间:主要信息 */}
<View style={styles.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={stat.label} style={styles.nutritionItem}>
<ThemedText style={styles.nutritionIcon}>{stat.icon}</ThemedText>
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
{stat.value}
</ThemedText>
</View>
<View key={index} style={styles.nutritionItem}>
<Text style={styles.nutritionValue}>{stat.value}<Text style={styles.nutritionUnit}>{stat.unit}</Text></Text>
<Text style={styles.nutritionLabel}>{stat.label}</Text>
</View>
))}
</View>
</View>
{/* 右侧:热量和餐次标签 */}
{/* 右侧:热量 */}
<View style={styles.rightSection}>
{/* 热量显示 */}
<View style={styles.caloriesContainer}>
<ThemedText style={[styles.caloriesText]}>
{record.estimatedCalories ? `${Math.round(record.estimatedCalories)} kcal` : '- kcal'}
</ThemedText>
</View>
{/* 餐次标签 */}
<View style={[styles.mealTypeBadge]}>
<ThemedText style={[styles.mealTypeText, { color: mealTypeColor }]}>
{mealTypeLabel}
</ThemedText>
</View>
<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: 16,
// iOS 阴影效果 - 更自然的阴影
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
marginBottom: 12,
marginHorizontal: 24,
shadowColor: 'rgba(30, 41, 59, 0.05)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 8,
// Android 阴影效果
elevation: 3,
shadowRadius: 12,
elevation: 2,
},
card: {
flex: 1,
minHeight: 100,
backgroundColor: '#FFFFFF',
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 14,
borderRadius: 24,
padding: 16,
},
mainContent: {
flex: 1,
flexDirection: 'row',
},
leftSection: {
marginRight: 12,
alignItems: 'center',
},
foodImageContainer: {
width: 48,
height: 48,
borderRadius: 12,
marginRight: 16,
mealIconContainer: {
width: 40,
height: 40,
borderRadius: 14,
backgroundColor: '#F8FAFC',
alignItems: 'center',
justifyContent: 'center',
},
mealIcon: {
width: 20,
height: 20,
opacity: 0.8,
},
centerSection: {
flex: 1,
marginRight: 12,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
gap: 8,
},
foodName: {
fontSize: 16,
fontWeight: '700',
color: '#1E293B',
fontFamily: 'AliBold',
flexShrink: 1,
},
mealTag: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 6,
},
mealTagText: {
fontSize: 10,
fontWeight: '600',
fontFamily: 'AliBold',
},
metaRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
timeText: {
fontSize: 12,
color: '#94A3B8',
marginLeft: 4,
fontFamily: 'AliRegular',
},
dotSeparator: {
marginHorizontal: 4,
color: '#CBD5E1',
},
portionText: {
fontSize: 12,
color: '#64748B',
fontFamily: 'AliRegular',
flex: 1,
},
nutritionRow: {
flexDirection: 'row',
gap: 12,
},
nutritionItem: {
flexDirection: 'row',
alignItems: 'baseline',
gap: 2,
},
nutritionValue: {
fontSize: 13,
fontWeight: '600',
color: '#475569',
fontFamily: 'AliBold',
},
nutritionUnit: {
fontSize: 10,
fontWeight: '500',
color: '#94A3B8',
marginLeft: 1,
},
nutritionLabel: {
fontSize: 10,
color: '#94A3B8',
marginLeft: 2,
fontFamily: 'AliRegular',
},
rightSection: {
alignItems: 'flex-end',
justifyContent: 'flex-start',
paddingTop: 2,
},
caloriesValue: {
fontSize: 18,
fontWeight: '800',
color: '#1E293B',
fontFamily: 'AliBold',
lineHeight: 22,
},
caloriesUnit: {
fontSize: 10,
color: '#94A3B8',
fontWeight: '500',
fontFamily: 'AliRegular',
},
imageSection: {
marginTop: 12,
height: 120,
width: '100%',
borderRadius: 16,
overflow: 'hidden',
backgroundColor: '#F1F5F9',
},
foodImage: {
width: '100%',
height: '100%',
borderRadius: 8,
},
foodImagePlaceholder: {
backgroundColor: '#F8F9FA',
justifyContent: 'center',
alignItems: 'center',
},
foodInfoContainer: {
flex: 1,
justifyContent: 'center',
gap: 4,
},
foodName: {
fontSize: 16,
fontWeight: '600',
color: '#333333',
lineHeight: 20,
},
mealTime: {
fontSize: 12,
fontWeight: '400',
color: '#999999',
lineHeight: 16,
},
nutritionContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
marginTop: 2,
},
nutritionItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
nutritionIcon: {
fontSize: 14,
},
nutritionValue: {
fontSize: 13,
fontWeight: '500',
color: '#666666',
},
rightSection: {
alignItems: 'flex-end',
justifyContent: 'center',
gap: 8,
minHeight: 60,
},
caloriesContainer: {
flexDirection: 'row',
alignItems: 'center',
},
caloriesText: {
fontSize: 14,
color: '#333333',
fontWeight: '600',
},
mealTypeBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: 'rgba(0,0,0,0.05)',
},
mealTypeText: {
fontSize: 12,
fontWeight: '600',
},
moreButton: {
padding: 2,
},
notesSection: {
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(0,0,0,0.06)',
},
notesText: {
fontSize: 13,
fontWeight: '500',
lineHeight: 18,
fontStyle: 'italic',
},
popoverContainer: {
borderRadius: 12,
backgroundColor: '#FFFFFF',
// iOS 阴影效果
shadowColor: '#000000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 12,
// Android 阴影效果
elevation: 8,
// 添加边框
borderWidth: 0.5,
borderColor: 'rgba(0, 0, 0, 0.08)',
},
popoverBackground: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
popoverContent: {
minWidth: 140,
paddingVertical: 8,
},
popoverItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
gap: 12,
},
popoverText: {
fontSize: 16,
fontWeight: '500',
},
deleteButton: {
backgroundColor: '#EF4444',
backgroundColor: '#FF6B6B',
justifyContent: 'center',
alignItems: 'center',
width: 80,
borderRadius: 12,
marginLeft: 8,
},
deleteButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 4,
width: 70,
height: '100%',
borderRadius: 24,
marginLeft: 12,
},
});