Files
digital-pilates/store/userSlice.ts
richarjiang 5814044cee feat: 更新 AI 教练聊天界面和个人信息页面
- 在 AI 教练聊天界面中添加训练记录分析功能,允许用户基于近期训练记录获取分析建议
- 更新 Redux 状态管理,集成每日步数和卡路里目标
- 在个人信息页面中优化用户头像显示,支持从库中选择头像
- 修改首页布局,添加可拖动的教练徽章,提升用户交互体验
- 更新样式以适应新功能的展示和交互
2025-08-13 10:09:55 +08:00

145 lines
4.7 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 { 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;