feat: 添加训练计划和打卡功能
- 新增训练计划页面,允许用户制定个性化的训练计划 - 集成打卡功能,用户可以记录每日的训练情况 - 更新 Redux 状态管理,添加训练计划相关的 reducer - 在首页中添加训练计划卡片,支持用户点击跳转 - 更新样式和布局,以适应新功能的展示和交互 - 添加日期选择器和相关依赖,支持用户选择训练日期
This commit is contained in:
148
store/trainingPlanSlice.ts
Normal file
148
store/trainingPlanSlice.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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' | '';
|
||||
};
|
||||
|
||||
export type TrainingPlanState = {
|
||||
current?: TrainingPlan | null;
|
||||
draft: Omit<TrainingPlan, 'id' | 'createdAt'>;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = '@training_plan';
|
||||
|
||||
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 = {
|
||||
current: 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: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const loadTrainingPlan = createAsyncThunk('trainingPlan/load', async () => {
|
||||
const str = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
if (!str) return null;
|
||||
try {
|
||||
return JSON.parse(str) as TrainingPlan;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const saveTrainingPlan = createAsyncThunk(
|
||||
'trainingPlan/save',
|
||||
async (_: void, { getState }) => {
|
||||
const s = (getState() as any).trainingPlan as TrainingPlanState;
|
||||
const draft = s.draft;
|
||||
const plan: TrainingPlan = {
|
||||
id: `plan_${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
...draft,
|
||||
};
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(plan));
|
||||
return plan;
|
||||
}
|
||||
);
|
||||
|
||||
const trainingPlanSlice = createSlice({
|
||||
name: 'trainingPlan',
|
||||
initialState,
|
||||
reducers: {
|
||||
setMode(state, action: PayloadAction<PlanMode>) {
|
||||
state.draft.mode = action.payload;
|
||||
},
|
||||
toggleDayOfWeek(state, action: PayloadAction<number>) {
|
||||
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<number>) {
|
||||
const n = Math.min(7, Math.max(1, action.payload));
|
||||
state.draft.sessionsPerWeek = n;
|
||||
},
|
||||
setGoal(state, action: PayloadAction<TrainingPlan['goal']>) {
|
||||
state.draft.goal = action.payload;
|
||||
},
|
||||
setStartWeight(state, action: PayloadAction<number | undefined>) {
|
||||
state.draft.startWeightKg = action.payload;
|
||||
},
|
||||
setStartDate(state, action: PayloadAction<string>) {
|
||||
state.draft.startDate = action.payload;
|
||||
},
|
||||
setPreferredTime(state, action: PayloadAction<TrainingPlan['preferredTimeOfDay']>) {
|
||||
state.draft.preferredTimeOfDay = action.payload;
|
||||
},
|
||||
setStartDateNextMonday(state) {
|
||||
state.draft.startDate = nextMondayISO();
|
||||
},
|
||||
resetDraft(state) {
|
||||
state.draft = initialState.draft;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(loadTrainingPlan.fulfilled, (state, action) => {
|
||||
state.current = action.payload;
|
||||
// 若存在历史计划,初始化 draft 基于该计划(便于编辑)
|
||||
if (action.payload) {
|
||||
const { id, createdAt, ...rest } = action.payload;
|
||||
state.draft = { ...rest };
|
||||
}
|
||||
})
|
||||
.addCase(saveTrainingPlan.fulfilled, (state, action) => {
|
||||
state.current = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setMode,
|
||||
toggleDayOfWeek,
|
||||
setSessionsPerWeek,
|
||||
setGoal,
|
||||
setStartWeight,
|
||||
setStartDate,
|
||||
setPreferredTime,
|
||||
setStartDateNextMonday,
|
||||
resetDraft,
|
||||
} = trainingPlanSlice.actions;
|
||||
|
||||
export default trainingPlanSlice.reducer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user