import { DateSelector } from '@/components/DateSelector'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppSelector } from '@/hooks/redux'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { selectUserAge, selectUserProfile } from '@/store/userSlice'; import { getLocalizedDateFormat, getMonthDays, getTodayIndexInMonth } from '@/utils/date'; import { fetchBasalEnergyBurned } from '@/utils/health'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import weekOfYear from 'dayjs/plugin/weekOfYear'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { BarChart } from 'react-native-chart-kit'; dayjs.extend(weekOfYear); type TabType = 'week' | 'month'; type BasalMetabolismData = { date: Date; value: number | null; }; export default function BasalMetabolismDetailScreen() { const { t, i18n } = useI18n(); const userProfile = useAppSelector(selectUserProfile); const userAge = useAppSelector(selectUserAge); const safeAreaTop = useSafeAreaTop() // 日期相关状态 const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); const [activeTab, setActiveTab] = useState('week'); // 说明弹窗状态 const [infoModalVisible, setInfoModalVisible] = useState(false); // 数据状态 const [chartData, setChartData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // 缓存和防抖相关,参照BasalMetabolismCard const [cacheRef] = useState(() => new Map()); const [loadingRef] = useState(() => new Map>()); const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 console.log('basal metabolism chartData', chartData); // 生成日期范围的函数 const generateDateRange = useCallback((tab: TabType): Date[] => { const today = new Date(); const dates: Date[] = []; switch (tab) { case 'week': // 获取最近7天 for (let i = 6; i >= 0; i--) { const date = dayjs(today).subtract(i, 'day').toDate(); dates.push(date); } break; case 'month': // 获取最近30天,按周分组 for (let i = 3; i >= 0; i--) { const date = dayjs(today).subtract(i * 7, 'day').toDate(); dates.push(date); } break; } return dates; }, []); // 优化的数据获取函数,包含缓存和去重复请求 const fetchBasalMetabolismData = useCallback(async (tab: TabType): Promise => { const cacheKey = `${tab}-${dayjs().format('YYYY-MM-DD')}`; const now = Date.now(); // 检查缓存 const cached = cacheRef.get(cacheKey); if (cached && (now - cached.timestamp) < CACHE_DURATION) { return cached.data; } // 检查是否已经在请求中(防止重复请求) const existingRequest = loadingRef.get(cacheKey); if (existingRequest) { return existingRequest; } // 创建新的请求 const request = (async () => { try { const dates = generateDateRange(tab); const results: BasalMetabolismData[] = []; // 并行获取所有日期的数据 const promises = dates.map(async (date) => { try { const options = { startDate: dayjs(date).startOf('day').toDate().toISOString(), endDate: dayjs(date).endOf('day').toDate().toISOString() }; const basalEnergy = await fetchBasalEnergyBurned(options); return { date, value: basalEnergy || null }; } catch (error) { console.error('获取单日基础代谢数据失败:', error); return { date, value: null }; } }); const data = await Promise.all(promises); results.push(...data); // 更新缓存 cacheRef.set(cacheKey, { data: results, timestamp: now }); return results; } catch (error) { console.error('获取基础代谢数据失败:', error); return []; } finally { // 清理请求记录 loadingRef.delete(cacheKey); } })(); // 记录请求 loadingRef.set(cacheKey, request); return request; }, [generateDateRange, cacheRef, loadingRef, CACHE_DURATION]); // 获取当前选中日期 const currentSelectedDate = useMemo(() => { const days = getMonthDays(undefined, i18n.language as 'zh' | 'en'); return days[selectedIndex]?.date?.toDate() ?? new Date(); }, [selectedIndex, i18n.language]); // 计算BMR范围 const bmrRange = useMemo(() => { const { gender, weight, height } = userProfile; // 检查是否有足够的信息来计算BMR if (!gender || !weight || !height || !userAge) { return null; } // 将体重和身高转换为数字 const weightNum = parseFloat(weight); const heightNum = parseFloat(height); if (isNaN(weightNum) || isNaN(heightNum) || weightNum <= 0 || heightNum <= 0 || userAge <= 0) { return null; } // 使用Mifflin-St Jeor公式计算BMR let bmr: number; if (gender === 'male') { bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge + 5; } else { bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge - 161; } // 计算正常范围(±15%) const minBMR = Math.round(bmr * 0.85); const maxBMR = Math.round(bmr * 1.15); return { min: minBMR, max: maxBMR, base: Math.round(bmr) }; }, [userProfile.gender, userProfile.weight, userProfile.height, userAge]); // 获取单个日期的代谢数据 const fetchSingleDateData = useCallback(async (date: Date): Promise => { try { const options = { startDate: dayjs(date).startOf('day').toDate().toISOString(), endDate: dayjs(date).endOf('day').toDate().toISOString() }; const basalEnergy = await fetchBasalEnergyBurned(options); return { date, value: basalEnergy || null }; } catch (error) { console.error('获取单日基础代谢数据失败:', error); return { date, value: null }; } }, []); // 日期选择回调 const onSelectDate = useCallback(async (index: number) => { setSelectedIndex(index); // 获取选中日期 const days = getMonthDays(undefined, i18n.language as 'zh' | 'en'); const selectedDate = days[index]?.date?.toDate(); if (selectedDate) { // 检查是否已经有该日期的数据 const existingData = chartData.find(item => dayjs(item.date).isSame(selectedDate, 'day') ); // 如果没有数据,则获取该日期的数据 if (!existingData) { try { const newData = await fetchSingleDateData(selectedDate); // 更新chartData,添加新数据并按日期排序 setChartData(prevData => { const updatedData = [...prevData, newData]; return updatedData.sort((a, b) => a.date.getTime() - b.date.getTime()); }); } catch (error) { console.error('获取选中日期数据失败:', error); } } } }, [chartData, fetchSingleDateData]); // Tab切换 const handleTabPress = useCallback((tab: TabType) => { setActiveTab(tab); }, []); // 初始化和Tab切换时加载数据 useEffect(() => { let isCancelled = false; const loadData = async () => { setIsLoading(true); setError(null); try { const data = await fetchBasalMetabolismData(activeTab); if (!isCancelled) { setChartData(data); } } catch (err) { if (!isCancelled) { setError(err instanceof Error ? err.message : t('basalMetabolismDetail.chart.error.fetchFailed')); } } finally { if (!isCancelled) { setIsLoading(false); } } }; loadData(); // 清理函数,防止组件卸载后的状态更新 return () => { isCancelled = true; }; }, [activeTab, fetchBasalMetabolismData]); // 处理图表数据 const processedChartData = useMemo(() => { if (!chartData || chartData.length === 0) { return { labels: [], datasets: [] }; } // 根据activeTab生成标签和数据 const labels = chartData.map(item => { switch (activeTab) { case 'week': // 显示星期几 return dayjs(item.date).format('dd'); case 'month': // 显示周数 const weekOfYear = dayjs(item.date).week(); const firstWeekOfYear = dayjs(item.date).startOf('year').week(); const weekNumber = weekOfYear - firstWeekOfYear + 1; return t('basalMetabolismDetail.chart.weekLabel', { week: weekNumber }); default: return dayjs(item.date).format('MM-DD'); } }); // 生成基础代谢数据集 const data = chartData.map(item => { const value = item.value; if (value === null || value === undefined) { return 0; // 明确处理null/undefined值 } // 对于非常小的正值,保证至少显示1,但对于0值保持为0 const roundedValue = Math.round(value); return value > 0 && roundedValue === 0 ? 1 : roundedValue; }); console.log('processedChartData:', { labels, data, originalValues: chartData.map(item => item.value) }); return { labels, datasets: [{ data }] }; }, [chartData, activeTab]); return ( {/* 背景渐变 */} {/* 头部导航 */} setInfoModalVisible(true)} style={styles.infoButton} > } /> {/* 日期选择器 */} {/* 当前日期基础代谢显示 */} {t('basalMetabolismDetail.currentData.title', { date: getLocalizedDateFormat(dayjs(currentSelectedDate), i18n.language as 'zh' | 'en') })} {(() => { const selectedDateData = chartData.find(item => dayjs(item.date).isSame(currentSelectedDate, 'day') ); if (selectedDateData?.value) { return Math.round(selectedDateData.value).toString(); } return t('basalMetabolismDetail.currentData.noData'); })()} {t('basalMetabolismDetail.currentData.unit')} {bmrRange && ( {t('basalMetabolismDetail.currentData.normalRange', { min: bmrRange.min, max: bmrRange.max })} )} {/* 基础代谢统计 */} {t('basalMetabolismDetail.stats.title')} {/* Tab 切换 */} handleTabPress('week')} activeOpacity={0.7} > {t('basalMetabolismDetail.stats.tabs.week')} handleTabPress('month')} activeOpacity={0.7} > {t('basalMetabolismDetail.stats.tabs.month')} {/* 柱状图 */} {isLoading ? ( {t('basalMetabolismDetail.chart.loadingText')} ) : error ? ( {t('basalMetabolismDetail.chart.error.text', { error })} { // {t('basalMetabolismDetail.comments.reloadData')} setIsLoading(true); setError(null); fetchBasalMetabolismData(activeTab).then(data => { setChartData(data); setIsLoading(false); }).catch(err => { setError(err instanceof Error ? err.message : t('basalMetabolismDetail.chart.error.fetchFailed')); setIsLoading(false); }); }} activeOpacity={0.7} > {t('basalMetabolismDetail.chart.error.retry')} ) : processedChartData.datasets.length > 0 && processedChartData.datasets[0].data.length > 0 ? ( `${Colors.light.primary}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`, // 使用主题紫色 labelColor: (opacity = 1) => `${Colors.light.text}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`, // 使用主题文字颜色 style: { borderRadius: 16, }, barPercentage: 0.7, // 增加柱体宽度 propsForBackgroundLines: { strokeDasharray: "2,2", stroke: Colors.light.border, // 使用主题边框颜色 strokeWidth: 1 }, propsForLabels: { fontSize: 12, fontWeight: '500', }, }} style={styles.chart} showValuesOnTopOfBars={true} fromZero={false} segments={4} /> ) : ( {t('basalMetabolismDetail.chart.empty')} )} {/* 基础代谢说明弹窗 */} setInfoModalVisible(false)} > {/* 关闭按钮 */} setInfoModalVisible(false)} > {t('basalMetabolismDetail.modal.closeButton')} {/* 标题 */} {t('basalMetabolismDetail.modal.title')} {/* 基础代谢定义 */} {t('basalMetabolismDetail.modal.description')} {/* 为什么重要 */} {t('basalMetabolismDetail.modal.sections.importance.title')} {t('basalMetabolismDetail.modal.sections.importance.content')} {/* 正常范围 */} {t('basalMetabolismDetail.modal.sections.normalRange.title')} - {t('basalMetabolismDetail.modal.sections.normalRange.formulas.male')} - {t('basalMetabolismDetail.modal.sections.normalRange.formulas.female')} {bmrRange ? ( <> {t('basalMetabolismDetail.modal.sections.normalRange.userRange', { min: bmrRange.min, max: bmrRange.max })} {t('basalMetabolismDetail.modal.sections.normalRange.rangeNote')} {t('basalMetabolismDetail.modal.sections.normalRange.userInfo', { gender: t(`basalMetabolismDetail.gender.${userProfile.gender === 'male' ? 'male' : 'female'}`), age: userAge, height: userProfile.height, weight: userProfile.weight })} ) : ( {t('basalMetabolismDetail.modal.sections.normalRange.incompleteInfo')} )} {/* 提高代谢率的策略 */} {t('basalMetabolismDetail.modal.sections.strategies.title')} {t('basalMetabolismDetail.modal.sections.strategies.subtitle')} {(t('basalMetabolismDetail.modal.sections.strategies.items', { returnObjects: true }) as string[]).map((item: string, index: number) => ( {item} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#FFFFFF', }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, scrollView: { flex: 1, }, infoButton: { padding: 4, }, dateContainer: { marginTop: 16, marginBottom: 20, }, currentDataCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.12, shadowRadius: 12, elevation: 6, alignItems: 'center', }, currentDataTitle: { fontSize: 16, fontWeight: '700', color: '#192126', marginBottom: 16, textAlign: 'center', }, currentValueContainer: { flexDirection: 'row', alignItems: 'baseline', marginBottom: 8, }, currentValue: { fontSize: 36, fontWeight: '700', color: '#4ECDC4', }, currentUnit: { fontSize: 16, color: '#666', marginLeft: 8, }, rangeText: { fontSize: 14, color: '#059669', textAlign: 'center', }, statsCard: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.12, shadowRadius: 12, elevation: 6, }, statsTitle: { fontSize: 16, fontWeight: '700', color: '#192126', marginBottom: 16, }, tabContainer: { flexDirection: 'row', backgroundColor: '#F5F5F7', borderRadius: 12, padding: 4, marginBottom: 20, }, tab: { flex: 1, paddingVertical: 8, paddingHorizontal: 16, borderRadius: 8, alignItems: 'center', }, activeTab: { backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, }, tabText: { fontSize: 14, fontWeight: '600', color: '#888', }, activeTabText: { color: '#192126', }, chart: { marginVertical: 8, borderRadius: 16, }, emptyChart: { height: 220, alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8F9FA', borderRadius: 16, marginVertical: 8, }, emptyChartText: { fontSize: 14, color: '#999', fontWeight: '500', }, loadingChart: { height: 220, alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8F9FA', borderRadius: 16, marginVertical: 8, }, loadingText: { fontSize: 14, color: '#666', marginTop: 8, fontWeight: '500', }, errorChart: { height: 220, alignItems: 'center', justifyContent: 'center', backgroundColor: '#FFF5F5', borderRadius: 16, marginVertical: 8, padding: 20, }, errorText: { fontSize: 14, color: '#E53E3E', textAlign: 'center', marginBottom: 12, }, retryButton: { backgroundColor: '#4ECDC4', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, }, retryText: { color: '#FFFFFF', fontSize: 14, fontWeight: '600', }, // Modal styles modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, modalContent: { backgroundColor: '#FFFFFF', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 24, maxHeight: '90%', width: '100%', shadowColor: '#000', shadowOffset: { width: 0, height: -5, }, shadowOpacity: 0.25, shadowRadius: 20, elevation: 10, }, closeButton: { position: 'absolute', top: 16, right: 16, width: 32, height: 32, borderRadius: 16, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center', zIndex: 1, }, closeButtonText: { fontSize: 20, color: '#64748B', fontWeight: '600', }, modalTitle: { fontSize: 24, fontWeight: '700', color: '#0F172A', marginBottom: 16, textAlign: 'center', }, modalDescription: { fontSize: 15, color: '#475569', lineHeight: 22, marginBottom: 24, }, sectionTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', marginBottom: 12, marginTop: 8, }, sectionContent: { fontSize: 15, color: '#475569', lineHeight: 22, marginBottom: 20, }, formulaText: { fontSize: 14, color: '#64748B', fontFamily: 'monospace', marginBottom: 4, paddingLeft: 8, }, rangeNote: { fontSize: 12, color: '#9CA3AF', textAlign: 'center', marginBottom: 20, }, userInfoText: { fontSize: 13, color: '#6B7280', textAlign: 'center', marginTop: 8, marginBottom: 16, fontStyle: 'italic', }, strategyText: { fontSize: 15, color: '#475569', marginBottom: 12, }, strategyList: { marginBottom: 20, }, strategyItem: { fontSize: 14, color: '#64748B', lineHeight: 20, marginBottom: 8, paddingLeft: 8, }, });