feat: 优化 AI 教练聊天界面和个人信息页面
- 在 AI 教练聊天界面中添加消息自动滚动功能,提升用户体验 - 更新消息发送逻辑,确保新消息渲染后再滚动 - 在个人信息页面中集成用户资料拉取功能,支持每次聚焦时更新用户信息 - 修改登录页面,增加身份令牌验证,确保安全性 - 更新样式以适应新功能的展示和交互
This commit is contained in:
@@ -46,15 +46,49 @@ 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,
|
||||
);
|
||||
let data: any = null;
|
||||
// 若为 Apple 登录,走新的后端接口 /api/users/auth/apple/login
|
||||
if (payload.appleIdentityToken) {
|
||||
const appleToken = payload.appleIdentityToken;
|
||||
// 智能尝试不同 DTO 键名(identityToken / idToken)以提升兼容性
|
||||
const candidates = [
|
||||
{ identityToken: appleToken },
|
||||
{ idToken: appleToken },
|
||||
];
|
||||
let lastError: any = null;
|
||||
for (const body of candidates) {
|
||||
try {
|
||||
data = await api.post('/api/users/auth/apple/login', body);
|
||||
lastError = null;
|
||||
break;
|
||||
} catch (err: any) {
|
||||
lastError = err;
|
||||
// 若为 4xx 尝试下一个映射;其他错误直接抛出
|
||||
const status = (err as any)?.status;
|
||||
if (status && status >= 500) throw err;
|
||||
}
|
||||
}
|
||||
if (!data && lastError) throw lastError;
|
||||
} else {
|
||||
// 其他登录(如游客/用户名密码)保持原有 '/api/login'
|
||||
data = await api.post('/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);
|
||||
// 兼容多种返回结构
|
||||
const token =
|
||||
(data as any).token ??
|
||||
(data as any).accessToken ??
|
||||
(data as any).access_token ??
|
||||
(data as any).jwt ??
|
||||
(data as any)?.profile?.token ??
|
||||
(data as any)?.user?.token ??
|
||||
null;
|
||||
|
||||
const profile: UserProfile | null =
|
||||
(data as any).profile ??
|
||||
(data as any).user ??
|
||||
(data as any).account ??
|
||||
(data as any);
|
||||
|
||||
if (!token) throw new Error('登录响应缺少 token');
|
||||
|
||||
@@ -92,6 +126,20 @@ export const logout = createAsyncThunk('user/logout', async () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// 拉取最新用户信息
|
||||
export const fetchMyProfile = createAsyncThunk('user/fetchMyProfile', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
// 固定使用后端文档的接口:/api/users/info
|
||||
const data: any = await api.get('/api/users/info');
|
||||
const profile: UserProfile = (data as any).profile ?? (data as any).user ?? (data as any).account ?? (data as any);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {}));
|
||||
return profile;
|
||||
} catch (err: any) {
|
||||
const message = err?.message ?? '获取用户信息失败';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
@@ -131,6 +179,16 @@ const userSlice = createSlice({
|
||||
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||||
}
|
||||
})
|
||||
.addCase(fetchMyProfile.fulfilled, (state, action) => {
|
||||
state.profile = action.payload || {};
|
||||
if (!state.profile?.fullName || !state.profile.fullName.trim()) {
|
||||
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||||
}
|
||||
})
|
||||
.addCase(fetchMyProfile.rejected, (state, action) => {
|
||||
// 不覆盖现有资料,仅记录错误
|
||||
state.error = (action.payload as string) ?? '获取用户信息失败';
|
||||
})
|
||||
.addCase(logout.fulfilled, (state) => {
|
||||
state.token = null;
|
||||
state.profile = {};
|
||||
|
||||
Reference in New Issue
Block a user