feat: 支持饮水记录卡片
This commit is contained in:
487
store/waterSlice.ts
Normal file
487
store/waterSlice.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
import {
|
||||
createWaterRecord,
|
||||
CreateWaterRecordDto,
|
||||
deleteWaterRecord,
|
||||
getTodayWaterStats,
|
||||
getWaterRecords,
|
||||
TodayWaterStats,
|
||||
updateWaterGoal,
|
||||
updateWaterRecord,
|
||||
UpdateWaterRecordDto,
|
||||
WaterRecord,
|
||||
} from '@/services/waterRecords';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import dayjs from 'dayjs';
|
||||
import { RootState } from './index';
|
||||
|
||||
// 状态接口
|
||||
interface WaterState {
|
||||
// 按日期存储的喝水记录
|
||||
waterRecords: Record<string, WaterRecord[]>;
|
||||
// 分页元数据
|
||||
waterRecordsMeta: Record<string, {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
hasMore: boolean;
|
||||
}>;
|
||||
// 今日喝水统计
|
||||
todayStats: TodayWaterStats | null;
|
||||
// 每日喝水目标
|
||||
dailyWaterGoal: number | null;
|
||||
// 当前选中的日期
|
||||
selectedDate: string;
|
||||
// 加载状态
|
||||
loading: {
|
||||
records: boolean;
|
||||
stats: boolean;
|
||||
goal: boolean;
|
||||
create: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
};
|
||||
// 错误信息
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// 初始状态
|
||||
const initialState: WaterState = {
|
||||
waterRecords: {},
|
||||
waterRecordsMeta: {},
|
||||
todayStats: null,
|
||||
dailyWaterGoal: null,
|
||||
selectedDate: dayjs().format('YYYY-MM-DD'),
|
||||
loading: {
|
||||
records: false,
|
||||
stats: false,
|
||||
goal: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
||||
// 异步 actions
|
||||
|
||||
// 获取指定日期的喝水记录
|
||||
export const fetchWaterRecords = createAsyncThunk(
|
||||
'water/fetchWaterRecords',
|
||||
async ({ date, page = 1, limit = 20 }: { date: string; page?: number; limit?: number }) => {
|
||||
const response = await getWaterRecords({
|
||||
date,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
|
||||
return {
|
||||
date,
|
||||
records: response.records,
|
||||
total: response.total,
|
||||
page: response.page,
|
||||
limit: response.limit,
|
||||
hasMore: response.hasMore
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// 获取指定日期范围的喝水记录
|
||||
export const fetchWaterRecordsByDateRange = createAsyncThunk(
|
||||
'water/fetchWaterRecordsByDateRange',
|
||||
async ({ startDate, endDate, page = 1, limit = 20 }: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const response = await getWaterRecords({
|
||||
startDate,
|
||||
endDate,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
// 获取今日喝水统计
|
||||
export const fetchTodayWaterStats = createAsyncThunk(
|
||||
'water/fetchTodayWaterStats',
|
||||
async () => {
|
||||
const stats = await getTodayWaterStats();
|
||||
return stats;
|
||||
}
|
||||
);
|
||||
|
||||
// 创建喝水记录
|
||||
export const createWaterRecordAction = createAsyncThunk(
|
||||
'water/createWaterRecord',
|
||||
async (dto: CreateWaterRecordDto) => {
|
||||
const newRecord = await createWaterRecord(dto);
|
||||
|
||||
return newRecord;
|
||||
}
|
||||
);
|
||||
|
||||
// 更新喝水记录
|
||||
export const updateWaterRecordAction = createAsyncThunk(
|
||||
'water/updateWaterRecord',
|
||||
async (dto: UpdateWaterRecordDto) => {
|
||||
const updatedRecord = await updateWaterRecord(dto);
|
||||
return updatedRecord;
|
||||
}
|
||||
);
|
||||
|
||||
// 删除喝水记录
|
||||
export const deleteWaterRecordAction = createAsyncThunk(
|
||||
'water/deleteWaterRecord',
|
||||
async (id: string) => {
|
||||
await deleteWaterRecord(id);
|
||||
return id;
|
||||
}
|
||||
);
|
||||
|
||||
// 更新喝水目标
|
||||
export const updateWaterGoalAction = createAsyncThunk(
|
||||
'water/updateWaterGoal',
|
||||
async (dailyWaterGoal: number) => {
|
||||
const result = await updateWaterGoal({ dailyWaterGoal });
|
||||
return result.dailyWaterGoal;
|
||||
}
|
||||
);
|
||||
|
||||
// 创建 slice
|
||||
const waterSlice = createSlice({
|
||||
name: 'water',
|
||||
initialState,
|
||||
reducers: {
|
||||
// 设置选中的日期
|
||||
setSelectedDate: (state, action: PayloadAction<string>) => {
|
||||
state.selectedDate = action.payload;
|
||||
},
|
||||
// 清除错误
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
// 清除所有数据
|
||||
clearWaterData: (state) => {
|
||||
state.waterRecords = {};
|
||||
state.todayStats = null;
|
||||
state.error = null;
|
||||
},
|
||||
// 清除喝水记录
|
||||
clearWaterRecords: (state) => {
|
||||
state.waterRecords = {};
|
||||
state.waterRecordsMeta = {};
|
||||
},
|
||||
// 设置每日喝水目标(本地)
|
||||
setDailyWaterGoal: (state, action: PayloadAction<number>) => {
|
||||
state.dailyWaterGoal = action.payload;
|
||||
if (state.todayStats) {
|
||||
state.todayStats.dailyGoal = action.payload;
|
||||
state.todayStats.completionRate =
|
||||
(state.todayStats.totalAmount / action.payload) * 100;
|
||||
}
|
||||
},
|
||||
// 添加本地喝水记录(用于离线场景)
|
||||
addLocalWaterRecord: (state, action: PayloadAction<WaterRecord>) => {
|
||||
const record = action.payload;
|
||||
const date = dayjs(record.recordedAt || record.createdAt).format('YYYY-MM-DD');
|
||||
|
||||
if (!state.waterRecords[date]) {
|
||||
state.waterRecords[date] = [];
|
||||
}
|
||||
|
||||
// 检查是否已存在相同ID的记录
|
||||
const existingIndex = state.waterRecords[date].findIndex(r => r.id === record.id);
|
||||
if (existingIndex >= 0) {
|
||||
state.waterRecords[date][existingIndex] = record;
|
||||
} else {
|
||||
state.waterRecords[date].push(record);
|
||||
}
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount += record.amount;
|
||||
state.todayStats.recordCount += 1;
|
||||
state.todayStats.completionRate =
|
||||
Math.min((state.todayStats.totalAmount / state.todayStats.dailyGoal) * 100, 100);
|
||||
}
|
||||
},
|
||||
// 更新本地喝水记录
|
||||
updateLocalWaterRecord: (state, action: PayloadAction<WaterRecord>) => {
|
||||
const updatedRecord = action.payload;
|
||||
const date = dayjs(updatedRecord.recordedAt || updatedRecord.createdAt).format('YYYY-MM-DD');
|
||||
|
||||
if (state.waterRecords[date]) {
|
||||
const index = state.waterRecords[date].findIndex(r => r.id === updatedRecord.id);
|
||||
if (index >= 0) {
|
||||
const oldRecord = state.waterRecords[date][index];
|
||||
const amountDiff = updatedRecord.amount - oldRecord.amount;
|
||||
|
||||
state.waterRecords[date][index] = updatedRecord;
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount += amountDiff;
|
||||
state.todayStats.completionRate =
|
||||
Math.min((state.todayStats.totalAmount / state.todayStats.dailyGoal) * 100, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 删除本地喝水记录
|
||||
deleteLocalWaterRecord: (state, action: PayloadAction<{ id: string; date: string }>) => {
|
||||
const { id, date } = action.payload;
|
||||
|
||||
if (state.waterRecords[date]) {
|
||||
const recordIndex = state.waterRecords[date].findIndex(r => r.id === id);
|
||||
if (recordIndex >= 0) {
|
||||
const deletedRecord = state.waterRecords[date][recordIndex];
|
||||
|
||||
// 从记录中删除
|
||||
state.waterRecords[date] = state.waterRecords[date].filter(r => r.id !== id);
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount -= deletedRecord.amount;
|
||||
state.todayStats.recordCount -= 1;
|
||||
state.todayStats.completionRate =
|
||||
Math.max(Math.min(state.todayStats.totalAmount / state.todayStats.dailyGoal, 1), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// fetchWaterRecords
|
||||
builder
|
||||
.addCase(fetchWaterRecords.pending, (state) => {
|
||||
state.loading.records = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchWaterRecords.fulfilled, (state, action) => {
|
||||
const { date, records, total, page, limit, hasMore } = action.payload;
|
||||
|
||||
// 如果是第一页,直接替换数据;如果是分页加载,则追加数据
|
||||
if (page === 1) {
|
||||
state.waterRecords[date] = records;
|
||||
} else {
|
||||
const existingRecords = state.waterRecords[date] || [];
|
||||
state.waterRecords[date] = [...existingRecords, ...records];
|
||||
}
|
||||
|
||||
// 更新分页元数据
|
||||
state.waterRecordsMeta[date] = {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
hasMore
|
||||
};
|
||||
|
||||
state.loading.records = false;
|
||||
})
|
||||
.addCase(fetchWaterRecords.rejected, (state, action) => {
|
||||
state.loading.records = false;
|
||||
state.error = action.error.message || '获取喝水记录失败';
|
||||
});
|
||||
|
||||
// fetchWaterRecordsByDateRange
|
||||
builder
|
||||
.addCase(fetchWaterRecordsByDateRange.pending, (state) => {
|
||||
state.loading.records = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchWaterRecordsByDateRange.fulfilled, (state, action) => {
|
||||
state.loading.records = false;
|
||||
// 这里可以根据需要处理日期范围的记录
|
||||
})
|
||||
.addCase(fetchWaterRecordsByDateRange.rejected, (state, action) => {
|
||||
state.loading.records = false;
|
||||
state.error = action.error.message || '获取喝水记录失败';
|
||||
});
|
||||
|
||||
// fetchTodayWaterStats
|
||||
builder
|
||||
.addCase(fetchTodayWaterStats.pending, (state) => {
|
||||
state.loading.stats = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchTodayWaterStats.fulfilled, (state, action) => {
|
||||
state.loading.stats = false;
|
||||
state.todayStats = action.payload;
|
||||
state.dailyWaterGoal = action.payload.dailyGoal;
|
||||
})
|
||||
.addCase(fetchTodayWaterStats.rejected, (state, action) => {
|
||||
state.loading.stats = false;
|
||||
state.error = action.error.message || '获取喝水统计失败';
|
||||
});
|
||||
|
||||
// createWaterRecord
|
||||
builder
|
||||
.addCase(createWaterRecordAction.pending, (state) => {
|
||||
state.loading.create = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(createWaterRecordAction.fulfilled, (state, action) => {
|
||||
state.loading.create = false;
|
||||
const newRecord = action.payload;
|
||||
const date = dayjs(newRecord.recordedAt || newRecord.createdAt).format('YYYY-MM-DD');
|
||||
|
||||
// 添加到对应日期的记录中
|
||||
if (!state.waterRecords[date]) {
|
||||
state.waterRecords[date] = [];
|
||||
}
|
||||
|
||||
// 检查是否已存在相同ID的记录
|
||||
const existingIndex = state.waterRecords[date].findIndex(r => r.id === newRecord.id);
|
||||
if (existingIndex >= 0) {
|
||||
state.waterRecords[date][existingIndex] = newRecord;
|
||||
} else {
|
||||
state.waterRecords[date].push(newRecord);
|
||||
}
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount += newRecord.amount;
|
||||
state.todayStats.recordCount += 1;
|
||||
state.todayStats.completionRate =
|
||||
Math.min((state.todayStats.totalAmount / state.todayStats.dailyGoal) * 100, 100);
|
||||
}
|
||||
})
|
||||
.addCase(createWaterRecordAction.rejected, (state, action) => {
|
||||
state.loading.create = false;
|
||||
state.error = action.error.message || '创建喝水记录失败';
|
||||
});
|
||||
|
||||
// updateWaterRecord
|
||||
builder
|
||||
.addCase(updateWaterRecordAction.pending, (state) => {
|
||||
state.loading.update = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateWaterRecordAction.fulfilled, (state, action) => {
|
||||
state.loading.update = false;
|
||||
const updatedRecord = action.payload;
|
||||
const date = dayjs(updatedRecord.recordedAt || updatedRecord.createdAt).format('YYYY-MM-DD');
|
||||
|
||||
if (state.waterRecords[date]) {
|
||||
const index = state.waterRecords[date].findIndex(r => r.id === updatedRecord.id);
|
||||
if (index >= 0) {
|
||||
const oldRecord = state.waterRecords[date][index];
|
||||
const amountDiff = updatedRecord.amount - oldRecord.amount;
|
||||
|
||||
state.waterRecords[date][index] = updatedRecord;
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount += amountDiff;
|
||||
state.todayStats.completionRate =
|
||||
Math.min((state.todayStats.totalAmount / state.todayStats.dailyGoal) * 100, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(updateWaterRecordAction.rejected, (state, action) => {
|
||||
state.loading.update = false;
|
||||
state.error = action.error.message || '更新喝水记录失败';
|
||||
});
|
||||
|
||||
// deleteWaterRecord
|
||||
builder
|
||||
.addCase(deleteWaterRecordAction.pending, (state) => {
|
||||
state.loading.delete = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(deleteWaterRecordAction.fulfilled, (state, action) => {
|
||||
state.loading.delete = false;
|
||||
const deletedId = action.payload;
|
||||
|
||||
// 从所有日期的记录中删除
|
||||
Object.keys(state.waterRecords).forEach(date => {
|
||||
const recordIndex = state.waterRecords[date].findIndex(r => r.id === deletedId);
|
||||
if (recordIndex >= 0) {
|
||||
const deletedRecord = state.waterRecords[date][recordIndex];
|
||||
|
||||
// 更新今日统计
|
||||
if (date === dayjs().format('YYYY-MM-DD') && state.todayStats) {
|
||||
state.todayStats.totalAmount -= deletedRecord.amount;
|
||||
state.todayStats.recordCount -= 1;
|
||||
state.todayStats.completionRate =
|
||||
Math.max(Math.min((state.todayStats.totalAmount / state.todayStats.dailyGoal) * 100, 100), 0);
|
||||
}
|
||||
|
||||
state.waterRecords[date] = state.waterRecords[date].filter(r => r.id !== deletedId);
|
||||
}
|
||||
});
|
||||
})
|
||||
.addCase(deleteWaterRecordAction.rejected, (state, action) => {
|
||||
state.loading.delete = false;
|
||||
state.error = action.error.message || '删除喝水记录失败';
|
||||
});
|
||||
|
||||
// updateWaterGoal
|
||||
builder
|
||||
.addCase(updateWaterGoalAction.pending, (state) => {
|
||||
state.loading.goal = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateWaterGoalAction.fulfilled, (state, action) => {
|
||||
state.loading.goal = false;
|
||||
state.dailyWaterGoal = action.payload;
|
||||
if (state.todayStats) {
|
||||
state.todayStats.dailyGoal = action.payload;
|
||||
state.todayStats.completionRate =
|
||||
Math.min((state.todayStats.totalAmount / action.payload) * 100, 100);
|
||||
}
|
||||
})
|
||||
.addCase(updateWaterGoalAction.rejected, (state, action) => {
|
||||
state.loading.goal = false;
|
||||
state.error = action.error.message || '更新喝水目标失败';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const {
|
||||
setSelectedDate,
|
||||
clearError,
|
||||
clearWaterData,
|
||||
clearWaterRecords,
|
||||
setDailyWaterGoal,
|
||||
addLocalWaterRecord,
|
||||
updateLocalWaterRecord,
|
||||
deleteLocalWaterRecord,
|
||||
} = waterSlice.actions;
|
||||
|
||||
// 选择器函数
|
||||
export const selectWaterState = (state: RootState) => state.water;
|
||||
|
||||
// 选择今日统计
|
||||
export const selectTodayStats = (state: RootState) => selectWaterState(state).todayStats;
|
||||
|
||||
// 选择每日喝水目标
|
||||
export const selectDailyWaterGoal = (state: RootState) => selectWaterState(state).dailyWaterGoal;
|
||||
|
||||
// 选择指定日期的喝水记录
|
||||
export const selectWaterRecordsByDate = (date: string) => (state: RootState) => {
|
||||
return selectWaterState(state).waterRecords[date] || [];
|
||||
};
|
||||
|
||||
// 选择当前选中日期的喝水记录
|
||||
export const selectSelectedDateWaterRecords = (state: RootState) => {
|
||||
const selectedDate = selectWaterState(state).selectedDate;
|
||||
return selectWaterRecordsByDate(selectedDate)(state);
|
||||
};
|
||||
|
||||
// 选择加载状态
|
||||
export const selectWaterLoading = (state: RootState) => selectWaterState(state).loading;
|
||||
|
||||
// 选择错误信息
|
||||
export const selectWaterError = (state: RootState) => selectWaterState(state).error;
|
||||
|
||||
// 选择当前选中日期
|
||||
export const selectSelectedDate = (state: RootState) => selectWaterState(state).selectedDate;
|
||||
|
||||
// 导出 reducer
|
||||
export default waterSlice.reducer;
|
||||
Reference in New Issue
Block a user