diff --git a/app.json b/app.json
index 0f4f770..fa5fa0f 100644
--- a/app.json
+++ b/app.json
@@ -1,10 +1,10 @@
{
"expo": {
- "name": "Health Bot",
+ "name": "Sealife",
"slug": "digital-pilates",
"version": "1.0.4",
"orientation": "portrait",
- "icon": "./assets/images/logo.png",
+ "icon": "./assets/images/Sealife.jpeg",
"scheme": "digitalpilates",
"userInterfaceStyle": "light",
"newArchEnabled": true,
@@ -21,7 +21,7 @@
},
"android": {
"adaptiveIcon": {
- "foregroundImage": "./assets/images/logo.png",
+ "foregroundImage": "./assets/images/Sealife.jpeg",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
@@ -30,14 +30,14 @@
"web": {
"bundler": "metro",
"output": "static",
- "favicon": "./assets/images/logo.png"
+ "favicon": "./assets/images/Sealife.jpeg"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
- "image": "./assets/images/logo.png",
+ "image": "./assets/images/Sealife.jpeg",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff"
diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx
index 36aeead..28fa6f0 100644
--- a/app/(tabs)/coach.tsx
+++ b/app/(tabs)/coach.tsx
@@ -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() {
- 选择合适的方式记录您的饮食,Health Bot会根据您的饮食情况给出专业的营养建议。
+ 选择合适的方式记录您的饮食,海豹助手会根据您的饮食情况给出专业的营养建议。
);
}
@@ -1275,7 +1275,7 @@ export default function CoachScreen() {
发送记录
- 详细描述您的饮食内容和分量,有助于Health Bot给出更精准的营养分析和建议。
+ 详细描述您的饮食内容和分量,有助于海豹助手给出更精准的营养分析和建议。
);
}
diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index 44b557d..d9422f2 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -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(null);
const [activeCalories, setActiveCalories] = useState(null);
+ // 睡眠时长(分钟)
+ const [sleepDuration, setSleepDuration] = useState(null);
+ // HRV数据
+ const [hrvValue, setHrvValue] = useState(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() {
健康数据
+ {/* HRV压力监测卡片 */}
+
+
+
+
+
+ 压力监测
+
+
+
+
+ {hrvValue}
+ 毫秒
+
+
+
+ {hrvStatus}
+
+ {hrvStatus === '放松' ? '😌' :
+ hrvStatus === '正常' ? '😊' :
+ '😰'}
+
+
+
+
+
+
+
+
+
+ 放松
+ 正常
+ 紧张
+
+
+
+
+ {/* 查看更多 */}
+
+ 查看更多
+
+
+
{/* 标题与日期选择 */}
{monthTitle}
setTimeout(resolve, 800));
-
- const data = getMockDietRecords({
+ const data = await getDietRecords({
startDate,
endDate,
page: currentPage,
@@ -140,7 +122,7 @@ export default function NutritionRecordsScreen() {
// 渲染视图模式切换器
const renderViewModeToggle = () => (
-
+
{monthTitle}
(
-
-
- {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'}
-
-
- {viewMode === 'daily' ? '点击右上角添加今日营养摄入' : '开始记录你的营养摄入吧'}
-
+
+
+
+
+
+
+
+
+
+ {viewMode === 'daily' ? '今天还没有记录' : '暂无营养记录'}
+
+
+ {viewMode === 'daily' ? '开始记录今日营养摄入' : '开始记录你的营养摄入吧'}
+
+
+
);
- const renderRecord = ({ item }: { item: DietRecord }) => (
-
+ const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => (
+
);
const renderFooter = () => {
@@ -296,17 +292,6 @@ export default function NutritionRecordsScreen() {
router.back()}
- right={
- {
- // TODO: 跳转到添加营养记录页面
- console.log('添加营养记录');
- }}
- >
-
-
- }
/>
{renderViewModeToggle()}
@@ -322,11 +307,11 @@ export default function NutritionRecordsScreen() {
) : (
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,
diff --git a/assets/images/Sealife.jpeg b/assets/images/Sealife.jpeg
new file mode 100644
index 0000000..2d9574b
Binary files /dev/null and b/assets/images/Sealife.jpeg differ
diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx
index 0d2ae48..800f20e 100644
--- a/components/NutritionRecordCard.tsx
+++ b/components/NutritionRecordCard.tsx
@@ -1,7 +1,6 @@
-import { RadarChart } from '@/components/RadarChart';
import { ThemedText } from '@/components/ThemedText';
import { useThemeColor } from '@/hooks/useThemeColor';
-import { DietRecord, calculateNutritionSummary, convertToRadarData } from '@/services/dietRecords';
+import { DietRecord } from '@/services/dietRecords';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import React, { useMemo } from 'react';
@@ -10,17 +9,11 @@ import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
export type NutritionRecordCardProps = {
record: DietRecord;
onPress?: () => void;
+ showTimeline?: boolean;
+ isFirst?: boolean;
+ isLast?: boolean;
};
-const NUTRITION_DIMENSIONS = [
- { key: 'calories', label: '热量' },
- { key: 'protein', label: '蛋白质' },
- { key: 'carbohydrate', label: '碳水' },
- { key: 'fat', label: '脂肪' },
- { key: 'fiber', label: '纤维' },
- { key: 'sodium', label: '钠' },
-];
-
const MEAL_TYPE_LABELS = {
breakfast: '早餐',
lunch: '午餐',
@@ -45,55 +38,45 @@ const MEAL_TYPE_COLORS = {
other: '#9AA3AE',
} as const;
-export function NutritionRecordCard({ record, onPress }: NutritionRecordCardProps) {
+export function NutritionRecordCard({
+ record,
+ onPress,
+ showTimeline = false,
+ isFirst = false,
+ isLast = false
+}: NutritionRecordCardProps) {
const surfaceColor = useThemeColor({}, 'surface');
const textColor = useThemeColor({}, 'text');
const textSecondaryColor = useThemeColor({}, 'textSecondary');
const primaryColor = useThemeColor({}, 'primary');
- // 计算单条记录的营养摘要
- const nutritionSummary = useMemo(() => {
- return calculateNutritionSummary([record]);
- }, [record]);
-
- // 计算雷达图数据
- const radarValues = useMemo(() => {
- return convertToRadarData(nutritionSummary);
- }, [nutritionSummary]);
-
- // 营养维度数据
+ // 营养数据统计
const nutritionStats = useMemo(() => {
return [
{
label: '热量',
value: record.estimatedCalories ? `${Math.round(record.estimatedCalories)} 千卡` : '-',
+ icon: 'flame-outline' as const,
color: '#FF6B6B'
},
{
label: '蛋白质',
value: record.proteinGrams ? `${record.proteinGrams.toFixed(1)} g` : '-',
+ icon: 'fitness-outline' as const,
color: '#4ECDC4'
},
{
label: '碳水',
value: record.carbohydrateGrams ? `${record.carbohydrateGrams.toFixed(1)} g` : '-',
+ icon: 'leaf-outline' as const,
color: '#45B7D1'
},
{
label: '脂肪',
value: record.fatGrams ? `${record.fatGrams.toFixed(1)} g` : '-',
+ icon: 'water-outline' as const,
color: '#FFA07A'
},
- {
- label: '纤维',
- value: record.fiberGrams ? `${record.fiberGrams.toFixed(1)} g` : '-',
- color: '#98D8C8'
- },
- {
- label: '钠',
- value: record.sodiumMg ? `${Math.round(record.sodiumMg)} mg` : '-',
- color: '#F7DC6F'
- },
];
}, [record]);
@@ -102,170 +85,173 @@ export function NutritionRecordCard({ record, onPress }: NutritionRecordCardProp
const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType];
return (
-
- {/* 卡片头部 */}
-
-
-
-
-
-
-
- {mealTypeLabel}
-
-
- {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'}
+
+ {/* 时间轴 */}
+ {showTimeline && (
+
+
+
+ {record.createdAt ? dayjs(record.createdAt).format('HH:mm') : '--:--'}
-
-
-
-
-
-
-
- {/* 食物信息 */}
-
-
- {record.imageUrl ? (
-
- ) : (
-
- )}
-
-
-
- {record.foodName}
-
- {record.foodDescription && (
-
- {record.foodDescription}
-
- )}
- {(record.weightGrams || record.portionDescription) && (
-
- {record.weightGrams ? `${record.weightGrams}g` : ''}
- {record.weightGrams && record.portionDescription ? ' • ' : ''}
- {record.portionDescription || ''}
-
- )}
-
-
-
- {/* 营养分析区域 */}
-
-
-
-
-
-
- {nutritionStats.slice(0, 4).map((stat) => (
-
-
-
- {stat.label}
-
-
- {stat.value}
-
+
+
+
- ))}
-
-
-
- {/* 额外的营养信息 */}
-
- {nutritionStats.slice(4).map((stat) => (
-
-
-
- {stat.label}
-
-
- {stat.value}
-
+ {!isLast && (
+
+ )}
- ))}
-
-
- {/* 备注信息 */}
- {record.notes && (
-
-
- 备注
-
-
- {record.notes}
-
)}
-
+
+ {/* 卡片内容 */}
+
+ {/* 主要内容区域 */}
+
+ {/* 左侧:食物图片 */}
+
+ {record.imageUrl ? (
+
+ ) : (
+
+ )}
+
+
+ {/* 右侧:食物信息 */}
+
+ {/* 餐次和操作按钮 */}
+
+
+
+
+ {mealTypeLabel}
+
+
+ {!showTimeline && (
+
+ {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'}
+
+ )}
+
+
+
+
+
+
+ {/* 食物名称和分量 */}
+
+
+ {record.foodName}
+
+ {(record.weightGrams || record.portionDescription) && (
+
+ {record.portionDescription || `${record.weightGrams}g`}
+
+ )}
+
+
+ {/* 营养信息网格 */}
+
+ {nutritionStats.map((stat) => (
+
+
+
+
+ {stat.label}
+
+
+
+ {stat.value}
+
+
+ ))}
+
+
+ {/* 备注信息 */}
+ {record.notes && (
+
+
+ {record.notes}
+
+
+ )}
+
+
+
+
);
}
const styles = StyleSheet.create({
- card: {
- borderRadius: 22,
- padding: 20,
+ timelineContainer: {
+ flexDirection: 'row',
marginBottom: 12,
+ },
+ timelineColumn: {
+ width: 64,
+ alignItems: 'center',
+ paddingTop: 8,
+ },
+ timeContainer: {
+ marginBottom: 8,
+ },
+ timeText: {
+ fontSize: 12,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ timelineNode: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ timelineDot: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.08,
- shadowRadius: 6,
- elevation: 3,
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
},
- cardHeader: {
+ timelineLine: {
+ width: 2,
+ flex: 1,
+ marginTop: 8,
+ opacity: 0.3,
+ },
+ card: {
+ flex: 1,
+ borderRadius: 16,
+ padding: 16,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.06,
+ shadowRadius: 8,
+ elevation: 2,
+ },
+ cardWithTimeline: {
+ marginLeft: 8,
+ },
+ mainContent: {
flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- },
- mealInfo: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- mealTypeIndicator: {
- width: 36,
- height: 36,
- borderRadius: 18,
- justifyContent: 'center',
- alignItems: 'center',
- marginRight: 12,
- },
- mealDetails: {
- justifyContent: 'center',
- },
- mealType: {
- fontSize: 16,
- fontWeight: '700',
- },
- mealTime: {
- fontSize: 13,
- fontWeight: '500',
- marginTop: 2,
- },
- moreButton: {
- padding: 4,
- },
- foodSection: {
- flexDirection: 'row',
- marginBottom: 20,
},
foodImageContainer: {
- width: 60,
- height: 60,
- borderRadius: 12,
- marginRight: 12,
+ width: 80,
+ height: 80,
+ borderRadius: 16,
+ marginRight: 16,
overflow: 'hidden',
},
foodImage: {
@@ -273,86 +259,88 @@ const styles = StyleSheet.create({
height: '100%',
},
foodImagePlaceholder: {
- backgroundColor: '#F5F5F5',
+ backgroundColor: '#F8F9FA',
justifyContent: 'center',
alignItems: 'center',
},
- foodInfo: {
+ foodInfoContainer: {
flex: 1,
- justifyContent: 'center',
+ },
+ headerRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: 8,
+ },
+ mealTypeContainer: {
+ flex: 1,
+ },
+ mealTypeBadge: {
+ alignSelf: 'flex-start',
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 8,
+ marginBottom: 4,
+ },
+ mealTypeText: {
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ mealTime: {
+ fontSize: 11,
+ fontWeight: '500',
+ },
+ moreButton: {
+ padding: 4,
+ marginLeft: 8,
+ },
+ foodNameSection: {
+ marginBottom: 12,
},
foodName: {
fontSize: 18,
fontWeight: '700',
- marginBottom: 4,
- },
- foodDescription: {
- fontSize: 14,
- fontWeight: '500',
- lineHeight: 20,
- marginBottom: 4,
+ lineHeight: 24,
+ marginBottom: 2,
},
portionInfo: {
- fontSize: 13,
- fontWeight: '600',
- },
- nutritionSection: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 16,
- },
- radarContainer: {
- marginRight: 16,
- },
- statsContainer: {
- flex: 1,
- },
- statItem: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 12,
- },
- statDot: {
- width: 8,
- height: 8,
- borderRadius: 4,
- marginRight: 8,
- },
- statLabel: {
- fontSize: 13,
- fontWeight: '600',
- flex: 1,
- },
- statValue: {
- fontSize: 13,
- fontWeight: '700',
- },
- additionalStats: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: 12,
- },
- additionalStatItem: {
- flexDirection: 'row',
- alignItems: 'center',
- flex: 1,
- },
- notesSection: {
- marginTop: 12,
- paddingTop: 16,
- borderTopWidth: 1,
- borderTopColor: 'rgba(0,0,0,0.08)',
- },
- notesLabel: {
- fontSize: 12,
- fontWeight: '600',
- marginBottom: 6,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- notesText: {
fontSize: 14,
fontWeight: '500',
- lineHeight: 20,
+ },
+ nutritionGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginBottom: 8,
+ },
+ nutritionItem: {
+ width: '50%',
+ marginBottom: 8,
+ paddingRight: 8,
+ },
+ nutritionItemHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 2,
+ },
+ nutritionLabel: {
+ fontSize: 12,
+ fontWeight: '500',
+ marginLeft: 4,
+ },
+ nutritionValue: {
+ fontSize: 14,
+ fontWeight: '700',
+ },
+ notesSection: {
+ marginTop: 8,
+ paddingTop: 12,
+ borderTopWidth: 1,
+ borderTopColor: 'rgba(0,0,0,0.06)',
+ },
+ notesText: {
+ fontSize: 13,
+ fontWeight: '500',
+ lineHeight: 18,
+ fontStyle: 'italic',
},
});
diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj
index 755430f..4658dfe 100644
--- a/ios/digitalpilates.xcodeproj/project.pbxproj
+++ b/ios/digitalpilates.xcodeproj/project.pbxproj
@@ -330,6 +330,7 @@
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = digitalpilates/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Sealife;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -367,6 +368,7 @@
DEVELOPMENT_TEAM = 756WVXJ6MT;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
INFOPLIST_FILE = digitalpilates/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Sealife;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -447,10 +449,7 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -505,10 +504,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = false;
diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist
index b7b771b..6fc1c63 100644
--- a/ios/digitalpilates/Info.plist
+++ b/ios/digitalpilates/Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Health Bot
+ Sealife
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/services/mockDietRecords.ts b/services/mockDietRecords.ts
deleted file mode 100644
index 5ad0c3b..0000000
--- a/services/mockDietRecords.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-import dayjs from 'dayjs';
-import { DietRecord } from './dietRecords';
-
-// 模拟营养记录数据,用于测试UI效果
-export const mockDietRecords: DietRecord[] = [
- // 今天的记录
- {
- id: 1,
- mealType: 'breakfast',
- foodName: '燕麦粥配蓝莓',
- foodDescription: '有机燕麦片,新鲜蓝莓,低脂牛奶',
- weightGrams: 300,
- portionDescription: '1大碗',
- estimatedCalories: 280,
- proteinGrams: 12.5,
- carbohydrateGrams: 45.2,
- fatGrams: 6.8,
- fiberGrams: 8.5,
- sugarGrams: 15.3,
- sodiumMg: 120,
- source: 'manual',
- mealTime: dayjs().hour(7).minute(30).toISOString(),
- imageUrl: 'https://images.unsplash.com/photo-1511690743698-d9d85f2fbf38?w=300&h=300&fit=crop',
- notes: '营养丰富的早餐,富含膳食纤维和抗氧化物质',
- createdAt: dayjs().hour(7).minute(35).toISOString(),
- updatedAt: dayjs().hour(7).minute(35).toISOString(),
- },
- {
- id: 2,
- mealType: 'lunch',
- foodName: '鸡胸肉沙拉',
- foodDescription: '烤鸡胸肉,混合蔬菜,橄榄油调味',
- weightGrams: 250,
- portionDescription: '1份',
- estimatedCalories: 320,
- proteinGrams: 35.6,
- carbohydrateGrams: 8.4,
- fatGrams: 15.2,
- fiberGrams: 6.2,
- sugarGrams: 5.8,
- sodiumMg: 480,
- source: 'manual',
- mealTime: dayjs().hour(12).minute(15).toISOString(),
- imageUrl: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=300&fit=crop',
- notes: '高蛋白低碳水,适合健身人群',
- createdAt: dayjs().hour(12).minute(20).toISOString(),
- updatedAt: dayjs().hour(12).minute(20).toISOString(),
- },
- {
- id: 3,
- mealType: 'snack',
- foodName: '混合坚果',
- foodDescription: '杏仁,核桃,腰果混合装',
- weightGrams: 30,
- portionDescription: '1小包',
- estimatedCalories: 180,
- proteinGrams: 6.5,
- carbohydrateGrams: 6.8,
- fatGrams: 15.5,
- fiberGrams: 3.2,
- sugarGrams: 2.1,
- sodiumMg: 5,
- source: 'manual',
- mealTime: dayjs().hour(15).minute(30).toISOString(),
- notes: '健康的下午茶零食',
- createdAt: dayjs().hour(15).minute(35).toISOString(),
- updatedAt: dayjs().hour(15).minute(35).toISOString(),
- },
- {
- id: 4,
- mealType: 'dinner',
- foodName: '三文鱼配蒸蔬菜',
- foodDescription: '挪威三文鱼,西兰花,胡萝卜',
- weightGrams: 350,
- portionDescription: '1份',
- estimatedCalories: 420,
- proteinGrams: 42.3,
- carbohydrateGrams: 12.5,
- fatGrams: 22.8,
- fiberGrams: 5.6,
- sugarGrams: 8.2,
- sodiumMg: 380,
- source: 'vision',
- mealTime: dayjs().hour(19).minute(0).toISOString(),
- imageUrl: 'https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=300&h=300&fit=crop',
- notes: '富含Omega-3脂肪酸,有益心血管健康',
- createdAt: dayjs().hour(19).minute(10).toISOString(),
- updatedAt: dayjs().hour(19).minute(10).toISOString(),
- },
-
- // 昨天的记录
- {
- id: 5,
- mealType: 'breakfast',
- foodName: '希腊酸奶杯',
- foodDescription: '无糖希腊酸奶,草莓,燕麦片',
- weightGrams: 200,
- portionDescription: '1杯',
- estimatedCalories: 220,
- proteinGrams: 20.4,
- carbohydrateGrams: 18.6,
- fatGrams: 8.2,
- fiberGrams: 4.1,
- sugarGrams: 12.5,
- sodiumMg: 85,
- source: 'manual',
- mealTime: dayjs().subtract(1, 'day').hour(8).minute(0).toISOString(),
- imageUrl: 'https://images.unsplash.com/photo-1488477181946-6428a0291777?w=300&h=300&fit=crop',
- notes: '高蛋白早餐,饱腹感强',
- createdAt: dayjs().subtract(1, 'day').hour(8).minute(5).toISOString(),
- updatedAt: dayjs().subtract(1, 'day').hour(8).minute(5).toISOString(),
- },
-
- // 更多历史记录,用于测试分页
- {
- id: 6,
- mealType: 'lunch',
- foodName: '牛肉面',
- foodDescription: '手拉面条,牛肉汤底,青菜',
- weightGrams: 400,
- portionDescription: '1碗',
- estimatedCalories: 580,
- proteinGrams: 28.0,
- carbohydrateGrams: 65.0,
- fatGrams: 18.5,
- fiberGrams: 4.8,
- sugarGrams: 6.2,
- sodiumMg: 1200,
- source: 'manual',
- mealTime: dayjs().subtract(2, 'day').hour(13).minute(0).toISOString(),
- notes: '传统中式午餐',
- createdAt: dayjs().subtract(2, 'day').hour(13).minute(10).toISOString(),
- updatedAt: dayjs().subtract(2, 'day').hour(13).minute(10).toISOString(),
- },
- {
- id: 7,
- mealType: 'breakfast',
- foodName: '全麦吐司配牛油果',
- foodDescription: '全麦面包,新鲜牛油果,煎蛋',
- weightGrams: 180,
- portionDescription: '2片吐司',
- estimatedCalories: 350,
- proteinGrams: 15.2,
- carbohydrateGrams: 28.5,
- fatGrams: 22.0,
- fiberGrams: 8.0,
- sugarGrams: 3.5,
- sodiumMg: 220,
- source: 'manual',
- mealTime: dayjs().subtract(3, 'day').hour(8).minute(30).toISOString(),
- imageUrl: 'https://images.unsplash.com/photo-1541519227354-08fa5d50c44d?w=300&h=300&fit=crop',
- notes: '健康脂肪和蛋白质的完美组合',
- createdAt: dayjs().subtract(3, 'day').hour(8).minute(35).toISOString(),
- updatedAt: dayjs().subtract(3, 'day').hour(8).minute(35).toISOString(),
- },
- {
- id: 8,
- mealType: 'dinner',
- foodName: '蒸蛋羹',
- foodDescription: '鸡蛋,温水,少许盐',
- weightGrams: 150,
- portionDescription: '1小碗',
- estimatedCalories: 140,
- proteinGrams: 12.0,
- carbohydrateGrams: 1.0,
- fatGrams: 10.0,
- fiberGrams: 0,
- sugarGrams: 1.0,
- sodiumMg: 180,
- source: 'manual',
- mealTime: dayjs().subtract(4, 'day').hour(18).minute(45).toISOString(),
- notes: '清淡易消化的晚餐',
- createdAt: dayjs().subtract(4, 'day').hour(19).minute(0).toISOString(),
- updatedAt: dayjs().subtract(4, 'day').hour(19).minute(0).toISOString(),
- },
-];
-
-// 模拟API响应,支持分页和日期过滤
-export function getMockDietRecords({
- startDate,
- endDate,
- page = 1,
- limit = 10,
-}: {
- startDate?: string;
- endDate?: string;
- page?: number;
- limit?: number;
-} = {}) {
- let filteredRecords = mockDietRecords;
-
- // 如果有日期范围,则过滤
- if (startDate && endDate) {
- filteredRecords = mockDietRecords.filter(record => {
- const recordDate = dayjs(record.mealTime).format('YYYY-MM-DD');
- return recordDate >= startDate && recordDate <= endDate;
- });
- }
-
- // 分页
- const startIndex = (page - 1) * limit;
- const endIndex = startIndex + limit;
- const paginatedRecords = filteredRecords.slice(startIndex, endIndex);
-
- return {
- records: paginatedRecords,
- total: filteredRecords.length,
- page,
- limit,
- };
-}
diff --git a/utils/health.ts b/utils/health.ts
index c92490d..a1fe3f5 100644
--- a/utils/health.ts
+++ b/utils/health.ts
@@ -6,6 +6,8 @@ const PERMISSIONS: HealthKitPermissions = {
read: [
AppleHealthKit.Constants.Permissions.StepCount,
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
+ AppleHealthKit.Constants.Permissions.SleepAnalysis,
+ AppleHealthKit.Constants.Permissions.HeartRateVariability,
],
write: [],
},
@@ -14,6 +16,8 @@ const PERMISSIONS: HealthKitPermissions = {
export type TodayHealthData = {
steps: number;
activeEnergyBurned: number; // kilocalories
+ sleepDuration: number; // 睡眠时长(分钟)
+ hrv: number | null; // 心率变异性 (ms)
};
export async function ensureHealthPermissions(): Promise {
@@ -46,46 +50,147 @@ export async function fetchHealthDataForDate(date: Date): Promise((resolve) => {
- AppleHealthKit.getStepCount(options, (err, res) => {
- if (err) {
- console.error('获取步数失败:', err);
- return resolve(0);
- }
- if (!res) {
- console.warn('步数数据为空');
- return resolve(0);
- }
- console.log('步数数据:', res);
- resolve(res.value || 0);
- });
- });
+ // 并行获取所有健康数据
+ const [steps, calories, sleepDuration, hrv] = await Promise.all([
+ // 获取步数
+ new Promise((resolve) => {
+ AppleHealthKit.getStepCount(options, (err, res) => {
+ if (err) {
+ console.error('获取步数失败:', err);
+ return resolve(0);
+ }
+ if (!res) {
+ console.warn('步数数据为空');
+ return resolve(0);
+ }
+ console.log('步数数据:', res);
+ resolve(res.value || 0);
+ });
+ }),
- const calories = await new Promise((resolve) => {
- AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
- if (err) {
- console.error('获取消耗卡路里失败:', err);
- return resolve(0);
- }
- if (!res || !Array.isArray(res) || res.length === 0) {
- console.warn('卡路里数据为空或格式错误');
- return resolve(0);
- }
- console.log('卡路里数据:', res);
- // 求和该日内的所有记录(单位:千卡)
- const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
- resolve(total);
- });
- });
+ // 获取消耗卡路里
+ new Promise((resolve) => {
+ AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
+ if (err) {
+ console.error('获取消耗卡路里失败:', err);
+ return resolve(0);
+ }
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ console.warn('卡路里数据为空或格式错误');
+ return resolve(0);
+ }
+ console.log('卡路里数据:', res);
+ // 求和该日内的所有记录(单位:千卡)
+ const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
+ resolve(total);
+ });
+ }),
- console.log('指定日期健康数据获取完成:', { steps, calories });
- return { steps, activeEnergyBurned: calories };
+ // 获取睡眠时长
+ new Promise((resolve) => {
+ AppleHealthKit.getSleepSamples(options, (err, res) => {
+ if (err) {
+ console.error('获取睡眠数据失败:', err);
+ return resolve(0);
+ }
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ console.warn('睡眠数据为空或格式错误');
+ return resolve(0);
+ }
+ console.log('睡眠数据:', res);
+
+ // 计算总睡眠时间(单位:分钟)
+ let totalSleepDuration = 0;
+ res.forEach((sample: any) => {
+ if (sample && sample.startDate && sample.endDate) {
+ const startTime = new Date(sample.startDate).getTime();
+ const endTime = new Date(sample.endDate).getTime();
+ const durationMinutes = (endTime - startTime) / (1000 * 60);
+ totalSleepDuration += durationMinutes;
+ }
+ });
+
+ resolve(totalSleepDuration);
+ });
+ }),
+
+ // 获取HRV数据
+ new Promise((resolve) => {
+ AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
+ if (err) {
+ console.error('获取HRV数据失败:', err);
+ return resolve(null);
+ }
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ console.warn('HRV数据为空或格式错误');
+ return resolve(null);
+ }
+ console.log('HRV数据:', res);
+
+ // 获取最新的HRV值
+ const latestHrv = res[res.length - 1];
+ if (latestHrv && latestHrv.value) {
+ resolve(latestHrv.value);
+ } else {
+ resolve(null);
+ }
+ });
+ })
+ ]);
+
+ console.log('指定日期健康数据获取完成:', { steps, calories, sleepDuration, hrv });
+ return { steps, activeEnergyBurned: calories, sleepDuration, hrv };
}
export async function fetchTodayHealthData(): Promise {
return fetchHealthDataForDate(new Date());
+}
+
+// 新增:专门获取HRV数据的函数
+export async function fetchHRVForDate(date: Date): Promise {
+ console.log('开始获取指定日期HRV数据...', date);
+
+ const start = new Date(date);
+ start.setHours(0, 0, 0, 0);
+ const end = new Date(date);
+ end.setHours(23, 59, 59, 999);
+
+ const options = {
+ startDate: start.toISOString(),
+ endDate: end.toISOString()
+ } as any;
+
+ return new Promise((resolve) => {
+ AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
+ if (err) {
+ console.error('获取HRV数据失败:', err);
+ return resolve(null);
+ }
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ console.warn('HRV数据为空或格式错误');
+ return resolve(null);
+ }
+ console.log('HRV数据:', res);
+
+ // 获取最新的HRV值
+ const latestHrv = res[res.length - 1];
+ if (latestHrv && latestHrv.value) {
+ resolve(latestHrv.value);
+ } else {
+ resolve(null);
+ }
+ });
+ });
+}
+
+// 新增:获取今日HRV数据
+export async function fetchTodayHRV(): Promise {
+ return fetchHRVForDate(new Date());
}
\ No newline at end of file