diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 57217ee..cfc4cf9 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -419,7 +419,7 @@ export default function CoachScreen() { showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => { try { if (e?.endCoordinates?.height) { - const height = Math.max(0, e.endCoordinates.height - insets.bottom); + const height = Math.max(0, e.endCoordinates.height); setKeyboardOffset(height); } } catch (error) { diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 6f4cd7b..1d35b28 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -18,7 +18,7 @@ import { useColorScheme } from '@/hooks/useColorScheme'; import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords'; import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; -import { ensureHealthPermissions, fetchHealthDataForDate, testOxygenSaturationData } from '@/utils/health'; +import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; @@ -124,14 +124,12 @@ export default function ExploreScreen() { const [oxygenSaturation, setOxygenSaturation] = useState(null); const [heartRate, setHeartRate] = useState(null); - const [isLoading, setIsLoading] = useState(false); // 用于触发动画重置的 token(当日期或数据变化时更新) const [animToken, setAnimToken] = useState(0); - const [trainingProgress, setTrainingProgress] = useState(0); // 暂定静态80% // 营养数据状态 const [nutritionSummary, setNutritionSummary] = useState(null); - const [isNutritionLoading, setIsNutritionLoading] = useState(false); + const { registerTask } = useBackgroundTasks(); // 心情相关状态 const dispatch = useAppDispatch(); @@ -182,7 +180,6 @@ export default function ExploreScreen() { const loadHealthData = async (targetDate?: Date) => { try { console.log('=== 开始HealthKit初始化流程 ==='); - setIsLoading(true); const ok = await ensureHealthPermissions(); if (!ok) { @@ -246,15 +243,12 @@ export default function ExploreScreen() { // 重置血氧饱和度和心率数据 setOxygenSaturation(null); setHeartRate(null); - } finally { - setIsLoading(false); } }; // 加载营养数据 const loadNutritionData = async (targetDate?: Date) => { try { - setIsNutritionLoading(true); // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 let derivedDate: Date; @@ -281,8 +275,6 @@ export default function ExploreScreen() { } catch (error) { console.error('营养数据加载失败:', error); setNutritionSummary(null); - } finally { - setIsNutritionLoading(false); } }; @@ -300,20 +292,20 @@ export default function ExploreScreen() { }, [selectedIndex]) ); - // useEffect(() => { - // // 注册任务 - // registerTask({ - // id: 'health-data-task', - // name: 'health-data-task', - // handler: async () => { - // try { - // await loadHealthData(); - // } catch (error) { - // console.error('健康数据任务执行失败:', error); - // } - // }, - // }); - // }, []); + useEffect(() => { + // 注册任务 + registerTask({ + id: 'health-data-task', + name: 'health-data-task', + handler: async () => { + try { + await loadHealthData(); + } catch (error) { + console.error('健康数据任务执行失败:', error); + } + }, + }); + }, []); // 日期点击时,加载对应日期数据 const onSelectDate = (index: number, date: Date) => { @@ -325,16 +317,6 @@ export default function ExploreScreen() { } }; - // 测试血氧饱和度数据 - const testOxygenData = async () => { - console.log('开始测试血氧饱和度数据...'); - const currentDate = getCurrentSelectedDate(); - await testOxygenSaturationData(currentDate); - }; - - // 使用统一的渐变背景色 - const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const; - return ( {/* 背景渐变 */} @@ -384,8 +366,8 @@ export default function ExploreScreen() { /> - - 消耗卡路里 + + 消耗卡路里 {activeCalories != null ? ( - + 步数 @@ -414,14 +396,14 @@ export default function ExploreScreen() { )} {/* 心情卡片 */} - + pushIfAuthedElseLogin('/mood/calendar')} @@ -444,7 +426,7 @@ export default function ExploreScreen() { /> - + 睡眠 @@ -473,13 +455,6 @@ export default function ExploreScreen() { style={styles.basalMetabolismCardOverride} oxygenSaturation={oxygenSaturation} /> - {/* 测试按钮 - 开发时使用 */} - - 测试血氧数据 - {/* 心率卡片 */} @@ -577,22 +552,11 @@ const styles = StyleSheet.create({ borderRadius: 22, padding: 16, }, - caloriesCard: { - backgroundColor: '#FFFFFF', - }, - trainingCard: { - backgroundColor: '#EEE9FF', - }, - cardTitleSecondary: { - color: '#9AA3AE', - fontSize: 10, - fontWeight: '600', - marginBottom: 10, - }, caloriesValue: { color: '#192126', fontSize: 18, fontWeight: '800', + marginTop: 18, }, trainingContent: { marginTop: 8, @@ -706,9 +670,6 @@ const styles = StyleSheet.create({ color: '#5B5B5B', fontWeight: '600', }, - stepsCard: { - backgroundColor: '#FFE4B8', - }, stepsValue: { fontSize: 14, color: '#7A6A42', @@ -797,9 +758,6 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', marginTop: 8, }, - sleepCard: { - backgroundColor: '#E8F4FD', - }, sleepValue: { fontSize: 16, color: '#1E40AF', @@ -821,14 +779,5 @@ const styles = StyleSheet.create({ top: 0, padding: 4, }, - moodCard: { - backgroundColor: '#F0FDF4', - }, - testButton: { - fontSize: 12, - color: '#3B82F6', - textAlign: 'center', - marginTop: 8, - padding: 4, - }, + }); diff --git a/components/BasalMetabolismCard.tsx b/components/BasalMetabolismCard.tsx index ce6ec7a..e8edeeb 100644 --- a/components/BasalMetabolismCard.tsx +++ b/components/BasalMetabolismCard.tsx @@ -1,5 +1,4 @@ import { AnimatedNumber } from '@/components/AnimatedNumber'; -import { LinearGradient } from 'expo-linear-gradient'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; @@ -32,17 +31,6 @@ export function BasalMetabolismCard({ value, resetToken, style }: BasalMetabolis return ( - {/* 渐变背景 */} - - - {/* 装饰性圆圈 */} - - {/* 头部区域 */} @@ -88,34 +76,7 @@ const styles = StyleSheet.create({ position: 'relative', overflow: 'hidden', }, - gradientBackground: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - opacity: 0.6, - }, - decorativeCircle1: { - position: 'absolute', - top: -20, - right: -20, - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: '#0EA5E9', - opacity: 0.1, - }, - decorativeCircle2: { - position: 'absolute', - bottom: -15, - left: -15, - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#0EA5E9', - opacity: 0.05, - }, + header: { flexDirection: 'row', alignItems: 'center', diff --git a/components/DateSelector.tsx b/components/DateSelector.tsx index f0f0ead..6d6346f 100644 --- a/components/DateSelector.tsx +++ b/components/DateSelector.tsx @@ -67,20 +67,39 @@ export const DateSelector: React.FC = ({ const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth); const finalOffset = Math.min(centerOffset, maxScrollOffset); - daysScrollRef.current.scrollTo({ x: finalOffset, animated }); + if (animated) { + // 使用原生动画API实现更平滑的滚动 + requestAnimationFrame(() => { + daysScrollRef.current?.scrollTo({ + x: finalOffset, + animated: true + }); + }); + } else { + // 非动画情况直接跳转 + daysScrollRef.current?.scrollTo({ + x: finalOffset, + animated: false + }); + } }; // 初始化时滚动到选中项 useEffect(() => { if (scrollWidth > 0 && autoScrollToSelected) { - scrollToIndex(selectedIndex, false); + scrollToIndex(selectedIndex, true); } }, [scrollWidth, selectedIndex, autoScrollToSelected]); // 当选中索引变化时,滚动到对应位置 useEffect(() => { if (scrollWidth > 0 && autoScrollToSelected) { - scrollToIndex(selectedIndex, true); + // 添加微小延迟以确保动画效果更明显 + const timer = setTimeout(() => { + scrollToIndex(selectedIndex, true); + }, 50); + + return () => clearTimeout(timer); } }, [selectedIndex, autoScrollToSelected]); @@ -94,6 +113,11 @@ export const DateSelector: React.FC = ({ return; } + // 先滚动到目标位置,再更新状态 + if (autoScrollToSelected) { + scrollToIndex(index, true); + } + // 更新内部状态(如果使用外部控制则不更新) if (externalSelectedIndex === undefined) { setInternalSelectedIndex(index); diff --git a/components/MoodCard.tsx b/components/MoodCard.tsx index 7e9f533..2f17185 100644 --- a/components/MoodCard.tsx +++ b/components/MoodCard.tsx @@ -14,20 +14,8 @@ export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardPr return ( - - - {moodCheckin ? ( - - {moodConfig?.emoji || '😊'} - - ) : ( - 😊 - )} - - 心情 - + 心情 - 记录你的每日心情 {isLoading ? ( @@ -53,39 +41,18 @@ const styles = StyleSheet.create({ moodCardContent: { width: '100%', }, - cardHeaderRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - }, - moodIconContainer: { - width: 24, - height: 24, - borderRadius: 8, - backgroundColor: '#DCFCE7', - alignItems: 'center', - justifyContent: 'center', - marginRight: 10, - }, - moodIcon: { - fontSize: 14, - }, + cardTitle: { fontSize: 14, fontWeight: '800', color: '#192126', }, - moodSubtitle: { - fontSize: 12, - color: '#6B7280', - marginTop: 4, - marginBottom: 8, - }, + moodPreview: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - marginTop: 4, + marginTop: 22, }, moodPreviewText: { fontSize: 14, @@ -100,12 +67,12 @@ const styles = StyleSheet.create({ fontSize: 12, color: '#9CA3AF', fontStyle: 'italic', - marginTop: 4, + marginTop: 22, }, moodLoadingText: { fontSize: 12, color: '#9CA3AF', fontStyle: 'italic', - marginTop: 4, + marginTop: 22, }, }); \ No newline at end of file diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index 73c4dc7..61bc8d3 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -68,7 +68,6 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP 压力 - {getStatusEmoji()} {/* 数值显示区域 */} @@ -83,7 +82,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP {/* 渐变背景进度条 */} = ({ title, value, unit, - icon, style }) => { return ( @@ -23,10 +21,6 @@ const HealthDataCard: React.FC = ({ exiting={FadeOut.duration(300)} style={[styles.card, style]} > - - - {icon} - {title} @@ -65,9 +59,9 @@ const styles = StyleSheet.create({ }, title: { fontSize: 14, - color: '#666', + color: '#192126', marginBottom: 4, - fontWeight: '600', + fontWeight: '800', }, valueContainer: { flexDirection: 'row', diff --git a/components/statistic/HeartRateCard.tsx b/components/statistic/HeartRateCard.tsx index 5a77ba4..b307a7b 100644 --- a/components/statistic/HeartRateCard.tsx +++ b/components/statistic/HeartRateCard.tsx @@ -23,7 +23,6 @@ const HeartRateCard: React.FC = ({ title="心率" value={heartRate !== null && heartRate !== undefined ? Math.round(heartRate).toString() : '--'} unit="bpm" - icon={heartIcon} style={style} /> ); diff --git a/components/statistic/OxygenSaturationCard.tsx b/components/statistic/OxygenSaturationCard.tsx index be69c59..fbc55e3 100644 --- a/components/statistic/OxygenSaturationCard.tsx +++ b/components/statistic/OxygenSaturationCard.tsx @@ -23,7 +23,6 @@ const OxygenSaturationCard: React.FC = ({ title="血氧饱和度" value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'} unit="%" - icon={oxygenIcon} style={style} /> ); diff --git a/utils/health.ts b/utils/health.ts index b4cbbc7..509e2de 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -221,13 +221,13 @@ export async function fetchHealthDataForDate(date: Date): Promise 0 && value < 1) { value = value * 100; console.log('血氧饱和度数据从小数转换为百分比:', latestOxygen.value, '->', value); } - + // 血氧饱和度通常在0-100之间,验证数据有效性 if (value >= 0 && value <= 100) { resolve(Number(value.toFixed(1))); @@ -280,12 +280,12 @@ export async function fetchHealthDataForDate(date: Date): Promise { console.log('=== 开始测试血氧饱和度数据获取 ==='); - + const start = dayjs(date).startOf('day').toDate(); const end = dayjs(date).endOf('day').toDate(); - + const options = { startDate: start.toISOString(), endDate: end.toISOString() } as any; - + return new Promise((resolve) => { AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { if (err) { @@ -373,15 +373,15 @@ export async function testOxygenSaturationData(date: Date = new Date()): Promise resolve(); return; } - + console.log('原始血氧饱和度数据:', res); - + if (!res || !Array.isArray(res) || res.length === 0) { console.warn('血氧饱和度数据为空'); resolve(); return; } - + // 分析所有数据样本 res.forEach((sample, index) => { console.log(`样本 ${index + 1}:`, { @@ -391,26 +391,26 @@ export async function testOxygenSaturationData(date: Date = new Date()): Promise endDate: sample.endDate }); }); - + // 获取最新的血氧饱和度值 const latestOxygen = res[res.length - 1]; if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { let value = Number(latestOxygen.value); - + console.log('处理前的值:', latestOxygen.value); console.log('转换为数字后的值:', value); - + // 检查数据格式:如果值小于1,可能是小数形式(0.0-1.0),需要转换为百分比 if (value > 0 && value < 1) { const originalValue = value; value = value * 100; console.log('血氧饱和度数据从小数转换为百分比:', originalValue, '->', value); } - + console.log('最终处理后的值:', value); console.log('数据有效性检查:', value >= 0 && value <= 100 ? '有效' : '无效'); } - + console.log('=== 血氧饱和度数据测试完成 ==='); resolve(); });