feat: 支持饮水记录卡片
This commit is contained in:
494
hooks/useWaterData.ts
Normal file
494
hooks/useWaterData.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
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 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();
|
||||
// 重新获取今日统计
|
||||
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 {
|
||||
await dispatch(deleteWaterRecordAction(id)).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 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();
|
||||
|
||||
// 重新获取当前日期的数据
|
||||
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 {
|
||||
await dispatch(deleteWaterRecordAction(id)).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 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user