From f9a175d76cc4c4e0b9cec135ca0d0ee59f9eb7e2 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 8 Sep 2025 10:09:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=9D=A1=E7=9C=A0?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=9D=A1=E7=9C=A0=E7=AD=89=E7=BA=A7=E5=92=8C=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=A1=86=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=BB=9F=E8=AE=A1=E5=8D=A1=E7=89=87=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=B5=8B=E8=AF=95=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/sleep-detail.tsx | 532 ++++++++++++++++++++++++------ services/backgroundTaskManager.ts | 3 - 2 files changed, 427 insertions(+), 108 deletions(-) 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();