import { CircularRing } from '@/components/CircularRing'; import { ThemedView } from '@/components/ThemedView'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { fetchActivityRingsForDate, fetchHourlyActiveCaloriesForDate, fetchHourlyExerciseMinutesForDate, fetchHourlyStandHoursForDate, type ActivityRingsData, type HourlyActivityData, type HourlyExerciseData, type HourlyStandData } from '@/utils/health'; import { getFitnessExerciseMinutesInfoDismissed, setFitnessExerciseMinutesInfoDismissed } from '@/utils/userPreferences'; import { Ionicons } from '@expo/vector-icons'; import DateTimePicker from '@react-native-community/datetimepicker'; import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; import weekday from 'dayjs/plugin/weekday'; import { router } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { Animated, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; // 配置 dayjs 插件 dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(weekday); // 设置默认时区为中国时区 dayjs.tz.setDefault('Asia/Shanghai'); type WeekData = { date: Date; data: ActivityRingsData | null; isToday: boolean; dayName: string; }; export default function FitnessRingsDetailScreen() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const colorScheme = useColorScheme(); const [weekData, setWeekData] = useState([]); const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedDayData, setSelectedDayData] = useState(null); const [datePickerVisible, setDatePickerVisible] = useState(false); const [pickerDate, setPickerDate] = useState(new Date()); // 每小时数据状态 const [hourlyCaloriesData, setHourlyCaloriesData] = useState([]); const [hourlyExerciseData, setHourlyExerciseData] = useState([]); const [hourlyStandData, setHourlyStandData] = useState([]); const [showExerciseInfo, setShowExerciseInfo] = useState(true); const exerciseInfoAnim = useRef(new Animated.Value(1)).current; useEffect(() => { // 加载周数据和选中日期的详细数据 loadWeekData(selectedDate); loadSelectedDayData(); loadExerciseInfoPreference(); }, [selectedDate]); const loadExerciseInfoPreference = async () => { try { const dismissed = await getFitnessExerciseMinutesInfoDismissed(); setShowExerciseInfo(!dismissed); if (!dismissed) { exerciseInfoAnim.setValue(1); } else { exerciseInfoAnim.setValue(0); } } catch (error) { console.error(t('fitnessRingsDetail.errors.loadExerciseInfoPreference'), error); } }; const loadWeekData = async (targetDate: Date) => { const target = dayjs(targetDate).tz('Asia/Shanghai'); const today = dayjs().tz('Asia/Shanghai'); const weekDays = []; // 获取目标日期所在周的数据 (周一到周日) // 使用 weekday() 确保周一为一周的开始 (0=Monday, 6=Sunday) const startOfWeek = target.weekday(0); // 周一开始 for (let i = 0; i < 7; i++) { const currentDay = startOfWeek.add(i, 'day'); const isToday = currentDay.isSame(today, 'day'); const dayNames = [ t('fitnessRingsDetail.weekDays.monday'), t('fitnessRingsDetail.weekDays.tuesday'), t('fitnessRingsDetail.weekDays.wednesday'), t('fitnessRingsDetail.weekDays.thursday'), t('fitnessRingsDetail.weekDays.friday'), t('fitnessRingsDetail.weekDays.saturday'), t('fitnessRingsDetail.weekDays.sunday') ]; try { const activityRingsData = await fetchActivityRingsForDate(currentDay.toDate()); weekDays.push({ date: currentDay.toDate(), data: activityRingsData, isToday, dayName: dayNames[i] }); } catch (error) { console.error('Failed to fetch activity rings data for', currentDay.format('YYYY-MM-DD'), error); weekDays.push({ date: currentDay.toDate(), data: null, isToday, dayName: dayNames[i] }); } } setWeekData(weekDays); }; const loadSelectedDayData = async () => { try { // 并行获取活动圆环数据和每小时详细数据 const [activityRingsData, hourlyCalories, hourlyExercise, hourlyStand] = await Promise.all([ fetchActivityRingsForDate(selectedDate), fetchHourlyActiveCaloriesForDate(selectedDate), fetchHourlyExerciseMinutesForDate(selectedDate), fetchHourlyStandHoursForDate(selectedDate) ]); setSelectedDayData(activityRingsData); setHourlyCaloriesData(hourlyCalories); setHourlyExerciseData(hourlyExercise); setHourlyStandData(hourlyStand); } catch (error) { console.error('Failed to fetch selected day activity rings data', error); setSelectedDayData(null); setHourlyCaloriesData([]); setHourlyExerciseData([]); setHourlyStandData([]); } }; // 日期选择器相关函数 const openDatePicker = () => { setPickerDate(selectedDate); setDatePickerVisible(true); }; const closeDatePicker = () => setDatePickerVisible(false); const onConfirmDate = async (date: Date) => { const today = dayjs().tz('Asia/Shanghai').startOf('day'); const picked = dayjs(date).tz('Asia/Shanghai').startOf('day'); const finalDate = picked.isAfter(today) ? today.toDate() : picked.toDate(); setSelectedDate(finalDate); closeDatePicker(); }; // 格式化头部显示的日期 const formatHeaderDate = (date: Date) => { const dayJsDate = dayjs(date).tz('Asia/Shanghai'); return `${dayJsDate.format('YYYY年MM月DD日')}`; }; const renderWeekRingItem = (item: WeekData, index: number) => { const isSelected = dayjs(item.date).tz('Asia/Shanghai').isSame(dayjs(selectedDate).tz('Asia/Shanghai'), 'day'); // 使用默认值确保即使没有数据也能显示圆环 const data = item.data || { activeEnergyBurned: 0, activeEnergyBurnedGoal: 350, appleExerciseTime: 0, appleExerciseTimeGoal: 30, appleStandHours: 0, appleStandHoursGoal: 12, }; const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data; // 计算进度百分比 const caloriesProgress = Math.min(1, Math.max(0, activeEnergyBurned / activeEnergyBurnedGoal)); const exerciseProgress = Math.min(1, Math.max(0, appleExerciseTime / appleExerciseTimeGoal)); const standProgress = Math.min(1, Math.max(0, appleStandHours / appleStandHoursGoal)); // 检查是否完成了所有目标 const isComplete = caloriesProgress >= 1 && exerciseProgress >= 1 && standProgress >= 1; return ( setSelectedDate(item.date)} > {/* {isComplete && ( )} */} {/* 外圈 - 活动卡路里 (红色) */} {/* 中圈 - 锻炼分钟 (橙色) */} {/* 内圈 - 站立小时 (蓝色) */} {dayjs(item.date).tz('Asia/Shanghai').date()} {item.dayName} ); }; const getClosedRingCount = () => { let count = 0; weekData.forEach(item => { // 使用默认值处理空数据情况 const data = item.data || { activeEnergyBurned: 0, activeEnergyBurnedGoal: 350, appleExerciseTime: 0, appleExerciseTimeGoal: 30, appleStandHours: 0, appleStandHoursGoal: 12, }; const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data; const caloriesComplete = activeEnergyBurned >= activeEnergyBurnedGoal; const exerciseComplete = appleExerciseTime >= appleExerciseTimeGoal; const standComplete = appleStandHours >= appleStandHoursGoal; if (caloriesComplete && exerciseComplete && standComplete) { count++; } }); return count; }; const handleKnowButtonPress = async () => { try { await setFitnessExerciseMinutesInfoDismissed(true); Animated.timing(exerciseInfoAnim, { toValue: 0, duration: 300, useNativeDriver: true, }).start(() => { setShowExerciseInfo(false); }); } catch (error) { console.error(t('fitnessRingsDetail.errors.saveExerciseInfoPreference'), error); } }; // 渲染简单的柱状图 const renderBarChart = (data: number[], maxValue: number, color: string, unit: string) => { // 确保始终有24小时的数据,没有数据时用0填充 const chartData = Array.from({ length: 24 }, (_, index) => { if (data && data.length > index) { return data[index] || 0; } return 0; }); // 计算最大值,如果所有数据都是0,使用传入的maxValue作为参考 const maxChartValue = Math.max(...chartData, 1); // 确保最小值为1,避免除零 const effectiveMaxValue = Math.max(maxChartValue, maxValue); return ( {chartData.map((value, index) => { const height = Math.max(2, (value / effectiveMaxValue) * 40); // 最小高度2,最大40 return ( 0 ? height : 2, // 没有数据时显示最小高度的灰色条 backgroundColor: value > 0 ? color : '#E5E5EA', opacity: value > 0 ? 1 : 0.5, marginHorizontal: 0.5 } ]} /> ); })} {chartData.map((_, index) => { // 只在关键时间点显示标签:0点、6点、12点、18点 if (index === 0 || index === 6 || index === 12 || index === 18) { const hour = index; return ( {hour.toString().padStart(2, '0')}:00 ); } // 对于不显示标签的小时,返回一个占位的View return ; })} ); }; const renderSelectedDayDetail = () => { // 使用默认值确保即使没有数据也能显示图表 const data = selectedDayData || { activeEnergyBurned: 0, activeEnergyBurnedGoal: 350, appleExerciseTime: 0, appleExerciseTimeGoal: 30, appleStandHours: 0, appleStandHoursGoal: 12, }; const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data; return ( {/* 活动热量卡片 */} {t('fitnessRingsDetail.cards.activeCalories.title')} ? {Math.round(activeEnergyBurned)}/{activeEnergyBurnedGoal} {t('fitnessRingsDetail.cards.activeCalories.unit')} {Math.round(activeEnergyBurned)}{t('fitnessRingsDetail.cards.activeCalories.unit')} {renderBarChart( hourlyCaloriesData.map(h => h.calories), Math.max(activeEnergyBurnedGoal / 24, 1), '#FF3B30', t('fitnessRingsDetail.cards.activeCalories.unit') )} {/* 锻炼分钟卡片 */} {t('fitnessRingsDetail.cards.exerciseMinutes.title')} ? {Math.round(appleExerciseTime)}/{appleExerciseTimeGoal} {t('fitnessRingsDetail.cards.exerciseMinutes.unit')} {Math.round(appleExerciseTime)}{t('fitnessRingsDetail.cards.exerciseMinutes.unit')} {renderBarChart( hourlyExerciseData.map(h => h.minutes), Math.max(appleExerciseTimeGoal / 8, 1), '#FF9500', t('fitnessRingsDetail.cards.exerciseMinutes.unit') )} {/* 锻炼分钟说明 */} {showExerciseInfo && ( {t('fitnessRingsDetail.cards.exerciseMinutes.info.title')} {t('fitnessRingsDetail.cards.exerciseMinutes.info.description')} {t('fitnessRingsDetail.cards.exerciseMinutes.info.recommendation')} {t('fitnessRingsDetail.cards.exerciseMinutes.info.knowButton')} )} {/* 活动小时数卡片 */} {t('fitnessRingsDetail.cards.standHours.title')} ? {Math.round(appleStandHours)}/{appleStandHoursGoal} {t('fitnessRingsDetail.cards.standHours.unit')} {Math.round(appleStandHours)}{t('fitnessRingsDetail.cards.standHours.unit')} {renderBarChart( hourlyStandData.map(h => h.hasStood), 1, '#007AFF', t('fitnessRingsDetail.cards.standHours.unit') )} ); }; return ( {/* 头部 */} router.back()} right={ } withSafeTop={true} transparent={true} variant="default" /> {/* 本周圆环横向滚动 */} {weekData.map((item, index) => renderWeekRingItem(item, index))} {/* 选中日期的详细数据 */} {renderSelectedDayDetail()} {/* 周闭环天数统计 */} {t('fitnessRingsDetail.stats.weeklyClosedRings')} {getClosedRingCount()}{t('fitnessRingsDetail.stats.daysUnit')} {/* 日期选择器弹窗 */} { if (Platform.OS === 'ios') { if (date) setPickerDate(date); } else { if (event.type === 'set' && date) { onConfirmDate(date); } else { closeDatePicker(); } } }} /> {Platform.OS === 'ios' && ( {t('fitnessRingsDetail.datePicker.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> {t('fitnessRingsDetail.datePicker.confirm')} )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, calendarButton: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center', }, scrollView: { flex: 1, }, scrollContent: { paddingBottom: 32, }, weekSection: { paddingVertical: 20, }, weekScrollView: { paddingHorizontal: 16, }, weekScrollContent: { paddingHorizontal: 8, }, weekRingItem: { alignItems: 'center', marginHorizontal: 8, padding: 8, borderRadius: 12, }, weekRingItemSelected: { backgroundColor: 'rgba(0, 122, 255, 0.1)', }, weekRingContainer: { position: 'relative', alignItems: 'center', justifyContent: 'center', marginBottom: 8, }, weekStarContainer: { position: 'absolute', top: -8, right: -8, zIndex: 10, }, weekStarIcon: { fontSize: 12, }, weekRingsWrapper: { position: 'relative', width: 50, height: 50, alignItems: 'center', justifyContent: 'center', }, ringPosition: { position: 'absolute', alignItems: 'center', justifyContent: 'center', }, weekDayNumber: { fontSize: 11, fontWeight: '600', marginTop: 6, }, weekTodayNumber: { color: '#007AFF', }, weekSelectedNumber: { fontWeight: '700', }, weekDayLabel: { fontSize: 10, fontWeight: '500', marginTop: 2, }, weekTodayLabel: { color: '#007AFF', }, weekSelectedLabel: { fontWeight: '600', }, detailContainer: { paddingHorizontal: 16, paddingVertical: 20, }, // 卡片样式 metricCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.06, shadowRadius: 8, elevation: 3, }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, cardTitle: { fontSize: 18, fontWeight: '600', color: '#1C1C1E', }, helpButton: { width: 24, height: 24, borderRadius: 12, backgroundColor: '#F2F2F7', alignItems: 'center', justifyContent: 'center', }, helpIcon: { fontSize: 14, fontWeight: '600', color: '#8E8E93', }, cardValue: { flexDirection: 'row', alignItems: 'baseline', marginBottom: 8, }, valueText: { fontSize: 32, fontWeight: '700', letterSpacing: -1, }, unitText: { fontSize: 18, fontWeight: '500', color: '#8E8E93', marginLeft: 4, }, cardSubtext: { fontSize: 14, color: '#8E8E93', marginBottom: 20, }, // 图表样式 chartContainer: { marginTop: 16, }, chartBars: { flexDirection: 'row', alignItems: 'flex-end', height: 60, marginBottom: 8, paddingHorizontal: 2, }, chartBar: { borderRadius: 1.5, }, chartLabels: { flexDirection: 'row', paddingHorizontal: 2, justifyContent: 'space-between', }, chartLabel: { fontSize: 10, color: '#8E8E93', fontWeight: '500', textAlign: 'center', flex: 6, // 给显示标签的元素更多空间 }, chartLabelSpacer: { flex: 1, // 占位元素使用较少空间 }, // 锻炼信息样式 exerciseInfo: { marginTop: 20, padding: 16, backgroundColor: '#F2F2F7', borderRadius: 12, }, exerciseTitle: { fontSize: 16, fontWeight: '600', color: '#1C1C1E', marginBottom: 8, }, exerciseDesc: { fontSize: 14, color: '#3C3C43', lineHeight: 20, marginBottom: 12, }, exerciseRecommendation: { fontSize: 14, color: '#3C3C43', lineHeight: 20, marginBottom: 16, }, knowButton: { alignSelf: 'flex-end', paddingHorizontal: 16, paddingVertical: 8, backgroundColor: '#007AFF', borderRadius: 20, }, knowButtonText: { fontSize: 14, fontWeight: '600', color: '#FFFFFF', }, noDataText: { fontSize: 16, textAlign: 'center', marginTop: 40, }, statsContainer: { marginHorizontal: 16, marginTop: 32, padding: 16, backgroundColor: 'rgba(0, 0, 0, 0.05)', borderRadius: 12, }, statRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, statLabel: { fontSize: 16, fontWeight: '500', }, statValue: { flexDirection: 'row', alignItems: 'center', }, statNumber: { fontSize: 16, fontWeight: '600', marginLeft: 4, }, starIcon: { fontSize: 16, }, // 日期选择器样式 modalBackdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)', }, modalSheet: { position: 'absolute', left: 0, right: 0, bottom: 0, padding: 16, backgroundColor: '#FFFFFF', borderTopLeftRadius: 16, borderTopRightRadius: 16, }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 8, gap: 12, }, modalBtn: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 10, backgroundColor: '#F1F5F9', }, modalBtnPrimary: { backgroundColor: '#7a5af8', }, modalBtnText: { color: '#334155', fontWeight: '700', }, modalBtnTextPrimary: { color: '#FFFFFF', fontWeight: '700', }, });