import { CreateTrainingPlanDto, trainingPlanApi } from '@/services/trainingPlanApi'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; export type PlanMode = 'daysOfWeek' | 'sessionsPerWeek'; export type PlanGoal = | 'postpartum_recovery' // 产后恢复 | 'fat_loss' // 减脂塑形 | 'posture_correction' // 体态矫正 | 'core_strength' // 核心力量 | 'flexibility' // 柔韧灵活 | 'rehab' // 康复保健 | 'stress_relief'; // 释压放松 export type TrainingPlan = { id: string; createdAt: string; // ISO startDate: string; // ISO (当天或下周一) mode: PlanMode; daysOfWeek: number[]; // 0(日) - 6(六) sessionsPerWeek: number; // 1..7 goal: PlanGoal | ''; startWeightKg?: number; preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | ''; name?: string; }; export type TrainingPlanState = { plans: TrainingPlan[]; currentId?: string | null; draft: Omit; loading: boolean; error: string | null; }; const STORAGE_KEY_LEGACY_SINGLE = '@training_plan'; const STORAGE_KEY_LIST = '@training_plans'; function nextMondayISO(): string { const now = new Date(); const day = now.getDay(); const diff = (8 - day) % 7 || 7; // 距下周一的天数 const next = new Date(now); next.setDate(now.getDate() + diff); next.setHours(0, 0, 0, 0); return next.toISOString(); } const initialState: TrainingPlanState = { plans: [], currentId: null, loading: false, error: null, draft: { startDate: new Date(new Date().setHours(0, 0, 0, 0)).toISOString(), mode: 'daysOfWeek', daysOfWeek: [1, 3, 5], sessionsPerWeek: 3, goal: '', startWeightKg: undefined, preferredTimeOfDay: '', name: '', }, }; /** * 从服务器加载训练计划列表,同时支持本地缓存迁移 */ export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, { rejectWithValue }) => { try { // 尝试从服务器获取数据 const response = await trainingPlanApi.list(1, 100); // 获取所有计划 console.log('response', response); const plans: TrainingPlan[] = response.list.map(summary => ({ id: summary.id, createdAt: summary.createdAt, startDate: summary.startDate, goal: summary.goal as PlanGoal, mode: 'daysOfWeek', // 默认值,需要从详情获取 daysOfWeek: [], sessionsPerWeek: 3, preferredTimeOfDay: '', name: '', })); // 读取最后一次使用的 currentId(从本地存储) const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null; return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null }; } catch (error: any) { // 如果API调用失败,回退到本地存储 console.warn('API调用失败,使用本地存储:', error.message); // 新版:列表 const listStr = await AsyncStorage.getItem(STORAGE_KEY_LIST); if (listStr) { try { const plans = JSON.parse(listStr) as TrainingPlan[]; const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null; return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null }; } catch { // 解析失败则视为无数据 } } // 旧版:单计划 const legacyStr = await AsyncStorage.getItem(STORAGE_KEY_LEGACY_SINGLE); if (legacyStr) { try { const legacy = JSON.parse(legacyStr) as TrainingPlan; const plans = [legacy]; const currentId = legacy.id; return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null }; } catch { // ignore } } return { plans: [], currentId: null } as { plans: TrainingPlan[]; currentId: string | null }; } }); /** * 将当前 draft 保存为新计划并设为当前计划。 */ export const saveDraftAsPlan = createAsyncThunk( 'trainingPlan/saveDraftAsPlan', async (_: void, { getState, rejectWithValue }) => { try { const s = (getState() as any).trainingPlan as TrainingPlanState; const draft = s.draft; const createDto: CreateTrainingPlanDto = { startDate: draft.startDate, name: draft.name, mode: draft.mode, daysOfWeek: draft.daysOfWeek, sessionsPerWeek: draft.sessionsPerWeek, goal: draft.goal, startWeightKg: draft.startWeightKg, preferredTimeOfDay: draft.preferredTimeOfDay, }; const response = await trainingPlanApi.create(createDto); const plan: TrainingPlan = { id: response.id, createdAt: response.createdAt, startDate: response.startDate, mode: response.mode, daysOfWeek: response.daysOfWeek, sessionsPerWeek: response.sessionsPerWeek, goal: response.goal as PlanGoal, startWeightKg: response.startWeightKg || undefined, preferredTimeOfDay: response.preferredTimeOfDay, name: response.name, }; const nextPlans = [...(s.plans || []), plan]; // 同时保存到本地存储作为缓存 await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans)); await AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, plan.id); return { plans: nextPlans, currentId: plan.id } as { plans: TrainingPlan[]; currentId: string }; } catch (error: any) { return rejectWithValue(error.message || '创建训练计划失败'); } } ); /** 删除计划 */ export const deletePlan = createAsyncThunk( 'trainingPlan/deletePlan', async (planId: string, { getState, rejectWithValue }) => { try { const s = (getState() as any).trainingPlan as TrainingPlanState; // 调用API删除 await trainingPlanApi.delete(planId); // 更新本地状态 const nextPlans = (s.plans || []).filter((p) => p.id !== planId); let nextCurrentId = s.currentId || null; if (nextCurrentId === planId) { nextCurrentId = nextPlans.length > 0 ? nextPlans[nextPlans.length - 1].id : null; } // 同时更新本地存储 await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans)); await AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, nextCurrentId ?? ''); return { plans: nextPlans, currentId: nextCurrentId } as { plans: TrainingPlan[]; currentId: string | null }; } catch (error: any) { return rejectWithValue(error.message || '删除训练计划失败'); } } ); const trainingPlanSlice = createSlice({ name: 'trainingPlan', initialState, reducers: { setCurrentPlan(state, action: PayloadAction) { state.currentId = action.payload ?? null; // 保存到本地存储 AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload ?? ''); }, setMode(state, action: PayloadAction) { state.draft.mode = action.payload; }, toggleDayOfWeek(state, action: PayloadAction) { const d = action.payload; const set = new Set(state.draft.daysOfWeek); if (set.has(d)) set.delete(d); else set.add(d); state.draft.daysOfWeek = Array.from(set).sort(); }, setSessionsPerWeek(state, action: PayloadAction) { const n = Math.min(7, Math.max(1, action.payload)); state.draft.sessionsPerWeek = n; }, setGoal(state, action: PayloadAction) { state.draft.goal = action.payload; }, setStartWeight(state, action: PayloadAction) { state.draft.startWeightKg = action.payload; }, setStartDate(state, action: PayloadAction) { state.draft.startDate = action.payload; }, setPreferredTime(state, action: PayloadAction) { state.draft.preferredTimeOfDay = action.payload; }, setName(state, action: PayloadAction) { state.draft.name = action.payload; }, setStartDateNextMonday(state) { state.draft.startDate = nextMondayISO(); }, resetDraft(state) { state.draft = initialState.draft; }, clearError(state) { state.error = null; }, }, extraReducers: (builder) => { builder // loadPlans .addCase(loadPlans.pending, (state) => { state.loading = true; state.error = null; }) .addCase(loadPlans.fulfilled, (state, action) => { state.loading = false; state.plans = action.payload.plans; state.currentId = action.payload.currentId; // 若存在历史计划,初始化 draft 基于该计划(便于编辑) const current = state.plans.find((p) => p.id === state.currentId) || state.plans[state.plans.length - 1]; if (current) { const { id, createdAt, ...rest } = current; state.draft = { ...rest }; } }) .addCase(loadPlans.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '加载训练计划失败'; }) // saveDraftAsPlan .addCase(saveDraftAsPlan.pending, (state) => { state.loading = true; state.error = null; }) .addCase(saveDraftAsPlan.fulfilled, (state, action) => { state.loading = false; state.plans = action.payload.plans; state.currentId = action.payload.currentId; }) .addCase(saveDraftAsPlan.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '创建训练计划失败'; }) // deletePlan .addCase(deletePlan.pending, (state) => { state.loading = true; state.error = null; }) .addCase(deletePlan.fulfilled, (state, action) => { state.loading = false; state.plans = action.payload.plans; state.currentId = action.payload.currentId; }) .addCase(deletePlan.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '删除训练计划失败'; }); }, }); export const { setCurrentPlan, setMode, toggleDayOfWeek, setSessionsPerWeek, setGoal, setStartWeight, setStartDate, setPreferredTime, setName, setStartDateNextMonday, resetDraft, clearError, } = trainingPlanSlice.actions; export default trainingPlanSlice.reducer;