feat: 支持营养圆环
This commit is contained in:
@@ -16,7 +16,7 @@ export default function TabLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
initialRouteName="coach"
|
initialRouteName="statistics"
|
||||||
screenOptions={({ route }) => {
|
screenOptions={({ route }) => {
|
||||||
const routeName = route.name;
|
const routeName = route.name;
|
||||||
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
|
||||||
import { BasalMetabolismCard } from '@/components/BasalMetabolismCard';
|
import { BasalMetabolismCard } from '@/components/BasalMetabolismCard';
|
||||||
import { DateSelector } from '@/components/DateSelector';
|
import { DateSelector } from '@/components/DateSelector';
|
||||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||||
@@ -332,7 +331,7 @@ export default function ExploreScreen() {
|
|||||||
{/* 营养摄入雷达图卡片 */}
|
{/* 营养摄入雷达图卡片 */}
|
||||||
<NutritionRadarCard
|
<NutritionRadarCard
|
||||||
nutritionSummary={nutritionSummary}
|
nutritionSummary={nutritionSummary}
|
||||||
burnedCalories={basalMetabolism || 0}
|
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
|
||||||
calorieDeficit={0}
|
calorieDeficit={0}
|
||||||
resetToken={animToken}
|
resetToken={animToken}
|
||||||
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
|
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
|
||||||
@@ -355,28 +354,6 @@ export default function ExploreScreen() {
|
|||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
|
||||||
<FloatingCard style={styles.masonryCard} delay={500}>
|
|
||||||
<Text style={styles.cardTitle}>消耗卡路里</Text>
|
|
||||||
<View style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
marginTop: 20
|
|
||||||
}}>
|
|
||||||
{activeCalories != null ? (
|
|
||||||
<AnimatedNumber
|
|
||||||
value={activeCalories}
|
|
||||||
resetToken={animToken}
|
|
||||||
style={styles.caloriesValue}
|
|
||||||
format={(v) => `${Math.round(v)}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.caloriesValue}>——</Text>
|
|
||||||
)}
|
|
||||||
<Text style={styles.caloriesUnit}>千卡</Text>
|
|
||||||
</View>
|
|
||||||
</FloatingCard>
|
|
||||||
|
|
||||||
<FloatingCard style={styles.masonryCard}>
|
<FloatingCard style={styles.masonryCard}>
|
||||||
<StepsCard
|
<StepsCard
|
||||||
stepCount={stepCount}
|
stepCount={stepCount}
|
||||||
@@ -392,6 +369,15 @@ export default function ExploreScreen() {
|
|||||||
hrvValue={hrvValue}
|
hrvValue={hrvValue}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
{/* 心率卡片 */}
|
||||||
|
<FloatingCard style={styles.masonryCard} delay={2000}>
|
||||||
|
<HeartRateCard
|
||||||
|
resetToken={animToken}
|
||||||
|
style={styles.basalMetabolismCardOverride}
|
||||||
|
heartRate={heartRate}
|
||||||
|
/>
|
||||||
|
</FloatingCard>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 右列 */}
|
{/* 右列 */}
|
||||||
@@ -439,14 +425,6 @@ export default function ExploreScreen() {
|
|||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
{/* 心率卡片 */}
|
|
||||||
<FloatingCard style={styles.masonryCard} delay={2000}>
|
|
||||||
<HeartRateCard
|
|
||||||
resetToken={animToken}
|
|
||||||
style={styles.basalMetabolismCardOverride}
|
|
||||||
heartRate={heartRate}
|
|
||||||
/>
|
|
||||||
</FloatingCard>
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function SplashScreen() {
|
|||||||
// router.replace('/onboarding');
|
// router.replace('/onboarding');
|
||||||
// }
|
// }
|
||||||
// setIsLoading(false);
|
// setIsLoading(false);
|
||||||
router.replace(ROUTES.TAB_COACH);
|
router.replace(ROUTES.TAB_STATISTICS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查引导状态失败:', error);
|
console.error('检查引导状态失败:', error);
|
||||||
// 如果出现错误,默认显示引导页面
|
// 如果出现错误,默认显示引导页面
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import { CalorieRingChart } from '@/components/CalorieRingChart';
|
||||||
import { DateSelector } from '@/components/DateSelector';
|
import { DateSelector } from '@/components/DateSelector';
|
||||||
import { NutritionRecordCard } from '@/components/NutritionRecordCard';
|
import { NutritionRecordCard } from '@/components/NutritionRecordCard';
|
||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { DietRecord, deleteDietRecord, getDietRecords } from '@/services/dietRecords';
|
import { DietRecord, deleteDietRecord, getDietRecords } from '@/services/dietRecords';
|
||||||
|
import { selectHealthDataByDate } from '@/store/healthSlice';
|
||||||
|
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -24,12 +28,26 @@ type ViewMode = 'daily' | 'all';
|
|||||||
export default function NutritionRecordsScreen() {
|
export default function NutritionRecordsScreen() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
||||||
const days = getMonthDaysZh();
|
const days = getMonthDaysZh();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||||
const monthTitle = getMonthTitleZh();
|
const monthTitle = getMonthTitleZh();
|
||||||
|
|
||||||
|
// 获取当前选中日期
|
||||||
|
const getCurrentSelectedDate = () => {
|
||||||
|
return days[selectedIndex]?.date?.toDate() ?? new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentSelectedDate = getCurrentSelectedDate();
|
||||||
|
const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 从 Redux 获取数据
|
||||||
|
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
||||||
|
const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString));
|
||||||
|
const userProfile = useAppSelector((state) => state.user.profile);
|
||||||
|
|
||||||
// 视图模式:按天查看 vs 全部查看
|
// 视图模式:按天查看 vs 全部查看
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('daily');
|
const [viewMode, setViewMode] = useState<ViewMode>('daily');
|
||||||
|
|
||||||
@@ -40,41 +58,6 @@ export default function NutritionRecordsScreen() {
|
|||||||
const [hasMoreData, setHasMoreData] = useState(true);
|
const [hasMoreData, setHasMoreData] = useState(true);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// 日期滚动相关
|
|
||||||
const daysScrollRef = useRef<ScrollView | null>(null);
|
|
||||||
const [scrollWidth, setScrollWidth] = useState(0);
|
|
||||||
const DAY_PILL_WIDTH = 60; // 48px width + 12px marginRight = 60px total per item
|
|
||||||
const DAY_PILL_SPACING = 0; // spacing is included in the width above
|
|
||||||
|
|
||||||
// 日期滚动控制
|
|
||||||
const scrollToIndex = (index: number, animated = true) => {
|
|
||||||
if (scrollWidth <= 0) return;
|
|
||||||
|
|
||||||
const itemOffset = index * DAY_PILL_WIDTH;
|
|
||||||
const scrollViewCenterX = scrollWidth / 2;
|
|
||||||
const itemCenterX = DAY_PILL_WIDTH / 2;
|
|
||||||
const centerOffset = Math.max(0, itemOffset - scrollViewCenterX + itemCenterX);
|
|
||||||
|
|
||||||
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化时滚动到选中位置
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollWidth > 0) {
|
|
||||||
// 延迟滚动以确保ScrollView已经完全渲染
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToIndex(selectedIndex, false);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [scrollWidth]);
|
|
||||||
|
|
||||||
// 选中日期变化时滚动
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollWidth > 0) {
|
|
||||||
scrollToIndex(selectedIndex, true);
|
|
||||||
}
|
|
||||||
}, [selectedIndex]);
|
|
||||||
|
|
||||||
// 加载记录数据
|
// 加载记录数据
|
||||||
const loadRecords = async (isRefresh = false, loadMore = false) => {
|
const loadRecords = async (isRefresh = false, loadMore = false) => {
|
||||||
try {
|
try {
|
||||||
@@ -126,10 +109,50 @@ export default function NutritionRecordsScreen() {
|
|||||||
loadRecords();
|
loadRecords();
|
||||||
}, [selectedIndex, viewMode]);
|
}, [selectedIndex, viewMode]);
|
||||||
|
|
||||||
|
// 当选中日期变化时获取营养数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewMode === 'daily') {
|
||||||
|
dispatch(fetchDailyNutritionData(currentSelectedDate));
|
||||||
|
}
|
||||||
|
}, [selectedIndex, viewMode, currentSelectedDate, dispatch]);
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
loadRecords(true);
|
loadRecords(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算营养目标
|
||||||
|
const calculateNutritionGoals = () => {
|
||||||
|
const weight = parseFloat(userProfile?.weight || '70'); // 默认70kg
|
||||||
|
const height = parseFloat(userProfile?.height || '170'); // 默认170cm
|
||||||
|
const age = userProfile?.birthDate ?
|
||||||
|
dayjs().diff(dayjs(userProfile.birthDate), 'year') : 25; // 默认25岁
|
||||||
|
const isWoman = userProfile?.gender === 'female';
|
||||||
|
|
||||||
|
// 基础代谢率计算(Mifflin-St Jeor Equation)
|
||||||
|
let bmr;
|
||||||
|
if (isWoman) {
|
||||||
|
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
|
||||||
|
} else {
|
||||||
|
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总热量需求(假设轻度活动)
|
||||||
|
const totalCalories = bmr * 1.375;
|
||||||
|
|
||||||
|
// 计算营养素目标
|
||||||
|
const proteinGoal = weight * 1.6; // 1.6g/kg
|
||||||
|
const fatGoal = totalCalories * 0.25 / 9; // 25%来自脂肪,9卡/克
|
||||||
|
const carbsGoal = (totalCalories - proteinGoal * 4 - fatGoal * 9) / 4; // 剩余来自碳水
|
||||||
|
|
||||||
|
return {
|
||||||
|
proteinGoal: Math.round(proteinGoal * 10) / 10,
|
||||||
|
fatGoal: Math.round(fatGoal * 10) / 10,
|
||||||
|
carbsGoal: Math.round(carbsGoal * 10) / 10,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const nutritionGoals = calculateNutritionGoals();
|
||||||
|
|
||||||
const loadMoreRecords = () => {
|
const loadMoreRecords = () => {
|
||||||
if (hasMoreData && !loading && !refreshing) {
|
if (hasMoreData && !loading && !refreshing) {
|
||||||
loadRecords(false, true);
|
loadRecords(false, true);
|
||||||
@@ -254,6 +277,20 @@ export default function NutritionRecordsScreen() {
|
|||||||
{renderViewModeToggle()}
|
{renderViewModeToggle()}
|
||||||
{renderDateSelector()}
|
{renderDateSelector()}
|
||||||
|
|
||||||
|
{/* Calorie Ring Chart */}
|
||||||
|
<CalorieRingChart
|
||||||
|
metabolism={healthData?.basalEnergyBurned || 1482}
|
||||||
|
exercise={healthData?.activeEnergyBurned || 0}
|
||||||
|
consumed={nutritionSummary?.totalCalories || 0}
|
||||||
|
goal={userProfile?.dailyCaloriesGoal || 200}
|
||||||
|
protein={nutritionSummary?.totalProtein || 0}
|
||||||
|
fat={nutritionSummary?.totalFat || 0}
|
||||||
|
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
||||||
|
proteinGoal={nutritionGoals.proteinGoal}
|
||||||
|
fatGoal={nutritionGoals.fatGoal}
|
||||||
|
carbsGoal={nutritionGoals.carbsGoal}
|
||||||
|
/>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colorTokens.primary} />
|
<ActivityIndicator size="large" color={colorTokens.primary} />
|
||||||
|
|||||||
300
components/CalorieRingChart.tsx
Normal file
300
components/CalorieRingChart.tsx
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||||
|
|
||||||
|
export type CalorieRingChartProps = {
|
||||||
|
metabolism: number;
|
||||||
|
exercise: number;
|
||||||
|
consumed: number;
|
||||||
|
goal: number;
|
||||||
|
protein: number;
|
||||||
|
fat: number;
|
||||||
|
carbs: number;
|
||||||
|
proteinGoal: number;
|
||||||
|
fatGoal: number;
|
||||||
|
carbsGoal: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CalorieRingChart({
|
||||||
|
metabolism,
|
||||||
|
exercise,
|
||||||
|
consumed,
|
||||||
|
goal,
|
||||||
|
protein,
|
||||||
|
fat,
|
||||||
|
carbs,
|
||||||
|
proteinGoal,
|
||||||
|
fatGoal,
|
||||||
|
carbsGoal,
|
||||||
|
}: CalorieRingChartProps) {
|
||||||
|
const surfaceColor = useThemeColor({}, 'surface');
|
||||||
|
const textColor = useThemeColor({}, 'text');
|
||||||
|
const textSecondaryColor = useThemeColor({}, 'textSecondary');
|
||||||
|
|
||||||
|
// 动画值
|
||||||
|
const animatedProgress = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// 计算还能吃多少卡路里
|
||||||
|
const remainingCalories = metabolism + exercise - consumed - goal;
|
||||||
|
const canEat = Math.max(0, remainingCalories);
|
||||||
|
|
||||||
|
// 计算进度百分比 (用于圆环显示)
|
||||||
|
const totalAvailable = metabolism + exercise - goal;
|
||||||
|
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
||||||
|
|
||||||
|
// 圆环参数 - 更小的圆环以适应布局
|
||||||
|
const radius = 62;
|
||||||
|
const strokeWidth = 6;
|
||||||
|
const center = radius + strokeWidth;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const strokeDasharray = circumference;
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedProgress, {
|
||||||
|
toValue: progressPercentage,
|
||||||
|
duration: 600,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [progressPercentage]);
|
||||||
|
|
||||||
|
// 使用动画值计算strokeDashoffset
|
||||||
|
const strokeDashoffset = animatedProgress.interpolate({
|
||||||
|
inputRange: [0, 100],
|
||||||
|
outputRange: [circumference, 0],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { backgroundColor: surfaceColor }]}>
|
||||||
|
{/* 左上角公式展示 */}
|
||||||
|
<View style={styles.formulaContainer}>
|
||||||
|
<ThemedText style={[styles.formulaText, { color: textSecondaryColor }]}>
|
||||||
|
还能吃 = 代谢 + 运动 - 饮食 - 目标
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<View style={styles.mainContent}>
|
||||||
|
{/* 左侧圆环图 */}
|
||||||
|
<View style={styles.chartContainer}>
|
||||||
|
<Svg width={center * 2} height={center * 2}>
|
||||||
|
{/* 背景圆环 */}
|
||||||
|
<Circle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke="#F0F0F0"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* 进度圆环 - 保持固定颜色 */}
|
||||||
|
<AnimatedCircle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke={progressPercentage > 80 ? "#FF6B6B" : "#E0E0E0"}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={`${strokeDasharray}`}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
transform={`rotate(-90 ${center} ${center})`}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
|
||||||
|
{/* 中心内容 */}
|
||||||
|
<View style={styles.centerContent}>
|
||||||
|
<ThemedText style={[styles.centerLabel, { color: textSecondaryColor }]}>
|
||||||
|
还能吃
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={[styles.centerValue, { color: textColor }]}>
|
||||||
|
{canEat.toLocaleString()}千卡
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={[styles.centerPercentage, { color: textSecondaryColor }]}>
|
||||||
|
{Math.round(progressPercentage)}%
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧数据展示 */}
|
||||||
|
<View style={styles.dataContainer}>
|
||||||
|
{/* 各项数值 */}
|
||||||
|
<View style={styles.dataItem}>
|
||||||
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
||||||
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
|
{metabolism.toLocaleString()}千卡
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.dataItem}>
|
||||||
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>运动</ThemedText>
|
||||||
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
|
{exercise}千卡
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.dataItem}>
|
||||||
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>饮食</ThemedText>
|
||||||
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
|
{consumed}千卡
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.dataItem}>
|
||||||
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>目标</ThemedText>
|
||||||
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
|
{goal}千卡
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部营养素展示 */}
|
||||||
|
<View style={styles.nutritionContainer}>
|
||||||
|
<View style={styles.nutritionItem}>
|
||||||
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
||||||
|
蛋白质
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
||||||
|
{protein.toFixed(2)}/{proteinGoal.toFixed(2)}g
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.nutritionItem}>
|
||||||
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
||||||
|
脂肪
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
||||||
|
{fat.toFixed(2)}/{fatGoal.toFixed(2)}g
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.nutritionItem}>
|
||||||
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
||||||
|
碳水化合物
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
||||||
|
{carbs.toFixed(2)}/{carbsGoal.toFixed(2)}g
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: '#000000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.04,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
formulaContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
formulaText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#999999',
|
||||||
|
lineHeight: 16,
|
||||||
|
},
|
||||||
|
mainContent: {
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 16,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
chartContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: 140,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
centerContent: {
|
||||||
|
position: 'absolute',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
centerLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#999999',
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
centerValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#333333',
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
|
centerPercentage: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#999999',
|
||||||
|
},
|
||||||
|
dataContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 32,
|
||||||
|
gap: 4,
|
||||||
|
paddingLeft: 8,
|
||||||
|
},
|
||||||
|
dataItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
paddingVertical: 2,
|
||||||
|
},
|
||||||
|
dataIcon: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
dataLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#999999',
|
||||||
|
minWidth: 28,
|
||||||
|
},
|
||||||
|
dataValue: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333333',
|
||||||
|
},
|
||||||
|
nutritionContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingTop: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: 'rgba(0,0,0,0.06)',
|
||||||
|
},
|
||||||
|
nutritionItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
nutritionLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#999999',
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
nutritionValue: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333333',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -463,7 +463,10 @@
|
|||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
@@ -518,7 +521,10 @@
|
|||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = false;
|
USE_HERMES = false;
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -5895,6 +5895,7 @@
|
|||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/css-select/-/css-select-5.2.2.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/css-select/-/css-select-5.2.2.tgz",
|
||||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boolbase": "^1.0.0",
|
"boolbase": "^1.0.0",
|
||||||
"css-what": "^6.1.0",
|
"css-what": "^6.1.0",
|
||||||
@@ -5921,6 +5922,7 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/css-tree/-/css-tree-1.1.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdn-data": "2.0.14",
|
"mdn-data": "2.0.14",
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
@@ -5933,6 +5935,7 @@
|
|||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6179,6 +6182,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0",
|
"domelementtype": "^2.3.0",
|
||||||
"domhandler": "^5.0.2",
|
"domhandler": "^5.0.2",
|
||||||
@@ -6204,6 +6208,7 @@
|
|||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/domhandler/-/domhandler-5.0.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0"
|
"domelementtype": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -6218,6 +6223,7 @@
|
|||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/domutils/-/domutils-3.2.2.tgz",
|
||||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dom-serializer": "^2.0.0",
|
"dom-serializer": "^2.0.0",
|
||||||
"domelementtype": "^2.3.0",
|
"domelementtype": "^2.3.0",
|
||||||
|
|||||||
85
types/react-native-svg.d.ts
vendored
85
types/react-native-svg.d.ts
vendored
@@ -1,85 +0,0 @@
|
|||||||
declare module 'react-native-svg' {
|
|
||||||
import * as React from 'react';
|
|
||||||
import { ViewProps } from 'react-native';
|
|
||||||
|
|
||||||
export interface SvgProps extends ViewProps {
|
|
||||||
width?: number | string;
|
|
||||||
height?: number | string;
|
|
||||||
viewBox?: string;
|
|
||||||
}
|
|
||||||
export default function Svg(props: React.PropsWithChildren<SvgProps>): React.ReactElement | null;
|
|
||||||
|
|
||||||
export interface CommonProps {
|
|
||||||
fill?: string;
|
|
||||||
stroke?: string;
|
|
||||||
strokeWidth?: number;
|
|
||||||
strokeLinecap?: 'butt' | 'round' | 'square';
|
|
||||||
strokeLinejoin?: 'miter' | 'round' | 'bevel';
|
|
||||||
strokeDasharray?: string | number[];
|
|
||||||
strokeDashoffset?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CircleProps extends CommonProps {
|
|
||||||
cx?: number;
|
|
||||||
cy?: number;
|
|
||||||
r?: number;
|
|
||||||
originX?: number;
|
|
||||||
originY?: number;
|
|
||||||
}
|
|
||||||
export const Circle: React.ComponentType<CircleProps>;
|
|
||||||
|
|
||||||
export interface GProps extends CommonProps {
|
|
||||||
rotation?: number;
|
|
||||||
originX?: number;
|
|
||||||
originY?: number;
|
|
||||||
}
|
|
||||||
export const G: React.ComponentType<React.PropsWithChildren<GProps>>;
|
|
||||||
|
|
||||||
export interface DefsProps { }
|
|
||||||
export const Defs: React.ComponentType<React.PropsWithChildren<DefsProps>>;
|
|
||||||
|
|
||||||
export interface LineProps extends CommonProps {
|
|
||||||
x1?: number | string;
|
|
||||||
y1?: number | string;
|
|
||||||
x2?: number | string;
|
|
||||||
y2?: number | string;
|
|
||||||
}
|
|
||||||
export const Line: React.ComponentType<LineProps>;
|
|
||||||
|
|
||||||
export interface LinearGradientProps {
|
|
||||||
id?: string;
|
|
||||||
x1?: number | string;
|
|
||||||
y1?: number | string;
|
|
||||||
x2?: number | string;
|
|
||||||
y2?: number | string;
|
|
||||||
}
|
|
||||||
export const LinearGradient: React.ComponentType<React.PropsWithChildren<LinearGradientProps>>;
|
|
||||||
|
|
||||||
export interface StopProps {
|
|
||||||
offset?: number | string;
|
|
||||||
stopColor?: string;
|
|
||||||
stopOpacity?: number;
|
|
||||||
}
|
|
||||||
export const Stop: React.ComponentType<StopProps>;
|
|
||||||
|
|
||||||
export interface PolygonProps extends CommonProps {
|
|
||||||
points?: string;
|
|
||||||
}
|
|
||||||
export const Polygon: React.ComponentType<PolygonProps>;
|
|
||||||
|
|
||||||
export interface PathProps extends CommonProps {
|
|
||||||
d?: string;
|
|
||||||
}
|
|
||||||
export const Path: React.ComponentType<PathProps>;
|
|
||||||
|
|
||||||
export interface TextProps extends CommonProps {
|
|
||||||
x?: number | string;
|
|
||||||
y?: number | string;
|
|
||||||
fontSize?: number | string;
|
|
||||||
fill?: string;
|
|
||||||
textAnchor?: 'start' | 'middle' | 'end';
|
|
||||||
}
|
|
||||||
export const Text: React.ComponentType<React.PropsWithChildren<TextProps>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user