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

487
store/waterSlice.ts Normal file
View 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;