Files
digital-pilates/store/medicationsSlice.ts
richarjiang 50525f82a1 feat(medications): 优化药品管理功能和登录流程
- 更新默认药品图片为专用图标
- 移除未使用的 loading 状态选择器
- 优化 Apple 登录按钮样式,支持毛玻璃效果和加载状态
- 添加登录成功后返回功能(shouldBack 参数)
- 药品详情页添加信息卡片点击交互
- 添加药品添加页面的登录状态检查
- 增强时间选择器错误处理和数据验证
- 修复药品图片显示逻辑,支持网络图片
- 优化药品卡片样式和布局
- 添加图片加载错误处理
2025-11-11 10:02:37 +08:00

732 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 药物管理 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<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;
})
.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<string, Medication>();
medications.forEach((med) => medicationMap.set(med.id, med));
// 转换为展示项
return 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);
};
// ==================== Export ====================
export default medicationsSlice.reducer;