feat: 新增健康档案模块,支持家庭邀请与个人健康数据管理
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import * as healthProfileApi from '@/services/healthProfile';
|
||||
import { AppDispatch, RootState } from './index';
|
||||
|
||||
// 健康数据类型定义
|
||||
@@ -22,10 +23,28 @@ export interface HealthData {
|
||||
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;
|
||||
|
||||
// 加载状态
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
@@ -37,6 +56,12 @@ export interface HealthState {
|
||||
// 初始状态
|
||||
const initialState: HealthState = {
|
||||
dataByDate: {},
|
||||
historyData: {
|
||||
allergy: { hasHistory: null, items: [] },
|
||||
disease: { hasHistory: null, items: [] },
|
||||
surgery: { hasHistory: null, items: [] },
|
||||
familyDisease: { hasHistory: null, items: [] },
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
lastUpdateTime: null,
|
||||
@@ -82,6 +107,96 @@ const healthSlice = createSlice({
|
||||
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: [] },
|
||||
};
|
||||
},
|
||||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,6 +207,9 @@ export const {
|
||||
setHealthData,
|
||||
clearHealthDataForDate,
|
||||
clearAllHealthData,
|
||||
updateHistoryData,
|
||||
setHistoryData,
|
||||
clearHistoryData,
|
||||
} = healthSlice.actions;
|
||||
|
||||
// Thunk action to fetch and set health data for a specific date
|
||||
@@ -112,10 +230,84 @@ export const fetchHealthDataForDate = (date: Date) => {
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 健康史 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 ?? '获取健康史进度失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
// 计算健康史完成度的 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;
|
||||
Reference in New Issue
Block a user