feat: 支持饮水记录卡片

This commit is contained in:
richarjiang
2025-09-02 15:50:35 +08:00
parent ed694f6142
commit 85a3c742df
16 changed files with 2066 additions and 56 deletions

494
hooks/useWaterData.ts Normal file
View 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,
};
};