feat: 优化提醒注册逻辑,确保用户姓名存在时注册午餐、晚餐和心情提醒;更新睡眠详情页面,添加清醒时间段的判断和模拟数据展示;调整样式以提升用户体验

This commit is contained in:
richarjiang
2025-09-08 17:45:30 +08:00
parent f9a175d76c
commit bf3304eb06
5 changed files with 198 additions and 136 deletions

View File

@@ -70,28 +70,26 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
// 当用户数据加载完成且用户名存在时,注册所有提醒 // 当用户数据加载完成且用户名存在时,注册所有提醒
React.useEffect(() => { React.useEffect(() => {
const registerAllReminders = async () => { const registerAllReminders = async () => {
if (userDataLoaded && profile?.name) { try {
try { await notificationService.initialize();
await notificationService.initialize(); // 后台任务
// 后台任务 await backgroundTaskManager.initialize()
await backgroundTaskManager.initialize() // 注册午餐提醒12:00
// 注册午餐提醒12:00 await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name); console.log('午餐提醒已注册');
console.log('午餐提醒已注册');
// 注册晚餐提醒18:00 // 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name); await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
console.log('晚餐提醒已注册'); console.log('晚餐提醒已注册');
// 注册心情提醒21:00 // 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name); await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
console.log('心情提醒已注册'); console.log('心情提醒已注册');
console.log('喝水提醒后台任务已注册'); console.log('喝水提醒后台任务已注册');
} catch (error) { } catch (error) {
console.error('注册提醒失败:', error); console.error('注册提醒失败:', error);
}
} }
}; };

View File

@@ -1,3 +1,4 @@
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router'; import { router } from 'expo-router';
@@ -14,7 +15,6 @@ import {
View View
} from 'react-native'; } from 'react-native';
import Svg, { Circle } from 'react-native-svg'; import Svg, { Circle } from 'react-native-svg';
import { Ionicons } from '@expo/vector-icons';
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
@@ -90,17 +90,20 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
return null; return null;
} }
// 根据时间判断可能的睡眠状态 // 根据时间判断可能的睡眠状态,包括清醒时间段
if (hour >= 0 && hour <= 6) { if (hour >= 0 && hour <= 6) {
// 凌晨0-6点主要睡眠时间 // 凌晨0-6点主要睡眠时间,包含一些清醒时段
if (hour <= 2) return SleepStage.Core; if (hour <= 1) return SleepStage.Core;
if (hour === 2) return SleepStage.Awake; // 添加清醒时间段
if (hour <= 4) return SleepStage.Deep; if (hour <= 4) return SleepStage.Deep;
if (hour === 5) return SleepStage.Awake; // 添加清醒时间段
return SleepStage.REM; return SleepStage.REM;
} else if (hour >= 22) { } else if (hour >= 22) {
// 晚上10点后开始入睡 // 晚上10点后开始入睡
if (hour === 23) return SleepStage.Awake; // 入睡前的清醒时间
return SleepStage.Core; return SleepStage.Core;
} }
return null; // 清醒时间 return null; // 白天清醒时间
}); });
return ( return (
@@ -142,11 +145,11 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
}; };
// Sleep Grade Component 睡眠等级组件 // Sleep Grade Component 睡眠等级组件
const SleepGradeCard = ({ const SleepGradeCard = ({
icon, icon,
grade, grade,
range, range,
isActive = false isActive = false
}: { }: {
icon: string; icon: string;
grade: string; grade: string;
@@ -155,7 +158,7 @@ const SleepGradeCard = ({
}) => { }) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme]; const colorTokens = Colors[theme];
const getGradeColor = (grade: string) => { const getGradeColor = (grade: string) => {
switch (grade) { switch (grade) {
case '低': return { bg: '#FECACA', text: '#DC2626' }; case '低': return { bg: '#FECACA', text: '#DC2626' };
@@ -171,7 +174,7 @@ const SleepGradeCard = ({
return ( return (
<View style={[ <View style={[
styles.gradeCard, styles.gradeCard,
{ {
backgroundColor: isActive ? colors.bg : colorTokens.pageBackgroundEmphasis, backgroundColor: isActive ? colors.bg : colorTokens.pageBackgroundEmphasis,
borderColor: isActive ? colors.text : 'transparent', borderColor: isActive ? colors.text : 'transparent',
} }
@@ -196,10 +199,10 @@ const SleepGradeCard = ({
}; };
// Info Modal 组件 // Info Modal 组件
const InfoModal = ({ const InfoModal = ({
visible, visible,
onClose, onClose,
title, title,
type type
}: { }: {
visible: boolean; visible: boolean;
@@ -213,6 +216,8 @@ const InfoModal = ({
React.useEffect(() => { React.useEffect(() => {
if (visible) { if (visible) {
// 重置动画值确保每次打开都有动画
slideAnim.setValue(0);
Animated.spring(slideAnim, { Animated.spring(slideAnim, {
toValue: 1, toValue: 1,
useNativeDriver: true, useNativeDriver: true,
@@ -254,7 +259,7 @@ const InfoModal = ({
]; ];
const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades; const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades;
const getDescription = () => { const getDescription = () => {
if (type === 'sleep-time') { if (type === 'sleep-time') {
return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。'; return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。';
@@ -270,14 +275,14 @@ const InfoModal = ({
animationType="none" animationType="none"
onRequestClose={onClose} onRequestClose={onClose}
> >
<TouchableOpacity <TouchableOpacity
style={styles.modalOverlay} style={styles.modalOverlay}
activeOpacity={1} activeOpacity={1}
onPress={onClose} onPress={onClose}
> >
<Animated.View style={[ <Animated.View style={[
styles.infoModalContent, styles.infoModalContent,
{ {
backgroundColor: colorTokens.background, backgroundColor: colorTokens.background,
transform: [{ translateY }], transform: [{ translateY }],
opacity, opacity,
@@ -292,7 +297,7 @@ const InfoModal = ({
<Ionicons name="close" size={20} color={colorTokens.textSecondary} /> <Ionicons name="close" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* 等级卡片区域 */} {/* 等级卡片区域 */}
<View style={styles.gradesContainer}> <View style={styles.gradesContainer}>
{currentGrades.map((grade, index) => ( {currentGrades.map((grade, index) => (
@@ -321,9 +326,9 @@ export default function SleepDetailScreen() {
const [sleepData, setSleepData] = useState<SleepDetailData | null>(null); const [sleepData, setSleepData] = useState<SleepDetailData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedDate] = useState(dayjs().toDate()); const [selectedDate] = useState(dayjs().toDate());
const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({ const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({
visible: false, visible: false,
title: '', title: '',
type: null type: null
}); });
@@ -428,10 +433,13 @@ export default function SleepDetailScreen() {
<View style={styles.statsContainer}> <View style={styles.statsContainer}>
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}> <View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}> <View style={styles.statCardHeader}>
<View style={styles.statCardIcon}> <View style={styles.statCardLeftGroup}>
<Text style={styles.statIcon}>🌙</Text> <View style={styles.statCardIcon}>
<Ionicons name="moon-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.infoButton} style={styles.infoButton}
onPress={() => setInfoModal({ onPress={() => setInfoModal({
visible: true, visible: true,
@@ -439,10 +447,9 @@ export default function SleepDetailScreen() {
type: 'sleep-time' type: 'sleep-time'
})} })}
> >
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} /> <Ionicons name="information-circle-outline" size={18} color={colorTokens.textMuted} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}> <Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'} {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'}
</Text> </Text>
@@ -453,10 +460,13 @@ export default function SleepDetailScreen() {
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}> <View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}> <View style={styles.statCardHeader}>
<View style={styles.statCardIcon}> <View style={styles.statCardLeftGroup}>
<Text style={styles.statIcon}>💎</Text> <View style={styles.statCardIcon}>
<Ionicons name="star-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.infoButton} style={styles.infoButton}
onPress={() => setInfoModal({ onPress={() => setInfoModal({
visible: true, visible: true,
@@ -467,7 +477,6 @@ export default function SleepDetailScreen() {
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} /> <Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}> <Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'} {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'}
</Text> </Text>
@@ -506,13 +515,61 @@ export default function SleepDetailScreen() {
</View> </View>
</View> </View>
)) : ( )) : (
<View style={styles.noDataContainer}> /* 当没有真实数据时,显示包含清醒时间的模拟数据 */
<Text style={styles.noDataText}></Text> <>
</View> {/* 深度睡眠 */}
<View style={styles.stageRow}>
<View style={styles.stageInfo}>
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Deep) }]} />
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Deep)}</Text>
</View>
<View style={styles.stageStats}>
<Text style={styles.stagePercentage}>28%</Text>
<Text style={styles.stageDuration}>2h 04m</Text>
<Text style={[styles.stageQuality, { color: '#10B981' }]}></Text>
</View>
</View>
{/* REM睡眠 */}
<View style={styles.stageRow}>
<View style={styles.stageInfo}>
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.REM) }]} />
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.REM)}</Text>
</View>
<View style={styles.stageStats}>
<Text style={styles.stagePercentage}>22%</Text>
<Text style={styles.stageDuration}>1h 37m</Text>
<Text style={[styles.stageQuality, { color: '#10B981' }]}></Text>
</View>
</View>
{/* 核心睡眠 */}
<View style={styles.stageRow}>
<View style={styles.stageInfo}>
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Core) }]} />
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Core)}</Text>
</View>
<View style={styles.stageStats}>
<Text style={styles.stagePercentage}>38%</Text>
<Text style={styles.stageDuration}>2h 48m</Text>
<Text style={[styles.stageQuality, { color: '#059669' }]}></Text>
</View>
</View>
{/* 清醒时间 */}
<View style={styles.stageRow}>
<View style={styles.stageInfo}>
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Awake) }]} />
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Awake)}</Text>
</View>
<View style={styles.stageStats}>
<Text style={styles.stagePercentage}>12%</Text>
<Text style={styles.stageDuration}>54m</Text>
<Text style={[styles.stageQuality, { color: '#F59E0B' }]}></Text>
</View>
</View>
</>
)} )}
</View> </View>
</ScrollView> </ScrollView>
{infoModal.type && ( {infoModal.type && (
<InfoModal <InfoModal
visible={infoModal.visible} visible={infoModal.visible}
@@ -594,7 +651,7 @@ const styles = StyleSheet.create({
newStatCard: { newStatCard: {
flex: 1, flex: 1,
borderRadius: 20, borderRadius: 20,
padding: 20, padding: 16,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08, shadowOpacity: 0.08,
@@ -606,19 +663,26 @@ const styles = StyleSheet.create({
statCardHeader: { statCardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 8, marginBottom: 8,
}, },
statCardLeftGroup: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statCardIcon: { statCardIcon: {
width: 32, width: 20,
height: 32, height: 20,
borderRadius: 8, borderRadius: 4,
backgroundColor: 'rgba(120, 120, 128, 0.08)', backgroundColor: 'rgba(120, 120, 128, 0.08)',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center',
}, },
infoButton: { infoButton: {
padding: 4, padding: 4,
alignSelf: 'center',
}, },
statCard: { statCard: {
flex: 1, flex: 1,
@@ -638,12 +702,12 @@ const styles = StyleSheet.create({
statLabel: { statLabel: {
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
marginBottom: 8,
letterSpacing: 0.2, letterSpacing: 0.2,
alignSelf: 'center',
}, },
newStatValue: { newStatValue: {
fontSize: 28, fontSize: 20,
fontWeight: '700', fontWeight: '600',
marginBottom: 12, marginBottom: 12,
letterSpacing: -0.5, letterSpacing: -0.5,
}, },

View File

@@ -163,10 +163,10 @@ async function executeBackgroundTasks(): Promise<void> {
} }
// 执行喝水提醒检查任务 // 执行喝水提醒检查任务
await executeWaterReminderTask(); executeWaterReminderTask();
// 执行站立提醒检查任务 // 执行站立提醒检查任务
await executeStandReminderTask(); executeStandReminderTask();
console.log('后台任务执行完成'); console.log('后台任务执行完成');
} catch (error) { } catch (error) {

View File

@@ -1,10 +1,10 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import AppleHealthKit, { HealthKitPermissions } from 'react-native-health'; import AppleHealthKit from 'react-native-health';
// 睡眠阶段枚举(与 HealthKit 保持一致) // 睡眠阶段枚举(与 HealthKit 保持一致)
export enum SleepStage { export enum SleepStage {
InBed = 'INBED', InBed = 'INBED',
Asleep = 'ASLEEP', Asleep = 'ASLEEP',
Awake = 'AWAKE', Awake = 'AWAKE',
Core = 'CORE', Core = 'CORE',
Deep = 'DEEP', Deep = 'DEEP',
@@ -48,22 +48,22 @@ export type SleepDetailData = {
sleepScore: number; // 睡眠得分 0-100 sleepScore: number; // 睡眠得分 0-100
totalSleepTime: number; // 总睡眠时间(分钟) totalSleepTime: number; // 总睡眠时间(分钟)
sleepQualityPercentage: number; // 睡眠质量百分比 sleepQualityPercentage: number; // 睡眠质量百分比
// 睡眠时间信息 // 睡眠时间信息
bedtime: string; // 上床时间 bedtime: string; // 上床时间
wakeupTime: string; // 起床时间 wakeupTime: string; // 起床时间
timeInBed: number; // 在床时间(分钟) timeInBed: number; // 在床时间(分钟)
// 睡眠阶段统计 // 睡眠阶段统计
sleepStages: SleepStageStats[]; sleepStages: SleepStageStats[];
// 心率数据 // 心率数据
averageHeartRate: number | null; // 平均心率 averageHeartRate: number | null; // 平均心率
sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据 sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据
// 睡眠效率 // 睡眠效率
sleepEfficiency: number; // 睡眠效率百分比 (总睡眠时间/在床时间) sleepEfficiency: number; // 睡眠效率百分比 (总睡眠时间/在床时间)
// 建议和评价 // 建议和评价
qualityDescription: string; // 睡眠质量描述 qualityDescription: string; // 睡眠质量描述
recommendation: string; // 睡眠建议 recommendation: string; // 睡眠建议
@@ -82,22 +82,22 @@ function createSleepDateRange(date: Date): { startDate: string; endDate: string
async function fetchSleepSamples(date: Date): Promise<SleepSample[]> { async function fetchSleepSamples(date: Date): Promise<SleepSample[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
const options = createSleepDateRange(date); const options = createSleepDateRange(date);
AppleHealthKit.getSleepSamples(options, (err, results) => { AppleHealthKit.getSleepSamples(options, (err, results) => {
if (err) { if (err) {
console.error('获取睡眠样本失败:', err); console.error('获取睡眠样本失败:', err);
resolve([]); resolve([]);
return; return;
} }
if (!results || !Array.isArray(results)) { if (!results || !Array.isArray(results)) {
console.warn('睡眠样本数据为空'); console.warn('睡眠样本数据为空');
resolve([]); resolve([]);
return; return;
} }
console.log('获取到睡眠样本:', results.length); console.log('获取到睡眠样本:', results.length);
resolve(results as SleepSample[]); resolve(results as unknown as SleepSample[]);
}); });
}); });
} }
@@ -110,24 +110,24 @@ async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Pro
endDate: wakeupTime, endDate: wakeupTime,
ascending: true ascending: true
}; };
AppleHealthKit.getHeartRateSamples(options, (err, results) => { AppleHealthKit.getHeartRateSamples(options, (err, results) => {
if (err) { if (err) {
console.error('获取睡眠心率数据失败:', err); console.error('获取睡眠心率数据失败:', err);
resolve([]); resolve([]);
return; return;
} }
if (!results || !Array.isArray(results)) { if (!results || !Array.isArray(results)) {
resolve([]); resolve([]);
return; return;
} }
const heartRateData: HeartRateData[] = results.map(sample => ({ const heartRateData: HeartRateData[] = results.map(sample => ({
timestamp: sample.startDate, timestamp: sample.startDate,
value: Math.round(sample.value) value: Math.round(sample.value)
})); }));
console.log('获取到睡眠心率数据:', heartRateData.length, '个样本'); console.log('获取到睡眠心率数据:', heartRateData.length, '个样本');
resolve(heartRateData); resolve(heartRateData);
}); });
@@ -137,52 +137,52 @@ async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Pro
// 计算睡眠阶段统计 // 计算睡眠阶段统计
function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] { function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
const stageMap = new Map<SleepStage, number>(); const stageMap = new Map<SleepStage, number>();
// 计算每个阶段的总时长 // 计算每个阶段的总时长
samples.forEach(sample => { samples.forEach(sample => {
const startTime = dayjs(sample.startDate); const startTime = dayjs(sample.startDate);
const endTime = dayjs(sample.endDate); const endTime = dayjs(sample.endDate);
const duration = endTime.diff(startTime, 'minute'); const duration = endTime.diff(startTime, 'minute');
const currentDuration = stageMap.get(sample.value) || 0; const currentDuration = stageMap.get(sample.value) || 0;
stageMap.set(sample.value, currentDuration + duration); stageMap.set(sample.value, currentDuration + duration);
}); });
// 计算总睡眠时间(排除在床时间) // 计算总睡眠时间(排除在床时间)
const totalSleepTime = Array.from(stageMap.entries()) const totalSleepTime = Array.from(stageMap.entries())
.filter(([stage]) => stage !== SleepStage.InBed && stage !== SleepStage.Awake) .filter(([stage]) => stage !== SleepStage.InBed)
.reduce((total, [, duration]) => total + duration, 0); .reduce((total, [, duration]) => total + duration, 0);
// 生成统计数据 // 生成统计数据
const stats: SleepStageStats[] = []; const stats: SleepStageStats[] = [];
stageMap.forEach((duration, stage) => { stageMap.forEach((duration, stage) => {
if (stage === SleepStage.InBed || stage === SleepStage.Awake) return; if (stage === SleepStage.InBed || stage === SleepStage.Awake) return;
const percentage = totalSleepTime > 0 ? (duration / totalSleepTime) * 100 : 0; const percentage = totalSleepTime > 0 ? (duration / totalSleepTime) * 100 : 0;
let quality: SleepQuality; let quality: SleepQuality;
// 根据睡眠阶段和比例判断质量 // 根据睡眠阶段和比例判断质量
switch (stage) { switch (stage) {
case SleepStage.Deep: case SleepStage.Deep:
quality = percentage >= 15 ? SleepQuality.Excellent : quality = percentage >= 15 ? SleepQuality.Excellent :
percentage >= 10 ? SleepQuality.Good : percentage >= 10 ? SleepQuality.Good :
percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
case SleepStage.REM: case SleepStage.REM:
quality = percentage >= 20 ? SleepQuality.Excellent : quality = percentage >= 20 ? SleepQuality.Excellent :
percentage >= 15 ? SleepQuality.Good : percentage >= 15 ? SleepQuality.Good :
percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
case SleepStage.Core: case SleepStage.Core:
quality = percentage >= 45 ? SleepQuality.Excellent : quality = percentage >= 45 ? SleepQuality.Excellent :
percentage >= 35 ? SleepQuality.Good : percentage >= 35 ? SleepQuality.Good :
percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
default: default:
quality = SleepQuality.Fair; quality = SleepQuality.Fair;
} }
stats.push({ stats.push({
stage, stage,
duration, duration,
@@ -190,7 +190,7 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
quality quality
}); });
}); });
// 按持续时间排序 // 按持续时间排序
return stats.sort((a, b) => b.duration - a.duration); return stats.sort((a, b) => b.duration - a.duration);
} }
@@ -198,26 +198,26 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
// 计算睡眠得分 // 计算睡眠得分
function calculateSleepScore(sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number { function calculateSleepScore(sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number {
let score = 0; let score = 0;
// 睡眠时长得分 (30分) // 睡眠时长得分 (30分)
const idealSleepHours = 8 * 60; // 8小时 const idealSleepHours = 8 * 60; // 8小时
const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30); const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30);
score += sleepDurationScore; score += sleepDurationScore;
// 睡眠效率得分 (25分) // 睡眠效率得分 (25分)
const efficiencyScore = (sleepEfficiency / 100) * 25; const efficiencyScore = (sleepEfficiency / 100) * 25;
score += efficiencyScore; score += efficiencyScore;
// 深度睡眠得分 (25分) // 深度睡眠得分 (25分)
const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep); const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep);
const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0; const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0;
score += deepSleepScore; score += deepSleepScore;
// REM睡眠得分 (20分) // REM睡眠得分 (20分)
const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM); const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM);
const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0; const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0;
score += remSleepScore; score += remSleepScore;
return Math.round(Math.min(100, score)); return Math.round(Math.min(100, score));
} }
@@ -290,48 +290,48 @@ export function getSleepStageColor(stage: SleepStage): string {
export async function fetchSleepDetailForDate(date: Date): Promise<SleepDetailData | null> { export async function fetchSleepDetailForDate(date: Date): Promise<SleepDetailData | null> {
try { try {
console.log('开始获取睡眠详情数据...', date); console.log('开始获取睡眠详情数据...', date);
// 获取睡眠样本数据 // 获取睡眠样本数据
const sleepSamples = await fetchSleepSamples(date); const sleepSamples = await fetchSleepSamples(date);
if (sleepSamples.length === 0) { if (sleepSamples.length === 0) {
console.warn('没有找到睡眠数据'); console.warn('没有找到睡眠数据');
return null; return null;
} }
// 找到上床时间和起床时间 // 找到上床时间和起床时间
const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed); const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed);
const bedtime = inBedSamples.length > 0 ? inBedSamples[0].startDate : sleepSamples[0].startDate; const bedtime = inBedSamples.length > 0 ? inBedSamples[0].startDate : sleepSamples[0].startDate;
const wakeupTime = inBedSamples.length > 0 ? const wakeupTime = inBedSamples.length > 0 ?
inBedSamples[inBedSamples.length - 1].endDate : inBedSamples[inBedSamples.length - 1].endDate :
sleepSamples[sleepSamples.length - 1].endDate; sleepSamples[sleepSamples.length - 1].endDate;
// 计算在床时间 // 计算在床时间
const timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute'); const timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute');
// 计算睡眠阶段统计 // 计算睡眠阶段统计
const sleepStages = calculateSleepStageStats(sleepSamples); const sleepStages = calculateSleepStageStats(sleepSamples);
// 计算总睡眠时间 // 计算总睡眠时间
const totalSleepTime = sleepStages.reduce((total, stage) => total + stage.duration, 0); const totalSleepTime = sleepStages.reduce((total, stage) => total + stage.duration, 0);
// 计算睡眠效率 // 计算睡眠效率
const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0;
// 获取睡眠期间心率数据 // 获取睡眠期间心率数据
const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime); const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime);
// 计算平均心率 // 计算平均心率
const averageHeartRate = sleepHeartRateData.length > 0 ? const averageHeartRate = sleepHeartRateData.length > 0 ?
Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) : Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) :
null; null;
// 计算睡眠得分 // 计算睡眠得分
const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime); const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime);
// 获取质量描述和建议 // 获取质量描述和建议
const qualityInfo = getSleepQualityInfo(sleepScore); const qualityInfo = getSleepQualityInfo(sleepScore);
const sleepDetailData: SleepDetailData = { const sleepDetailData: SleepDetailData = {
sleepScore, sleepScore,
totalSleepTime, totalSleepTime,
@@ -346,10 +346,10 @@ export async function fetchSleepDetailForDate(date: Date): Promise<SleepDetailDa
qualityDescription: qualityInfo.description, qualityDescription: qualityInfo.description,
recommendation: qualityInfo.recommendation recommendation: qualityInfo.recommendation
}; };
console.log('睡眠详情数据获取完成:', sleepDetailData); console.log('睡眠详情数据获取完成:', sleepDetailData);
return sleepDetailData; return sleepDetailData;
} catch (error) { } catch (error) {
console.error('获取睡眠详情数据失败:', error); console.error('获取睡眠详情数据失败:', error);
return null; return null;
@@ -360,7 +360,7 @@ export async function fetchSleepDetailForDate(date: Date): Promise<SleepDetailDa
export function formatSleepTime(minutes: number): string { export function formatSleepTime(minutes: number): string {
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const mins = minutes % 60; const mins = minutes % 60;
if (hours > 0 && mins > 0) { if (hours > 0 && mins > 0) {
return `${hours}h ${mins}m`; return `${hours}h ${mins}m`;
} else if (hours > 0) { } else if (hours > 0) {

View File

@@ -651,7 +651,7 @@ export class WaterNotificationHelpers {
* @returns 是否发送了通知 * @returns 是否发送了通知
*/ */
static async checkWaterGoalAndNotify( static async checkWaterGoalAndNotify(
userName: string, userName: string,
todayStats: { totalAmount: number; dailyGoal: number; completionRate: number }, todayStats: { totalAmount: number; dailyGoal: number; completionRate: number },
currentHour: number = new Date().getHours() currentHour: number = new Date().getHours()
): Promise<boolean> { ): Promise<boolean> {
@@ -749,7 +749,7 @@ export class WaterNotificationHelpers {
*/ */
static async sendWaterReminder(userName: string, message?: string) { static async sendWaterReminder(userName: string, message?: string) {
const defaultMessage = `${userName},记得要多喝水哦!保持身体水分充足很重要~💧`; const defaultMessage = `${userName},记得要多喝水哦!保持身体水分充足很重要~💧`;
return notificationService.sendImmediateNotification({ return notificationService.sendImmediateNotification({
title: '💧 喝水提醒', title: '💧 喝水提醒',
body: message || defaultMessage, body: message || defaultMessage,
@@ -770,12 +770,12 @@ export class WaterNotificationHelpers {
static async scheduleRegularWaterReminders(userName: string): Promise<string[]> { static async scheduleRegularWaterReminders(userName: string): Promise<string[]> {
try { try {
const notificationIds: string[] = []; const notificationIds: string[] = [];
// 检查是否已经存在定期喝水提醒 // 检查是否已经存在定期喝水提醒
const existingNotifications = await notificationService.getAllScheduledNotifications(); const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingWaterReminders = existingNotifications.filter( const existingWaterReminders = existingNotifications.filter(
notification => notification =>
notification.content.data?.type === 'regular_water_reminder' && notification.content.data?.type === 'regular_water_reminder' &&
notification.content.data?.isRegularReminder === true notification.content.data?.isRegularReminder === true
); );
@@ -787,7 +787,7 @@ export class WaterNotificationHelpers {
// 创建多个时间点的喝水提醒9:00-21:00每2小时一次 // 创建多个时间点的喝水提醒9:00-21:00每2小时一次
const reminderHours = [9, 11, 13, 15, 17, 19, 21]; const reminderHours = [9, 11, 13, 15, 17, 19, 21];
for (const hour of reminderHours) { for (const hour of reminderHours) {
const notificationId = await notificationService.scheduleCalendarRepeatingNotification( const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{ {
@@ -808,7 +808,7 @@ export class WaterNotificationHelpers {
minute: 0, minute: 0,
} }
); );
notificationIds.push(notificationId); notificationIds.push(notificationId);
console.log(`已安排${hour}:00的定期喝水提醒通知ID: ${notificationId}`); console.log(`已安排${hour}:00的定期喝水提醒通知ID: ${notificationId}`);
} }
@@ -830,8 +830,8 @@ export class WaterNotificationHelpers {
const notifications = await notificationService.getAllScheduledNotifications(); const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) { for (const notification of notifications) {
if (notification.content.data?.type === 'water_reminder' || if (notification.content.data?.type === 'water_reminder' ||
notification.content.data?.type === 'regular_water_reminder') { notification.content.data?.type === 'regular_water_reminder') {
await notificationService.cancelNotification(notification.identifier); await notificationService.cancelNotification(notification.identifier);
console.log('已取消喝水提醒:', notification.identifier); console.log('已取消喝水提醒:', notification.identifier);
} }
@@ -931,12 +931,12 @@ export class StandReminderHelpers {
// 动态导入健康工具,避免循环依赖 // 动态导入健康工具,避免循环依赖
const { getCurrentHourStandStatus } = await import('@/utils/health'); const { getCurrentHourStandStatus } = await import('@/utils/health');
// 获取当前小时站立状态 // 获取当前小时站立状态
const standStatus = await getCurrentHourStandStatus(); const standStatus = await getCurrentHourStandStatus();
console.log('当前站立状态:', standStatus); console.log('当前站立状态:', standStatus);
// 如果已经站立过,不需要提醒 // 如果已经站立过,不需要提醒
if (standStatus.hasStood) { if (standStatus.hasStood) {
console.log('用户当前小时已经站立,无需提醒'); console.log('用户当前小时已经站立,无需提醒');
@@ -963,7 +963,7 @@ export class StandReminderHelpers {
await notificationService.sendImmediateNotification({ await notificationService.sendImmediateNotification({
title: '站立提醒', title: '站立提醒',
body: reminderMessage, body: reminderMessage,
data: { data: {
type: 'stand_reminder', type: 'stand_reminder',
currentStandHours: standStatus.standHours, currentStandHours: standStatus.standHours,
standHoursGoal: standStatus.standHoursGoal, standHoursGoal: standStatus.standHoursGoal,
@@ -988,7 +988,7 @@ export class StandReminderHelpers {
private static generateStandReminderMessage(userName: string, currentStandHours: number, goalHours: number): string { private static generateStandReminderMessage(userName: string, currentStandHours: number, goalHours: number): string {
const currentHour = new Date().getHours(); const currentHour = new Date().getHours();
const progress = Math.round((currentStandHours / goalHours) * 100); const progress = Math.round((currentStandHours / goalHours) * 100);
const messages = [ const messages = [
`${userName},该站起来活动一下了!当前已完成${progress}%的站立目标`, `${userName},该站起来活动一下了!当前已完成${progress}%的站立目标`,
`${userName},久坐伤身,起来走走吧~已站立${currentStandHours}/${goalHours}小时`, `${userName},久坐伤身,起来走走吧~已站立${currentStandHours}/${goalHours}小时`,