feat: Add Personal Center module and UI improvements

- Add ProfilePage with avatar upload, password change, and module status display

- Update logo and favicon for login page and browser tab

- Redirect Data Cleaning module default route to Tool C

- Hide Settings button from top navigation for MVP

- Add avatar display in top navigation bar with refresh sync

- Add Prompt knowledge base integration development plan docs
This commit is contained in:
2026-01-28 18:18:09 +08:00
parent 5d5a174dd7
commit 3a4aa9123c
17 changed files with 1309 additions and 22 deletions

View File

@@ -188,6 +188,19 @@ export function AuthProvider({ children }: AuthProviderProps) {
return user.modules?.includes(moduleCode) || false;
}, [user]);
/**
* 刷新用户信息(头像更新等场景使用)
*/
const refreshUser = useCallback(async () => {
try {
const freshUser = await authApi.getCurrentUser();
setUser(freshUser);
authApi.saveUser(freshUser);
} catch (err) {
console.error('刷新用户信息失败:', err);
}
}, []);
const value: AuthContextType = {
user,
isAuthenticated: !!user,
@@ -201,7 +214,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
refreshToken,
hasPermission,
hasRole,
hasModule, // 新增
hasModule,
refreshUser, // 2026-01-28: 头像更新等场景使用
};
return (

View File

@@ -198,6 +198,29 @@ export async function changePassword(request: ChangePasswordRequest): Promise<vo
}
}
/**
* 更新头像
*/
export async function updateAvatar(avatarUrl: string): Promise<{ avatarUrl: string }> {
const response = await authFetch<{ avatarUrl: string }>(`${API_BASE}/me/avatar`, {
method: 'PUT',
body: JSON.stringify({ avatarUrl }),
});
if (!response.success || !response.data) {
throw new Error(response.message || '更新头像失败');
}
// 更新本地存储的用户信息
const user = getSavedUser();
if (user) {
user.avatarUrl = avatarUrl;
saveUser(user);
}
return response.data;
}
/**
* 刷新Token
*/

View File

@@ -29,6 +29,13 @@ export interface AuthUser {
isDefaultPassword: boolean;
permissions: string[];
modules: string[]; // 用户可访问的模块代码列表(如 ['AIA', 'PKB', 'RVW']
// 2026-01-28: 个人中心扩展字段
avatarUrl?: string | null;
status?: string;
kbQuota?: number;
kbUsed?: number;
isTrial?: boolean;
trialEndsAt?: string | null; // ISO date string
}
/** Token信息 */
@@ -100,6 +107,8 @@ export interface AuthContextType extends AuthState {
hasRole: (...roles: UserRole[]) => boolean;
/** 检查模块权限 */
hasModule: (moduleCode: string) => boolean;
/** 刷新用户信息(头像更新等场景使用) */
refreshUser: () => Promise<void>;
}

View File

@@ -1,17 +1,15 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Dropdown, Avatar } from 'antd'
import {
UserOutlined,
LogoutOutlined,
SettingOutlined,
// SettingOutlined, // MVP阶段暂时隐藏设置按钮
ControlOutlined,
BankOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import { MODULES } from '../modules/moduleRegistry'
import { useAuth } from '../auth'
import type { ModuleDefinition } from '../modules/types'
/**
* 顶部导航栏组件
@@ -53,11 +51,12 @@ const TopNavigation = () => {
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '设置',
},
// MVP阶段暂时隐藏设置按钮
// {
// key: 'settings',
// icon: <SettingOutlined />,
// label: '设置',
// },
// 切换入口 - 根据权限显示
...(canAccessOrg || canAccessAdmin ? [{ type: 'divider' as const }] : []),
...(canAccessOrg ? [{
@@ -103,9 +102,9 @@ const TopNavigation = () => {
onClick={() => navigate('/')}
>
<img
src="/logo.jpg"
src="/logo-new.png"
alt="AI临床研究平台"
className="h-[52px] w-auto"
className="h-[48px] w-auto"
/>
<span className="text-xl font-bold text-blue-600">AI临床研究平台</span>
</div>
@@ -133,14 +132,15 @@ const TopNavigation = () => {
})}
</div>
{/* 用户菜单 - 显示真实用户信息 */}
{/* 用户菜单 - 显示真实用户信息和头像 */}
<Dropdown
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
placement="bottomRight"
>
<div className="flex items-center gap-2 cursor-pointer px-3 py-2 rounded-md hover:bg-gray-50">
<Avatar
icon={<UserOutlined />}
src={user?.avatarUrl}
icon={!user?.avatarUrl && <UserOutlined />}
size="small"
/>
<div className="flex flex-col">