/** * 药物管理 Redux Slice */ import * as medicationsApi from '@/services/medications'; import type { DailyMedicationStats, Medication, MedicationRecord, MedicationStatus, } from '@/types/medication'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import type { RootState } from './index'; // ==================== 状态接口 ==================== interface MedicationsState { // 药物列表 medications: Medication[]; // 激活的药物列表(快速访问) activeMedications: Medication[]; // 按日期存储的服药记录 { 'YYYY-MM-DD': MedicationRecord[] } medicationRecords: Record; // 每日统计 { 'YYYY-MM-DD': DailyMedicationStats } dailyStats: Record; // 总体统计 overallStats: { totalMedications: number; totalRecords: number; completionRate: number; streak: number; } | null; // 当前选中的日期 selectedDate: string; // 加载状态 loading: { medications: boolean; records: boolean; stats: boolean; create: boolean; update: boolean; delete: boolean; takeMedication: boolean; }; // 错误信息 error: string | null; } // ==================== 初始状态 ==================== const initialState: MedicationsState = { medications: [], activeMedications: [], medicationRecords: {}, dailyStats: {}, overallStats: null, selectedDate: dayjs().format('YYYY-MM-DD'), loading: { medications: false, records: false, stats: false, create: false, update: false, delete: false, takeMedication: false, }, error: null, }; // ==================== 异步 Thunks ==================== /** * 获取药物列表 */ export const fetchMedications = createAsyncThunk( 'medications/fetchMedications', async (params?: medicationsApi.GetMedicationsParams) => { return await medicationsApi.getMedications(params); } ); /** * 获取指定日期的服药记录 */ export const fetchMedicationRecords = createAsyncThunk( 'medications/fetchMedicationRecords', async (params: { date: string }) => { const records = await medicationsApi.getMedicationRecords(params); return { date: params.date, records }; } ); /** * 获取今日服药记录 */ export const fetchTodayMedicationRecords = createAsyncThunk( 'medications/fetchTodayMedicationRecords', async () => { const records = await medicationsApi.getTodayMedicationRecords(); const today = dayjs().format('YYYY-MM-DD'); return { date: today, records }; } ); /** * 获取日期范围内的服药记录 */ export const fetchMedicationRecordsByDateRange = createAsyncThunk( 'medications/fetchMedicationRecordsByDateRange', async (params: { startDate: string; endDate: string }) => { const records = await medicationsApi.getMedicationRecords(params); return { params, records }; } ); /** * 获取每日统计 */ export const fetchDailyStats = createAsyncThunk( 'medications/fetchDailyStats', async (date: string) => { const stats = await medicationsApi.getDailyStats(date); return { date, stats }; } ); /** * 获取总体统计 */ export const fetchOverallStats = createAsyncThunk( 'medications/fetchOverallStats', async () => { return await medicationsApi.getOverallStats(); } ); /** * 创建新药物 */ export const createMedicationAction = createAsyncThunk( 'medications/createMedication', async (dto: medicationsApi.CreateMedicationDto) => { return await medicationsApi.createMedication(dto); } ); /** * 更新药物信息 */ export const updateMedicationAction = createAsyncThunk( 'medications/updateMedication', async (dto: medicationsApi.UpdateMedicationDto) => { return await medicationsApi.updateMedication(dto); } ); /** * 删除药物 */ export const deleteMedicationAction = createAsyncThunk( 'medications/deleteMedication', async (id: string) => { await medicationsApi.deleteMedication(id); return id; } ); /** * 停用药物 */ export const deactivateMedicationAction = createAsyncThunk( 'medications/deactivateMedication', async (id: string) => { return await medicationsApi.deactivateMedication(id); } ); /** * 服用药物 */ export const takeMedicationAction = createAsyncThunk( 'medications/takeMedication', async (params: { recordId: string; actualTime?: string }) => { return await medicationsApi.takeMedication(params.recordId, params.actualTime); } ); /** * 跳过药物 */ export const skipMedicationAction = createAsyncThunk( 'medications/skipMedication', async (params: { recordId: string; note?: string }) => { return await medicationsApi.skipMedication(params.recordId, params.note); } ); /** * 更新服药记录 */ export const updateMedicationRecordAction = createAsyncThunk( 'medications/updateMedicationRecord', async (dto: medicationsApi.UpdateMedicationRecordDto) => { return await medicationsApi.updateMedicationRecord(dto); } ); // ==================== Slice ==================== const medicationsSlice = createSlice({ name: 'medications', initialState, reducers: { /** * 设置选中的日期 */ setSelectedDate: (state, action: PayloadAction) => { state.selectedDate = action.payload; }, /** * 清除错误信息 */ clearError: (state) => { state.error = null; }, /** * 清除所有药物数据 */ clearMedicationsData: (state) => { state.medications = []; state.activeMedications = []; state.medicationRecords = {}; state.dailyStats = {}; state.overallStats = null; state.error = null; }, /** * 清除服药记录 */ clearMedicationRecords: (state) => { state.medicationRecords = {}; state.dailyStats = {}; }, /** * 本地更新记录状态(用于乐观更新) */ updateRecordStatusLocally: ( state, action: PayloadAction<{ recordId: string; status: MedicationStatus; date: string; actualTime?: string; }> ) => { const { recordId, status, date, actualTime } = action.payload; const records = state.medicationRecords[date]; if (records) { const record = records.find((r) => r.id === recordId); if (record) { record.status = status; if (actualTime) { record.actualTime = actualTime; } } } // 更新统计数据 const stats = state.dailyStats[date]; if (stats) { if (status === 'taken') { stats.taken += 1; stats.upcoming = Math.max(0, stats.upcoming - 1); } else if (status === 'missed') { stats.missed += 1; stats.upcoming = Math.max(0, stats.upcoming - 1); } else if (status === 'skipped') { stats.upcoming = Math.max(0, stats.upcoming - 1); } stats.completionRate = stats.totalScheduled > 0 ? (stats.taken / stats.totalScheduled) * 100 : 0; } }, /** * 添加本地服药记录(用于离线场景) */ addLocalMedicationRecord: (state, action: PayloadAction) => { const record = action.payload; const date = dayjs(record.scheduledTime).format('YYYY-MM-DD'); if (!state.medicationRecords[date]) { state.medicationRecords[date] = []; } // 检查是否已存在相同ID的记录 const existingIndex = state.medicationRecords[date].findIndex( (r) => r.id === record.id ); if (existingIndex >= 0) { state.medicationRecords[date][existingIndex] = record; } else { state.medicationRecords[date].push(record); } }, }, extraReducers: (builder) => { // ==================== fetchMedications ==================== builder .addCase(fetchMedications.pending, (state) => { state.loading.medications = true; state.error = null; }) .addCase(fetchMedications.fulfilled, (state, action) => { console.log('action', action); state.loading.medications = false; state.medications = action.payload; state.activeMedications = action.payload.filter((m) => m.isActive); }) .addCase(fetchMedications.rejected, (state, action) => { state.loading.medications = false; state.error = action.error.message || '获取药物列表失败'; }); // ==================== fetchMedicationRecords ==================== builder .addCase(fetchMedicationRecords.pending, (state) => { state.loading.records = true; state.error = null; }) .addCase(fetchMedicationRecords.fulfilled, (state, action) => { state.loading.records = false; const { date, records } = action.payload; state.medicationRecords[date] = records; }) .addCase(fetchMedicationRecords.rejected, (state, action) => { state.loading.records = false; state.error = action.error.message || '获取服药记录失败'; }); // ==================== fetchTodayMedicationRecords ==================== builder .addCase(fetchTodayMedicationRecords.pending, (state) => { state.loading.records = true; state.error = null; }) .addCase(fetchTodayMedicationRecords.fulfilled, (state, action) => { state.loading.records = false; const { date, records } = action.payload; state.medicationRecords[date] = records; }) .addCase(fetchTodayMedicationRecords.rejected, (state, action) => { state.loading.records = false; state.error = action.error.message || '获取今日服药记录失败'; }); // ==================== fetchMedicationRecordsByDateRange ==================== builder .addCase(fetchMedicationRecordsByDateRange.pending, (state) => { state.loading.records = true; state.error = null; }) .addCase(fetchMedicationRecordsByDateRange.fulfilled, (state, action) => { state.loading.records = false; const { records } = action.payload; // 按日期分组存储记录 records.forEach((record) => { const date = dayjs(record.scheduledTime).format('YYYY-MM-DD'); if (!state.medicationRecords[date]) { state.medicationRecords[date] = []; } // 检查是否已存在相同ID的记录 const existingIndex = state.medicationRecords[date].findIndex( (r) => r.id === record.id ); if (existingIndex >= 0) { state.medicationRecords[date][existingIndex] = record; } else { state.medicationRecords[date].push(record); } }); }) .addCase(fetchMedicationRecordsByDateRange.rejected, (state, action) => { state.loading.records = false; state.error = action.error.message || '获取服药记录失败'; }); // ==================== fetchDailyStats ==================== builder .addCase(fetchDailyStats.pending, (state) => { state.loading.stats = true; state.error = null; }) .addCase(fetchDailyStats.fulfilled, (state, action) => { state.loading.stats = false; const { date, stats } = action.payload; state.dailyStats[date] = stats; }) .addCase(fetchDailyStats.rejected, (state, action) => { state.loading.stats = false; state.error = action.error.message || '获取统计数据失败'; }); // ==================== fetchOverallStats ==================== builder .addCase(fetchOverallStats.pending, (state) => { state.loading.stats = true; state.error = null; }) .addCase(fetchOverallStats.fulfilled, (state, action) => { state.loading.stats = false; state.overallStats = action.payload; }) .addCase(fetchOverallStats.rejected, (state, action) => { state.loading.stats = false; state.error = action.error.message || '获取总体统计失败'; }); // ==================== createMedication ==================== builder .addCase(createMedicationAction.pending, (state) => { state.loading.create = true; state.error = null; }) .addCase(createMedicationAction.fulfilled, (state, action) => { state.loading.create = false; const newMedication = action.payload; state.medications.push(newMedication); if (newMedication.isActive) { state.activeMedications.push(newMedication); } }) .addCase(createMedicationAction.rejected, (state, action) => { state.loading.create = false; state.error = action.error.message || '创建药物失败'; }); // ==================== updateMedication ==================== builder .addCase(updateMedicationAction.pending, (state) => { state.loading.update = true; state.error = null; }) .addCase(updateMedicationAction.fulfilled, (state, action) => { state.loading.update = false; const updated = action.payload; const index = state.medications.findIndex((m) => m.id === updated.id); if (index >= 0) { // 只有当 isActive 状态改变时才更新 activeMedications const wasActive = state.medications[index].isActive; const isActiveNow = updated.isActive; // 更新药品信息 state.medications[index] = updated; // 优化:只有当 isActive 状态改变时才重新计算 activeMedications if (wasActive !== isActiveNow) { if (isActiveNow) { // 激活药品:添加到 activeMedications(如果不在其中) if (!state.activeMedications.some(m => m.id === updated.id)) { state.activeMedications.push(updated); } } else { // 停用药品:从 activeMedications 中移除 state.activeMedications = state.activeMedications.filter( (m) => m.id !== updated.id ); } } else { // isActive 状态未改变,只需更新 activeMedications 中的对应项 const activeIndex = state.activeMedications.findIndex((m) => m.id === updated.id); if (activeIndex >= 0) { state.activeMedications[activeIndex] = updated; } } } }) .addCase(updateMedicationAction.rejected, (state, action) => { state.loading.update = false; state.error = action.error.message || '更新药物失败'; }); // ==================== deleteMedication ==================== builder .addCase(deleteMedicationAction.pending, (state) => { console.log('[MEDICATIONS_SLICE] Delete operation pending'); state.loading.delete = true; state.error = null; }) .addCase(deleteMedicationAction.fulfilled, (state, action) => { console.log('[MEDICATIONS_SLICE] Delete operation fulfilled', { deletedId: action.payload }); state.loading.delete = false; const deletedId = action.payload; state.medications = state.medications.filter((m) => m.id !== deletedId); state.activeMedications = state.activeMedications.filter( (m) => m.id !== deletedId ); console.log('[MEDICATIONS_SLICE] Medications after delete', { totalMedications: state.medications.length, activeMedications: state.activeMedications.length }); }) .addCase(deleteMedicationAction.rejected, (state, action) => { console.log('[MEDICATIONS_SLICE] Delete operation rejected', action.error); state.loading.delete = false; state.error = action.error.message || '删除药物失败'; }); // ==================== deactivateMedication ==================== builder .addCase(deactivateMedicationAction.pending, (state) => { state.loading.update = true; state.error = null; }) .addCase(deactivateMedicationAction.fulfilled, (state, action) => { state.loading.update = false; const updated = action.payload; const index = state.medications.findIndex((m) => m.id === updated.id); if (index >= 0) { state.medications[index] = updated; } // 从激活列表中移除 state.activeMedications = state.activeMedications.filter( (m) => m.id !== updated.id ); }) .addCase(deactivateMedicationAction.rejected, (state, action) => { state.loading.update = false; state.error = action.error.message || '停用药物失败'; }); // ==================== takeMedication ==================== builder .addCase(takeMedicationAction.pending, (state) => { state.loading.takeMedication = true; state.error = null; }) .addCase(takeMedicationAction.fulfilled, (state, action) => { state.loading.takeMedication = false; const updated = action.payload; const date = dayjs(updated.scheduledTime).format('YYYY-MM-DD'); const records = state.medicationRecords[date]; if (records) { const index = records.findIndex((r) => r.id === updated.id); if (index >= 0) { records[index] = updated; } } // 更新统计数据 const stats = state.dailyStats[date]; if (stats) { stats.taken += 1; stats.upcoming = Math.max(0, stats.upcoming - 1); stats.completionRate = stats.totalScheduled > 0 ? (stats.taken / stats.totalScheduled) * 100 : 0; } }) .addCase(takeMedicationAction.rejected, (state, action) => { state.loading.takeMedication = false; state.error = action.error.message || '服药操作失败'; }); // ==================== skipMedication ==================== builder .addCase(skipMedicationAction.pending, (state) => { state.loading.takeMedication = true; state.error = null; }) .addCase(skipMedicationAction.fulfilled, (state, action) => { state.loading.takeMedication = false; const updated = action.payload; const date = dayjs(updated.scheduledTime).format('YYYY-MM-DD'); const records = state.medicationRecords[date]; if (records) { const index = records.findIndex((r) => r.id === updated.id); if (index >= 0) { records[index] = updated; } } // 更新统计数据 const stats = state.dailyStats[date]; if (stats) { stats.upcoming = Math.max(0, stats.upcoming - 1); } }) .addCase(skipMedicationAction.rejected, (state, action) => { state.loading.takeMedication = false; state.error = action.error.message || '跳过操作失败'; }); // ==================== updateMedicationRecord ==================== builder .addCase(updateMedicationRecordAction.pending, (state) => { state.loading.update = true; state.error = null; }) .addCase(updateMedicationRecordAction.fulfilled, (state, action) => { state.loading.update = false; const updated = action.payload; const date = dayjs(updated.scheduledTime).format('YYYY-MM-DD'); const records = state.medicationRecords[date]; if (records) { const index = records.findIndex((r) => r.id === updated.id); if (index >= 0) { records[index] = updated; } } }) .addCase(updateMedicationRecordAction.rejected, (state, action) => { state.loading.update = false; state.error = action.error.message || '更新服药记录失败'; }); }, }); // ==================== Actions ==================== export const { setSelectedDate, clearError, clearMedicationsData, clearMedicationRecords, updateRecordStatusLocally, addLocalMedicationRecord, } = medicationsSlice.actions; // ==================== Selectors ==================== export const selectMedicationsState = (state: RootState) => state.medications; export const selectMedications = (state: RootState) => state.medications.medications; export const selectActiveMedications = (state: RootState) => state.medications.activeMedications; export const selectSelectedDate = (state: RootState) => state.medications.selectedDate; export const selectMedicationsLoading = (state: RootState) => state.medications.loading; export const selectMedicationsError = (state: RootState) => state.medications.error; export const selectOverallStats = (state: RootState) => state.medications.overallStats; /** * 获取指定日期的服药记录 */ export const selectMedicationRecordsByDate = (date: string) => (state: RootState) => { return state.medications.medicationRecords[date] || []; }; /** * 获取当前选中日期的服药记录 */ export const selectSelectedDateMedicationRecords = (state: RootState) => { const selectedDate = state.medications.selectedDate; return state.medications.medicationRecords[selectedDate] || []; }; /** * 获取指定日期的统计数据 */ export const selectDailyStatsByDate = (date: string) => (state: RootState) => { return state.medications.dailyStats[date]; }; /** * 获取当前选中日期的统计数据 */ export const selectSelectedDateStats = (state: RootState) => { const selectedDate = state.medications.selectedDate; return state.medications.dailyStats[selectedDate]; }; /** * 获取指定日期的展示项列表(用于UI渲染) * 将药物记录和药物信息合并为展示项 */ export const selectMedicationDisplayItemsByDate = (date: string) => (state: RootState) => { const records = state.medications.medicationRecords[date] || []; const medications = state.medications.medications; // 创建药物ID到药物的映射 const medicationMap = new Map(); medications.forEach((med) => medicationMap.set(med.id, med)); // 转换为展示项 return records .map((record) => { const medication = record.medication || medicationMap.get(record.medicationId); if (!medication) return null; // 格式化剂量 const dosage = `${medication.dosageValue} ${medication.dosageUnit}`; // 提取并格式化为当地时间(HH:mm格式) // 服务端返回的是UTC时间,需要转换为用户本地时间显示 const localTime = dayjs(record.scheduledTime).format('HH:mm'); const scheduledTime = localTime || '00:00'; // 频率描述 const frequency = medication.repeatPattern === 'daily' ? '每日' : '自定义'; return { id: record.id, name: medication.name, dosage, scheduledTime, frequency, status: record.status, recordId: record.id, medicationId: medication.id, } as import('@/types/medication').MedicationDisplayItem; }) .filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null); }; // ==================== Export ==================== export default medicationsSlice.reducer;