feat: 新增营养摄入分析卡片并优化相关页面

- 在统计页面中引入营养摄入分析卡片,展示用户的营养数据
- 更新探索页面,增加营养数据加载逻辑,确保用户体验一致性
- 移除不再使用的推荐文章逻辑,简化代码结构
- 更新路由常量,确保路径管理集中化
- 优化体重历史记录卡片,提升用户交互体验
This commit is contained in:
richarjiang
2025-08-19 10:01:26 +08:00
parent c7d7255312
commit 9aa0a692a8
7 changed files with 452 additions and 201 deletions

View File

@@ -1,8 +1,9 @@
import { ROUTES } from '@/constants/Routes';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { fetchWeightHistory } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
@@ -26,13 +27,14 @@ type WeightHistoryItem = {
};
export function WeightHistoryCard() {
const router = useRouter();
const dispatch = useAppDispatch();
const userProfile = useAppSelector((s) => s.user.profile);
const weightHistory = useAppSelector((s) => s.user.weightHistory);
const [isLoading, setIsLoading] = useState(false);
const [showChart, setShowChart] = useState(false);
const { pushIfAuthedElseLogin } = useAuthGuard();
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
useEffect(() => {
@@ -53,7 +55,7 @@ export function WeightHistoryCard() {
};
const navigateToCoach = () => {
router.push('/(tabs)/coach');
pushIfAuthedElseLogin(ROUTES.TAB_COACH);
};
// 如果正在加载,显示加载状态
@@ -83,22 +85,19 @@ export function WeightHistoryCard() {
</View>
<Text style={styles.cardTitle}></Text>
</View>
<View style={styles.emptyContent}>
<View style={styles.emptyIconContainer}>
<Ionicons name="scale-outline" size={32} color="#BBF246" />
</View>
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyDescription}>
</Text>
<TouchableOpacity
style={styles.recordButton}
<TouchableOpacity
style={styles.recordButton}
onPress={navigateToCoach}
activeOpacity={0.8}
>
<Ionicons name="add" size={18} color="#FFFFFF" />
<Text style={styles.recordButtonText}></Text>
<Ionicons name="add" size={18} color="#192126" />
<Text style={styles.recordButtonText}></Text>
</TouchableOpacity>
</View>
</View>
@@ -119,13 +118,13 @@ export function WeightHistoryCard() {
</View>
<Text style={styles.cardTitle}></Text>
</View>
<View style={styles.emptyContent}>
<Text style={styles.emptyDescription}>
</Text>
<TouchableOpacity
style={styles.recordButton}
<TouchableOpacity
style={styles.recordButton}
onPress={navigateToCoach}
activeOpacity={0.8}
>
@@ -158,8 +157,8 @@ export function WeightHistoryCard() {
}).join(' ');
// 如果只有一个数据点,显示为水平线
const singlePointPath = points.length === 1 ?
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
const singlePointPath = points.length === 1 ?
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
pathData;
return (
@@ -170,19 +169,19 @@ export function WeightHistoryCard() {
</View>
<Text style={styles.cardTitle}></Text>
<View style={styles.headerButtons}>
<TouchableOpacity
style={styles.chartToggleButton}
<TouchableOpacity
style={styles.chartToggleButton}
onPress={() => setShowChart(!showChart)}
activeOpacity={0.8}
>
<Ionicons
name={showChart ? "chevron-up" : "chevron-down"}
size={16}
color="#BBF246"
<Ionicons
name={showChart ? "chevron-up" : "chevron-down"}
size={16}
color="#BBF246"
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.addButton}
<TouchableOpacity
style={styles.addButton}
onPress={navigateToCoach}
activeOpacity={0.8}
>
@@ -210,8 +209,8 @@ export function WeightHistoryCard() {
</Text>
</View>
</View>
<TouchableOpacity
style={styles.viewTrendButton}
<TouchableOpacity
style={styles.viewTrendButton}
onPress={() => setShowChart(true)}
activeOpacity={0.8}
>
@@ -226,100 +225,100 @@ export function WeightHistoryCard() {
<View style={styles.chartContainer}>
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
{/* 背景网格线 */}
{[0, 1, 2, 3, 4].map(i => (
<Line
key={`grid-${i}`}
x1={PADDING}
y1={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
x2={CHART_WIDTH - PADDING}
y2={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
stroke="#F0F0F0"
strokeWidth={1}
{[0, 1, 2, 3, 4].map(i => (
<Line
key={`grid-${i}`}
x1={PADDING}
y1={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
x2={CHART_WIDTH - PADDING}
y2={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
stroke="#F0F0F0"
strokeWidth={1}
/>
))}
{/* 折线 */}
<Path
d={singlePointPath}
stroke="#BBF246"
strokeWidth={3}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
))}
{/* 折线 */}
<Path
d={singlePointPath}
stroke="#BBF246"
strokeWidth={3}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* 数据点和标签 */}
{points.map((point, index) => {
const isLastPoint = index === points.length - 1;
const isFirstPoint = index === 0;
const showLabel = isFirstPoint || isLastPoint || points.length <= 3 || points.length === 1;
{/* 数据点和标签 */}
{points.map((point, index) => {
const isLastPoint = index === points.length - 1;
const isFirstPoint = index === 0;
const showLabel = isFirstPoint || isLastPoint || points.length <= 3 || points.length === 1;
return (
<React.Fragment key={index}>
<Circle
cx={point.x}
cy={point.y}
r={isLastPoint ? 6 : 4}
fill="#BBF246"
stroke="#FFFFFF"
strokeWidth={2}
/>
{/* 体重标签 - 只在关键点显示 */}
{showLabel && (
<>
<Circle
cx={point.x}
cy={point.y - 15}
r={10}
fill="rgba(255,255,255,0.9)"
stroke="#BBF246"
strokeWidth={1}
/>
<SvgText
x={point.x}
y={point.y - 12}
fontSize="9"
fill="#192126"
textAnchor="middle"
>
{point.weight}
</SvgText>
</>
)}
</React.Fragment>
);
})}
return (
<React.Fragment key={index}>
<Circle
cx={point.x}
cy={point.y}
r={isLastPoint ? 6 : 4}
fill="#BBF246"
stroke="#FFFFFF"
strokeWidth={2}
/>
{/* 体重标签 - 只在关键点显示 */}
{showLabel && (
<>
<Circle
cx={point.x}
cy={point.y - 15}
r={10}
fill="rgba(255,255,255,0.9)"
stroke="#BBF246"
strokeWidth={1}
/>
<SvgText
x={point.x}
y={point.y - 12}
fontSize="9"
fill="#192126"
textAnchor="middle"
>
{point.weight}
</SvgText>
</>
)}
</React.Fragment>
);
})}
</Svg>
</Svg>
{/* 图表底部信息 */}
<View style={styles.chartInfo}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{userProfile.weight}kg</Text>
{/* 图表底部信息 */}
<View style={styles.chartInfo}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{userProfile.weight}kg</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text>
</View>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
{/* 最近记录时间 */}
{sortedHistory.length > 0 && (
<Text style={styles.lastRecordText}>
{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
</Text>
</View>
)}
</View>
{/* 最近记录时间 */}
{sortedHistory.length > 0 && (
<Text style={styles.lastRecordText}>
{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
</Text>
)}
</View>
)}
</View>
)}
</View>
);
}
@@ -378,16 +377,6 @@ const styles = StyleSheet.create({
},
emptyContent: {
alignItems: 'center',
paddingVertical: 20,
},
emptyIconContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#F0F8E0',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
emptyTitle: {
fontSize: 16,