557 lines
16 KiB
TypeScript
557 lines
16 KiB
TypeScript
import { CreateWaterRecordDto, UpdateWaterRecordDto, WaterRecordSource } from '@/services/waterRecords';
|
||
import { AppDispatch, RootState } from '@/store';
|
||
import {
|
||
createWaterRecordAction,
|
||
deleteWaterRecordAction,
|
||
fetchTodayWaterStats,
|
||
fetchWaterRecords,
|
||
fetchWaterRecordsByDateRange,
|
||
setSelectedDate,
|
||
updateWaterGoalAction,
|
||
updateWaterRecordAction,
|
||
} from '@/store/waterSlice';
|
||
import { Toast } from '@/utils/toast.utils';
|
||
import { saveWaterIntakeToHealthKit, deleteWaterIntakeFromHealthKit } from '@/utils/health';
|
||
import dayjs from 'dayjs';
|
||
import { useCallback, useEffect, useMemo } from 'react';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
|
||
export const useWaterData = () => {
|
||
const dispatch = useDispatch<AppDispatch>();
|
||
|
||
// 选择器
|
||
const todayStats = useSelector((state: RootState) => state.water.todayStats);
|
||
const dailyWaterGoal = useSelector((state: RootState) => state.water.dailyWaterGoal);
|
||
const waterRecords = useSelector((state: RootState) =>
|
||
state.water.waterRecords[dayjs().format('YYYY-MM-DD')] || []
|
||
);
|
||
const waterRecordsMeta = useSelector((state: RootState) =>
|
||
state.water.waterRecordsMeta[dayjs().format('YYYY-MM-DD')] || {
|
||
total: 0,
|
||
page: 1,
|
||
limit: 20,
|
||
hasMore: false
|
||
}
|
||
);
|
||
const selectedDate = useSelector((state: RootState) => state.water.selectedDate);
|
||
const loading = useSelector((state: RootState) => state.water.loading);
|
||
const error = useSelector((state: RootState) => state.water.error);
|
||
|
||
// 获取指定日期的记录(支持分页)
|
||
const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => {
|
||
await dispatch(fetchWaterRecords({ date, page, limit }));
|
||
}, [dispatch]);
|
||
|
||
// 加载更多记录
|
||
const loadMoreWaterRecords = useCallback(async () => {
|
||
const currentMeta = waterRecordsMeta;
|
||
if (currentMeta.hasMore && !loading.records) {
|
||
const nextPage = currentMeta.page + 1;
|
||
await dispatch(fetchWaterRecords({
|
||
date: selectedDate,
|
||
page: nextPage,
|
||
limit: currentMeta.limit
|
||
}));
|
||
}
|
||
}, [dispatch, waterRecordsMeta, loading.records, selectedDate]);
|
||
|
||
// 获取日期范围的记录
|
||
const getWaterRecordsByDateRange = useCallback(async (
|
||
startDate: string,
|
||
endDate: string,
|
||
page = 1,
|
||
limit = 20
|
||
) => {
|
||
await dispatch(fetchWaterRecordsByDateRange({ startDate, endDate, page, limit }));
|
||
}, [dispatch]);
|
||
|
||
// 加载今日数据
|
||
const loadTodayData = useCallback(() => {
|
||
dispatch(fetchTodayWaterStats());
|
||
dispatch(fetchWaterRecords({ date: dayjs().format('YYYY-MM-DD') }));
|
||
}, [dispatch]);
|
||
|
||
// 加载指定日期数据
|
||
const loadDataByDate = useCallback((date: string) => {
|
||
dispatch(setSelectedDate(date));
|
||
dispatch(fetchWaterRecords({ date }));
|
||
}, [dispatch]);
|
||
|
||
// 创建喝水记录
|
||
const addWaterRecord = useCallback(async (amount: number, recordedAt?: string) => {
|
||
const dto: CreateWaterRecordDto = {
|
||
amount,
|
||
source: WaterRecordSource.Manual,
|
||
recordedAt,
|
||
};
|
||
|
||
try {
|
||
await dispatch(createWaterRecordAction(dto)).unwrap();
|
||
|
||
// 同步到 HealthKit
|
||
try {
|
||
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordedAt);
|
||
if (!healthKitSuccess) {
|
||
console.warn('同步饮水记录到 HealthKit 失败,但应用内记录已保存');
|
||
}
|
||
} catch (healthError) {
|
||
console.error('HealthKit 同步错误:', healthError);
|
||
// HealthKit 同步失败不影响主要功能
|
||
}
|
||
|
||
// 重新获取今日统计
|
||
dispatch(fetchTodayWaterStats());
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('添加喝水记录失败:', error);
|
||
|
||
// 根据错误类型显示不同的提示信息
|
||
let errorMessage = '添加喝水记录失败';
|
||
|
||
if (error?.message) {
|
||
if (error.message.includes('网络')) {
|
||
errorMessage = '网络连接失败,请检查网络后重试';
|
||
} else if (error.message.includes('参数')) {
|
||
errorMessage = '参数错误,请重新输入';
|
||
} else if (error.message.includes('权限')) {
|
||
errorMessage = '权限不足,请重新登录';
|
||
} else if (error.message.includes('服务器')) {
|
||
errorMessage = '服务器繁忙,请稍后重试';
|
||
} else {
|
||
errorMessage = error.message;
|
||
}
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// 更新喝水记录
|
||
const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => {
|
||
const dto: UpdateWaterRecordDto = {
|
||
id,
|
||
amount,
|
||
note,
|
||
recordedAt,
|
||
};
|
||
|
||
try {
|
||
await dispatch(updateWaterRecordAction(dto)).unwrap();
|
||
// 重新获取今日统计
|
||
dispatch(fetchTodayWaterStats());
|
||
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('更新喝水记录失败:', error);
|
||
|
||
let errorMessage = '更新喝水记录失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// 删除喝水记录
|
||
const removeWaterRecord = useCallback(async (id: string) => {
|
||
try {
|
||
// 在删除前,尝试获取记录信息用于 HealthKit 同步
|
||
const recordToDelete = waterRecords.find(record => record.id === id);
|
||
|
||
await dispatch(deleteWaterRecordAction(id)).unwrap();
|
||
|
||
// 同步删除到 HealthKit
|
||
if (recordToDelete) {
|
||
try {
|
||
const healthKitSuccess = await deleteWaterIntakeFromHealthKit(
|
||
id,
|
||
recordToDelete.recordedAt || recordToDelete.createdAt
|
||
);
|
||
if (!healthKitSuccess) {
|
||
console.warn('从 HealthKit 删除饮水记录失败,但应用内记录已删除');
|
||
}
|
||
} catch (healthError) {
|
||
console.error('HealthKit 删除同步错误:', healthError);
|
||
// HealthKit 同步失败不影响主要功能
|
||
}
|
||
}
|
||
|
||
// 重新获取今日统计
|
||
dispatch(fetchTodayWaterStats());
|
||
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('删除喝水记录失败:', error);
|
||
|
||
let errorMessage = '删除喝水记录失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch, waterRecords]);
|
||
|
||
// 更新喝水目标
|
||
const updateWaterGoal = useCallback(async (goal: number) => {
|
||
try {
|
||
await dispatch(updateWaterGoalAction(goal)).unwrap();
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('更新喝水目标失败:', error);
|
||
|
||
let errorMessage = '更新喝水目标失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// 计算总喝水量
|
||
const getTotalAmount = useCallback((records: any[]) => {
|
||
return records.reduce((total, record) => total + record.amount, 0);
|
||
}, []);
|
||
|
||
// 按小时分组数据
|
||
const getHourlyData = useCallback((records: any[]) => {
|
||
const hourlyData: { hour: number; amount: number }[] = Array.from({ length: 24 }, (_, i) => ({
|
||
hour: i,
|
||
amount: 0,
|
||
}));
|
||
|
||
records.forEach(record => {
|
||
// 优先使用 recordedAt,如果没有则使用 createdAt
|
||
const dateTime = record.recordedAt || record.createdAt;
|
||
const hour = dayjs(dateTime).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(() => {
|
||
loadTodayData();
|
||
}, [loadTodayData]);
|
||
|
||
return {
|
||
// 数据
|
||
todayStats,
|
||
dailyWaterGoal,
|
||
waterRecords,
|
||
waterRecordsMeta,
|
||
selectedDate,
|
||
loading,
|
||
error,
|
||
|
||
// 方法
|
||
loadTodayData,
|
||
loadDataByDate,
|
||
getWaterRecordsByDate,
|
||
loadMoreWaterRecords,
|
||
getWaterRecordsByDateRange,
|
||
addWaterRecord,
|
||
updateWaterRecord,
|
||
removeWaterRecord,
|
||
updateWaterGoal,
|
||
getTotalAmount,
|
||
getHourlyData,
|
||
calculateCompletionRate,
|
||
};
|
||
};
|
||
|
||
// 简化的Hook,只返回今日数据
|
||
export const useTodayWaterData = () => {
|
||
const {
|
||
todayStats,
|
||
dailyWaterGoal,
|
||
waterRecords,
|
||
waterRecordsMeta,
|
||
selectedDate,
|
||
loading,
|
||
error,
|
||
getWaterRecordsByDate,
|
||
loadMoreWaterRecords,
|
||
getWaterRecordsByDateRange,
|
||
addWaterRecord,
|
||
updateWaterRecord,
|
||
removeWaterRecord,
|
||
updateWaterGoal,
|
||
} = useWaterData();
|
||
|
||
// 获取今日记录(默认第一页)
|
||
const todayWaterRecords = useSelector((state: RootState) =>
|
||
state.water.waterRecords[dayjs().format('YYYY-MM-DD')] || []
|
||
);
|
||
|
||
const todayMeta = useSelector((state: RootState) =>
|
||
state.water.waterRecordsMeta[dayjs().format('YYYY-MM-DD')] || {
|
||
total: 0,
|
||
page: 1,
|
||
limit: 20,
|
||
hasMore: false
|
||
}
|
||
);
|
||
|
||
// 获取今日记录(向后兼容)
|
||
const fetchTodayWaterRecords = useCallback(async (page = 1, limit = 20) => {
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
await getWaterRecordsByDate(today, page, limit);
|
||
}, [getWaterRecordsByDate]);
|
||
|
||
return {
|
||
todayStats,
|
||
dailyWaterGoal,
|
||
waterRecords: todayWaterRecords,
|
||
waterRecordsMeta: todayMeta,
|
||
selectedDate,
|
||
loading,
|
||
error,
|
||
fetchTodayWaterRecords,
|
||
loadMoreWaterRecords,
|
||
getWaterRecordsByDateRange,
|
||
addWaterRecord,
|
||
updateWaterRecord,
|
||
removeWaterRecord,
|
||
updateWaterGoal,
|
||
};
|
||
};
|
||
|
||
// 新增:按日期获取饮水数据的 hook
|
||
export const useWaterDataByDate = (targetDate?: string) => {
|
||
const dispatch = useDispatch<AppDispatch>();
|
||
|
||
// 如果没有传入日期,默认使用今天
|
||
const dateToUse = targetDate || dayjs().format('YYYY-MM-DD');
|
||
|
||
// 选择器 - 获取指定日期的数据
|
||
const dailyWaterGoal = useSelector((state: RootState) => state.water.dailyWaterGoal) || 0;
|
||
const waterRecords = useSelector((state: RootState) =>
|
||
state.water.waterRecords[dateToUse] || []
|
||
);
|
||
const waterRecordsMeta = useSelector((state: RootState) =>
|
||
state.water.waterRecordsMeta[dateToUse] || {
|
||
total: 0,
|
||
page: 1,
|
||
limit: 20,
|
||
hasMore: false
|
||
}
|
||
);
|
||
const loading = useSelector((state: RootState) => state.water.loading);
|
||
const error = useSelector((state: RootState) => state.water.error);
|
||
|
||
// 计算指定日期的统计数据
|
||
const waterStats = useMemo(() => {
|
||
if (!waterRecords || waterRecords.length === 0) {
|
||
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;
|
||
|
||
return {
|
||
totalAmount,
|
||
completionRate,
|
||
recordCount: waterRecords.length
|
||
};
|
||
}, [waterRecords, dailyWaterGoal]);
|
||
|
||
// 获取指定日期的记录
|
||
const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => {
|
||
await dispatch(fetchWaterRecords({ date, page, limit }));
|
||
}, [dispatch]);
|
||
|
||
// 创建喝水记录
|
||
const addWaterRecord = useCallback(async (amount: number, recordedAt?: string) => {
|
||
const dto: CreateWaterRecordDto = {
|
||
amount,
|
||
source: WaterRecordSource.Manual,
|
||
recordedAt: recordedAt || dayjs().toISOString(),
|
||
};
|
||
|
||
try {
|
||
await dispatch(createWaterRecordAction(dto)).unwrap();
|
||
|
||
// 同步到 HealthKit
|
||
try {
|
||
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, dto.recordedAt);
|
||
if (!healthKitSuccess) {
|
||
console.warn('同步饮水记录到 HealthKit 失败,但应用内记录已保存');
|
||
}
|
||
} catch (healthError) {
|
||
console.error('HealthKit 同步错误:', healthError);
|
||
// HealthKit 同步失败不影响主要功能
|
||
}
|
||
|
||
// 重新获取当前日期的数据
|
||
await getWaterRecordsByDate(dateToUse);
|
||
|
||
// 如果是今天的数据,也更新今日统计
|
||
if (dateToUse === dayjs().format('YYYY-MM-DD')) {
|
||
dispatch(fetchTodayWaterStats());
|
||
}
|
||
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('添加喝水记录失败:', error);
|
||
|
||
// 根据错误类型显示不同的提示信息
|
||
let errorMessage = '添加喝水记录失败';
|
||
|
||
if (error?.message) {
|
||
if (error.message.includes('网络')) {
|
||
errorMessage = '网络连接失败,请检查网络后重试';
|
||
} else if (error.message.includes('参数')) {
|
||
errorMessage = '参数错误,请重新输入';
|
||
} else if (error.message.includes('权限')) {
|
||
errorMessage = '权限不足,请重新登录';
|
||
} else if (error.message.includes('服务器')) {
|
||
errorMessage = '服务器繁忙,请稍后重试';
|
||
} else {
|
||
errorMessage = error.message;
|
||
}
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch, dateToUse, getWaterRecordsByDate]);
|
||
|
||
// 更新喝水记录
|
||
const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => {
|
||
const dto: UpdateWaterRecordDto = {
|
||
id,
|
||
amount,
|
||
note,
|
||
recordedAt,
|
||
};
|
||
|
||
try {
|
||
await dispatch(updateWaterRecordAction(dto)).unwrap();
|
||
|
||
// 重新获取当前日期的数据
|
||
await getWaterRecordsByDate(dateToUse);
|
||
|
||
// 如果是今天的数据,也更新今日统计
|
||
if (dateToUse === dayjs().format('YYYY-MM-DD')) {
|
||
dispatch(fetchTodayWaterStats());
|
||
}
|
||
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('更新喝水记录失败:', error);
|
||
|
||
let errorMessage = '更新喝水记录失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch, dateToUse, getWaterRecordsByDate]);
|
||
|
||
// 删除喝水记录
|
||
const removeWaterRecord = useCallback(async (id: string) => {
|
||
try {
|
||
// 在删除前,尝试获取记录信息用于 HealthKit 同步
|
||
const recordToDelete = waterRecords.find(record => record.id === id);
|
||
|
||
await dispatch(deleteWaterRecordAction(id)).unwrap();
|
||
|
||
// 同步删除到 HealthKit
|
||
if (recordToDelete) {
|
||
try {
|
||
const healthKitSuccess = await deleteWaterIntakeFromHealthKit(
|
||
id,
|
||
recordToDelete.recordedAt || recordToDelete.createdAt
|
||
);
|
||
if (!healthKitSuccess) {
|
||
console.warn('从 HealthKit 删除饮水记录失败,但应用内记录已删除');
|
||
}
|
||
} catch (healthError) {
|
||
console.error('HealthKit 删除同步错误:', healthError);
|
||
// HealthKit 同步失败不影响主要功能
|
||
}
|
||
}
|
||
|
||
// 重新获取当前日期的数据
|
||
await getWaterRecordsByDate(dateToUse);
|
||
|
||
// 如果是今天的数据,也更新今日统计
|
||
if (dateToUse === dayjs().format('YYYY-MM-DD')) {
|
||
dispatch(fetchTodayWaterStats());
|
||
}
|
||
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('删除喝水记录失败:', error);
|
||
|
||
let errorMessage = '删除喝水记录失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch, dateToUse, getWaterRecordsByDate, waterRecords]);
|
||
|
||
// 更新喝水目标
|
||
const updateWaterGoal = useCallback(async (goal: number) => {
|
||
try {
|
||
await dispatch(updateWaterGoalAction(goal)).unwrap();
|
||
return true;
|
||
} catch (error: any) {
|
||
console.error('更新喝水目标失败:', error);
|
||
|
||
let errorMessage = '更新喝水目标失败';
|
||
if (error?.message) {
|
||
errorMessage = error.message;
|
||
}
|
||
|
||
Toast.error(errorMessage);
|
||
return false;
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// 初始化加载指定日期的数据
|
||
useEffect(() => {
|
||
if (dateToUse) {
|
||
getWaterRecordsByDate(dateToUse);
|
||
}
|
||
}, [dateToUse, getWaterRecordsByDate]);
|
||
|
||
return {
|
||
waterStats,
|
||
dailyWaterGoal,
|
||
waterRecords,
|
||
waterRecordsMeta,
|
||
loading,
|
||
error,
|
||
addWaterRecord,
|
||
updateWaterRecord,
|
||
removeWaterRecord,
|
||
updateWaterGoal,
|
||
getWaterRecordsByDate,
|
||
};
|
||
}; |