feat: 优化提醒注册逻辑,确保用户姓名存在时注册午餐、晚餐和心情提醒;更新睡眠详情页面,添加清醒时间段的判断和模拟数据展示;调整样式以提升用户体验
This commit is contained in:
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -163,10 +163,10 @@ async function executeBackgroundTasks(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行喝水提醒检查任务
|
// 执行喝水提醒检查任务
|
||||||
await executeWaterReminderTask();
|
executeWaterReminderTask();
|
||||||
|
|
||||||
// 执行站立提醒检查任务
|
// 执行站立提醒检查任务
|
||||||
await executeStandReminderTask();
|
executeStandReminderTask();
|
||||||
|
|
||||||
console.log('后台任务执行完成');
|
console.log('后台任务执行完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}小时`,
|
||||||
|
|||||||
Reference in New Issue
Block a user