diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx
index 958aac8..b14ada1 100644
--- a/app/sleep-detail.tsx
+++ b/app/sleep-detail.tsx
@@ -1,40 +1,44 @@
-import React, { useEffect, useState } from 'react';
-import {
- StyleSheet,
- Text,
- View,
- ScrollView,
- TouchableOpacity,
- Dimensions,
- ActivityIndicator,
-} from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { router } from 'expo-router';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
+import { router } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+ ActivityIndicator,
+ Animated,
+ Dimensions,
+ Modal,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
import Svg, { Circle } from 'react-native-svg';
+import { Ionicons } from '@expo/vector-icons';
-import {
- fetchSleepDetailForDate,
- SleepDetailData,
- SleepStage,
- getSleepStageDisplayName,
- getSleepStageColor,
+import { HeaderBar } from '@/components/ui/HeaderBar';
+import { Colors } from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+import {
+ fetchSleepDetailForDate,
formatSleepTime,
- formatTime
+ formatTime,
+ getSleepStageColor,
+ getSleepStageDisplayName,
+ SleepDetailData,
+ SleepStage
} from '@/services/sleepService';
import { ensureHealthPermissions } from '@/utils/health';
-import { Colors } from '@/constants/Colors';
const { width } = Dimensions.get('window');
// 圆形进度条组件
-const CircularProgress = ({
- size,
- strokeWidth,
- progress,
- color,
- backgroundColor = '#E5E7EB'
+const CircularProgress = ({
+ size,
+ strokeWidth,
+ progress,
+ color,
+ backgroundColor = '#E5E7EB'
}: {
size: number;
strokeWidth: number;
@@ -78,14 +82,14 @@ const CircularProgress = ({
const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
const chartWidth = width - 80;
const maxHeight = 120;
-
+
// 生成24小时的睡眠阶段数据(模拟数据,实际应根据真实样本计算)
const hourlyData = Array.from({ length: 24 }, (_, hour) => {
// 如果没有数据,显示空状态
if (sleepData.totalSleepTime === 0) {
return null;
}
-
+
// 根据时间判断可能的睡眠状态
if (hour >= 0 && hour <= 6) {
// 凌晨0-6点,主要睡眠时间
@@ -112,12 +116,12 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
☀️ {sleepData.totalSleepTime > 0 ? formatTime(sleepData.wakeupTime) : '--:--'}
-
+
{hourlyData.map((stage, index) => {
const barHeight = stage ? Math.random() * 0.6 + 0.4 : 0.1; // 随机高度模拟真实数据
const color = stage ? getSleepStageColor(stage) : '#E5E7EB';
-
+
return (
{
);
};
+// Sleep Grade Component 睡眠等级组件
+const SleepGradeCard = ({
+ icon,
+ grade,
+ range,
+ isActive = false
+}: {
+ icon: string;
+ grade: string;
+ range: string;
+ isActive?: boolean;
+}) => {
+ const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
+ const colorTokens = Colors[theme];
+
+ const getGradeColor = (grade: string) => {
+ switch (grade) {
+ case '低': return { bg: '#FECACA', text: '#DC2626' };
+ case '正常': return { bg: '#D1FAE5', text: '#065F46' };
+ case '良好': return { bg: '#D1FAE5', text: '#065F46' };
+ case '优秀': return { bg: '#FEF3C7', text: '#92400E' };
+ default: return { bg: colorTokens.pageBackgroundEmphasis, text: colorTokens.textSecondary };
+ }
+ };
+
+ const colors = getGradeColor(grade);
+
+ return (
+
+
+ {icon}
+
+ {grade}
+
+
+
+ {range}
+
+
+ );
+};
+
+// Info Modal 组件
+const InfoModal = ({
+ visible,
+ onClose,
+ title,
+ type
+}: {
+ visible: boolean;
+ onClose: () => void;
+ title: string;
+ type: 'sleep-time' | 'sleep-quality';
+}) => {
+ const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
+ const colorTokens = Colors[theme];
+ const slideAnim = useState(new Animated.Value(0))[0];
+
+ React.useEffect(() => {
+ if (visible) {
+ Animated.spring(slideAnim, {
+ toValue: 1,
+ useNativeDriver: true,
+ tension: 100,
+ friction: 8,
+ }).start();
+ } else {
+ Animated.spring(slideAnim, {
+ toValue: 0,
+ useNativeDriver: true,
+ tension: 100,
+ friction: 8,
+ }).start();
+ }
+ }, [visible]);
+
+ const translateY = slideAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [300, 0],
+ });
+
+ const opacity = slideAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+
+ const sleepTimeGrades = [
+ { icon: '⚠️', grade: '低', range: '< 6h', isActive: false },
+ { icon: '✅', grade: '正常', range: '6h - 7h or > 9h', isActive: false },
+ { icon: '✅', grade: '良好', range: '7h - 8h', isActive: true },
+ { icon: '⭐', grade: '优秀', range: '8h - 9h', isActive: false },
+ ];
+
+ const sleepQualityGrades = [
+ { icon: '⚠️', grade: '较差', range: '< 55%', isActive: false },
+ { icon: '✅', grade: '一般', range: '55% - 69%', isActive: false },
+ { icon: '✅', grade: '良好', range: '70% - 84%', isActive: false },
+ { icon: '⭐', grade: '优秀', range: '85% - 100%', isActive: true },
+ ];
+
+ const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades;
+
+ const getDescription = () => {
+ if (type === 'sleep-time') {
+ return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。';
+ } else {
+ return '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长,还包括睡眠的连续性和各睡眠阶段的平衡。';
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+ {/* 等级卡片区域 */}
+
+ {currentGrades.map((grade, index) => (
+
+ ))}
+
+
+
+ {getDescription()}
+
+
+
+
+ );
+};
+
export default function SleepDetailScreen() {
- const insets = useSafeAreaInsets();
+ const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
+ const colorTokens = Colors[theme];
const [sleepData, setSleepData] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedDate] = useState(dayjs().toDate());
+ const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({
+ visible: false,
+ title: '',
+ type: null
+ });
useEffect(() => {
loadSleepData();
@@ -150,7 +334,7 @@ export default function SleepDetailScreen() {
const loadSleepData = async () => {
try {
setLoading(true);
-
+
// 确保有健康权限
const hasPermission = await ensureHealthPermissions();
if (!hasPermission) {
@@ -161,7 +345,7 @@ export default function SleepDetailScreen() {
// 获取睡眠详情数据
const data = await fetchSleepDetailForDate(selectedDate);
setSleepData(data);
-
+
} catch (error) {
console.error('加载睡眠数据失败:', error);
} finally {
@@ -205,17 +389,14 @@ export default function SleepDetailScreen() {
/>
{/* 顶部导航 */}
-
- router.back()}>
- ‹
-
- 今天, {dayjs(selectedDate).format('M月DD日')}
-
- ›
-
-
+ router.back()}
+ withSafeTop={true}
+ transparent={true}
+ />
- {displayData.qualityDescription}
-
+
{/* 建议文本 */}
{displayData.recommendation}
{/* 睡眠统计卡片 */}
-
- 🌙
- 睡眠时间
- {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '--'}
-
- {displayData.totalSleepTime > 0 ? '良好' : '--'}
+
+
+
+ 🌙
+
+ setInfoModal({
+ visible: true,
+ title: '睡眠时间',
+ type: 'sleep-time'
+ })}
+ >
+
+
+
+ 睡眠时间
+
+ {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'}
+
+ ✓ 良好
+
-
- 💎
- 睡眠质量
- {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '--'}
-
- {displayData.sleepQualityPercentage > 0 ? '优秀' : '--'}
+
+
+
+ 💎
+
+ setInfoModal({
+ visible: true,
+ title: '睡眠质量',
+ type: 'sleep-quality'
+ })}
+ >
+
+
+
+ 睡眠质量
+
+ {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'}
+
+ ★ 优秀
+
@@ -280,13 +493,15 @@ export default function SleepDetailScreen() {
{formatSleepTime(stage.duration)}
{stage.quality === 'excellent' ? '优秀' :
- stage.quality === 'good' ? '良好' :
- stage.quality === 'fair' ? '一般' : '偏低'}
+ stage.quality === 'good' ? '良好' :
+ stage.quality === 'fair' ? '一般' : '偏低'}
@@ -297,6 +512,15 @@ export default function SleepDetailScreen() {
)}
+
+ {infoModal.type && (
+ setInfoModal({ ...infoModal, visible: false })}
+ title={infoModal.title}
+ type={infoModal.type}
+ />
+ )}
);
}
@@ -313,45 +537,6 @@ const styles = StyleSheet.create({
top: 0,
bottom: 0,
},
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 20,
- paddingBottom: 16,
- backgroundColor: 'transparent',
- },
- backButton: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- backButtonText: {
- fontSize: 24,
- fontWeight: '300',
- color: '#374151',
- },
- headerTitle: {
- fontSize: 16,
- fontWeight: '600',
- color: '#111827',
- },
- navButton: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- navButtonText: {
- fontSize: 24,
- fontWeight: '300',
- color: '#9CA3AF',
- },
scrollView: {
flex: 1,
},
@@ -402,8 +587,38 @@ const styles = StyleSheet.create({
},
statsContainer: {
flexDirection: 'row',
- gap: 16,
+ gap: 12,
marginBottom: 32,
+ paddingHorizontal: 4,
+ },
+ newStatCard: {
+ flex: 1,
+ borderRadius: 20,
+ padding: 20,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.08,
+ shadowRadius: 12,
+ elevation: 4,
+ borderWidth: 1,
+ borderColor: 'rgba(0, 0, 0, 0.06)',
+ },
+ statCardHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: 8,
+ },
+ statCardIcon: {
+ width: 32,
+ height: 32,
+ borderRadius: 8,
+ backgroundColor: 'rgba(120, 120, 128, 0.08)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ infoButton: {
+ padding: 4,
},
statCard: {
flex: 1,
@@ -418,13 +633,42 @@ const styles = StyleSheet.create({
elevation: 3,
},
statIcon: {
- fontSize: 24,
- marginBottom: 8,
+ fontSize: 18,
},
statLabel: {
fontSize: 12,
- color: '#6B7280',
- marginBottom: 4,
+ fontWeight: '500',
+ marginBottom: 8,
+ letterSpacing: 0.2,
+ },
+ newStatValue: {
+ fontSize: 28,
+ fontWeight: '700',
+ marginBottom: 12,
+ letterSpacing: -0.5,
+ },
+ qualityBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 8,
+ alignSelf: 'flex-start',
+ },
+ goodQualityBadge: {
+ backgroundColor: '#D1FAE5',
+ },
+ excellentQualityBadge: {
+ backgroundColor: '#FEF3C7',
+ },
+ qualityBadgeText: {
+ fontSize: 12,
+ fontWeight: '600',
+ letterSpacing: 0.1,
+ },
+ goodQualityText: {
+ color: '#065F46',
+ },
+ excellentQualityText: {
+ color: '#92400E',
},
statValue: {
fontSize: 18,
@@ -566,4 +810,82 @@ const styles = StyleSheet.create({
color: '#9CA3AF',
fontStyle: 'italic',
},
+ // Info Modal 样式
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'flex-end',
+ },
+ infoModalContent: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ paddingTop: 12,
+ paddingHorizontal: 20,
+ paddingBottom: 34,
+ minHeight: 200,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: -4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 16,
+ elevation: 8,
+ },
+ modalHandle: {
+ width: 36,
+ height: 4,
+ backgroundColor: '#D1D5DB',
+ borderRadius: 2,
+ alignSelf: 'center',
+ marginBottom: 20,
+ },
+ infoModalHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ infoModalTitle: {
+ fontSize: 18,
+ fontWeight: '700',
+ letterSpacing: -0.3,
+ },
+ infoModalCloseButton: {
+ padding: 4,
+ },
+ infoModalText: {
+ fontSize: 15,
+ lineHeight: 22,
+ letterSpacing: -0.1,
+ },
+ // Grade Cards 样式
+ gradesContainer: {
+ marginBottom: 20,
+ gap: 8,
+ },
+ gradeCard: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderRadius: 12,
+ borderWidth: 1,
+ },
+ gradeCardLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ gradeIcon: {
+ fontSize: 16,
+ },
+ gradeText: {
+ fontSize: 16,
+ fontWeight: '600',
+ letterSpacing: -0.2,
+ },
+ gradeRange: {
+ fontSize: 16,
+ fontWeight: '700',
+ letterSpacing: -0.3,
+ },
});
\ No newline at end of file
diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts
index a12e7d7..3a9e191 100644
--- a/services/backgroundTaskManager.ts
+++ b/services/backgroundTaskManager.ts
@@ -162,9 +162,6 @@ async function executeBackgroundTasks(): Promise {
return;
}
- // 发送测试通知以验证任务是否正在执行
- await sendTestNotification();
-
// 执行喝水提醒检查任务
await executeWaterReminderTask();