import { CreateTrainingPlanDto, PlanGoal, PlanMode, TrainingPlan, trainingPlanApi } from '@/services/trainingPlanApi'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; export type TrainingPlanState = { plans: TrainingPlan[]; editingId?: string | null; draft: Omit; loading: boolean; error: string | null; }; 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: [], editingId: 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); // 获取所有计划 const plans = response.list; return { plans }; } 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[]; return { plans } as { plans: TrainingPlan[] }; } catch { // 解析失败则视为无数据 } } return { plans: [] } as { plans: TrainingPlan[] }; } }); /** * 将当前 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 newPlan = await trainingPlanApi.create(createDto); return newPlan; } catch (error: any) { return rejectWithValue(error.message || '创建训练计划失败'); } } ); /** * 加载某个计划用于编辑:会写入 draft 与 editingId */ export const loadPlanForEdit = createAsyncThunk( 'trainingPlan/loadPlanForEdit', async (id: string, { getState, rejectWithValue }) => { try { const detail = await trainingPlanApi.detail(id); const draft = { startDate: detail.startDate, name: detail.name, mode: detail.mode, daysOfWeek: detail.daysOfWeek, sessionsPerWeek: detail.sessionsPerWeek, goal: detail.goal as PlanGoal, startWeightKg: detail.startWeightKg ?? undefined, preferredTimeOfDay: detail.preferredTimeOfDay, } as TrainingPlanState['draft']; return { id, draft } as { id: string; draft: TrainingPlanState['draft'] }; } catch (error: any) { return rejectWithValue(error.message || '加载计划详情失败'); } } ); /** * 使用当前 draft 更新 editingId 对应的计划 */ export const updatePlanFromDraft = createAsyncThunk( 'trainingPlan/updatePlanFromDraft', async (_: void, { getState, rejectWithValue }) => { try { const s = (getState() as any).trainingPlan as TrainingPlanState; if (!s.editingId) throw new Error('无有效编辑对象'); const draft = s.draft; const dto: 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 resp = await trainingPlanApi.update(s.editingId, dto); const updated: TrainingPlan = { id: resp.id, createdAt: resp.createdAt, startDate: resp.startDate, mode: resp.mode, daysOfWeek: resp.daysOfWeek, sessionsPerWeek: resp.sessionsPerWeek, goal: resp.goal as PlanGoal, startWeightKg: resp.startWeightKg ?? undefined, preferredTimeOfDay: resp.preferredTimeOfDay, name: resp.name, }; // 更新本地 plans const idx = (s.plans || []).findIndex(p => p.id === updated.id); const nextPlans = [...(s.plans || [])]; if (idx >= 0) nextPlans[idx] = updated; else nextPlans.push(updated); await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans)); return { plans: nextPlans } as { plans: TrainingPlan[] }; } catch (error: any) { return rejectWithValue(error.message || '更新训练计划失败'); } } ); // 激活计划 export const activatePlan = createAsyncThunk( 'trainingPlan/activatePlan', async (planId: string, { rejectWithValue }) => { try { await trainingPlanApi.activate(planId); return { id: planId } as { id: 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); // 同时更新本地存储 await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans)); return { plans: nextPlans } as { plans: TrainingPlan[] }; } catch (error: any) { return rejectWithValue(error.message || '删除训练计划失败'); } } ); const trainingPlanSlice = createSlice({ name: 'trainingPlan', initialState, reducers: { 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; }, setEditingId(state, action: PayloadAction) { state.editingId = action.payload; }, }, 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; }) .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; }) .addCase(saveDraftAsPlan.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '创建训练计划失败'; }) // loadPlanForEdit .addCase(loadPlanForEdit.pending, (state) => { state.loading = true; state.error = null; }) .addCase(loadPlanForEdit.fulfilled, (state, action) => { state.loading = false; state.editingId = action.payload.id; state.draft = action.payload.draft; }) .addCase(loadPlanForEdit.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '加载计划详情失败'; }) // updatePlanFromDraft .addCase(updatePlanFromDraft.pending, (state) => { state.loading = true; state.error = null; }) .addCase(updatePlanFromDraft.fulfilled, (state, action) => { state.loading = false; state.plans = action.payload.plans; }) .addCase(updatePlanFromDraft.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '更新训练计划失败'; }) // activatePlan .addCase(activatePlan.pending, (state) => { state.loading = true; state.error = null; }) .addCase(activatePlan.fulfilled, (state, action) => { state.loading = false; }) .addCase(activatePlan.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; }) .addCase(deletePlan.rejected, (state, action) => { state.loading = false; state.error = action.payload as string || '删除训练计划失败'; }); }, }); export const { setMode, toggleDayOfWeek, setSessionsPerWeek, setGoal, setStartWeight, setStartDate, setPreferredTime, setName, setStartDateNextMonday, resetDraft, clearError, setEditingId, } = trainingPlanSlice.actions; export default trainingPlanSlice.reducer;