799 lines
26 KiB
TypeScript
799 lines
26 KiB
TypeScript
/**
|
||
* 药物管理 Redux Slice
|
||
*/
|
||
|
||
import * as medicationsApi from '@/services/medications';
|
||
import type {
|
||
DailyMedicationStats,
|
||
Medication,
|
||
MedicationRecord,
|
||
MedicationStatus,
|
||
} from '@/types/medication';
|
||
import { convertMedicationDataToWidget, syncMedicationDataToWidget } from '@/utils/widgetDataSync';
|
||
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<string, MedicationRecord[]>;
|
||
|
||
// 每日统计 { 'YYYY-MM-DD': DailyMedicationStats }
|
||
dailyStats: Record<string, DailyMedicationStats>;
|
||
|
||
// 总体统计
|
||
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<string>) => {
|
||
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<MedicationRecord>) => {
|
||
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;
|
||
|
||
// 如果是今天的记录,同步到小组件
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
if (date === today) {
|
||
const medicationData = convertMedicationDataToWidget(records, state.medications, date);
|
||
syncMedicationDataToWidget(medicationData).catch(error => {
|
||
console.error('Failed to sync medication data to widget:', error);
|
||
});
|
||
}
|
||
})
|
||
.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;
|
||
|
||
// 同步今天的记录到小组件
|
||
const medicationData = convertMedicationDataToWidget(records, state.medications, date);
|
||
syncMedicationDataToWidget(medicationData).catch(error => {
|
||
console.error('Failed to sync medication data to widget:', error);
|
||
});
|
||
})
|
||
.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);
|
||
}
|
||
});
|
||
|
||
// 同步今天的记录到小组件
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
if (state.medicationRecords[today]) {
|
||
const medicationData = convertMedicationDataToWidget(state.medicationRecords[today], state.medications, today);
|
||
syncMedicationDataToWidget(medicationData).catch(error => {
|
||
console.error('Failed to sync medication data to widget:', error);
|
||
});
|
||
}
|
||
})
|
||
.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;
|
||
}
|
||
|
||
// 如果是今天的记录,同步到小组件
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
if (date === today && records) {
|
||
const medicationData = convertMedicationDataToWidget(records, state.medications, date);
|
||
syncMedicationDataToWidget(medicationData).catch(error => {
|
||
console.error('Failed to sync medication data to widget:', error);
|
||
});
|
||
}
|
||
})
|
||
.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);
|
||
}
|
||
|
||
// 如果是今天的记录,同步到小组件
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
if (date === today && records) {
|
||
const medicationData = convertMedicationDataToWidget(records, state.medications, date);
|
||
syncMedicationDataToWidget(medicationData).catch(error => {
|
||
console.error('Failed to sync medication data to widget:', error);
|
||
});
|
||
}
|
||
})
|
||
.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渲染)
|
||
* 将药物记录和药物信息合并为展示项
|
||
* 排序规则:优先显示未服用的药品(upcoming、missed),然后是已服用的药品(taken、skipped)
|
||
*/
|
||
export const selectMedicationDisplayItemsByDate = (date: string) => (state: RootState) => {
|
||
const records = state.medications.medicationRecords[date] || [];
|
||
const medications = state.medications.medications;
|
||
|
||
// 创建药物ID到药物的映射
|
||
const medicationMap = new Map<string, Medication>();
|
||
medications.forEach((med) => medicationMap.set(med.id, med));
|
||
|
||
// 转换为展示项
|
||
const displayItems = records
|
||
.map((record) => {
|
||
const 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,
|
||
image: medication.photoUrl ? { uri: medication.photoUrl } : undefined
|
||
} as import('@/types/medication').MedicationDisplayItem;
|
||
})
|
||
.filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null);
|
||
|
||
// 排序:未服用的药品(upcoming、missed)优先,已服用的药品(taken、skipped)其次
|
||
// 在同一组内,按计划时间升序排列
|
||
return displayItems.sort((a, b) => {
|
||
// 定义状态优先级:数值越小优先级越高
|
||
const statusPriority: Record<MedicationStatus, number> = {
|
||
'missed': 1, // 已错过 - 最高优先级
|
||
'upcoming': 2, // 待服用
|
||
'taken': 3, // 已服用
|
||
'skipped': 4, // 已跳过
|
||
};
|
||
|
||
const priorityA = statusPriority[a.status];
|
||
const priorityB = statusPriority[b.status];
|
||
|
||
// 首先按状态优先级排序
|
||
if (priorityA !== priorityB) {
|
||
return priorityA - priorityB;
|
||
}
|
||
|
||
// 状态相同时,按计划时间升序排列
|
||
return a.scheduledTime.localeCompare(b.scheduledTime);
|
||
});
|
||
};
|
||
|
||
// ==================== Export ====================
|
||
|
||
export default medicationsSlice.reducer; |