diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index fd4f424..8ece39b 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -12,17 +12,14 @@ import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
-import { notificationService } from '@/services/notifications';
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
-import { ensureHealthPermissions, fetchHealthDataForDate, fetchRecentHRV } from '@/utils/health';
+import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData';
-import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { calculateNutritionGoals } from '@/utils/nutrition';
-import AsyncStorage from '@react-native-async-storage/async-storage';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
@@ -412,121 +409,6 @@ export default function ExploreScreen() {
};
}, [loadAllData, currentSelectedDate]);
- // 检查压力水平并发送通知
- const checkStressLevelAndNotify = React.useCallback(async () => {
- try {
- console.log('开始检查压力水平...');
-
- // 确保有健康权限
- const hasPermission = await ensureHealthPermissions();
- if (!hasPermission) {
- console.log('没有健康权限,跳过压力检查');
- return;
- }
-
- // 获取最近2小时内的实时HRV数据
- const recentHRV = await fetchRecentHRV(2);
- console.log('获取到的最近2小时HRV值:', recentHRV);
-
- if (recentHRV === null || recentHRV === undefined) {
- console.log('没有最近的HRV数据,跳过压力检查');
- return;
- }
-
- // 判断压力水平(HRV值低于60表示压力过大)
- if (recentHRV < 60) {
- console.log(`检测到压力过大,HRV值: ${recentHRV},准备发送鼓励通知`);
-
- // 检查是否在过去2小时内已经发送过压力提醒,避免重复打扰
- const lastNotificationKey = '@last_stress_notification';
- const lastNotificationTime = await AsyncStorage.getItem(lastNotificationKey);
- const now = new Date().getTime();
- const twoHoursAgo = now - (2 * 60 * 60 * 1000); // 2小时前
-
- if (lastNotificationTime && parseInt(lastNotificationTime) > twoHoursAgo) {
- console.log('2小时内已发送过压力提醒,跳过本次通知');
- return;
- }
-
- // 随机选择一条鼓励性消息
- const encouragingMessages = [
- '放松一下吧 🌸\n检测到您的压力指数较高,不妨暂停一下,做几个深呼吸,或者来一段轻松的普拉提练习。您的健康最重要!',
- '该休息一下了 🧘♀️\n您的身体在提醒您需要放松。试试冥想、散步或听听舒缓的音乐,让心情平静下来。',
- '压力山大?我们来帮您 💆♀️\n高压力对健康不利,建议您做一些放松运动,比如瑜伽或普拉提,释放身心压力。',
- '关爱自己,从现在开始 💝\n检测到您可能承受较大压力,记得给自己一些时间,做喜欢的事情,保持身心健康。',
- '深呼吸,一切都会好的 🌈\n压力只是暂时的,试试腹式呼吸或简单的伸展运动,让身体和心灵都得到放松。'
- ];
-
- const randomMessage = encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)];
- const [title, body] = randomMessage.split('\n');
-
- // 发送鼓励性推送通知
- await notificationService.sendImmediateNotification({
- title: title,
- body: body,
- data: {
- type: 'stress_alert',
- hrvValue: recentHRV,
- timestamp: new Date().toISOString(),
- url: '/mood/calendar' // 点击通知跳转到心情页面
- },
- sound: true,
- priority: 'high'
- });
-
- // 记录通知发送时间
- await AsyncStorage.setItem(lastNotificationKey, now.toString());
-
- console.log('压力提醒通知已发送');
- } else {
- console.log(`压力水平正常,HRV值: ${recentHRV}`);
- }
- } catch (error) {
- console.error('检查压力水平失败:', error);
- }
- }, []);
-
- // 检查喝水目标并发送通知
- const checkWaterGoalAndNotify = React.useCallback(async () => {
- try {
- console.log('开始检查喝水目标完成情况...');
-
- // 获取最新的喝水统计数据
- if (!todayWaterStats || !todayWaterStats.dailyGoal || todayWaterStats.dailyGoal <= 0) {
- console.log('没有设置喝水目标或目标无效,跳过喝水检查');
- return;
- }
-
- // 获取用户名
- const userName = userProfile?.name || '朋友';
- const currentHour = new Date().getHours();
-
- // 构造今日统计数据
- const waterStatsForCheck = {
- totalAmount: todayWaterStats.totalAmount || 0,
- dailyGoal: todayWaterStats.dailyGoal,
- completionRate: todayWaterStats.completionRate || 0
- };
-
- // 调用喝水通知检查函数
- const notificationSent = await WaterNotificationHelpers.checkWaterGoalAndNotify(
- userName,
- waterStatsForCheck,
- currentHour
- );
-
- if (notificationSent) {
- console.log('喝水提醒通知已发送');
- } else {
- console.log('无需发送喝水提醒通知');
- }
-
- } catch (error) {
- console.error('检查喝水目标失败:', error);
- }
- }, [todayWaterStats, userProfile]);
-
-
// 日期点击时,加载对应日期数据
const onSelectDate = React.useCallback((index: number, date: Date) => {
diff --git a/app/steps/detail.tsx b/app/steps/detail.tsx
index 95d46c1..77dbd7a 100644
--- a/app/steps/detail.tsx
+++ b/app/steps/detail.tsx
@@ -176,6 +176,39 @@ export default function StepsDetailScreen() {
return maxStepsData.steps > 0 ? maxStepsData : null;
}, [hourlySteps]);
+ // 活动等级配置
+ const activityLevels = useMemo(() => [
+ { key: 'inactive', label: '不怎么动', minSteps: 0, maxSteps: 3000, color: '#B8C8D6' },
+ { key: 'light', label: '轻度活跃', minSteps: 3000, maxSteps: 7500, color: '#93C5FD' },
+ { key: 'moderate', label: '中等活跃', minSteps: 7500, maxSteps: 10000, color: '#FCD34D' },
+ { key: 'very_active', label: '非常活跃', minSteps: 10000, maxSteps: Infinity, color: '#FB923C' }
+ ], []);
+
+ // 计算当前活动等级
+ const currentActivityLevel = useMemo(() => {
+ return activityLevels.find(level =>
+ totalSteps >= level.minSteps && totalSteps < level.maxSteps
+ ) || activityLevels[0];
+ }, [totalSteps, activityLevels]);
+
+ // 计算下一等级
+ const nextActivityLevel = useMemo(() => {
+ const currentIndex = activityLevels.indexOf(currentActivityLevel);
+ return currentIndex < activityLevels.length - 1 ? activityLevels[currentIndex + 1] : null;
+ }, [currentActivityLevel, activityLevels]);
+
+ // 计算进度百分比
+ const progressPercentage = useMemo(() => {
+ if (!nextActivityLevel) return 100; // 已达到最高级
+
+ const rangeSize = nextActivityLevel.minSteps - currentActivityLevel.minSteps;
+ const currentProgress = totalSteps - currentActivityLevel.minSteps;
+ return Math.min(Math.max((currentProgress / rangeSize) * 100, 0), 100);
+ }, [totalSteps, currentActivityLevel, nextActivityLevel]);
+
+ // 倒序显示的活动等级(用于图例)
+ const reversedActivityLevels = useMemo(() => [...activityLevels].reverse(), [activityLevels]);
+
return (
{/* 背景渐变 */}
@@ -343,6 +376,63 @@ export default function StepsDetailScreen() {
+
+ {/* 活动等级展示卡片 */}
+
+
+
+ {/* 活动级别文本 */}
+ 你今天的活动量处于
+ {currentActivityLevel.label}
+
+ {/* 进度条 */}
+
+
+
+
+
+
+ {/* 步数信息 */}
+
+
+ {totalSteps.toLocaleString()} 步
+ 当前
+
+
+
+ {nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'}
+
+
+ {nextActivityLevel ? `下一级: ${nextActivityLevel.label}` : '已达最高级'}
+
+
+
+
+ {/* 活动等级图例 */}
+
+ {reversedActivityLevels.map((level) => (
+
+
+ 🏃
+
+ {level.label}
+
+ {level.maxSteps === Infinity
+ ? `> ${level.minSteps.toLocaleString()}`
+ : `${level.minSteps.toLocaleString()} - ${level.maxSteps.toLocaleString()}`}
+
+
+ ))}
+
+
@@ -541,4 +631,127 @@ const styles = StyleSheet.create({
borderWidth: 0.5,
borderColor: '#FFA726',
},
+ activityLevelCard: {
+ backgroundColor: '#FFFFFF',
+ borderRadius: 16,
+ padding: 24,
+ marginVertical: 16,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 0.08,
+ shadowRadius: 12,
+ elevation: 6,
+ },
+ activityIconContainer: {
+ marginBottom: 16,
+ },
+ activityIcon: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: '#E0F2FE',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 2,
+ borderColor: '#93C5FD',
+ borderStyle: 'dashed',
+ },
+ meditationIcon: {
+ width: 50,
+ height: 50,
+ borderRadius: 25,
+ backgroundColor: '#93C5FD',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ meditationEmoji: {
+ fontSize: 24,
+ },
+ activityMainText: {
+ fontSize: 16,
+ color: '#64748B',
+ marginBottom: 4,
+ },
+ activityLevelText: {
+ fontSize: 24,
+ fontWeight: '700',
+ color: '#192126',
+ marginBottom: 20,
+ },
+ progressBarContainer: {
+ width: '100%',
+ marginBottom: 24,
+ },
+ progressBarBackground: {
+ width: '100%',
+ height: 8,
+ backgroundColor: '#F0F9FF',
+ borderRadius: 4,
+ overflow: 'hidden',
+ },
+ progressBarFill: {
+ height: '100%',
+ borderRadius: 4,
+ },
+ stepsInfoContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%',
+ marginBottom: 32,
+ },
+ currentStepsInfo: {
+ alignItems: 'flex-start',
+ },
+ nextStepsInfo: {
+ alignItems: 'flex-end',
+ },
+ stepsValue: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: '#192126',
+ marginBottom: 4,
+ },
+ stepsLabel: {
+ fontSize: 14,
+ color: '#64748B',
+ },
+ activityLegendContainer: {
+ width: '100%',
+ },
+ legendItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ backgroundColor: '#F8FAFC',
+ marginBottom: 8,
+ borderRadius: 12,
+ },
+ legendIcon: {
+ width: 32,
+ height: 32,
+ borderRadius: 8,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 12,
+ },
+ legendIconText: {
+ fontSize: 16,
+ },
+ legendLabel: {
+ flex: 1,
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#192126',
+ },
+ legendRange: {
+ fontSize: 14,
+ color: '#64748B',
+ fontWeight: '500',
+ },
});
\ No newline at end of file
diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx
index 91b4a00..5ee464a 100644
--- a/components/StressMeter.tsx
+++ b/components/StressMeter.tsx
@@ -1,3 +1,4 @@
+import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
@@ -11,42 +12,47 @@ interface StressMeterProps {
}
export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterProps) {
+ // 格式化更新时间显示
+ const formatUpdateTime = (date: Date): string => {
+ const now = dayjs();
+ const updateTime = dayjs(date);
+ const diffMinutes = now.diff(updateTime, 'minute');
+ const diffHours = now.diff(updateTime, 'hour');
+ const diffDays = now.diff(updateTime, 'day');
+
+ if (diffMinutes < 1) {
+ return '刚刚更新';
+ } else if (diffMinutes < 60) {
+ return `${diffMinutes}分钟前更新`;
+ } else if (diffHours < 24) {
+ return `${diffHours}小时前更新`;
+ } else if (diffDays < 7) {
+ return `${diffDays}天前更新`;
+ } else {
+ return updateTime.format('MM-DD HH:mm');
+ }
+ };
+
// 将HRV值转换为压力指数(0-100)
// HRV值范围:30-110ms,映射到压力指数100-0
// HRV值越高,压力越小;HRV值越低,压力越大
- // 根据压力指数计算状态
- const getStressStatus = () => {
- if (value === null) {
- return '未知';
- } else if (value >= 70) {
- return '放松';
- } else if (value >= 30) {
- return '正常';
- } else {
- return '紧张';
- }
+ const convertHrvToStressIndex = (hrv: number | null): number | null => {
+ if (hrv === null || hrv === 0) return null;
+
+ // HRV 范围: 30-110ms,对应压力指数: 100-0
+ // 线性映射: stressIndex = 100 - ((hrv - 30) / (110 - 30)) * 100
+ const normalizedHrv = Math.max(30, Math.min(110, hrv));
+ const stressIndex = 100 - ((normalizedHrv - 30) / (110 - 30)) * 100;
+
+ return Math.round(stressIndex);
};
- // 根据状态获取表情
- const getStatusEmoji = () => {
- // 当HRV值为null或0时,不展示表情
- if (value === null || value === 0) {
- return '';
- }
-
- const status = getStressStatus();
- switch (status) {
- case '未知': return '';
- case '放松': return '😌';
- case '正常': return '😊';
- case '紧张': return '😰';
- default: return '😊';
- }
- };
+ // 使用传入的 hrvValue 进行转换
+ const stressIndex = convertHrvToStressIndex(hrvValue);
// 计算进度条位置(0-100%)
// 压力指数越高,进度条越满(红色区域越多)
- const progressPercentage = value !== null ? Math.max(0, Math.min(100, value)) : 0;
+ const progressPercentage = stressIndex !== null ? Math.max(0, Math.min(100, stressIndex)) : 0;
// 在组件内部添加状态
const [showStressModal, setShowStressModal] = useState(false);
@@ -68,12 +74,15 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
压力
+ {updateTime && (
+ {formatUpdateTime(updateTime)}
+ )}
{/* 数值显示区域 */}
- {value === null ? '--' : value}
- 指数
+ {stressIndex === null ? '--' : stressIndex}
+ ms
{/* 进度条区域 */}
@@ -89,14 +98,10 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
/>
{/* 白色圆形指示器 */}
-
+
- {/* 更新时间
- {updateTime && (
- {formatUpdateTime(updateTime)}
- )} */}
{/* 压力分析浮窗 */}
@@ -207,4 +212,9 @@ const styles = StyleSheet.create({
textAlign: 'right',
marginTop: 2,
},
+ headerUpdateTime: {
+ fontSize: 11,
+ color: '#9AA3AE',
+ fontWeight: '500',
+ },
});
diff --git a/utils/health.ts b/utils/health.ts
index 55f9833..486807d 100644
--- a/utils/health.ts
+++ b/utils/health.ts
@@ -465,7 +465,8 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise