feat: 更新应用名称及图标,新增HRV数据管理,优化营养记录展示
This commit is contained in:
@@ -172,7 +172,7 @@ export default function CoachScreen() {
|
||||
const generateWelcomeMessage = useCallback(() => {
|
||||
const hour = new Date().getHours();
|
||||
const name = userProfile?.name || '朋友';
|
||||
const botName = (params?.name || 'Health Bot').toString();
|
||||
const botName = (params?.name || '海豹助手').toString();
|
||||
|
||||
// 时段问候
|
||||
let timeGreeting = '';
|
||||
@@ -197,7 +197,7 @@ export default function CoachScreen() {
|
||||
messages: [
|
||||
`${timeGreeting},${name}!我是${botName},你的专属健康管理助手。新的一天开始了,让我们一起为你的健康目标努力吧!`,
|
||||
`${timeGreeting}!早晨是制定健康计划的最佳时机,我是${botName},可以帮你管理营养摄入、运动计划和生活作息。`,
|
||||
`${timeGreeting},${name}!作为你的Health Bot,我很高兴能陪伴你的健康之旅。无论是饮食营养、健身锻炼还是生活管理,我都能为你提供专业建议。`
|
||||
`${timeGreeting},${name}!作为你的海豹助手,我很高兴能陪伴你的健康之旅。无论是饮食营养、健身锻炼还是生活管理,我都能为你提供专业建议。`
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -213,7 +213,7 @@ export default function CoachScreen() {
|
||||
messages: [
|
||||
`${timeGreeting},${name}!午餐时间很关键呢,合理的营养搭配能为下午提供充足能量。我是${botName},可以为你分析饮食营养和热量管理。`,
|
||||
`${timeGreeting}!忙碌的上午结束了,该关注一下身体需求啦。我是你的健康助手${botName},无论是饮食调整、运动安排还是休息建议,都可以找我。`,
|
||||
`${timeGreeting},${name}!午间是调整状态的好时机。作为你的Health Bot,我建议关注饮食均衡和适度放松~`
|
||||
`${timeGreeting},${name}!午间是调整状态的好时机。作为你的海豹助手,我建议关注饮食均衡和适度放松~`
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -229,7 +229,7 @@ export default function CoachScreen() {
|
||||
messages: [
|
||||
`${timeGreeting},${name}!忙碌了一天,现在是时候关注身心平衡了。我是${botName},可以为你提供放松建议、营养补充和恢复方案。`,
|
||||
`${timeGreeting}!夜幕降临,这是一天中最适合总结和调整的时刻。我是你的健康伙伴${botName},让我们一起回顾今天的健康表现,规划明天的目标。`,
|
||||
`${timeGreeting},${name}!晚间时光属于你自己,也是关爱身体的珍贵时间。作为你的Health Bot,我想陪你聊聊如何更好地管理健康生活。`
|
||||
`${timeGreeting},${name}!晚间时光属于你自己,也是关爱身体的珍贵时间。作为你的海豹助手,我想陪你聊聊如何更好地管理健康生活。`
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -250,7 +250,7 @@ export default function CoachScreen() {
|
||||
},
|
||||
{
|
||||
condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0),
|
||||
message: `${timeGreeting},${name}!作为你的Health Bot,我想更好地了解你的健康需求。告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~`
|
||||
message: `${timeGreeting},${name}!作为你的海豹助手,我想更好地了解你的健康需求。告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~`
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1233,7 +1233,7 @@ export default function CoachScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={{ color: '#687076', fontSize: 12 }}>选择合适的方式记录您的饮食,Health Bot会根据您的饮食情况给出专业的营养建议。</Text>
|
||||
<Text style={{ color: '#687076', fontSize: 12 }}>选择合适的方式记录您的饮食,海豹助手会根据您的饮食情况给出专业的营养建议。</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1275,7 +1275,7 @@ export default function CoachScreen() {
|
||||
<Text style={{ color: '#192126', fontWeight: '700' }}>发送记录</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={{ color: '#687076', fontSize: 12 }}>详细描述您的饮食内容和分量,有助于Health Bot给出更精准的营养分析和建议。</Text>
|
||||
<Text style={{ color: '#687076', fontSize: 12 }}>详细描述您的饮食内容和分量,有助于海豹助手给出更精准的营养分析和建议。</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
export default function ExploreScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||
const userProfile = useAppSelector((s) => s.user.profile);
|
||||
|
||||
@@ -68,6 +69,12 @@ export default function ExploreScreen() {
|
||||
// HealthKit: 每次页面聚焦都拉取今日数据
|
||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||||
// 睡眠时长(分钟)
|
||||
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||||
// HRV数据
|
||||
const [hrvValue, setHrvValue] = useState<number>(69);
|
||||
const [hrvStatus, setHrvStatus] = useState<'放松' | '正常' | '紧张'>('正常');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||||
const [animToken, setAnimToken] = useState(0);
|
||||
@@ -108,6 +115,21 @@ export default function ExploreScreen() {
|
||||
if (latestRequestKeyRef.current === requestKey) {
|
||||
setStepCount(data.steps);
|
||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||
setSleepDuration(data.sleepDuration);
|
||||
|
||||
// 模拟HRV数据(实际应用中应从HealthKit获取)
|
||||
const simulatedHrv = Math.floor(Math.random() * 80) + 30; // 30-110ms范围
|
||||
setHrvValue(simulatedHrv);
|
||||
|
||||
// 根据HRV值判断状态
|
||||
if (simulatedHrv >= 70) {
|
||||
setHrvStatus('放松');
|
||||
} else if (simulatedHrv >= 40) {
|
||||
setHrvStatus('正常');
|
||||
} else {
|
||||
setHrvStatus('紧张');
|
||||
}
|
||||
|
||||
setAnimToken((t) => t + 1);
|
||||
} else {
|
||||
console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current);
|
||||
@@ -187,6 +209,61 @@ export default function ExploreScreen() {
|
||||
<Text style={styles.sectionTitle}>健康数据</Text>
|
||||
<WeightHistoryCard />
|
||||
|
||||
{/* HRV压力监测卡片 */}
|
||||
<View style={[styles.hrvCard, { backgroundColor: '#F0F7FF' }]}>
|
||||
<View style={styles.hrvHeader}>
|
||||
<View style={styles.hrvIconContainer}>
|
||||
<Ionicons name="heart" size={24} color="#3B82F6" />
|
||||
</View>
|
||||
<Text style={styles.hrvTitle}>压力监测</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.hrvContent}>
|
||||
<View style={styles.hrvValueContainer}>
|
||||
<Text style={styles.hrvValue}>{hrvValue}</Text>
|
||||
<Text style={styles.hrvUnit}>毫秒</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.hrvStatus,
|
||||
hrvStatus === '放松' ? { backgroundColor: '#DCFCE7' } :
|
||||
hrvStatus === '正常' ? { backgroundColor: '#FEF3C7' } :
|
||||
{ backgroundColor: '#FEE2E2' }
|
||||
]}>
|
||||
<Text style={[styles.hrvStatusText,
|
||||
hrvStatus === '放松' ? { color: '#166534' } :
|
||||
hrvStatus === '正常' ? { color: '#92400E' } :
|
||||
{ color: '#991B1B' }
|
||||
]}>{hrvStatus}</Text>
|
||||
<Text style={styles.hrvEmoji}>
|
||||
{hrvStatus === '放松' ? '😌' :
|
||||
hrvStatus === '正常' ? '😊' :
|
||||
'😰'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.hrvSliderContainer}>
|
||||
<View style={styles.hrvSliderTrack}>
|
||||
<View style={[styles.hrvSliderProgress, {
|
||||
width: `${Math.min(100, Math.max(0, (hrvValue / 150) * 100))}%`,
|
||||
backgroundColor: hrvStatus === '放松' ? '#10B981' :
|
||||
hrvStatus === '正常' ? '#F59E0B' : '#EF4444'
|
||||
}]} />
|
||||
</View>
|
||||
<View style={styles.hrvLabels}>
|
||||
<Text style={styles.hrvLabel}>放松</Text>
|
||||
<Text style={styles.hrvLabel}>正常</Text>
|
||||
<Text style={styles.hrvLabel}>紧张</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 查看更多 */}
|
||||
<View style={styles.viewMoreContainer}>
|
||||
<Text style={styles.viewMoreText}>查看更多</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#192126" style={styles.viewMoreIcon} />
|
||||
</View>
|
||||
|
||||
{/* 标题与日期选择 */}
|
||||
<Text style={styles.monthTitle}>{monthTitle}</Text>
|
||||
<ScrollView
|
||||
@@ -549,4 +626,114 @@ const styles = StyleSheet.create({
|
||||
padding: 4,
|
||||
marginLeft: 8,
|
||||
},
|
||||
viewMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
viewMoreText: {
|
||||
fontSize: 14,
|
||||
color: '#192126',
|
||||
},
|
||||
viewMoreIcon: {
|
||||
fontSize: 16,
|
||||
color: '#192126',
|
||||
marginLeft: 4,
|
||||
},
|
||||
hrvCard: {
|
||||
backgroundColor: '#F0F7FF',
|
||||
borderRadius: 22,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 5,
|
||||
},
|
||||
hrvHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
hrvIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#DBEAFE',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
hrvTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
},
|
||||
hrvContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
},
|
||||
hrvValueContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
hrvValue: {
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
marginRight: 4,
|
||||
},
|
||||
hrvUnit: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#64748B',
|
||||
},
|
||||
hrvStatus: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#DCFCE7',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
hrvStatusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#166534',
|
||||
marginRight: 4,
|
||||
},
|
||||
hrvEmoji: {
|
||||
fontSize: 16,
|
||||
},
|
||||
hrvSliderContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
hrvSliderTrack: {
|
||||
height: 8,
|
||||
backgroundColor: '#E2E8F0',
|
||||
borderRadius: 4,
|
||||
marginBottom: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
hrvSliderProgress: {
|
||||
height: '100%',
|
||||
backgroundColor: '#3B82F6',
|
||||
borderRadius: 4,
|
||||
},
|
||||
hrvLabels: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
hrvLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#64748B',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,11 +2,9 @@ import { NutritionRecordCard } from '@/components/NutritionRecordCard';
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { DietRecord } from '@/services/dietRecords';
|
||||
import { getMockDietRecords } from '@/services/mockDietRecords';
|
||||
import { DietRecord, getDietRecords } from '@/services/dietRecords';
|
||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
// import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import dayjs from 'dayjs';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
@@ -20,15 +18,12 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type ViewMode = 'daily' | 'all';
|
||||
|
||||
export default function NutritionRecordsScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
// const tabBarHeight = useBottomTabBarHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
||||
const days = getMonthDaysZh();
|
||||
@@ -83,24 +78,11 @@ export default function NutritionRecordsScreen() {
|
||||
|
||||
if (viewMode === 'daily') {
|
||||
// 按天查看时,获取选中日期的数据
|
||||
const selectedDate = days[selectedIndex]?.date?.format('YYYY-MM-DD') ?? dayjs().format('YYYY-MM-DD');
|
||||
startDate = selectedDate;
|
||||
endDate = selectedDate;
|
||||
startDate = days[selectedIndex]?.date.startOf('day').toISOString();
|
||||
endDate = days[selectedIndex]?.date.endOf('day').toISOString();
|
||||
}
|
||||
// viewMode === 'all' 时不设置日期范围,获取所有数据
|
||||
|
||||
// 使用模拟数据进行测试
|
||||
// const data = await getDietRecords({
|
||||
// startDate,
|
||||
// endDate,
|
||||
// page: currentPage,
|
||||
// limit: 10,
|
||||
// });
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const data = getMockDietRecords({
|
||||
const data = await getDietRecords({
|
||||
startDate,
|
||||
endDate,
|
||||
page: currentPage,
|
||||
@@ -140,7 +122,7 @@ export default function NutritionRecordsScreen() {
|
||||
|
||||
// 渲染视图模式切换器
|
||||
const renderViewModeToggle = () => (
|
||||
<View style={[styles.viewModeContainer, { backgroundColor: colorTokens.surface }]}>
|
||||
<View style={[styles.viewModeContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
|
||||
<Text style={[styles.monthTitle, { color: colorTokens.text }]}>{monthTitle}</Text>
|
||||
<View style={[styles.toggleContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
|
||||
<TouchableOpacity
|
||||
@@ -253,18 +235,32 @@ export default function NutritionRecordsScreen() {
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="restaurant-outline" size={64} color={colorTokens.textSecondary} />
|
||||
<Text style={[styles.emptyTitle, { color: colorTokens.text }]}>
|
||||
{viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtitle, { color: colorTokens.textSecondary }]}>
|
||||
{viewMode === 'daily' ? '点击右上角添加今日营养摄入' : '开始记录你的营养摄入吧'}
|
||||
</Text>
|
||||
<View style={styles.emptyTimelineContainer}>
|
||||
<View style={styles.emptyTimeline}>
|
||||
<View style={[styles.emptyTimelineDot, { backgroundColor: colorTokens.primary }]}>
|
||||
<Ionicons name="add-outline" size={16} color="#FFFFFF" />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.emptyContent}>
|
||||
<Ionicons name="restaurant-outline" size={48} color={colorTokens.textSecondary} />
|
||||
<Text style={[styles.emptyTitle, { color: colorTokens.text }]}>
|
||||
{viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtitle, { color: colorTokens.textSecondary }]}>
|
||||
{viewMode === 'daily' ? '开始记录今日营养摄入' : '开始记录你的营养摄入吧'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderRecord = ({ item }: { item: DietRecord }) => (
|
||||
<NutritionRecordCard record={item} />
|
||||
const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => (
|
||||
<NutritionRecordCard
|
||||
record={item}
|
||||
showTimeline={true}
|
||||
isFirst={index === 0}
|
||||
isLast={index === records.length - 1}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFooter = () => {
|
||||
@@ -296,17 +292,6 @@ export default function NutritionRecordsScreen() {
|
||||
<HeaderBar
|
||||
title="营养记录"
|
||||
onBack={() => router.back()}
|
||||
right={
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { backgroundColor: colorTokens.primary }]}
|
||||
onPress={() => {
|
||||
// TODO: 跳转到添加营养记录页面
|
||||
console.log('添加营养记录');
|
||||
}}
|
||||
>
|
||||
<Ionicons name="add" size={20} color={colorTokens.onPrimary} />
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
|
||||
{renderViewModeToggle()}
|
||||
@@ -322,11 +307,11 @@ export default function NutritionRecordsScreen() {
|
||||
) : (
|
||||
<FlatList
|
||||
data={records}
|
||||
renderItem={renderRecord}
|
||||
renderItem={({ item, index }) => renderRecord({ item, index })}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={[
|
||||
styles.listContainer,
|
||||
{ paddingBottom: 40 }
|
||||
{ paddingBottom: 40, paddingTop: 16 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
@@ -387,8 +372,8 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 34,
|
||||
marginRight: 12,
|
||||
alignItems: 'center',
|
||||
@@ -434,18 +419,47 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
emptyTimelineContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
maxWidth: 320,
|
||||
},
|
||||
emptyTimeline: {
|
||||
width: 64,
|
||||
alignItems: 'center',
|
||||
paddingTop: 8,
|
||||
},
|
||||
emptyTimelineDot: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
emptyContent: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginLeft: 16,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 20,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
lineHeight: 20,
|
||||
},
|
||||
footerContainer: {
|
||||
paddingVertical: 20,
|
||||
|
||||
Reference in New Issue
Block a user