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:
@@ -1,9 +1,9 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, StyleSheet, View } from 'react-native';
|
||||
import Svg, { Circle } from 'react-native-svg';
|
||||
import Svg, { Circle, Defs, Stop, LinearGradient as SvgLinearGradient } from 'react-native-svg';
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||
|
||||
@@ -26,12 +26,8 @@ export function CalorieRingChart({
|
||||
protein,
|
||||
fat,
|
||||
carbs,
|
||||
proteinGoal,
|
||||
fatGoal,
|
||||
carbsGoal,
|
||||
|
||||
}: CalorieRingChartProps) {
|
||||
const surfaceColor = useThemeColor({}, 'surface');
|
||||
const { t } = useI18n();
|
||||
const textColor = useThemeColor({}, 'text');
|
||||
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
||||
|
||||
@@ -46,9 +42,9 @@ export function CalorieRingChart({
|
||||
const totalAvailable = metabolism + exercise;
|
||||
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
||||
|
||||
// 圆环参数 - 减小尺寸以优化空间占用
|
||||
const radius = 48;
|
||||
const strokeWidth = 8; // 增加圆环厚度
|
||||
// 圆环参数 - 缩小尺寸
|
||||
const radius = 42;
|
||||
const strokeWidth = 8;
|
||||
const center = radius + strokeWidth;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const strokeDasharray = circumference;
|
||||
@@ -70,34 +66,32 @@ export function CalorieRingChart({
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: surfaceColor }]}>
|
||||
{/* 左上角公式展示 */}
|
||||
<View style={styles.formulaContainer}>
|
||||
<ThemedText style={[styles.formulaText, { color: textSecondaryColor }]}>
|
||||
还能吃 = 代谢 + 运动 - 饮食
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<View style={styles.container}>
|
||||
<View style={styles.mainContent}>
|
||||
{/* 左侧圆环图 */}
|
||||
<View style={styles.chartContainer}>
|
||||
<Svg width={center * 2} height={center * 2}>
|
||||
<Defs>
|
||||
<SvgLinearGradient id="progressGradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<Stop offset="0" stopColor={progressPercentage > 80 ? "#FF9966" : "#4facfe"} stopOpacity="1" />
|
||||
<Stop offset="1" stopColor={progressPercentage > 80 ? "#FF5E62" : "#00f2fe"} stopOpacity="1" />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
{/* 背景圆环 */}
|
||||
<Circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke="#F0F0F0"
|
||||
stroke="#F5F7FA"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
{/* 进度圆环 - 保持固定颜色 */}
|
||||
{/* 进度圆环 */}
|
||||
<AnimatedCircle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke={progressPercentage > 80 ? "#FF6B6B" : "#4ECDC4"}
|
||||
stroke="url(#progressGradient)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={`${strokeDasharray}`}
|
||||
@@ -109,68 +103,68 @@ export function CalorieRingChart({
|
||||
|
||||
{/* 中心内容 */}
|
||||
<View style={styles.centerContent}>
|
||||
<ThemedText style={[styles.centerLabel, { color: textSecondaryColor }]}>
|
||||
还能吃
|
||||
<ThemedText style={styles.centerLabel}>
|
||||
{t('nutritionRecords.chart.remaining')}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.centerValue, { color: textColor }]}>
|
||||
{Math.round(canEat)}千卡
|
||||
<ThemedText style={styles.centerValue}>
|
||||
{Math.round(canEat)}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.centerUnit}>
|
||||
{t('nutritionRecords.nutrients.caloriesUnit')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧数据展示 */}
|
||||
{/* 右侧数据展示 - 优化布局 */}
|
||||
<View style={styles.dataContainer}>
|
||||
<View style={styles.dataBackground}>
|
||||
{/* 左右两列布局 */}
|
||||
<View style={styles.dataColumns}>
|
||||
{/* 左列:卡路里数据 */}
|
||||
<View style={styles.dataColumn}>
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(metabolism)}千卡
|
||||
</ThemedText>
|
||||
</View>
|
||||
{/* 公式 */}
|
||||
<View style={styles.formulaContainer}>
|
||||
<ThemedText style={styles.formulaText}>
|
||||
{t('nutritionRecords.chart.formula')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>运动</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(exercise)}千卡
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>饮食</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(consumed)}千卡
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右列:营养数据 */}
|
||||
<View style={styles.dataColumn}>
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>蛋白质</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(protein)}g
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>脂肪</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(fat)}g
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.dataItem}>
|
||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>碳水</ThemedText>
|
||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||
{Math.round(carbs)}g
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
{/* 代谢 & 运动 & 饮食 */}
|
||||
<View style={styles.statsGroup}>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotMetabolism} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.metabolism')}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.statValue}>{Math.round(metabolism)}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotExercise} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.exercise')}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.statValue}>{Math.round(exercise)}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.statRowCompact}>
|
||||
<View style={styles.labelWithDot}>
|
||||
<View style={styles.dotConsumed} />
|
||||
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.diet')}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.statValue}>{Math.round(consumed)}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* 营养素 - 水平排布 */}
|
||||
<View style={styles.nutritionRow}>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(protein)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.protein')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(fat)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.fat')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<ThemedText style={styles.statValueSmall}>{Math.round(carbs)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
|
||||
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.carbs')}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -181,40 +175,35 @@ export function CalorieRingChart({
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
marginHorizontal: 20,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 16,
|
||||
elevation: 6,
|
||||
},
|
||||
formulaContainer: {
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
formulaText: {
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
lineHeight: 16,
|
||||
color: '#94A3B8',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
mainContent: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 0, // 移除底部间距,因为不再有底部营养容器
|
||||
paddingHorizontal: 8,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
chartContainer: {
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 112, // 减少宽度以匹配更小的圆环 (48*2 + 8*2)
|
||||
flexShrink: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginTop: 8,
|
||||
},
|
||||
centerContent: {
|
||||
position: 'absolute',
|
||||
@@ -222,71 +211,95 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
centerLabel: {
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
marginBottom: 2,
|
||||
color: '#94A3B8',
|
||||
marginBottom: 1,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
centerValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333333',
|
||||
marginBottom: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
lineHeight: 24,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
centerPercentage: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
centerUnit: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
dataContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
marginLeft: 20,
|
||||
},
|
||||
dataBackground: {
|
||||
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 3,
|
||||
elevation: 1,
|
||||
// 添加边框增强毛玻璃效果
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255, 255, 255, 0.8)',
|
||||
gap: 4,
|
||||
statsGroup: {
|
||||
gap: 6,
|
||||
},
|
||||
dataItem: {
|
||||
statRowCompact: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
dataIcon: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
labelWithDot: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dataLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#999999',
|
||||
minWidth: 28,
|
||||
dotMetabolism: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#94A3B8',
|
||||
marginRight: 6,
|
||||
},
|
||||
dataValue: {
|
||||
fontSize: 11,
|
||||
dotExercise: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#4facfe',
|
||||
marginRight: 6,
|
||||
},
|
||||
dotConsumed: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#FF9966',
|
||||
marginRight: 6,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#333333',
|
||||
color: '#334155',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dataColumns: {
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
marginVertical: 10,
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
},
|
||||
dataColumn: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
nutritionItem: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
statLabelSmall: {
|
||||
fontSize: 10,
|
||||
color: '#94A3B8',
|
||||
marginTop: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
statValueSmall: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#475569',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
});
|
||||
@@ -463,16 +463,16 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
monthTitle: {
|
||||
fontSize: 22,
|
||||
fontSize: 26,
|
||||
fontWeight: '800',
|
||||
color: '#1a1a1a',
|
||||
letterSpacing: -0.5,
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
marginLeft: 8,
|
||||
},
|
||||
calendarIconButton: {
|
||||
padding: 4,
|
||||
borderRadius: 6,
|
||||
marginLeft: 4,
|
||||
padding: 6,
|
||||
borderRadius: 12,
|
||||
marginLeft: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
calendarIconFallback: {
|
||||
@@ -481,22 +481,19 @@ const styles = StyleSheet.create({
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
todayButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
marginRight: 8,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 16,
|
||||
marginRight: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
todayButtonFallback: {
|
||||
backgroundColor: '#EEF2FF',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(124, 58, 237, 0.2)',
|
||||
},
|
||||
todayButtonText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#7c3aed',
|
||||
letterSpacing: 0.2,
|
||||
color: '#5F6BF0',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
daysContainer: {
|
||||
@@ -508,8 +505,8 @@ const styles = StyleSheet.create({
|
||||
marginRight: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 40,
|
||||
height: 60,
|
||||
width: 48,
|
||||
height: 68,
|
||||
borderRadius: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -523,14 +520,12 @@ const styles = StyleSheet.create({
|
||||
transform: [{ scale: 0.96 }],
|
||||
},
|
||||
dayPillSelectedFallback: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: '#5F6BF0',
|
||||
shadowColor: 'rgba(95, 107, 240, 0.3)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
dayPillDisabled: {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -538,30 +533,30 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
color: '#8e8e93',
|
||||
marginBottom: 2,
|
||||
letterSpacing: 0.1,
|
||||
fontFamily: 'AliBold',
|
||||
fontWeight: '600',
|
||||
color: '#94A3B8',
|
||||
marginBottom: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
dayLabelSelected: {
|
||||
color: '#1a1a1a',
|
||||
fontWeight: '800',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
opacity: 0.9,
|
||||
},
|
||||
dayLabelDisabled: {
|
||||
color: '#c7c7cc',
|
||||
},
|
||||
dayDate: {
|
||||
fontSize: 13,
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: '#8e8e93',
|
||||
letterSpacing: -0.2,
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dayDateSelected: {
|
||||
color: '#1a1a1a',
|
||||
fontWeight: '800',
|
||||
fontSize: 16,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
dayDateDisabled: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { useRouter } from 'expo-router';
|
||||
@@ -20,6 +21,7 @@ interface FloatingFoodOverlayProps {
|
||||
|
||||
export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { pushIfAuthedElseLogin } = useAuthGuard()
|
||||
|
||||
@@ -41,21 +43,21 @@ export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: F
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'scan',
|
||||
title: 'AI识别',
|
||||
title: t('nutritionRecords.overlay.scan'),
|
||||
icon: '📷',
|
||||
backgroundColor: '#4FC3F7',
|
||||
onPress: handlePhotoRecognition,
|
||||
},
|
||||
{
|
||||
id: 'food-library',
|
||||
title: '食物库',
|
||||
title: t('nutritionRecords.overlay.foodLibrary'),
|
||||
icon: '🍎',
|
||||
backgroundColor: '#FF9500',
|
||||
onPress: handleFoodLibrary,
|
||||
},
|
||||
{
|
||||
id: 'voice-record',
|
||||
title: '一句话记录',
|
||||
title: t('nutritionRecords.overlay.voiceRecord'),
|
||||
icon: '🎤',
|
||||
backgroundColor: '#7B68EE',
|
||||
onPress: handleVoiceRecord,
|
||||
@@ -81,7 +83,7 @@ export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: F
|
||||
<View style={styles.container}>
|
||||
<BlurView intensity={80} tint="light" style={styles.blurContainer}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>记录方式</Text>
|
||||
<Text style={styles.title}>{t('nutritionRecords.overlay.title')}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.menuGrid}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
import { DietRecord } from '@/services/dietRecords';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -15,14 +16,6 @@ export type NutritionRecordCardProps = {
|
||||
onDelete?: () => void;
|
||||
};
|
||||
|
||||
const MEAL_TYPE_LABELS = {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
other: '其他',
|
||||
} as const;
|
||||
|
||||
const MEAL_TYPE_ICONS = {
|
||||
breakfast: 'sunny-outline',
|
||||
lunch: 'partly-sunny-outline',
|
||||
@@ -44,46 +37,40 @@ export function NutritionRecordCard({
|
||||
onPress,
|
||||
onDelete
|
||||
}: NutritionRecordCardProps) {
|
||||
const surfaceColor = useThemeColor({}, 'surface');
|
||||
const { t } = useI18n();
|
||||
const textColor = useThemeColor({}, 'text');
|
||||
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
||||
|
||||
// Popover 状态管理
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const popoverRef = useRef<any>(null);
|
||||
|
||||
// 左滑删除相关
|
||||
const swipeableRef = useRef<Swipeable>(null);
|
||||
|
||||
// 添加滑动状态管理,防止滑动时触发点击事件
|
||||
const [isSwiping, setIsSwiping] = useState(false);
|
||||
|
||||
// 营养数据统计
|
||||
const nutritionStats = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: '蛋白质',
|
||||
value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)}g` : '-',
|
||||
icon: '🥩',
|
||||
color: '#FF6B6B'
|
||||
label: t('nutritionRecords.nutrients.protein'),
|
||||
value: record.proteinGrams ? `${Math.round(record.proteinGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
{
|
||||
label: '脂肪',
|
||||
value: record.fatGrams ? `${record.fatGrams.toFixed(1)}g` : '-',
|
||||
icon: '🥑',
|
||||
color: '#FFB366'
|
||||
label: t('nutritionRecords.nutrients.fat'),
|
||||
value: record.fatGrams ? `${Math.round(record.fatGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
{
|
||||
label: '碳水',
|
||||
value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)}g` : '-',
|
||||
icon: '🍞',
|
||||
color: '#4ECDC4'
|
||||
label: t('nutritionRecords.nutrients.carbs'),
|
||||
value: record.carbohydrateGrams ? `${Math.round(record.carbohydrateGrams)}` : '-',
|
||||
unit: t('nutritionRecords.nutrients.unit'),
|
||||
color: '#64748B'
|
||||
},
|
||||
];
|
||||
}, [record]);
|
||||
}, [record, t]);
|
||||
|
||||
const mealTypeColor = MEAL_TYPE_COLORS[record.mealType];
|
||||
const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType];
|
||||
const mealTypeLabel = t(`nutritionRecords.mealTypes.${record.mealType}`);
|
||||
|
||||
// 处理点击事件,只有在非滑动状态下才触发
|
||||
const handlePress = () => {
|
||||
@@ -92,31 +79,17 @@ export function NutritionRecordCard({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理滑动开始
|
||||
const handleSwipeableWillOpen = () => {
|
||||
setIsSwiping(true);
|
||||
};
|
||||
const handleSwipeableWillOpen = () => setIsSwiping(true);
|
||||
const handleSwipeableClose = () => setTimeout(() => setIsSwiping(false), 100);
|
||||
|
||||
// 处理滑动结束
|
||||
const handleSwipeableClose = () => {
|
||||
// 延迟重置滑动状态,防止滑动结束时立即触发点击
|
||||
setTimeout(() => {
|
||||
setIsSwiping(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Alert.alert(
|
||||
'确认删除',
|
||||
`确定要删除这条营养记录吗?此操作无法撤销。`,
|
||||
t('nutritionRecords.delete.title'),
|
||||
t('nutritionRecords.delete.message'),
|
||||
[
|
||||
{ text: t('nutritionRecords.delete.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: '取消',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
text: t('nutritionRecords.delete.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
onDelete?.();
|
||||
@@ -127,7 +100,6 @@ export function NutritionRecordCard({
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染删除按钮
|
||||
const renderRightActions = () => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -136,7 +108,6 @@ export function NutritionRecordCard({
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
||||
<Text style={styles.deleteButtonText}>删除</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -152,239 +123,228 @@ export function NutritionRecordCard({
|
||||
onSwipeableClose={handleSwipeableClose}
|
||||
>
|
||||
<RectButton
|
||||
style={[
|
||||
styles.card,
|
||||
|
||||
]}
|
||||
style={styles.card}
|
||||
onPress={handlePress}
|
||||
// activeOpacity={0.7}
|
||||
>
|
||||
{/* 主要内容区域 - 水平布局 */}
|
||||
<View style={styles.mainContent}>
|
||||
{/* 左侧:食物图片 */}
|
||||
<View style={[styles.foodImageContainer, !record.imageUrl && styles.foodImagePlaceholder]}>
|
||||
{record.imageUrl ? (
|
||||
<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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import CustomCheckBox from '@/components/ui/CheckBox';
|
||||
import { USER_AGREEMENT_URL } from '@/constants/Agree';
|
||||
import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import {
|
||||
MEMBERSHIP_PLAN_META,
|
||||
extractMembershipProductsFromOfferings,
|
||||
@@ -65,51 +66,6 @@ interface BenefitItem {
|
||||
regular: PermissionConfig;
|
||||
}
|
||||
|
||||
// 权益对比配置
|
||||
const BENEFIT_COMPARISON: BenefitItem[] = [
|
||||
{
|
||||
title: 'AI拍照记录热量',
|
||||
description: '通过拍照识别食物并自动记录热量',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '无限次使用',
|
||||
vipText: '无限次使用'
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: '有限次使用',
|
||||
vipText: '每日3次'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'AI拍照识别包装',
|
||||
description: '识别食品包装上的营养成分信息',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '无限次使用',
|
||||
vipText: '无限次使用'
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: '有限次使用',
|
||||
vipText: '每日5次'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '每日健康提醒',
|
||||
description: '根据个人目标提供个性化健康提醒',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '完全支持',
|
||||
vipText: '智能提醒'
|
||||
},
|
||||
regular: {
|
||||
type: 'unlimited',
|
||||
text: '基础提醒',
|
||||
vipText: '基础提醒'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlanType, { gradient: readonly [string, string]; accent: string }> = {
|
||||
lifetime: {
|
||||
@@ -151,6 +107,7 @@ const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
|
||||
};
|
||||
|
||||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -165,6 +122,80 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 保存监听器引用,用于移除监听器
|
||||
const purchaseListenerRef = useRef<((customerInfo: CustomerInfo) => void) | null>(null);
|
||||
|
||||
// 权益对比配置 - Move inside component to use t function
|
||||
const benefitComparison: BenefitItem[] = [
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiCalories.title'),
|
||||
description: t('membershipModal.benefits.items.aiCalories.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 3 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiNutrition.title'),
|
||||
description: t('membershipModal.benefits.items.aiNutrition.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 5 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.healthReminder.title'),
|
||||
description: t('membershipModal.benefits.items.healthReminder.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.fullSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.smartReminder')
|
||||
},
|
||||
regular: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.basicSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.basicSupport')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiMedication.title'),
|
||||
description: t('membershipModal.benefits.items.aiMedication.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.fullAnalysis'),
|
||||
vipText: t('membershipModal.benefits.permissions.fullAnalysis')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.customChallenge.title'),
|
||||
description: t('membershipModal.benefits.items.customChallenge.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.createUnlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.createUnlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// 根据选中的产品生成tips内容
|
||||
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
|
||||
if (!product) return '';
|
||||
@@ -176,11 +207,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
switch (plan.type) {
|
||||
case 'lifetime':
|
||||
return '终身陪伴,见证您的每一次健康蜕变';
|
||||
return t('membershipModal.plans.lifetime.subtitle');
|
||||
case 'quarterly':
|
||||
return '3个月科学计划,让健康成为生活习惯';
|
||||
return t('membershipModal.plans.quarterly.subtitle');
|
||||
case 'weekly':
|
||||
return '7天体验期,感受专业健康指导的力量';
|
||||
return t('membershipModal.plans.weekly.subtitle');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -326,7 +357,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
// 显示成功提示
|
||||
GlobalToast.show({
|
||||
message: '会员开通成功',
|
||||
message: t('membershipModal.success.purchase'),
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
@@ -492,11 +523,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否已同意协议
|
||||
if (!agreementAccepted) {
|
||||
Alert.alert(
|
||||
'请阅读并同意相关协议',
|
||||
'购买前需要同意用户协议、会员协议和自动续费协议',
|
||||
t('membershipModal.agreements.alert.title'),
|
||||
t('membershipModal.agreements.alert.message'),
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -517,11 +548,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否选择了产品
|
||||
if (!selectedProduct) {
|
||||
Alert.alert(
|
||||
'请选择会员套餐',
|
||||
t('membershipModal.errors.selectPlan'),
|
||||
'',
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -579,32 +610,32 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
// 用户取消购买
|
||||
GlobalToast.show({
|
||||
message: '购买已取消',
|
||||
message: t('membershipModal.errors.purchaseCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR) {
|
||||
// 商品已拥有
|
||||
GlobalToast.show({
|
||||
message: '您已拥有此商品',
|
||||
message: t('membershipModal.errors.alreadyPurchased'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
// 网络错误
|
||||
GlobalToast.show({
|
||||
message: '网络连接失败',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
||||
// 支付待处理
|
||||
GlobalToast.show({
|
||||
message: '支付正在处理中',
|
||||
message: t('membershipModal.errors.paymentPending'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
// 凭据无效
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
// 其他错误
|
||||
GlobalToast.show({
|
||||
message: '购买失败',
|
||||
message: t('membershipModal.errors.purchaseFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -701,7 +732,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onClose?.();
|
||||
|
||||
GlobalToast.show({
|
||||
message: '恢复购买成功',
|
||||
message: t('membershipModal.errors.restoreSuccess'),
|
||||
});
|
||||
|
||||
} catch (apiError: any) {
|
||||
@@ -720,7 +751,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
|
||||
// 但不关闭弹窗,让用户知道可能需要重试
|
||||
GlobalToast.show({
|
||||
message: '恢复购买部分失败',
|
||||
message: t('membershipModal.errors.restorePartialFailed'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -734,7 +765,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeSubscriptionsCount: activeSubscriptionIds.length
|
||||
});
|
||||
GlobalToast.show({
|
||||
message: '没有找到购买记录',
|
||||
message: t('membershipModal.errors.noPurchasesFound'),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -754,19 +785,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 处理特定的恢复购买错误
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买已取消',
|
||||
message: t('membershipModal.errors.restoreCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '网络错误',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买失败',
|
||||
message: t('membershipModal.errors.restoreFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -780,7 +811,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||||
const planMeta = getPlanMetaById(product.identifier);
|
||||
const isSelected = selectedProduct === product;
|
||||
const displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
|
||||
// 优先使用翻译的标题,如果找不到 meta 则回退到产品标题
|
||||
let displayTitle = product.title;
|
||||
let displaySubtitle = planMeta?.subtitle ?? '';
|
||||
|
||||
if (planMeta) {
|
||||
displayTitle = t(`membershipModal.plans.${planMeta.type}.title`);
|
||||
displaySubtitle = t(`membershipModal.plans.${planMeta.type}.subtitle`);
|
||||
} else {
|
||||
// 如果没有 meta,尝试使用 resolvePlanDisplayName (虽然这里主要依赖 meta)
|
||||
displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
}
|
||||
|
||||
const priceLabel = product.priceString || '';
|
||||
const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined;
|
||||
|
||||
@@ -797,7 +840,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeOpacity={loading ? 1 : 0.8}
|
||||
accessible={true}
|
||||
accessibilityLabel={`${displayTitle} ${priceLabel}`}
|
||||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||||
accessibilityHint={loading ? t('membershipModal.loading.purchase') : t('membershipModal.actions.selectPlan', { plan: displayTitle })}
|
||||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||||
>
|
||||
<LinearGradient
|
||||
@@ -809,7 +852,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.planCardTopSection}>
|
||||
{planMeta?.tag && (
|
||||
<View style={styles.planTag}>
|
||||
<Text style={styles.planTagText}>{planMeta.tag}</Text>
|
||||
<Text style={styles.planTagText}>{t('membershipModal.plans.tag')}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||||
@@ -825,7 +868,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.planCardBottomSection}>
|
||||
<Text style={styles.planCardDescription}>{planMeta?.subtitle ?? ''}</Text>
|
||||
<Text style={styles.planCardDescription}>{displaySubtitle}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
@@ -854,8 +897,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
accessible={true}
|
||||
accessibilityLabel="返回"
|
||||
accessibilityHint="关闭会员购买弹窗"
|
||||
accessibilityLabel={t('membershipModal.actions.back')}
|
||||
accessibilityHint={t('membershipModal.actions.close')}
|
||||
style={styles.floatingBackButtonContainer}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
@@ -887,14 +930,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="star" size={16} color="#7B2CBF" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>会员套餐</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.sectionTitle.plans')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>灵活选择,跟随节奏稳步提升</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.sectionTitle.plansSubtitle')}</Text>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
{t('membershipModal.errors.noProducts')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -917,17 +960,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="checkbox" size={16} color="#FF9F0A" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>权益对比</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.benefits.title')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>核心权益一目了然,选择更安心</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.benefits.subtitle')}</Text>
|
||||
|
||||
<View style={styles.comparisonTable}>
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>权益</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>VIP</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>普通用户</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>{t('membershipModal.benefits.table.benefit')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>{t('membershipModal.benefits.table.vip')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>{t('membershipModal.benefits.table.regular')}</Text>
|
||||
</View>
|
||||
{BENEFIT_COMPARISON.map((row, index) => (
|
||||
{benefitComparison.map((row, index) => (
|
||||
<View
|
||||
key={row.title}
|
||||
style={[
|
||||
@@ -963,39 +1006,46 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.bottomSection}>
|
||||
<View style={styles.agreementRow}>
|
||||
<CustomCheckBox
|
||||
checked={agreementAccepted}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
size={16}
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
<Text style={styles.agreementPrefix}>开通即视为同意</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《用户协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
captureMessage('click membership agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《会员协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《自动续费协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.agreementContainer}>
|
||||
<View style={styles.checkboxWrapper}>
|
||||
<CustomCheckBox
|
||||
checked={agreementAccepted}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
size={16}
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.agreementText}>
|
||||
{t('membershipModal.agreements.prefix')}
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.userAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click membership agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.membershipAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.autoRenewalAgreement')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -1006,10 +1056,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{restoring ? (
|
||||
<View style={styles.restoreButtonContent}>
|
||||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restoring')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restore')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1031,15 +1081,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1047,10 +1097,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</GlassView>
|
||||
@@ -1066,15 +1116,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1082,10 +1132,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1168,12 +1218,14 @@ const styles = StyleSheet.create({
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2B2B2E',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#6B6B73',
|
||||
marginTop: 6,
|
||||
marginBottom: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
configurationNotice: {
|
||||
borderRadius: 16,
|
||||
@@ -1185,6 +1237,7 @@ const styles = StyleSheet.create({
|
||||
color: '#B86A04',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
plansContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1217,35 +1270,40 @@ const styles = StyleSheet.create({
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#2F2F36',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 6,
|
||||
marginBottom: 12,
|
||||
},
|
||||
planTagText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardOriginalPrice: {
|
||||
fontSize: 13,
|
||||
color: '#8E8EA1',
|
||||
textDecorationLine: 'line-through',
|
||||
marginTop: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardDescription: {
|
||||
fontSize: 12,
|
||||
color: '#6C6C77',
|
||||
lineHeight: 17,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardTopSection: {
|
||||
flex: 1,
|
||||
@@ -1275,6 +1333,7 @@ const styles = StyleSheet.create({
|
||||
color: '#9B6200',
|
||||
marginLeft: 6,
|
||||
lineHeight: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
comparisonTable: {
|
||||
borderRadius: 16,
|
||||
@@ -1298,10 +1357,12 @@ const styles = StyleSheet.create({
|
||||
color: '#575764',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.4,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
tableCellText: {
|
||||
fontSize: 13,
|
||||
color: '#3E3E44',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
tableTitleCell: {
|
||||
flex: 1.5,
|
||||
@@ -1361,6 +1422,7 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1369,29 +1431,34 @@ const styles = StyleSheet.create({
|
||||
loadingSpinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementRow: {
|
||||
agreementContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
marginBottom: 16,
|
||||
marginBottom: 20,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
agreementPrefix: {
|
||||
fontSize: 10,
|
||||
checkboxWrapper: {
|
||||
marginTop: 2, // Align with text line-height
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementText: {
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
color: '#666672',
|
||||
marginRight: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
agreementLink: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#E91E63',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
marginHorizontal: 2,
|
||||
textDecorationLine: 'underline',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
agreementSeparator: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#A0A0B0',
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
restoreButton: {
|
||||
alignSelf: 'center',
|
||||
@@ -1401,6 +1468,7 @@ const styles = StyleSheet.create({
|
||||
color: '#6F6F7A',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
disabledRestoreButton: {
|
||||
opacity: 0.5,
|
||||
@@ -1422,6 +1490,7 @@ const styles = StyleSheet.create({
|
||||
color: '#8E8E93',
|
||||
marginTop: 2,
|
||||
lineHeight: 14,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
permissionContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -1435,5 +1504,6 @@ const styles = StyleSheet.create({
|
||||
marginTop: 4,
|
||||
textAlign: 'center',
|
||||
lineHeight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -15,11 +20,11 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
|
||||
const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF'];
|
||||
const CTA_DISABLED_GRADIENT: [string, string] = ['#d3d7e8', '#c1c6da'];
|
||||
|
||||
export interface CreateCustomFoodModalProps {
|
||||
visible: boolean;
|
||||
@@ -43,9 +48,10 @@ export function CreateCustomFoodModal({
|
||||
onClose,
|
||||
onSave
|
||||
}: CreateCustomFoodModalProps) {
|
||||
const { t } = useI18n();
|
||||
const [foodName, setFoodName] = useState('');
|
||||
const [defaultAmount, setDefaultAmount] = useState('100');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState('千卡');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState(t('createCustomFood.units.kcal'));
|
||||
const [calories, setCalories] = useState('100');
|
||||
const [imageUrl, setImageUrl] = useState<string>('');
|
||||
const [protein, setProtein] = useState('0');
|
||||
@@ -93,7 +99,7 @@ export function CreateCustomFoodModal({
|
||||
if (visible) {
|
||||
setFoodName('');
|
||||
setDefaultAmount('100');
|
||||
setCaloriesUnit('千卡');
|
||||
setCaloriesUnit(t('createCustomFood.units.kcal'));
|
||||
setCalories('100');
|
||||
setImageUrl('');
|
||||
setProtein('0');
|
||||
@@ -102,16 +108,16 @@ export function CreateCustomFoodModal({
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// 选择热量单位
|
||||
|
||||
|
||||
// 选择图片
|
||||
const handleSelectImage = async () => {
|
||||
try {
|
||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||
if (!libGranted) {
|
||||
Alert.alert('权限不足', '需要相册权限以选择照片');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.permissionDenied.title'),
|
||||
t('createCustomFood.alerts.permissionDenied.message')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,11 +143,17 @@ export function CreateCustomFoodModal({
|
||||
setImageUrl(url);
|
||||
} catch (e) {
|
||||
console.warn('上传照片失败', e);
|
||||
Alert.alert('上传失败', '照片上传失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.uploadFailed.title'),
|
||||
t('createCustomFood.alerts.uploadFailed.message')
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('发生错误', '选择照片失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.error.title'),
|
||||
t('createCustomFood.alerts.error.message')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,12 +163,18 @@ export function CreateCustomFoodModal({
|
||||
// 保存自定义食物
|
||||
const handleSave = () => {
|
||||
if (!foodName.trim()) {
|
||||
Alert.alert('提示', '请输入食物名称');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.nameRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calories.trim() || parseFloat(calories) <= 0) {
|
||||
Alert.alert('提示', '请输入有效的热量值');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.caloriesRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,75 +193,99 @@ export function CreateCustomFoodModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isSaveDisabled = !foodName.trim() || !calories.trim();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="fade"
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={onClose}
|
||||
presentationStyle="overFullScreen"
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<BlurView intensity={20} tint="dark" style={styles.overlay}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<View style={[
|
||||
styles.modalContainer,
|
||||
keyboardHeight > 0 && {
|
||||
height: screenHeight - keyboardHeight,
|
||||
maxHeight: screenHeight - keyboardHeight,
|
||||
}
|
||||
]}>
|
||||
<TouchableOpacity activeOpacity={1} onPress={onClose} style={styles.dismissArea} />
|
||||
<View
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
keyboardHeight > 0 && {
|
||||
height: screenHeight - keyboardHeight - 60,
|
||||
maxHeight: screenHeight - keyboardHeight - 60,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.modalHeaderBar}>
|
||||
<View style={styles.dragIndicator} />
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 0
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 40,
|
||||
}}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton}>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton} activeOpacity={0.7}>
|
||||
<Ionicons name="close-circle" size={32} color="#E2E8F0" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>创建自定义食物</Text>
|
||||
<Text style={styles.headerTitle}>{t('createCustomFood.title')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.saveButton,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonDisabled
|
||||
]}
|
||||
style={[styles.saveButton, isSaveDisabled && styles.saveButtonDisabled]}
|
||||
onPress={handleSave}
|
||||
disabled={!foodName.trim() || !calories.trim()}
|
||||
disabled={isSaveDisabled}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[
|
||||
styles.saveButtonText,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonTextDisabled
|
||||
]}>保存</Text>
|
||||
<LinearGradient
|
||||
colors={isSaveDisabled ? CTA_DISABLED_GRADIENT : CTA_GRADIENT}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.saveButtonGradient}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>{t('createCustomFood.save')}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 效果预览区域 */}
|
||||
<View style={styles.previewSection}>
|
||||
<Text style={styles.sectionTitle}>效果预览</Text>
|
||||
<View style={styles.previewCard}>
|
||||
<LinearGradient
|
||||
colors={['#ffffff', '#F8F9FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<View style={styles.previewHeader}>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.preview.title')}</Text>
|
||||
</View>
|
||||
<View style={styles.previewContent}>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.previewImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.previewImagePlaceholder}>
|
||||
<Ionicons name="restaurant" size={20} color="#999" />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.imageWrapper}>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.previewImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.previewImagePlaceholder}>
|
||||
<Ionicons name="restaurant" size={24} color="#94A3B8" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.previewInfo}>
|
||||
<Text style={styles.previewName}>
|
||||
{foodName || '食物名称'}
|
||||
</Text>
|
||||
<Text style={styles.previewCalories}>
|
||||
{actualCalories}{caloriesUnit}/{defaultAmount}g
|
||||
<Text style={styles.previewName} numberOfLines={1}>
|
||||
{foodName || t('createCustomFood.preview.defaultName')}
|
||||
</Text>
|
||||
<View style={styles.previewBadge}>
|
||||
<Ionicons name="flame" size={14} color="#F59E0B" />
|
||||
<Text style={styles.previewCalories}>
|
||||
{actualCalories} {caloriesUnit} / {defaultAmount}
|
||||
{t('createCustomFood.units.g')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -252,21 +294,21 @@ export function CreateCustomFoodModal({
|
||||
{/* 基本信息 */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>基本信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.basicInfo.title')}</Text>
|
||||
<Text style={styles.requiredIndicator}>*</Text>
|
||||
</View>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 食物名称和单位 */}
|
||||
{/* 食物名称 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>食物名称</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.name')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={foodName}
|
||||
onChangeText={setFoodName}
|
||||
placeholder="例如,汉堡"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder={t('createCustomFood.basicInfo.namePlaceholder')}
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -274,36 +316,36 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 默认数量 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>默认数量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.defaultAmount')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={defaultAmount}
|
||||
onChangeText={setDefaultAmount}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>g</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.g')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 食物热量 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>食物热量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.calories')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={calories}
|
||||
onChangeText={setCalories}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>千卡</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.kcal')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -312,23 +354,26 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 可选信息 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>可选信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.optionalInfo.title')}</Text>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 照片 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>照片</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.photo')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<TouchableOpacity
|
||||
style={styles.modernImageSelector}
|
||||
<TouchableOpacity
|
||||
style={styles.modernImageSelector}
|
||||
onPress={handleSelectImage}
|
||||
disabled={uploading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.selectedImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.modernImagePlaceholder}>
|
||||
<Ionicons name="camera" size={28} color="#A0A0A0" />
|
||||
<Text style={styles.imagePlaceholderText}>添加照片</Text>
|
||||
<Ionicons name="camera-outline" size={28} color="#94A3B8" />
|
||||
<Text style={styles.imagePlaceholderText}>
|
||||
{t('createCustomFood.optionalInfo.addPhoto')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{uploading && (
|
||||
@@ -342,54 +387,56 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 蛋白质 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>蛋白质</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.protein')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={protein}
|
||||
onChangeText={setProtein}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 脂肪 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>脂肪</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.fat')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={fat}
|
||||
onChangeText={setFat}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 碳水化合物 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>碳水化合物</Text>
|
||||
<Text style={styles.inputRowLabel}>
|
||||
{t('createCustomFood.optionalInfo.carbohydrate')}
|
||||
</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={carbohydrate}
|
||||
onChangeText={setCarbohydrate}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -398,7 +445,7 @@ export function CreateCustomFoodModal({
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</BlurView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -408,331 +455,272 @@ const { height: screenHeight } = Dimensions.get('window');
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
dismissArea: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
marginTop: 50,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
backgroundColor: '#F1F5F9', // Slate 100
|
||||
borderTopLeftRadius: 32,
|
||||
borderTopRightRadius: 32,
|
||||
height: '90%',
|
||||
maxHeight: '90%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalHeaderBar: {
|
||||
width: '100%',
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
dragIndicator: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: '#CBD5E1',
|
||||
borderRadius: 2,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
backButton: {
|
||||
padding: 4,
|
||||
marginLeft: -8,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
flex: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
textAlign: 'center',
|
||||
marginHorizontal: 20,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
saveButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
opacity: 0.6,
|
||||
},
|
||||
saveButtonGradient: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
saveButtonText: {
|
||||
fontSize: 16,
|
||||
color: Colors.light.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
saveButtonTextDisabled: {
|
||||
color: Colors.light.textMuted,
|
||||
fontSize: 14,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewSection: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
previewCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.6)',
|
||||
},
|
||||
previewHeader: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
previewContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageWrapper: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
previewImage: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
previewImagePlaceholder: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#E5E5E5',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F1F5F9',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
},
|
||||
previewInfo: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
marginLeft: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
previewName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
marginBottom: 2,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1E293B',
|
||||
marginBottom: 6,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFBEB',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
alignSelf: 'flex-start',
|
||||
gap: 4,
|
||||
},
|
||||
previewCalories: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontSize: 13,
|
||||
color: '#D97706',
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
marginLeft: 8
|
||||
fontWeight: '700',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliBold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
requiredIndicator: {
|
||||
fontSize: 16,
|
||||
color: '#FF4444',
|
||||
fontSize: 14,
|
||||
color: '#EF4444',
|
||||
marginLeft: 4,
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowItem: {
|
||||
flex: 1,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernTextInput: {
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
marginLeft: 20,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
numberInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
textAlign: 'right',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modernSelectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
selectButtonText: {
|
||||
fontSize: 14,
|
||||
color: 'gray',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
selectedImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8F8F8',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 12,
|
||||
color: '#A0A0A0',
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
nutritionItem: {
|
||||
flex: 1,
|
||||
},
|
||||
// 保留旧样式以防兼容性问题
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
numberInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
textAlign: 'right',
|
||||
},
|
||||
inputWithUnit: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
inputUnit: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
minWidth: 30,
|
||||
},
|
||||
selectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
imageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imagePlaceholder: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F0F0F0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
},
|
||||
disclaimer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
lineHeight: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
sectionCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
},
|
||||
// 新增行布局样式
|
||||
inputRowContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputRowLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
width: 80,
|
||||
fontSize: 15,
|
||||
color: '#475569',
|
||||
fontWeight: '600',
|
||||
width: 90,
|
||||
marginRight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
inputRowContent: {
|
||||
flex: 1,
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
modernInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#1E293B',
|
||||
textAlign: 'right',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#94A3B8',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
selectedImage: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F8FAFC',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 11,
|
||||
color: '#94A3B8',
|
||||
marginTop: 4,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { formatTime, getSleepStageColor, SleepStage, type SleepSample } from '@/utils/sleepHealthKit';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -26,6 +27,7 @@ export const SleepStageTimeline = ({
|
||||
hideHeader = false,
|
||||
style
|
||||
}: SleepStageTimelineProps) => {
|
||||
const { t } = useI18n();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
@@ -139,7 +141,9 @@ export const SleepStageTimeline = ({
|
||||
<View style={[styles.container, { backgroundColor: colorTokens.background }, style]}>
|
||||
{!hideHeader && (
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>睡眠阶段图</Text>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
||||
{t('sleepDetail.sleepStages')}
|
||||
</Text>
|
||||
{onInfoPress && (
|
||||
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
|
||||
@@ -149,7 +153,7 @@ export const SleepStageTimeline = ({
|
||||
)}
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: colorTokens.textSecondary }]}>
|
||||
暂无睡眠阶段数据
|
||||
{t('sleepDetail.noData')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -161,7 +165,9 @@ export const SleepStageTimeline = ({
|
||||
{/* 标题栏 */}
|
||||
{!hideHeader && (
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>睡眠阶段图</Text>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
||||
{t('sleepDetail.sleepStages')}
|
||||
</Text>
|
||||
{onInfoPress && (
|
||||
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
|
||||
@@ -173,13 +179,17 @@ export const SleepStageTimeline = ({
|
||||
{/* 睡眠时间范围 */}
|
||||
<View style={styles.timeRange}>
|
||||
<View style={styles.timePoint}>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>入睡</Text>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.infoModalTitles.sleepTime')}
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colorTokens.text }]}>
|
||||
{formatTime(bedtime)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.timePoint}>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>起床</Text>
|
||||
<Text style={[styles.timeLabel, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.sleepDuration')}
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colorTokens.text }]}>
|
||||
{formatTime(wakeupTime)}
|
||||
</Text>
|
||||
@@ -233,21 +243,29 @@ export const SleepStageTimeline = ({
|
||||
<View style={styles.legendRow}>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Deep) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>深度睡眠</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.deep')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Core) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>核心睡眠</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.core')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.legendRow}>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.REM) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>快速眼动</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.rem')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Awake) }]} />
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>清醒时间</Text>
|
||||
<Text style={[styles.legendText, { color: colorTokens.textSecondary }]}>
|
||||
{t('sleepDetail.awake')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WeightHistoryItem } from '@/store/userSlice';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef } from 'react';
|
||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
|
||||
interface WeightRecordCardProps {
|
||||
@@ -58,124 +58,174 @@ export const WeightRecordCard: React.FC<WeightRecordCardProps> = ({
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
||||
<Text style={styles.deleteButtonText}>{t('weightRecords.card.deleteButton')}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
overshootRight={false}
|
||||
>
|
||||
<View
|
||||
style={[styles.recordCard]}
|
||||
<View style={styles.cardContainer}>
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
overshootRight={false}
|
||||
>
|
||||
<View style={styles.recordHeader}>
|
||||
<Text style={[styles.recordDateTime, { color: themeColors.textSecondary }]}>
|
||||
{dayjs(record.createdAt).format('MM[月]DD[日] HH:mm')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.recordEditButton}
|
||||
onPress={() => onPress?.(record)}
|
||||
>
|
||||
<Ionicons name="create-outline" size={16} color="#FF9500" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.recordContent}>
|
||||
<Text style={[styles.recordWeightLabel, { color: themeColors.textSecondary }]}>{t('weightRecords.card.weightLabel')}:</Text>
|
||||
<Text style={[styles.recordWeightValue, { color: themeColors.text }]}>{record.weight}{t('weightRecords.modal.unit')}</Text>
|
||||
{Math.abs(weightChange) > 0 && (
|
||||
<View style={[
|
||||
styles.weightChangeTag,
|
||||
{ backgroundColor: weightChange < 0 ? '#E8F5E8' : '#FFF2E8' }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={weightChange < 0 ? "arrow-down" : "arrow-up"}
|
||||
size={12}
|
||||
color={weightChange < 0 ? Colors.light.accentGreen : '#FF9500'}
|
||||
<View style={styles.recordCard}>
|
||||
<View style={styles.leftContent}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/iconWeight.png')}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.weightChangeText,
|
||||
{ color: weightChange < 0 ? Colors.light.accentGreen : '#FF9500' }
|
||||
]}>
|
||||
{Math.abs(weightChange).toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.textContent}>
|
||||
<View style={styles.dateTimeContainer}>
|
||||
<Text style={styles.dateText}>
|
||||
{dayjs(record.createdAt).format('MM-DD')}
|
||||
</Text>
|
||||
<Text style={styles.timeText}>
|
||||
{dayjs(record.createdAt).format('HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.weightInfo}>
|
||||
<Text style={styles.weightValue}>{record.weight}<Text style={styles.unitText}>{t('weightRecords.modal.unit')}</Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.rightContent}>
|
||||
{Math.abs(weightChange) > 0 && (
|
||||
<View style={[
|
||||
styles.changeTag,
|
||||
{ backgroundColor: weightChange < 0 ? '#E8F5E8' : '#FFF2E8' }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={weightChange < 0 ? "arrow-down" : "arrow-up"}
|
||||
size={10}
|
||||
color={weightChange < 0 ? '#22C55E' : '#FF9500'}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.changeText,
|
||||
{ color: weightChange < 0 ? '#22C55E' : '#FF9500' }
|
||||
]}>
|
||||
{Math.abs(weightChange).toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.editButton}
|
||||
onPress={() => onPress?.(record)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="ellipsis-vertical" size={16} color="#9ba3c7" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Swipeable>
|
||||
</Swipeable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardContainer: {
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
recordCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
recordHeader: {
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
recordDateTime: {
|
||||
fontSize: 14,
|
||||
color: '#687076',
|
||||
fontWeight: '500',
|
||||
},
|
||||
recordEditButton: {
|
||||
padding: 6,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||
},
|
||||
recordContent: {
|
||||
leftContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
recordWeightLabel: {
|
||||
fontSize: 16,
|
||||
color: '#687076',
|
||||
fontWeight: '500',
|
||||
},
|
||||
recordWeightValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#192126',
|
||||
marginLeft: 4,
|
||||
flex: 1,
|
||||
},
|
||||
weightChangeTag: {
|
||||
iconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: '#F0F2F5',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: '#4F5BD5',
|
||||
},
|
||||
textContent: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dateTimeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
marginLeft: 12,
|
||||
marginBottom: 4,
|
||||
},
|
||||
weightChangeText: {
|
||||
fontSize: 12,
|
||||
dateText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#1c1f3a',
|
||||
marginRight: 8,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
color: '#6f7ba7',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
weightInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
weightValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1c1f3a',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#6f7ba7',
|
||||
marginLeft: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
rightContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
changeTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 8,
|
||||
},
|
||||
changeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
marginLeft: 2,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
editButton: {
|
||||
padding: 4,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#EF4444',
|
||||
backgroundColor: '#FF6B6B',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
width: 70,
|
||||
borderRadius: 24,
|
||||
marginLeft: 12,
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user