feat: 更新应用图标和启动画面

- 将应用图标更改为 logo.jpeg,更新相关配置文件
- 删除旧的图标文件,确保资源整洁
- 更新启动画面使用新的 logo 图片,提升视觉一致性
- 在训练计划相关功能中集成新的 API 接口,支持训练计划的创建和管理
- 优化 Redux 状态管理,支持训练计划的加载和删除功能
- 更新样式以适应新图标和功能的展示
This commit is contained in:
richarjiang
2025-08-14 19:28:38 +08:00
parent 5d09cc05dc
commit 56d4c7fd7f
18 changed files with 1411 additions and 536 deletions

View File

@@ -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;