feat: 更新应用图标和启动画面
- 将应用图标更改为 logo.jpeg,更新相关配置文件 - 删除旧的图标文件,确保资源整洁 - 更新启动画面使用新的 logo 图片,提升视觉一致性 - 在训练计划相关功能中集成新的 API 接口,支持训练计划的创建和管理 - 优化 Redux 状态管理,支持训练计划的加载和删除功能 - 更新样式以适应新图标和功能的展示
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { CreateTrainingPlanDto, trainingPlanApi } from '@/services/trainingPlanApi';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
@@ -22,14 +23,19 @@ export type TrainingPlan = {
|
||||
goal: PlanGoal | '';
|
||||
startWeightKg?: number;
|
||||
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TrainingPlanState = {
|
||||
current?: TrainingPlan | null;
|
||||
plans: TrainingPlan[];
|
||||
currentId?: string | null;
|
||||
draft: Omit<TrainingPlan, 'id' | 'createdAt'>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = '@training_plan';
|
||||
const STORAGE_KEY_LEGACY_SINGLE = '@training_plan';
|
||||
const STORAGE_KEY_LIST = '@training_plans';
|
||||
|
||||
function nextMondayISO(): string {
|
||||
const now = new Date();
|
||||
@@ -42,7 +48,10 @@ function nextMondayISO(): string {
|
||||
}
|
||||
|
||||
const initialState: TrainingPlanState = {
|
||||
current: null,
|
||||
plans: [],
|
||||
currentId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
draft: {
|
||||
startDate: new Date(new Date().setHours(0, 0, 0, 0)).toISOString(),
|
||||
mode: 'daysOfWeek',
|
||||
@@ -51,31 +60,141 @@ const initialState: TrainingPlanState = {
|
||||
goal: '',
|
||||
startWeightKg: undefined,
|
||||
preferredTimeOfDay: '',
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const loadTrainingPlan = createAsyncThunk('trainingPlan/load', async () => {
|
||||
const str = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
if (!str) return null;
|
||||
/**
|
||||
* 从服务器加载训练计划列表,同时支持本地缓存迁移
|
||||
*/
|
||||
export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
return JSON.parse(str) as TrainingPlan;
|
||||
} catch {
|
||||
return null;
|
||||
// 尝试从服务器获取数据
|
||||
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 };
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
/**
|
||||
* 将当前 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 || '删除训练计划失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,6 +202,11 @@ const trainingPlanSlice = createSlice({
|
||||
name: 'trainingPlan',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentPlan(state, action: PayloadAction<string | null | undefined>) {
|
||||
state.currentId = action.payload ?? null;
|
||||
// 保存到本地存储
|
||||
AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload ?? '');
|
||||
},
|
||||
setMode(state, action: PayloadAction<PlanMode>) {
|
||||
state.draft.mode = action.payload;
|
||||
},
|
||||
@@ -108,30 +232,74 @@ const trainingPlanSlice = createSlice({
|
||||
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;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(loadTrainingPlan.fulfilled, (state, action) => {
|
||||
state.current = action.payload;
|
||||
// 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 基于该计划(便于编辑)
|
||||
if (action.payload) {
|
||||
const { id, createdAt, ...rest } = action.payload;
|
||||
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(saveTrainingPlan.fulfilled, (state, action) => {
|
||||
state.current = action.payload;
|
||||
.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,
|
||||
@@ -139,8 +307,10 @@ export const {
|
||||
setStartWeight,
|
||||
setStartDate,
|
||||
setPreferredTime,
|
||||
setName,
|
||||
setStartDateNextMonday,
|
||||
resetDraft,
|
||||
clearError,
|
||||
} = trainingPlanSlice.actions;
|
||||
|
||||
export default trainingPlanSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user