feat: 优化统计和步数详情页面,添加活动等级计算和展示,更新压力计组件以支持HRV值直接显示
This commit is contained in:
@@ -12,17 +12,14 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { notificationService } from '@/services/notifications';
|
|
||||||
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
||||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||||
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
||||||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
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 { getTestHealthData } from '@/utils/mockHealthData';
|
||||||
import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
|
||||||
import { calculateNutritionGoals } from '@/utils/nutrition';
|
import { calculateNutritionGoals } from '@/utils/nutrition';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -412,121 +409,6 @@ export default function ExploreScreen() {
|
|||||||
};
|
};
|
||||||
}, [loadAllData, currentSelectedDate]);
|
}, [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) => {
|
const onSelectDate = React.useCallback((index: number, date: Date) => {
|
||||||
|
|||||||
@@ -176,6 +176,39 @@ export default function StepsDetailScreen() {
|
|||||||
return maxStepsData.steps > 0 ? maxStepsData : null;
|
return maxStepsData.steps > 0 ? maxStepsData : null;
|
||||||
}, [hourlySteps]);
|
}, [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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* 背景渐变 */}
|
{/* 背景渐变 */}
|
||||||
@@ -343,6 +376,63 @@ export default function StepsDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 活动等级展示卡片 */}
|
||||||
|
<View style={styles.activityLevelCard}>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 活动级别文本 */}
|
||||||
|
<Text style={styles.activityMainText}>你今天的活动量处于</Text>
|
||||||
|
<Text style={styles.activityLevelText}>{currentActivityLevel.label}</Text>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<View style={styles.progressBarContainer}>
|
||||||
|
<View style={styles.progressBarBackground}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.progressBarFill,
|
||||||
|
{
|
||||||
|
width: `${progressPercentage}%`,
|
||||||
|
backgroundColor: currentActivityLevel.color
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 步数信息 */}
|
||||||
|
<View style={styles.stepsInfoContainer}>
|
||||||
|
<View style={styles.currentStepsInfo}>
|
||||||
|
<Text style={styles.stepsValue}>{totalSteps.toLocaleString()} 步</Text>
|
||||||
|
<Text style={styles.stepsLabel}>当前</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.nextStepsInfo}>
|
||||||
|
<Text style={styles.stepsValue}>
|
||||||
|
{nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.stepsLabel}>
|
||||||
|
{nextActivityLevel ? `下一级: ${nextActivityLevel.label}` : '已达最高级'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 活动等级图例 */}
|
||||||
|
<View style={styles.activityLegendContainer}>
|
||||||
|
{reversedActivityLevels.map((level) => (
|
||||||
|
<View key={level.key} style={styles.legendItem}>
|
||||||
|
<View style={[styles.legendIcon, { backgroundColor: level.color }]}>
|
||||||
|
<Text style={styles.legendIconText}>🏃</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.legendLabel}>{level.label}</Text>
|
||||||
|
<Text style={styles.legendRange}>
|
||||||
|
{level.maxSteps === Infinity
|
||||||
|
? `> ${level.minSteps.toLocaleString()}`
|
||||||
|
: `${level.minSteps.toLocaleString()} - ${level.maxSteps.toLocaleString()}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
@@ -541,4 +631,127 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 0.5,
|
borderWidth: 0.5,
|
||||||
borderColor: '#FFA726',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
@@ -11,42 +12,47 @@ interface StressMeterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StressMeter({ value, updateTime, style, hrvValue }: 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值转换为压力指数(0-100)
|
||||||
// HRV值范围:30-110ms,映射到压力指数100-0
|
// HRV值范围:30-110ms,映射到压力指数100-0
|
||||||
// HRV值越高,压力越小;HRV值越低,压力越大
|
// HRV值越高,压力越小;HRV值越低,压力越大
|
||||||
// 根据压力指数计算状态
|
const convertHrvToStressIndex = (hrv: number | null): number | null => {
|
||||||
const getStressStatus = () => {
|
if (hrv === null || hrv === 0) return null;
|
||||||
if (value === null) {
|
|
||||||
return '未知';
|
// HRV 范围: 30-110ms,对应压力指数: 100-0
|
||||||
} else if (value >= 70) {
|
// 线性映射: stressIndex = 100 - ((hrv - 30) / (110 - 30)) * 100
|
||||||
return '放松';
|
const normalizedHrv = Math.max(30, Math.min(110, hrv));
|
||||||
} else if (value >= 30) {
|
const stressIndex = 100 - ((normalizedHrv - 30) / (110 - 30)) * 100;
|
||||||
return '正常';
|
|
||||||
} else {
|
return Math.round(stressIndex);
|
||||||
return '紧张';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据状态获取表情
|
// 使用传入的 hrvValue 进行转换
|
||||||
const getStatusEmoji = () => {
|
const stressIndex = convertHrvToStressIndex(hrvValue);
|
||||||
// 当HRV值为null或0时,不展示表情
|
|
||||||
if (value === null || value === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = getStressStatus();
|
|
||||||
switch (status) {
|
|
||||||
case '未知': return '';
|
|
||||||
case '放松': return '😌';
|
|
||||||
case '正常': return '😊';
|
|
||||||
case '紧张': return '😰';
|
|
||||||
default: return '😊';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算进度条位置(0-100%)
|
// 计算进度条位置(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);
|
const [showStressModal, setShowStressModal] = useState(false);
|
||||||
@@ -68,12 +74,15 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
|||||||
<View style={styles.leftSection}>
|
<View style={styles.leftSection}>
|
||||||
<Text style={styles.title}>压力</Text>
|
<Text style={styles.title}>压力</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{updateTime && (
|
||||||
|
<Text style={styles.headerUpdateTime}>{formatUpdateTime(updateTime)}</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 数值显示区域 */}
|
{/* 数值显示区域 */}
|
||||||
<View style={styles.valueSection}>
|
<View style={styles.valueSection}>
|
||||||
<Text style={styles.value}>{value === null ? '--' : value}</Text>
|
<Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
|
||||||
<Text style={styles.unit}>指数</Text>
|
<Text style={styles.unit}>ms</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 进度条区域 */}
|
{/* 进度条区域 */}
|
||||||
@@ -89,14 +98,10 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{/* 白色圆形指示器 */}
|
{/* 白色圆形指示器 */}
|
||||||
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} />
|
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage))}%` }]} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 更新时间
|
|
||||||
{updateTime && (
|
|
||||||
<Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text>
|
|
||||||
)} */}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* 压力分析浮窗 */}
|
{/* 压力分析浮窗 */}
|
||||||
@@ -207,4 +212,9 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
headerUpdateTime: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#9AA3AE',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -465,7 +465,8 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise<nu
|
|||||||
|
|
||||||
const latestHrv = res[res.length - 1];
|
const latestHrv = res[res.length - 1];
|
||||||
if (latestHrv && latestHrv.value) {
|
if (latestHrv && latestHrv.value) {
|
||||||
resolve(Math.round(latestHrv.value * 1000));
|
// HealthKit 中的 HRV 数据已经是毫秒单位,无需转换
|
||||||
|
resolve(Math.round(latestHrv.value));
|
||||||
} else {
|
} else {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user