- 在 AI 教练聊天界面中添加训练记录分析功能,允许用户基于近期训练记录获取分析建议 - 更新 Redux 状态管理,集成每日步数和卡路里目标 - 在个人信息页面中优化用户头像显示,支持从库中选择头像 - 修改首页布局,添加可拖动的教练徽章,提升用户交互体验 - 更新样式以适应新功能的展示和交互
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import { api, loadPersistedToken, setAuthToken, STORAGE_KEYS } from '@/services/api';
|
||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||
|
||
export type Gender = 'male' | 'female' | '';
|
||
|
||
export type UserProfile = {
|
||
fullName?: string;
|
||
email?: string;
|
||
gender?: Gender;
|
||
age?: string; // 个人中心是字符串展示
|
||
weightKg?: number;
|
||
heightCm?: number;
|
||
avatarUri?: string | null;
|
||
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
||
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
||
pilatesPurposes?: string[]; // 普拉提目的(多选)
|
||
};
|
||
|
||
export type UserState = {
|
||
token: string | null;
|
||
profile: UserProfile;
|
||
loading: boolean;
|
||
error: string | null;
|
||
};
|
||
|
||
export const DEFAULT_MEMBER_NAME = '普拉提星球学员';
|
||
|
||
const initialState: UserState = {
|
||
token: null,
|
||
profile: {
|
||
fullName: DEFAULT_MEMBER_NAME,
|
||
},
|
||
loading: false,
|
||
error: null,
|
||
};
|
||
|
||
export type LoginPayload = Record<string, any> & {
|
||
// 可扩展:用户名密码、Apple 身份、短信验证码等
|
||
username?: string;
|
||
password?: string;
|
||
appleIdentityToken?: string;
|
||
};
|
||
|
||
export const login = createAsyncThunk(
|
||
'user/login',
|
||
async (payload: LoginPayload, { rejectWithValue }) => {
|
||
try {
|
||
// 后端路径允许传入 '/api/login' 或 'login'
|
||
const data = await api.post<{ token?: string; profile?: UserProfile } | (UserProfile & { token?: string })>(
|
||
'/api/login',
|
||
payload,
|
||
);
|
||
|
||
// 兼容两种返回结构
|
||
const token = (data as any).token ?? (data as any)?.profile?.token ?? null;
|
||
const profile: UserProfile | null = (data as any).profile ?? (data as any);
|
||
|
||
if (!token) throw new Error('登录响应缺少 token');
|
||
|
||
await AsyncStorage.setItem(STORAGE_KEYS.authToken, token);
|
||
await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {}));
|
||
await setAuthToken(token);
|
||
|
||
return { token, profile } as { token: string; profile: UserProfile };
|
||
} catch (err: any) {
|
||
const message = err?.message ?? '登录失败';
|
||
return rejectWithValue(message);
|
||
}
|
||
}
|
||
);
|
||
|
||
export const rehydrateUser = createAsyncThunk('user/rehydrate', async () => {
|
||
const [token, profileStr] = await Promise.all([
|
||
loadPersistedToken(),
|
||
AsyncStorage.getItem(STORAGE_KEYS.userProfile),
|
||
]);
|
||
await setAuthToken(token);
|
||
let profile: UserProfile = {};
|
||
if (profileStr) {
|
||
try { profile = JSON.parse(profileStr) as UserProfile; } catch { profile = {}; }
|
||
}
|
||
return { token, profile } as { token: string | null; profile: UserProfile };
|
||
});
|
||
|
||
export const logout = createAsyncThunk('user/logout', async () => {
|
||
await Promise.all([
|
||
AsyncStorage.removeItem(STORAGE_KEYS.authToken),
|
||
AsyncStorage.removeItem(STORAGE_KEYS.userProfile),
|
||
]);
|
||
await setAuthToken(null);
|
||
return true;
|
||
});
|
||
|
||
const userSlice = createSlice({
|
||
name: 'user',
|
||
initialState,
|
||
reducers: {
|
||
updateProfile(state, action: PayloadAction<Partial<UserProfile>>) {
|
||
state.profile = { ...(state.profile ?? {}), ...action.payload };
|
||
},
|
||
setDailyStepsGoal(state, action: PayloadAction<number>) {
|
||
state.profile = { ...(state.profile ?? {}), dailyStepsGoal: action.payload };
|
||
},
|
||
setDailyCaloriesGoal(state, action: PayloadAction<number>) {
|
||
state.profile = { ...(state.profile ?? {}), dailyCaloriesGoal: action.payload };
|
||
},
|
||
setPilatesPurposes(state, action: PayloadAction<string[]>) {
|
||
state.profile = { ...(state.profile ?? {}), pilatesPurposes: action.payload };
|
||
},
|
||
},
|
||
extraReducers: (builder) => {
|
||
builder
|
||
.addCase(login.pending, (state) => { state.loading = true; state.error = null; })
|
||
.addCase(login.fulfilled, (state, action) => {
|
||
state.loading = false;
|
||
state.token = action.payload.token;
|
||
state.profile = action.payload.profile;
|
||
if (!state.profile?.fullName || !state.profile.fullName.trim()) {
|
||
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||
}
|
||
})
|
||
.addCase(login.rejected, (state, action) => {
|
||
state.loading = false;
|
||
state.error = (action.payload as string) ?? '登录失败';
|
||
})
|
||
.addCase(rehydrateUser.fulfilled, (state, action) => {
|
||
state.token = action.payload.token;
|
||
state.profile = action.payload.profile;
|
||
if (!state.profile?.fullName || !state.profile.fullName.trim()) {
|
||
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||
}
|
||
})
|
||
.addCase(logout.fulfilled, (state) => {
|
||
state.token = null;
|
||
state.profile = {};
|
||
});
|
||
},
|
||
});
|
||
|
||
export const { updateProfile, setDailyStepsGoal, setDailyCaloriesGoal, setPilatesPurposes } = userSlice.actions;
|
||
export default userSlice.reducer;
|
||
|
||
|