feat: 新增健康档案模块,支持家庭邀请与个人健康数据管理

This commit is contained in:
richarjiang
2025-12-04 17:56:04 +08:00
parent e713ffbace
commit a254af92c7
28 changed files with 4177 additions and 315 deletions

View File

@@ -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;