import { buildDefaultCustomFromPlan, DayPlan, ExerciseCustomConfig, generatePilates30DayPlan, PilatesLevel } from '@/utils/pilatesPlan'; import AsyncStorage from '@/utils/kvStore'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; export type DayStatus = 'locked' | 'available' | 'completed'; export type ChallengeDayState = { plan: DayPlan; status: DayStatus; completedAt?: string | null; // ISO notes?: string; custom?: ExerciseCustomConfig[]; // 用户自定义:启用/禁用、组数、时长 }; export type ChallengeState = { startedAt?: string | null; level: PilatesLevel; days: ChallengeDayState[]; // 1..30 streak: number; // 连续天数 }; const STORAGE_KEY = '@pilates_challenge_30d'; const initialState: ChallengeState = { startedAt: null, level: 'beginner', days: [], streak: 0, }; function computeStreak(days: ChallengeDayState[]): number { // 连续从第1天开始的已完成天数 let s = 0; for (let i = 0; i < days.length; i += 1) { if (days[i].status === 'completed') s += 1; else break; } return s; } export const initChallenge = createAsyncThunk( 'challenge/init', async (_: void, { getState }) => { const persisted = await AsyncStorage.getItem(STORAGE_KEY); if (persisted) { try { const parsed = JSON.parse(persisted) as ChallengeState; return parsed; } catch {} } // 默认生成 const level: PilatesLevel = 'beginner'; const plans = generatePilates30DayPlan(level); const days: ChallengeDayState[] = plans.map((p, idx) => ({ plan: p, status: idx === 0 ? 'available' : 'locked', custom: buildDefaultCustomFromPlan(p), })); const state: ChallengeState = { startedAt: new Date().toISOString(), level, days, streak: 0, }; await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(state)); return state; } ); export const persistChallenge = createAsyncThunk( 'challenge/persist', async (_: void, { getState }) => { const s = (getState() as any).challenge as ChallengeState; await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(s)); return true; } ); export const completeDay = createAsyncThunk( 'challenge/completeDay', async (dayNumber: number, { getState, dispatch }) => { const state = (getState() as any).challenge as ChallengeState; const idx = dayNumber - 1; const days = [...state.days]; if (!days[idx] || days[idx].status === 'completed') return state; days[idx] = { ...days[idx], status: 'completed', completedAt: new Date().toISOString() }; if (days[idx + 1]) { days[idx + 1] = { ...days[idx + 1], status: 'available' }; } const next: ChallengeState = { ...state, days, streak: computeStreak(days), }; await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next)); return next; } ); const challengeSlice = createSlice({ name: 'challenge', initialState, reducers: { setLevel(state, action: PayloadAction) { state.level = action.payload; }, setNote(state, action: PayloadAction<{ dayNumber: number; notes: string }>) { const idx = action.payload.dayNumber - 1; if (state.days[idx]) state.days[idx].notes = action.payload.notes; }, setCustom(state, action: PayloadAction<{ dayNumber: number; custom: ExerciseCustomConfig[] }>) { const idx = action.payload.dayNumber - 1; if (state.days[idx]) state.days[idx].custom = action.payload.custom; }, }, extraReducers: (builder) => { builder .addCase(initChallenge.fulfilled, (_state, action) => { return action.payload as ChallengeState; }) .addCase(completeDay.fulfilled, (_state, action) => { return action.payload as ChallengeState; }); }, }); export const { setLevel, setNote, setCustom } = challengeSlice.actions; export default challengeSlice.reducer;