440 lines
13 KiB
TypeScript
440 lines
13 KiB
TypeScript
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||
import * as healthProfileApi from '@/services/healthProfile';
|
||
import { AppDispatch, RootState } from './index';
|
||
import {
|
||
HistoryData,
|
||
HistoryItemDetail,
|
||
MedicalRecordItem,
|
||
MedicalRecordsData,
|
||
MedicalRecordType,
|
||
} from '@/services/healthProfile';
|
||
|
||
// 健康数据类型定义
|
||
export interface FitnessRingsData {
|
||
activeCalories: number;
|
||
activeCaloriesGoal: number;
|
||
exerciseMinutes: number;
|
||
exerciseMinutesGoal: number;
|
||
standHours: number;
|
||
standHoursGoal: number;
|
||
}
|
||
|
||
export interface HealthData {
|
||
activeCalories: number | null;
|
||
heartRate: number | null;
|
||
activeEnergyBurned: number;
|
||
activeCaloriesGoal: number;
|
||
exerciseMinutes: number;
|
||
exerciseMinutesGoal: number;
|
||
standHours: number;
|
||
standHoursGoal: number;
|
||
}
|
||
|
||
// 健康史数据类型定义
|
||
export interface HistoryItemDetail {
|
||
id: string;
|
||
name: string;
|
||
date?: string; // ISO Date string
|
||
isRecommendation?: boolean;
|
||
}
|
||
|
||
export interface HistoryData {
|
||
[key: string]: {
|
||
hasHistory: boolean | null;
|
||
items: HistoryItemDetail[];
|
||
};
|
||
}
|
||
|
||
export interface HealthState {
|
||
// 按日期存储的历史数据
|
||
dataByDate: Record<string, HealthData>;
|
||
|
||
// 健康史数据
|
||
historyData: HistoryData;
|
||
|
||
// 就医资料数据
|
||
medicalRecords: MedicalRecordsData;
|
||
|
||
// 加载状态
|
||
loading: boolean;
|
||
error: string | null;
|
||
|
||
// 最后更新时间
|
||
lastUpdateTime: string | null;
|
||
}
|
||
|
||
// 初始状态
|
||
const initialState: HealthState = {
|
||
dataByDate: {},
|
||
historyData: {
|
||
allergy: { hasHistory: null, items: [] },
|
||
disease: { hasHistory: null, items: [] },
|
||
surgery: { hasHistory: null, items: [] },
|
||
familyDisease: { hasHistory: null, items: [] },
|
||
},
|
||
medicalRecords: {
|
||
records: [],
|
||
prescriptions: [],
|
||
},
|
||
loading: false,
|
||
error: null,
|
||
lastUpdateTime: null,
|
||
};
|
||
|
||
const healthSlice = createSlice({
|
||
name: 'health',
|
||
initialState,
|
||
reducers: {
|
||
// 设置加载状态
|
||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||
state.loading = action.payload;
|
||
},
|
||
|
||
// 设置错误信息
|
||
setError: (state, action: PayloadAction<string | null>) => {
|
||
state.error = action.payload;
|
||
},
|
||
|
||
// 设置完整的健康数据
|
||
setHealthData: (state, action: PayloadAction<{
|
||
date: string;
|
||
data: HealthData;
|
||
}>) => {
|
||
const { date, data } = action.payload;
|
||
|
||
// 存储到历史数据
|
||
state.dataByDate[date] = data;
|
||
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
|
||
// 清除特定日期的数据
|
||
clearHealthDataForDate: (state, action: PayloadAction<string>) => {
|
||
const date = action.payload;
|
||
delete state.dataByDate[date];
|
||
|
||
},
|
||
|
||
// 清除所有健康数据
|
||
clearAllHealthData: (state) => {
|
||
state.dataByDate = {};
|
||
state.error = null;
|
||
state.lastUpdateTime = null;
|
||
},
|
||
|
||
// 更新健康史数据(本地更新,用于乐观更新或离线模式)
|
||
updateHistoryData: (state, action: PayloadAction<{
|
||
type: string;
|
||
data: {
|
||
hasHistory: boolean | null;
|
||
items: HistoryItemDetail[];
|
||
};
|
||
}>) => {
|
||
const { type, data } = action.payload;
|
||
state.historyData[type] = data;
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
|
||
// 设置完整的健康史数据(从服务端同步)
|
||
setHistoryData: (state, action: PayloadAction<HistoryData>) => {
|
||
state.historyData = action.payload;
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
|
||
// 清除健康史数据
|
||
clearHistoryData: (state) => {
|
||
state.historyData = {
|
||
allergy: { hasHistory: null, items: [] },
|
||
disease: { hasHistory: null, items: [] },
|
||
surgery: { hasHistory: null, items: [] },
|
||
familyDisease: { hasHistory: null, items: [] },
|
||
};
|
||
},
|
||
|
||
// 更新就医资料列表
|
||
setMedicalRecords: (state, action: PayloadAction<MedicalRecordsData>) => {
|
||
state.medicalRecords = action.payload;
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
|
||
// 添加就医资料项
|
||
addMedicalRecordItem: (state, action: PayloadAction<MedicalRecordItem>) => {
|
||
const item = action.payload;
|
||
if (item.type === 'medical_record') {
|
||
state.medicalRecords.records.unshift(item);
|
||
} else {
|
||
state.medicalRecords.prescriptions.unshift(item);
|
||
}
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
|
||
// 删除就医资料项
|
||
removeMedicalRecordItem: (state, action: PayloadAction<{ id: string; type: MedicalRecordType }>) => {
|
||
const { id, type } = action.payload;
|
||
if (type === 'medical_record') {
|
||
state.medicalRecords.records = state.medicalRecords.records.filter(item => item.id !== id);
|
||
} else {
|
||
state.medicalRecords.prescriptions = state.medicalRecords.prescriptions.filter(item => item.id !== id);
|
||
}
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
},
|
||
},
|
||
extraReducers: (builder) => {
|
||
builder
|
||
// 获取健康史
|
||
.addCase(fetchHealthHistory.pending, (state) => {
|
||
state.loading = true;
|
||
state.error = null;
|
||
})
|
||
.addCase(fetchHealthHistory.fulfilled, (state, action) => {
|
||
state.loading = false;
|
||
// 转换服务端数据格式到本地格式
|
||
const serverData = action.payload;
|
||
const categories = ['allergy', 'disease', 'surgery', 'familyDisease'] as const;
|
||
|
||
categories.forEach(category => {
|
||
if (serverData[category]) {
|
||
state.historyData[category] = {
|
||
hasHistory: serverData[category].hasHistory,
|
||
items: serverData[category].items.map(item => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
date: item.diagnosisDate,
|
||
isRecommendation: item.isRecommendation,
|
||
})),
|
||
};
|
||
}
|
||
});
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
})
|
||
.addCase(fetchHealthHistory.rejected, (state, action) => {
|
||
state.loading = false;
|
||
state.error = action.payload as string;
|
||
})
|
||
// 保存健康史分类
|
||
.addCase(saveHealthHistoryCategory.pending, (state) => {
|
||
state.loading = true;
|
||
state.error = null;
|
||
})
|
||
.addCase(saveHealthHistoryCategory.fulfilled, (state, action) => {
|
||
state.loading = false;
|
||
const { category, data } = action.payload;
|
||
// 更新对应分类的数据
|
||
state.historyData[category] = {
|
||
hasHistory: data.hasHistory,
|
||
items: data.items.map(item => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
date: item.diagnosisDate,
|
||
isRecommendation: item.isRecommendation,
|
||
})),
|
||
};
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
})
|
||
.addCase(saveHealthHistoryCategory.rejected, (state, action) => {
|
||
state.loading = false;
|
||
state.error = action.payload as string;
|
||
})
|
||
// 获取健康史进度
|
||
.addCase(fetchHealthHistoryProgress.rejected, (state, action) => {
|
||
state.error = action.payload as string;
|
||
})
|
||
// 获取就医资料
|
||
.addCase(fetchMedicalRecords.pending, (state) => {
|
||
state.loading = true;
|
||
state.error = null;
|
||
})
|
||
.addCase(fetchMedicalRecords.fulfilled, (state, action) => {
|
||
state.loading = false;
|
||
state.medicalRecords = action.payload;
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
})
|
||
.addCase(fetchMedicalRecords.rejected, (state, action) => {
|
||
state.loading = false;
|
||
state.error = action.payload as string;
|
||
})
|
||
// 添加就医资料
|
||
.addCase(addNewMedicalRecord.fulfilled, (state, action) => {
|
||
const item = action.payload;
|
||
if (item.type === 'medical_record') {
|
||
state.medicalRecords.records.unshift(item);
|
||
} else {
|
||
state.medicalRecords.prescriptions.unshift(item);
|
||
}
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
})
|
||
// 删除就医资料
|
||
.addCase(deleteMedicalRecordItem.fulfilled, (state, action) => {
|
||
const { id, type } = action.payload;
|
||
if (type === 'medical_record') {
|
||
state.medicalRecords.records = state.medicalRecords.records.filter(item => item.id !== id);
|
||
} else {
|
||
state.medicalRecords.prescriptions = state.medicalRecords.prescriptions.filter(item => item.id !== id);
|
||
}
|
||
state.lastUpdateTime = new Date().toISOString();
|
||
});
|
||
},
|
||
});
|
||
|
||
// Action creators
|
||
export const {
|
||
setLoading,
|
||
setError,
|
||
setHealthData,
|
||
clearHealthDataForDate,
|
||
clearAllHealthData,
|
||
updateHistoryData,
|
||
setHistoryData,
|
||
clearHistoryData,
|
||
setMedicalRecords,
|
||
addMedicalRecordItem,
|
||
removeMedicalRecordItem,
|
||
} = healthSlice.actions;
|
||
|
||
// Thunk action to fetch and set health data for a specific date
|
||
export const fetchHealthDataForDate = (date: Date) => {
|
||
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||
try {
|
||
dispatch(setLoading(true));
|
||
dispatch(setError(null));
|
||
|
||
// 这里可以添加实际的 API 调用逻辑
|
||
// 目前我们假设数据已经通过其他方式获取
|
||
|
||
dispatch(setLoading(false));
|
||
} catch (error) {
|
||
dispatch(setError(error instanceof Error ? error.message : '获取健康数据失败'));
|
||
dispatch(setLoading(false));
|
||
}
|
||
};
|
||
};
|
||
|
||
// ==================== 健康史 API Thunks ====================
|
||
|
||
/**
|
||
* 从服务端获取完整健康史数据
|
||
*/
|
||
export const fetchHealthHistory = createAsyncThunk(
|
||
'health/fetchHistory',
|
||
async (_, { rejectWithValue }) => {
|
||
try {
|
||
const data = await healthProfileApi.getHealthHistory();
|
||
return data;
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '获取健康史失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 保存健康史分类到服务端
|
||
*/
|
||
export const saveHealthHistoryCategory = createAsyncThunk(
|
||
'health/saveHistoryCategory',
|
||
async (
|
||
{
|
||
category,
|
||
data,
|
||
}: {
|
||
category: healthProfileApi.HealthHistoryCategory;
|
||
data: healthProfileApi.UpdateHealthHistoryRequest;
|
||
},
|
||
{ rejectWithValue }
|
||
) => {
|
||
try {
|
||
const result = await healthProfileApi.updateHealthHistory(category, data);
|
||
return { category, data: result };
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '保存健康史失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取健康史完成进度
|
||
*/
|
||
export const fetchHealthHistoryProgress = createAsyncThunk(
|
||
'health/fetchHistoryProgress',
|
||
async (_, { rejectWithValue }) => {
|
||
try {
|
||
const data = await healthProfileApi.getHealthHistoryProgress();
|
||
return data;
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '获取健康史进度失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
// ==================== 就医资料 API Thunks ====================
|
||
|
||
/**
|
||
* 获取就医资料
|
||
*/
|
||
export const fetchMedicalRecords = createAsyncThunk(
|
||
'health/fetchMedicalRecords',
|
||
async (_, { rejectWithValue }) => {
|
||
try {
|
||
const data = await healthProfileApi.getMedicalRecords();
|
||
return data;
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '获取就医资料失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 添加就医资料
|
||
*/
|
||
export const addNewMedicalRecord = createAsyncThunk(
|
||
'health/addMedicalRecord',
|
||
async (data: Omit<MedicalRecordItem, 'id' | 'createdAt' | 'updatedAt'>, { rejectWithValue }) => {
|
||
try {
|
||
const result = await healthProfileApi.addMedicalRecord(data);
|
||
return result;
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '添加就医资料失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 删除就医资料
|
||
*/
|
||
export const deleteMedicalRecordItem = createAsyncThunk(
|
||
'health/deleteMedicalRecord',
|
||
async ({ id, type }: { id: string; type: MedicalRecordType }, { rejectWithValue }) => {
|
||
try {
|
||
await healthProfileApi.deleteMedicalRecord(id);
|
||
return { id, type };
|
||
} catch (err: any) {
|
||
return rejectWithValue(err?.message ?? '删除就医资料失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
// Selectors
|
||
export const selectHealthDataByDate = (date: string) => (state: RootState) => state.health.dataByDate[date];
|
||
export const selectHealthLoading = (state: RootState) => state.health.loading;
|
||
export const selectHealthError = (state: RootState) => state.health.error;
|
||
export const selectLastUpdateTime = (state: RootState) => state.health.lastUpdateTime;
|
||
export const selectHistoryData = (state: RootState) => state.health.historyData;
|
||
export const selectMedicalRecords = (state: RootState) => state.health.medicalRecords;
|
||
|
||
// 计算健康史完成度的 selector
|
||
export const selectHealthHistoryProgress = (state: RootState) => {
|
||
const historyData = state.health.historyData;
|
||
const categories = ['allergy', 'disease', 'surgery', 'familyDisease'];
|
||
|
||
let answeredCount = 0;
|
||
categories.forEach(category => {
|
||
const data = historyData[category];
|
||
// 只要回答了是否有历史(hasHistory !== null),就算已完成
|
||
if (data && data.hasHistory !== null) {
|
||
answeredCount++;
|
||
}
|
||
});
|
||
|
||
return Math.round((answeredCount / categories.length) * 100);
|
||
};
|
||
|
||
export default healthSlice.reducer; |