487 lines
15 KiB
TypeScript
487 lines
15 KiB
TypeScript
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; |