import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { ChallengeType } from '@/services/challengesApi'; import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice'; import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health'; import { logger } from '@/utils/logger'; import { Toast } from '@/utils/toast.utils'; import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences'; import { refreshWidget, syncWaterDataToWidget } from '@/utils/widgetDataSync'; import dayjs from 'dayjs'; import { useCallback, useEffect, useMemo, useState } from 'react'; // 水分记录数据结构 export interface WaterRecord { id: string; amount: number; recordedAt: string; createdAt: string; note?: string; } // 水分统计数据结构 export interface WaterStats { totalAmount: number; completionRate: number; recordCount: number; } // 将 HealthKit 数据转换为应用数据格式 const convertHealthKitToWaterRecord = (healthKitRecord: any): WaterRecord => { return { id: healthKitRecord.id || `${healthKitRecord.startDate}_${healthKitRecord.value}`, amount: Math.round(healthKitRecord.value), // HealthKit 已经返回毫升数值 recordedAt: healthKitRecord.startDate, createdAt: healthKitRecord.endDate, note: healthKitRecord.metadata?.note || undefined, }; }; // 创建日期范围选项 function createDateRange(date: string): { startDate: string; endDate: string } { return { startDate: dayjs(date).startOf('day').toDate().toISOString(), endDate: dayjs(date).endOf('day').toDate().toISOString() }; } const useWaterChallengeProgressReporter = () => { const dispatch = useAppDispatch(); const allChallenges = useAppSelector(selectChallengeList); const joinedWaterChallenges = useMemo( () => allChallenges.filter((challenge) => challenge.type === ChallengeType.WATER && challenge.isJoined), [allChallenges] ); return useCallback( async (value: number) => { if (!joinedWaterChallenges.length) { return; } for (const challenge of joinedWaterChallenges) { try { await dispatch(reportChallengeProgress({ id: challenge.id, value })).unwrap(); } catch (error) { console.warn('挑战进度上报失败', { error, challengeId: challenge.id }); } } }, [dispatch, joinedWaterChallenges] ); }; export const useWaterData = () => { // 本地状态管理 const [loading, setLoading] = useState({ records: false, stats: false, goal: false }); const [error, setError] = useState(null); const [dailyWaterGoal, setDailyWaterGoal] = useState(2000); const [waterRecords, setWaterRecords] = useState<{ [date: string]: WaterRecord[] }>({}); const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD')); // 获取指定日期的记录 const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => { setLoading(prev => ({ ...prev, records: true })); setError(null); try { const options = createDateRange(date); const healthKitRecords = await getWaterIntakeFromHealthKit(options); // 转换数据格式并按时间排序 const convertedRecords = healthKitRecords .map(convertHealthKitToWaterRecord) .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime()); // 应用分页逻辑 const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedRecords = convertedRecords.slice(startIndex, endIndex); setWaterRecords(prev => ({ ...prev, [date]: page === 1 ? paginatedRecords : [...(prev[date] || []), ...paginatedRecords] })); return paginatedRecords; } catch (error) { console.error('获取饮水记录失败:', error); setError('获取饮水记录失败'); Toast.error('获取饮水记录失败'); return []; } finally { setLoading(prev => ({ ...prev, records: false })); } }, []); // 加载更多记录(占位符,HealthKit一次性返回所有数据) const loadMoreWaterRecords = useCallback(async () => { // HealthKit通常一次性返回所有数据,这里保持接口一致性 return; }, []); // 获取日期范围的记录 const getWaterRecordsByDateRange = useCallback(async ( startDate: string, endDate: string, page = 1, limit = 20 ) => { setLoading(prev => ({ ...prev, records: true })); setError(null); try { const options = { startDate: dayjs(startDate).startOf('day').toDate().toISOString(), endDate: dayjs(endDate).endOf('day').toDate().toISOString() }; const healthKitRecords = await getWaterIntakeFromHealthKit(options); // 转换数据格式并按时间排序 const convertedRecords = healthKitRecords .map(convertHealthKitToWaterRecord) .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime()); // 按日期分组 const recordsByDate: { [date: string]: WaterRecord[] } = {}; convertedRecords.forEach(record => { const date = dayjs(record.recordedAt).format('YYYY-MM-DD'); if (!recordsByDate[date]) { recordsByDate[date] = []; } recordsByDate[date].push(record); }); setWaterRecords(prev => ({ ...prev, ...recordsByDate })); return recordsByDate; } catch (error) { console.error('获取日期范围饮水记录失败:', error); setError('获取日期范围饮水记录失败'); Toast.error('获取日期范围饮水记录失败'); return {}; } finally { setLoading(prev => ({ ...prev, records: false })); } }, []); // 加载今日数据 const loadTodayData = useCallback(() => { const today = dayjs().format('YYYY-MM-DD'); getWaterRecordsByDate(today); }, [getWaterRecordsByDate]); // 加载指定日期数据 const loadDataByDate = useCallback((date: string) => { setSelectedDate(date); getWaterRecordsByDate(date); }, [getWaterRecordsByDate]); // 创建喝水记录 const reportWaterChallengeProgress = useWaterChallengeProgressReporter(); const addWaterRecord = useCallback( async (amount: number, recordedAt?: string) => { try { const recordTime = recordedAt || dayjs().toISOString(); const date = dayjs(recordTime).format('YYYY-MM-DD'); const isToday = dayjs(recordTime).isSame(dayjs(), 'day'); // 保存到 HealthKit const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime); if (!healthKitSuccess) { Toast.error('保存到 HealthKit 失败'); return false; } // 重新获取当前日期的数据以刷新界面 const updatedRecords = await getWaterRecordsByDate(date); const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0); // 如果是今天的数据,更新Widget if (isToday) { const quickAddAmount = await getQuickWaterAmount(); try { await syncWaterDataToWidget({ currentIntake: totalAmount, dailyGoal: dailyWaterGoal, quickAddAmount, }); await refreshWidget(); } catch (widgetError) { console.error('Widget 同步错误:', widgetError); } } await reportWaterChallengeProgress(totalAmount); return true; } catch (error: any) { console.error('添加喝水记录失败:', error); Toast.error(error?.message || '添加喝水记录失败'); return false; } }, [dailyWaterGoal, getWaterRecordsByDate, reportWaterChallengeProgress] ); // 更新喝水记录(HealthKit不支持更新,只能删除后重新添加) const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => { try { // 找到要更新的记录 let recordToUpdate: WaterRecord | null = null; let recordDate = ''; for (const [date, records] of Object.entries(waterRecords)) { const record = records.find(r => r.id === id); if (record) { recordToUpdate = record; recordDate = date; break; } } if (!recordToUpdate) { Toast.error('找不到要更新的记录'); return false; } // 先删除旧记录 await deleteWaterIntakeFromHealthKit(id, recordToUpdate.recordedAt); // 添加新记录 const newAmount = amount ?? recordToUpdate.amount; const newRecordedAt = recordedAt ?? recordToUpdate.recordedAt; const success = await addWaterRecord(newAmount, newRecordedAt); if (success) { Toast.success('更新饮水记录成功'); } return success; } catch (error: any) { console.error('更新喝水记录失败:', error); Toast.error(error?.message || '更新喝水记录失败'); return false; } }, [waterRecords, addWaterRecord]); // 删除喝水记录 const removeWaterRecord = useCallback(async (id: string) => { try { // 找到要删除的记录 let recordToDelete: WaterRecord | null = null; let recordDate = ''; for (const [date, records] of Object.entries(waterRecords)) { const record = records.find(r => r.id === id); if (record) { recordToDelete = record; recordDate = date; break; } } if (!recordToDelete) { Toast.error('找不到要删除的记录'); return false; } // 从 HealthKit 删除 const healthKitSuccess = await deleteWaterIntakeFromHealthKit(id, recordToDelete.recordedAt); if (!healthKitSuccess) { console.warn('从 HealthKit 删除记录失败,但继续更新本地数据'); } // 更新本地状态 setWaterRecords(prev => ({ ...prev, [recordDate]: prev[recordDate]?.filter(record => record.id !== id) || [] })); // 如果是今天的数据,更新Widget if (recordDate === dayjs().format('YYYY-MM-DD')) { const updatedTodayRecords = waterRecords[recordDate]?.filter(record => record.id !== id) || []; const totalAmount = updatedTodayRecords.reduce((sum, record) => sum + record.amount, 0); const quickAddAmount = await getQuickWaterAmount(); try { await syncWaterDataToWidget({ currentIntake: totalAmount, dailyGoal: dailyWaterGoal, quickAddAmount, }); await refreshWidget(); } catch (widgetError) { console.error('Widget 删除同步错误:', widgetError); } } Toast.success('删除饮水记录成功'); return true; } catch (error: any) { console.error('删除喝水记录失败:', error); Toast.error(error?.message || '删除喝水记录失败'); return false; } }, [waterRecords, dailyWaterGoal]); // 更新喝水目标 const updateWaterGoal = useCallback(async (goal: number) => { try { await setWaterGoalToStorage(goal); setDailyWaterGoal(goal); // 更新Widget const today = dayjs().format('YYYY-MM-DD'); const todayRecords = waterRecords[today] || []; const totalAmount = todayRecords.reduce((sum, record) => sum + record.amount, 0); try { const quickAddAmount = await getQuickWaterAmount(); await syncWaterDataToWidget({ dailyGoal: goal, currentIntake: totalAmount, quickAddAmount, }); await refreshWidget(); } catch (widgetError) { console.error('Widget 目标同步错误:', widgetError); } Toast.success('更新饮水目标成功'); return true; } catch (error: any) { console.error('更新喝水目标失败:', error); Toast.error(error?.message || '更新喝水目标失败'); return false; } }, [waterRecords]); // 计算总喝水量 const getTotalAmount = useCallback((records: WaterRecord[]) => { return records.reduce((total, record) => total + record.amount, 0); }, []); // 按小时分组数据 const getHourlyData = useCallback((records: WaterRecord[]) => { const hourlyData: { hour: number; amount: number }[] = Array.from({ length: 24 }, (_, i) => ({ hour: i, amount: 0, })); records.forEach(record => { const hour = dayjs(record.recordedAt).hour(); if (hour >= 0 && hour < 24) { hourlyData[hour].amount += record.amount; } }); return hourlyData; }, []); // 计算完成率(返回百分比) const calculateCompletionRate = useCallback((totalAmount: number, goal: number) => { if (goal <= 0) return 0; return Math.min((totalAmount / goal) * 100, 100); }, []); // 加载初始数据 useEffect(() => { const loadInitialData = async () => { try { // 加载饮水目标 const goal = await getWaterGoalFromStorage(); setDailyWaterGoal(goal); // 加载今日数据 loadTodayData(); } catch (error) { console.error('加载初始数据失败:', error); } }; loadInitialData(); }, [loadTodayData]); // 计算今日统计数据 const todayStats = useMemo(() => { const today = dayjs().format('YYYY-MM-DD'); const todayRecords = waterRecords[today] || []; const totalAmount = getTotalAmount(todayRecords); return { totalAmount, completionRate: calculateCompletionRate(totalAmount, dailyWaterGoal), recordCount: todayRecords.length }; }, [waterRecords, dailyWaterGoal, getTotalAmount, calculateCompletionRate]); // 同步初始数据到Widget useEffect(() => { const syncInitialDataToWidget = async () => { if (todayStats && dailyWaterGoal) { try { const quickAddAmount = await getQuickWaterAmount(); await syncWaterDataToWidget({ currentIntake: todayStats.totalAmount, dailyGoal: dailyWaterGoal, quickAddAmount, }); } catch (error) { console.error('初始Widget数据同步失败:', error); } } }; syncInitialDataToWidget(); }, [todayStats, dailyWaterGoal]); return { // 数据 todayStats, dailyWaterGoal, waterRecords: waterRecords[selectedDate] || [], waterRecordsMeta: { total: waterRecords[selectedDate]?.length || 0, page: 1, limit: 20, hasMore: false }, selectedDate, loading, error, // 方法 loadTodayData, loadDataByDate, getWaterRecordsByDate, loadMoreWaterRecords, getWaterRecordsByDateRange, addWaterRecord, updateWaterRecord, removeWaterRecord, updateWaterGoal, getTotalAmount, getHourlyData, calculateCompletionRate, }; }; // 简化的Hook,只返回今日数据 export const useTodayWaterData = () => { const waterData = useWaterData(); const todayRecords = useMemo(() => { const today = dayjs().format('YYYY-MM-DD'); return waterData.waterRecords || []; }, [waterData.waterRecords]); const todayMeta = useMemo(() => ({ total: todayRecords.length, page: 1, limit: 20, hasMore: false }), [todayRecords.length]); const fetchTodayWaterRecords = useCallback(async (page = 1, limit = 20) => { const today = dayjs().format('YYYY-MM-DD'); await waterData.getWaterRecordsByDate(today, page, limit); }, [waterData.getWaterRecordsByDate]); return { todayStats: waterData.todayStats, dailyWaterGoal: waterData.dailyWaterGoal, waterRecords: todayRecords, waterRecordsMeta: todayMeta, selectedDate: waterData.selectedDate, loading: waterData.loading, error: waterData.error, fetchTodayWaterRecords, loadMoreWaterRecords: waterData.loadMoreWaterRecords, getWaterRecordsByDateRange: waterData.getWaterRecordsByDateRange, addWaterRecord: waterData.addWaterRecord, updateWaterRecord: waterData.updateWaterRecord, removeWaterRecord: waterData.removeWaterRecord, updateWaterGoal: waterData.updateWaterGoal, }; }; // 按日期获取饮水数据的 hook export const useWaterDataByDate = (targetDate?: string) => { const dateToUse = targetDate || dayjs().format('YYYY-MM-DD'); // 本地状态管理 const [loading, setLoading] = useState({ records: false, stats: false, goal: false }); const [error, setError] = useState(null); const [dailyWaterGoal, setDailyWaterGoal] = useState(2000); const [waterRecords, setWaterRecords] = useState([]); // 获取指定日期的记录 const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => { setLoading(prev => ({ ...prev, records: true })); setError(null); try { logger.debug('🚰 开始获取饮水记录,日期:', date); const options = createDateRange(date); logger.debug('🚰 查询选项:', options); const healthKitRecords = await getWaterIntakeFromHealthKit(options); logger.debug('🚰 从HealthKit获取到的原始数据:', healthKitRecords); // 转换数据格式并按时间排序 const convertedRecords = healthKitRecords .map(convertHealthKitToWaterRecord) .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime()); logger.debug('🚰 转换后的记录:', convertedRecords); logger.debug('🚰 记录数量:', convertedRecords.length); setWaterRecords(convertedRecords); return convertedRecords; } catch (error) { console.error('🚰 获取饮水记录失败:', error); setError('获取饮水记录失败'); Toast.error('获取饮水记录失败'); return []; } finally { setLoading(prev => ({ ...prev, records: false })); } }, []); // 创建喝水记录 const reportWaterChallengeProgress = useWaterChallengeProgressReporter(); const addWaterRecord = useCallback( async (amount: number, recordedAt?: string) => { try { const recordTime = recordedAt || dayjs().toISOString(); // 保存到 HealthKit const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime); if (!healthKitSuccess) { Toast.error('保存到 HealthKit 失败'); return false; } // 重新获取当前日期的数据以刷新界面 const updatedRecords = await getWaterRecordsByDate(dateToUse); const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0); // 如果是今天的数据,更新Widget if (dateToUse === dayjs().format('YYYY-MM-DD')) { const quickAddAmount = await getQuickWaterAmount(); try { await syncWaterDataToWidget({ currentIntake: totalAmount, dailyGoal: dailyWaterGoal, quickAddAmount, }); await refreshWidget(); } catch (widgetError) { console.error('Widget 同步错误:', widgetError); } } await reportWaterChallengeProgress(totalAmount); return true; } catch (error: any) { console.error('添加喝水记录失败:', error); Toast.error(error?.message || '添加喝水记录失败'); return false; } }, [dailyWaterGoal, dateToUse, getWaterRecordsByDate, reportWaterChallengeProgress] ); // 更新喝水记录 const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => { try { const recordToUpdate = waterRecords.find(r => r.id === id); if (!recordToUpdate) { Toast.error('找不到要更新的记录'); return false; } // 先删除旧记录 await deleteWaterIntakeFromHealthKit(id, recordToUpdate.recordedAt); // 添加新记录 const newAmount = amount ?? recordToUpdate.amount; const newRecordedAt = recordedAt ?? recordToUpdate.recordedAt; const success = await addWaterRecord(newAmount, newRecordedAt); return success; } catch (error: any) { console.error('更新喝水记录失败:', error); Toast.error(error?.message || '更新喝水记录失败'); return false; } }, [waterRecords, addWaterRecord]); // 删除喝水记录 const removeWaterRecord = useCallback(async (id: string) => { try { const recordToDelete = waterRecords.find(r => r.id === id); if (!recordToDelete) { Toast.error('找不到要删除的记录'); return false; } // 从 HealthKit 删除 await deleteWaterIntakeFromHealthKit(id, recordToDelete.recordedAt); // 重新获取数据 await getWaterRecordsByDate(dateToUse); // 如果是今天的数据,更新Widget if (dateToUse === dayjs().format('YYYY-MM-DD')) { const updatedRecords = waterRecords.filter(record => record.id !== id); const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0); const quickAddAmount = await getQuickWaterAmount(); try { await syncWaterDataToWidget({ currentIntake: totalAmount, dailyGoal: dailyWaterGoal, quickAddAmount, }); await refreshWidget(); } catch (widgetError) { console.error('Widget 删除同步错误:', widgetError); } } Toast.success('删除饮水记录成功'); return true; } catch (error: any) { console.error('删除喝水记录失败:', error); Toast.error(error?.message || '删除喝水记录失败'); return false; } }, [waterRecords, getWaterRecordsByDate, dateToUse, dailyWaterGoal]); // 更新喝水目标 const updateWaterGoal = useCallback(async (goal: number) => { try { await setWaterGoalToStorage(goal); setDailyWaterGoal(goal); Toast.success('更新饮水目标成功'); return true; } catch (error: any) { console.error('更新喝水目标失败:', error); Toast.error(error?.message || '更新喝水目标失败'); return false; } }, []); // 计算指定日期的统计数据 const waterStats = useMemo(() => { logger.debug('🚰 计算waterStats - waterRecords:', waterRecords); logger.debug('🚰 计算waterStats - dailyWaterGoal:', dailyWaterGoal); if (!waterRecords || waterRecords.length === 0) { logger.debug('🚰 没有饮水记录,返回默认值'); return { totalAmount: 0, completionRate: 0, recordCount: 0 }; } const totalAmount = waterRecords.reduce((total, record) => total + record.amount, 0); const completionRate = dailyWaterGoal > 0 ? Math.min((totalAmount / dailyWaterGoal) * 100, 100) : 0; logger.debug('🚰 计算结果 - totalAmount:', totalAmount); logger.debug('🚰 计算结果 - completionRate:', completionRate); logger.debug('🚰 计算结果 - recordCount:', waterRecords.length); return { totalAmount, completionRate, recordCount: waterRecords.length }; }, [waterRecords, dailyWaterGoal]); // 初始化加载指定日期的数据 useEffect(() => { const loadInitialData = async () => { try { // 加载饮水目标 const goal = await getWaterGoalFromStorage(); setDailyWaterGoal(goal); // 加载指定日期数据 await getWaterRecordsByDate(dateToUse); } catch (error) { console.error('加载初始数据失败:', error); } }; loadInitialData(); }, [dateToUse, getWaterRecordsByDate]); return { waterStats, dailyWaterGoal, waterRecords, waterRecordsMeta: { total: waterRecords.length, page: 1, limit: 20, hasMore: false }, loading, error, addWaterRecord, updateWaterRecord, removeWaterRecord, updateWaterGoal, getWaterRecordsByDate, }; };