Files
digital-pilates/store/trainingPlanSlice.ts
richarjiang 5a4d86ff7d feat: 更新应用配置和引入新依赖
- 修改 app.json,禁用平板支持以优化用户体验
- 在 package.json 和 package-lock.json 中新增 react-native-toast-message 依赖,支持消息提示功能
- 在多个组件中集成 Toast 组件,提升用户交互反馈
- 更新训练计划相关逻辑,优化状态管理和数据处理
- 调整样式以适应新功能的展示和交互
2025-08-16 09:42:33 +08:00

354 lines
11 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 { 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<TrainingPlan, 'id' | 'createdAt'>;
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<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;
},
setName(state, action: PayloadAction<string>) {
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<string | null>) {
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;