Files
digital-pilates/store/healthSlice.ts

313 lines
8.8 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.

import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import * as healthProfileApi from '@/services/healthProfile';
import { AppDispatch, RootState } from './index';
// 健康数据类型定义
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;
// 加载状态
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: [] },
},
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: [] },
};
},
},
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;
});
},
});
// Action creators
export const {
setLoading,
setError,
setHealthData,
clearHealthDataForDate,
clearAllHealthData,
updateHistoryData,
setHistoryData,
clearHistoryData,
} = 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 ?? '获取健康史进度失败');
}
}
);
// 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;