Files
digital-pilates/store/trainingPlanSlice.ts
richarjiang 2357596665 refactor(storage): 迁移 AsyncStorage 至 expo-sqlite/kv-store
- 统一替换所有 @react-native-async-storage/async-storage 导入为自定义 kvStore
- 新增 kvStore.ts 封装 expo-sqlite/kv-store,保持与 AsyncStorage 完全兼容
- 新增同步读写方法,提升性能
- 引入 expo-sqlite 依赖并更新 lock 文件

BREAKING CHANGE: 移除 @react-native-async-storage/async-storage 依赖,需重新安装依赖并清理旧数据
2025-09-15 12:51:18 +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 '@/utils/kvStore';
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;