- 添加 Redux 状态管理,支持用户登录和个人信息的持久化 - 新增目标管理页面,允许用户设置每日卡路里和步数目标 - 更新首页,移除旧的活动展示,改为固定的热点功能卡片 - 修改布局以适应新功能的展示和交互 - 更新依赖,添加 @reduxjs/toolkit 和 react-redux 库以支持状态管理 - 新增 API 服务模块,处理与后端的交互
125 lines
3.6 KiB
TypeScript
125 lines
3.6 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;
|
||
};
|
||
|
||
export type UserState = {
|
||
token: string | null;
|
||
profile: UserProfile;
|
||
loading: boolean;
|
||
error: string | null;
|
||
};
|
||
|
||
const initialState: UserState = {
|
||
token: null,
|
||
profile: {
|
||
|
||
},
|
||
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 };
|
||
},
|
||
},
|
||
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;
|
||
})
|
||
.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;
|
||
})
|
||
.addCase(logout.fulfilled, (state) => {
|
||
state.token = null;
|
||
state.profile = {};
|
||
});
|
||
},
|
||
});
|
||
|
||
export const { updateProfile } = userSlice.actions;
|
||
export default userSlice.reducer;
|
||
|
||
|