feat: 集成 Redux 状态管理和用户目标管理功能
- 添加 Redux 状态管理,支持用户登录和个人信息的持久化 - 新增目标管理页面,允许用户设置每日卡路里和步数目标 - 更新首页,移除旧的活动展示,改为固定的热点功能卡片 - 修改布局以适应新功能的展示和交互 - 更新依赖,添加 @reduxjs/toolkit 和 react-redux 库以支持状态管理 - 新增 API 服务模块,处理与后端的交互
This commit is contained in:
14
store/index.ts
Normal file
14
store/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import userReducer from './userSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
user: userReducer,
|
||||
},
|
||||
// React Native 环境默认即可
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
|
||||
124
store/userSlice.ts
Normal file
124
store/userSlice.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user